さて、どんな感じにできるかがわかったので、コードを理解しましょう。
2-5-1. SageMaker Studio Domain
いろいろなリソースを作成しますが、今回のメインとなる SageMaker Studio Domain から見てみましょう。CDK の場合には CfnDomain で SageMaker Studio Domain を作成できます。Terraform もほとんど一緒ですね。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// Studio Domain
const domain = new sagemaker.CfnDomain(this, 'Domain', {
authMode: 'IAM',
defaultUserSettings: {
executionRole: defaultRole.roleArn,
},
domainName,
vpcId: vpc.vpcId,
// VPC の private subnets を使用
subnetIds: vpc.privateSubnets.map((subnet) => subnet.subnetId),
})
domain.applyRemovalPolicy(removalPolicy)
Python
# case01/cdk-python/chococones/chococones.py より抜粋
# Studio Domain
domain = sagemaker.CfnDomain(self, "domain",
auth_mode="IAM",
default_user_settings={
"executionRole": default_role.role_arn,
},
domain_name=DOMAIN_NAME,
vpc_id=vpc.vpc_id,
# VPC の private subnets を使用
subnet_ids=[subnet.subnet_id for subnet in vpc.private_subnets],
)
domain.apply_removal_policy(removal_policy)
Terraform
# case01/terraform/examples/main.tf より抜粋
# Studio Domain
resource "aws_sagemaker_domain" "domain" {
domain_name = var.project_name
auth_mode = "IAM"
vpc_id = module.vpc_for_sagemaker.vpc_id
subnet_ids = [module.vpc_for_sagemaker.private_subnets[0]]
default_user_settings {
execution_role = aws_iam_role.sagemaker_execution_role_default.arn
}
}
CDK では第 3 引数以下に Domain の設定が入っています。ここでは、以下を指定しています。(Terraform の引数もほぼ一緒です)
認証方式は IAM (SageMaker Studio では AWS SSO も利用できる)
SageMaker Studio Domain 配下の User Profile が使用するデフォルトの Role は 別途作成した defaultRole
Domain の名前は別途 Name で定義した文字列
別途定義した VPC, Subnet 内に Domain を作成する
IAM は認証方式の指定、Domain の名前はただの文字列なので良いとして、ロールと VPC は別途定義しているのでコードを読んでみましょう。
2-5-2. Studio Domain に割り当てる defaultRole
Role を作成している部分は以下のコードです。iam.Role で作成できます。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// SageMaker Domain 用の default execution Role
const defaultRole = new iam.Role(this, 'DefaultRole', {
assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'),
],
})
Python
# case01/cdk-python/chococones/chococones.py より抜粋
# SageMaker Domain 用の default execution Role
default_role = iam.Role(self, "DefaultRole",
assumed_by=iam.ServicePrincipal("sagemaker.amazonaws.com"),
managed_policies=[
iam.ManagedPolicy.from_aws_managed_policy_name(
"AmazonSageMakerFullAccess"
),
],
)
Terraform
# SageMaker Domain 用の default execution Role
resource "aws_iam_role" "sagemaker_execution_role_default" {
name = "sagemaker-execution-role-default"
assume_role_policy = data.aws_iam_policy_document.sagemaker_assume_role.json
}
# IAM policy attach - Sagemaker権限の付与
resource "aws_iam_policy_attachment" "sagemaker_execution_default_role_full_access" {
name = "sagemaker-execution-default-role-full-access"
roles = [aws_iam_role.sagemaker_execution_role_default.name]
policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
depends_on = [aws_iam_role.sagemaker_execution_role_default]
}
data "aws_iam_policy_document" "sagemaker_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["sagemaker.amazonaws.com"]
}
}
}
SageMaker が引き受けられる role を作って、AmazonSageMakerFullAccess ポリシーをアタッチしているだけです。とても簡単ですね。
ただ気をつけていただきたい点が一点あります。ここから SageMakerFullAccess ポリシーをアタッチする箇所が何箇所ありますが、以下の権限を含むため、必要に応じて権限をカスタマイズしてください。
SageMaker や aws-glue といった名前などを含むバケットに対して読み書きが自由にできること
バケットを自由に作成できること (ただし読み書きには上記のバケット名の制限を受ける)
2-5-3. Studio Domain を配置する VPC/Subnet
VPC/Subnet にいたっては CDK だと 1 行です。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// Domain 用の VPC
const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 })
Python
# case01/cdk-python/chococones/chococones.py より抜粋
# Domain 用の VPC
vpc = ec2.Vpc(self, "vpc", max_azs=2)
Terraform
# VPC
module "vpc_for_sagemaker" {
source = "terraform-aws-modules/vpc/aws"
name = "${var.project_name}-${var.environment}-vpc-sagemaker"
cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1c"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = false
one_nat_gateway_per_az = false
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "vpc_for_sagemaker"
}
}
CDK の場合はこれだけで 2 つの Available Zone にまたがって public subnet が 2 つ、private subnet が 2 つ、natGateway とかも含めて自動で生成されます。これは ec2.Vpc のデフォルトの挙動であり、もちろんカスタマイズもできます。
マネジメントコンソールや Boto3 で作成すると、 create_vpc, create_subnet, create_nat_gateway… の API を必要な個数分叩く必要があるのに対する、CDK の大きなメリットですね!また、Terraform も CDK に比べると記述量は多いですが、AZ の指定や、サブネットなどをしっかり記述することで、厳格な運用ができます。
2-5-4. 登場人物毎のリソース
Studio Domain と関連するリソースが出来たのでここから 登場人物関連のコードを読んでいきましょう。
まず登場人物起点でどんなものを作らないといけないかを考えます。
作りたいアーキテクチャー (再掲)
まず、今回は工藤さんと津郷さんが登場し、工藤さんと津郷さんはそれぞれ IAM User でログインして、工藤さん用の User Profile、津郷さん用の User Profile、にそれぞれアクセスします。津郷さんは津郷さんの Bucket と共用 Bucket と共用リポジトリにフルアクセスでき、工藤さんも同様です。
これらを AWS のリソースに落とし込むと、各々の人物用の以下が必要です。
工藤さん、津郷さんが使用する SageMaker Sutdio のUser Profile
工藤さん、津郷さん用の IAM User
マネジメントコンソールから個人用と共用の S3 対して読み書きするポリシー作成とアタッチ
マネジメントコンソールから他人の S3 バケットに対して読み取りするポリシー作成とアタッチ
マネジメントコンソールから共用リポジトリに対して読み書きするポリシー作成とアタッチ
マネジメントコンソールから SageMaker の各操作ができるように SageMakerFullAccess ポリシーのアタッチ
お互いの User Profile にアクセスできないよう制限するポリシーの作成とアタッチ
工藤さん、津郷さんが使用する User Profile にアタッチするロール
SageMaker Studio から個人用と共用の S3 対して読み書きするポリシー作成とアタッチ
SageMaker Studio から他人の S3 バケットに対して読み取りするポリシー作成とアタッチ
SageMaker Studio から共用リポジトリに対して読み書きするポリシー作成とアタッチ
SageMaker Studio から SageMaker の各操作ができるように SageMakerFullAccess ポリシーのアタッチ
これらは 2 人分、人ごとに名前などが変わるだけの似たような処理を行うため、クラスを用意して楽をしましょう。
2-5-5. 登場人物をコードに起こす
まずは登場人物をインスタンスにします。
CDK では User クラスを定義することにしたので、User クラスを使って人ごとにインスタンス生成します。後の SageMaker Studio の User Profile を作るにあたって必要な SageMaker Studio Domain の識別子や、共通で設定するポリシーなどを引数に入れておきます。Terraform はリストとモジュールを使用し、プログラミングっぽく記述できます。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
const users: User[] = [
// ユーザーの作成
new User(this, 'Tsugo', {
name: 'tsugo',
domainId: domain.attrDomainId,
policy,
removalPolicy,
}),
new User(this, 'Kudo', {
name: 'kudo',
domainId: domain.attrDomainId,
policy,
removalPolicy,
})
]
Python
# case01/cdk-python/chococones/chococones.py より抜粋# 以下に使用する人分、User インスタンスを列挙する
users = [
# ユーザーの作成
User(self, "Tsugo",
name="tsugo",
policy=policy,
domain=domain,
),
User(self, "Kudo",
name="kudo",
policy=policy,
domain=domain,
),
]
Terraform
# case01/terraform/examples/variable.tf より抜粋
# Userを追加する時はここに追加する
variable "user_list" {
type = list(string)
default = ["tsugo", "kudo"]
}
# case01/terraform/examples/main.tf より抜粋
module "user" {
source = "../modules/iam/user"
for_each = toset(var.user_list)
aws_iam_username = "${var.project_name}-${each.value}"
aws_iam_grouppath = "/users/"
}
呉「ちょ、ちょっと待ってください。この書き方、CDK 歴 3 時間のワイくんには気持ち悪いんですが。」
工藤「というと ?」
呉「User クラスの呼び方が全く一緒なので、例えばリストとループを使って書かないんですか ?」
// 呉の思いつきの CDK TypeScript コード
const names = ['Tsugo', 'Kudo'];
for (const name of names) {
new User(this, name,{
name: name,
domainId: domain.attrDomainId,
policy,
removalPolicy,
})
}
呉「もっと言うならば、使用者のリストとか settings.yml 的な設定ファイルを外部に持ってそれを読んで管理したほうが・・・」
工藤「基本的には、ソースコードとしてベタ書きすることをおすすめしています。型を付けて書くことで、実行する前にエラーに気づくことができます。また、記述量を減らしたり最適化を優先するよりは、あえてシンプルな形で書くことを心がけています。気持ちとしては インフラの設定ファイルを書いている感じですね。」
呉「なるほど、自前メソッドをたくさん作ったりして上手くプログラミングするというより、CDK の API 一個一個を設定ファイルのように使う感じがいいんですね。 共通部分があったとしたら Class などでまとめて 1 つずつインスタンス生成する程度にするようなイメージですね。」
工藤「とはいえ、呉さんのリストと for を使った書き方も決して悪いことはなく、もしこれが 10 人とかのオーダーだったらこのような書き方をきっとしますね。」
2-5-6. 人ごとの IAM User 作成
さて、自作した User クラスからインスタンスを生成したので、User クラスの中身を見ていきましょう。
まずは工藤さんや津郷さんがマネジメントコンソールにログインするための IAM User の作成するところを見てみましょう。
TypeScript
// case01/cdk-typescript/lib/constructs/user.ts
// IAM ユーザー
this.user = new iam.User(this, 'User', {
userName: props.name,
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'),
iam.ManagedPolicy.fromAwsManagedPolicyName('IAMUserChangePassword'),
props.policy,
],
password: secret.secretValue,
passwordResetRequired: true,
})
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# IAM ユーザー
self.user = iam.User(self, "User",
user_name=name,
managed_policies=[
iam.ManagedPolicy.from_aws_managed_policy_name(
"AmazonSageMakerFullAccess"
),
iam.ManagedPolicy.from_aws_managed_policy_name("IAMUserChangePassword"),
policy,
],
password=self.secret.secret_value,
password_reset_required=True,
)
Terraform
# case01/terraform/modules/iam/user/iamuser.tf
resource "aws_iam_user" "iam-user" {
name = "${var.aws_iam_username}"
path = "${var.aws_iam_grouppath}"
}
# case01/terraform/examples/main.tf より抜粋
# IAM policy attach - Sagemaker権限の付与
resource "aws_iam_policy_attachment" "user_sagemaker_full_access" {
name = "user-sagemaker-full-access"
for_each = toset(var.user_list)
users = ["${var.project_name}-${each.value}"]
policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
depends_on = [module.user]
}
# IAM Userマネジメントコンソールログイン用プロファイル
resource "aws_iam_user_login_profile" "iam_login_profile" {
for_each = toset(var.user_list)
user = "${var.project_name}-${each.value}"
pgp_key = filebase64("../cert/terraform.public.gpg")
password_reset_required = true
password_length = "20"
depends_on = [module.user]
}
# IAM policy attach - パスワード変更権限の付与
resource "aws_iam_policy_attachment" "change_password_policy_attach" {
name = "change-password-policy-attach"
for_each = toset(var.user_list)
users = ["${var.project_name}-${each.value}"]
policy_arn = "arn:aws:iam::aws:policy/IAMUserChangePassword"
depends_on = [module.user]
}
CDK で IAM User を作成するときは、CDK の User クラスを使用します。ここでは以下を設定しています。
userName/user_name 引数で IAM User の名前を指定
ここで指定した名前でマネジメントコンソールにログインします。ここでは User クラスを呼び出した時の tsugo, kudo という値がそのまま引き継がれます。
managed_policies 引数でアタッチするポリシーを指定
ManagedPolicy クラスを使って 2 つのマネージドポリシーをアタッチしています。SageMaker を使うために AmazonSageMakerFullAccess を、マネジメントコンソールにログインするためのパスワードを変更できるようにIAMUserChangePassword をアタッチしています。また、Policy という自作のポリシーを最後にアタッチしています。policy の詳細は こちら をご参照ください。
password 引数で初期パスワードを設定します。パスワードは、AWS Secrets Manager というサービスを利用し、パスワードを乱数で生成しつつ、安全に格納します。こちらの中身も後ほど見てみましょう。
最後に password_reset_required 引数で初回ログイン時にパスワード変更を強制し、ユーザーにパスワードを設定してもらいます (ログインしてパスワードを変更してもらうことで、パスワードの管理を CDK の管理者の手から離す)。ちなみに IAMUserChangePassword ポリシーとセットで設定しないと、パスワード変更を強制されるのにパスワードを変更できない、という詰みの状態になります (我々が今回このコードを作るときにやからかしました)。
Terraform も設定も書き方が違うだけで、ほとんど同じですね。
2-5-7. 人ごとの User Profile 作成
さて、SageMaker Studio はユーザー (User Profile) を作成し、人ごとに独立した環境でノートブックを実行できる、とお伝えしました。その User Profile を作成しましょう。
TypeScript
// case01/cdk-typescript/lib/constructs/user.ts
// SageMaker User Profile
this.userProfile = new sagemaker.CfnUserProfile(this, 'UserProfile', {
domainId: props.domainId,
userProfileName: props.name,
userSettings: {
executionRole: this.role.roleArn,
},
})
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# SageMaker User Profile
self.user_profile = sagemaker.CfnUserProfile(self, "UserProfile",
domain_id=domain.attr_domain_id,
user_profile_name=name,
user_settings={
"executionRole": self.role.role_arn,
},
)
Terraform
# case01/terraform/examples/main.tf より抜粋
# SageMaker User Profile
resource "aws_sagemaker_user_profile" "sagemaker_studio_user" {
domain_id = aws_sagemaker_domain.domain.id
for_each = toset(var.user_list)
user_profile_name = "sagemaker-user-${each.value}"
user_settings {
execution_role = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/sagemaker-execution-role-${each.value}"
}
}
CDK での User Profile の作成は CfnUserProfile クラスを使います。ここでは以下を設定しています。
domainId/domain_id で User Profile を作成するSageMaker Studio の Domain を指定
定義済の SageMaker Studio のドメインから attr_domain_id というプロパティから ID を取得できます。
userProfileName/user_profile_name で作成する User Profile の名前を指定
マネジメントコンソールから SageMaker Studio を使う際に、個人用の User Profile を選択(tsugo, kudo) する際に使用します。
userSettings/user_settings で User Profile が使用するロールを設定
SageMaker Studio から S3 や CodeCommit などにアクセスしますが、その時に使用する IAM Role を指定します。使用する IAM Role についての詳細は こちら を参照してください。
Terraform も使用している引数はほぼ一緒ですね。一箇所 for_each を使っているところがありますが、Terraform ではクラスを使わずリストでやっているため、ここにユーザー名を使っています。
2-5-8. User Profile に設定する IAM Role 作成
さて、作成した User Profile で作業ができるようにするために、 IAM Role を作成します。
TypeScript
// case01/cdk-typescript/lib/constructs/user.ts
// ユーザー専用の execution Role
this.role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('sagemaker.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSageMakerFullAccess'),
props.policy,
],
})
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# SageMaker User Profile# ユーザー専用の execution Role
self.role = iam.Role(self, "Role",
assumed_by=iam.ServicePrincipal("sagemaker.amazonaws.com"),
managed_policies=[
iam.ManagedPolicy.from_aws_managed_policy_name("AmazonSageMakerFullAccess"),
policy,
],
)
Terraform
# case01/terraform/examples/main.tf より抜粋
# SageMaker Domain 用の default execution Role
resource "aws_iam_role" "sagemaker_execution_role_default" {
name = "sagemaker-execution-role-default"
assume_role_policy = data.aws_iam_policy_document.sagemaker_assume_role.json
}
# IAM policy attach - Sagemaker権限の付与
resource "aws_iam_policy_attachment" "sagemaker_execution_default_role_full_access" {
name = "sagemaker-execution-default-role-full-access"
roles = [aws_iam_role.sagemaker_execution_role_default.name]
policy_arn = "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess"
depends_on = [aws_iam_role.sagemaker_execution_role_default]
}
data "aws_iam_policy_document" "sagemaker_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["sagemaker.amazonaws.com"]
}
}
}
IAM Role の作成は Role クラスを使います。ここでは以下を設定しています。
assumedBy/assumed_by で作成した IAM Role を SageMaker サービスが引き受けられるようにする
managedPolicies/managed_policies 引数でアタッチするポリシーを指定
IAM User と同じように AmazonSageMakerFullAccess と、自作のポリシーである policy をアタッチしています。 policy の詳細は こちら をご参照ください。
Terraform も大きな違いはなく、作成したロールが SageMaker が引き受けられるようにしたり、AmazonSageMakerFullAccess をアタッチしています。
2-5-9. IAM User, IAM Role に共通でアタッチした policy とは ?
IAM User と IAM Role で managed_policy 引数の最後に入れてあった policy が何者なのか見てみましょう。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// 全員の IAM User・IAM Role につける共用 Policy
const policy = new iam.ManagedPolicy(this, 'SharedPolicy')
// 共用 CodeCommit リポジトリへの読み書き許可
policy.addStatements(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['codecommit:*'],
resources: [repository.repositoryArn],
})
)
Python
# case01/cdk-python/chococones/çhococones.py より抜粋
# 全員の IAM User・IAM Role につける共用 Policy
policy = iam.ManagedPolicy(self, "SharedPolicy")
# 共用 CodeCommit リポジトリへの読み書き許可
policy.add_statements(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=["codecommit:*"],
resources=[repository.repository_arn],
)
)
Terraform
# case01/terraform/examples/main.tf より抜粋
# IAM policy document for access shared CodeCommit
data "aws_iam_policy_document" "codecommit_access" {
statement {
actions = ["codecommit:*"]
resources = ["arn:aws:codecommit:::${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${var.project_name}-${var.environment}-repo*"]
effect = "Allow"
}
}
# IAM policy for access shared CodeCommit
resource "aws_iam_policy" "allow_codecommit_access" {
name = "allow-access-codecommit-repository"
policy = data.aws_iam_policy_document.codecommit_access.json
}
ManagedPolicy クラスを使って自作のポリシーを作成しています。ポリシーに与える権限として、add_statements メソッドを使ってステートメントを追加しています。追加するステートメントは PolicyStatement クラスを使って、作成した CodeCommit のリポジトリに対して全てのアクションが行えるようにしています。この policy を IAM User, IAM Role にアタッチすることによって、津郷さんや工藤さんが、マネジメントコンソールからだろうと SageMaker Studio からだろうと CodeCommit のリポジトリを操作できるようになります。
Terraform も CDK のコードを読めれば理解できるレベルの違いしかないですね。
ここでは工藤さん、津郷さんの CodeCommit に対する権限を同じように扱っていますが、人によって変えたい場合は必要に応じて修正してください。特にここでは CodeCommit に対するアクションを全許可していますが、破壊的な作業を全員にしてほしくない場合はアクションを制限してください。(ただし操作できるリソースは今回作ったリポジトリに限られる)
2-5-10. お互いに自分の User Profile しか使えないように制限するポリシー
津郷さんが工藤さんの User Profile を、工藤さんが津郷さんの User Profile を使えたら環境を分離した意味がありませんので制限をかけるポリシーを作成します。ちなみにここまでで定義済の IAM User にすでに AmazonSageMakerFullAccess をアタッチしているので、全ての User Profile にアクセスできるようになっています。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// 自分以外の UserProfile へのアクセスを禁止
const statement = new iam.PolicyStatement({
effect: iam.Effect.DENY,
actions: [
'sagemaker:CreatePresignedDomainUrl',
'sagemaker:DescribeUserProfile',
],
notResources: [
this.userProfile.attrUserProfileArn,
],
})
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋# 自分以外の UserProfile へのアクセスを禁止
# 自分以外の UserProfile へのアクセスを禁止
self.deny_other_user_profile = iam.PolicyStatement(
effect=iam.Effect.DENY,
actions=[
"sagemaker:CreatePresignedDomainUrl",
"sagemaker:DescribeUserProfile",
],
not_resources=[
self.user_profile.attr_user_profile_arn
],
)
Terraform
# case01/terraform/examples/main.tf より抜粋
# IAM policy - 個人UserProfile用
resource "aws_iam_policy" "user_access_only_own_profile" {
for_each = toset(var.user_list)
name = "user-access-only-own-profile-${each.value}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"license-manager:ListReceivedLicenses"
],
"Effect": "Allow",
"Resource": ["*"]
},
{
"Action": [
"sagemaker:CreatePresignedDomainUrl"
],
"Effect": "Allow",
"Resource": ["arn:aws:sagemaker:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:user-profile/${aws_sagemaker_domain.domain.id}/sagemaker-user-${each.value}"]
},
{
"Action": [
"sagemaker:CreatePresignedDomainUrl"
],
"Effect": "Deny",
"NotResource": ["arn:aws:sagemaker:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:user-profile/${aws_sagemaker_domain.domain.id}/sagemaker-user-${each.value}"]
}
]
}
EOF
}
既出の PolicyStatement クラスを使います。PolicyStatement は許可だけでなく拒否も表現できます。effect 引数に拒否を設定するだけです。SageMaker Studio を使う、という行為は sagemaker:CreatePresignedDomainUrl というアクションに相当するので、action 引数に指定します。また、他の人に設定が見られないよう、DescribUserProfile アクションも追加します。最後に notResources/not_resource 引数で自分の User Profile を指定することで、自分の User Profile だけは見られるようにします。
Terraform については一気に書いているので、次の項目 (自分の User Profile を確認しているときのエラー防止ポリシー) の内容まで書かれていますが、同じことを json の中で記述しています。というわけで早く次に行きましょう。
2-5-11. 自分の User Profile を確認しているときのエラー防止ポリシー
さて、SageMaker で R 言語を利用する こともできます。R 言語を使用するには AWS License Manager でライセンスを管理する必要があります。
もちろん今回は R 言語を利用しませんが、 AWS License Manager の権限がないとマネジメントコンソールでエラーが表示されてしまいます。エラーが出ていても実用上問題ないのですが、エラーを表示させっぱなしも気持ち悪いので、しかるべきポリシーを作成しておきます。
AWS License Manager のエラー
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// User Profile 設定画面でライセンスマネージャーを見られるようにしてエラーを防ぐ
const allowListRecievedLicenses = new iam.PolicyStatement({
effect: iam.Effect.DENY,
actions: [
'license-manager:ListReceivedLicenses'
],
resources: ['*'],
})
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# User Profile 設定画面でライセンスマネージャーを見られるようにしてエラーを防ぐ
self.allow_list_recieved_licenses= iam.PolicyStatement(
effect=iam.Effect.ALLOW,
actions=[
"license-manager:ListReceivedLicenses"
],
resources=["*"],
)
Terraform
(前の項に記載済)
2-5-12. 作成したポリシーを IAM User にアタッチする
さて、今まで作った User Profile がらみのポリシーをアタッチしましょう。User Profile を使うのは IAM User ですので、IAM User に対してアタッチします。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// 作成したポリシーを IAM User にアタッチする
this.user.attachInlinePolicy(
new iam.Policy(this, 'UserPolicy', {
statements: [
denyOtherUserProfile,
allowListRecievedLicenses,
],
})
)
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# 作成したポリシーを IAM User にアタッチする
self.user.attach_inline_policy(
iam.Policy(self, "CustomPolicy",
statements=[
self.deny_other_user_profile,
self.allow_list_recieved_licenses,
],
)
)
Terraform
# case01/terraform/examples/main.tf より抜粋
# IAM policy attach - 個人User用に個人UserProfile用policyのattache
resource "aws_iam_policy_attachment" "access_own_profile" {
name = "access-own-profile"
for_each = toset(var.user_list)
users = ["${var.project_name}-${each.value}"]
policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/user-access-only-own-profile-${each.value}"
depends_on = [aws_iam_policy.user_access_only_own_profile, module.user]
}
User クラスのメソッドに attach_inline_policy というメソッドがあり、そこに Policy クラスに作成したポリシー 2 つを渡せばポリシーがアタッチできます。
2-5-13. 個人用のバケット作成と読み書き権限の設定
さて、個人が実験などをするような専用のバケットを用意します。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// 個人バケット
this.bucket = new s3.Bucket(this, 'Bucket', {
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
encryption: s3.BucketEncryption.S3_MANAGED,
enforceSSL: true,
removalPolicy: props.removalPolicy,
})
// 持ち主は読み書きできる
this.bucket.grantReadWrite(this.user)
this.bucket.grantReadWrite(this.role)
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# 個人バケット
self.bucket = s3.Bucket(self, "Bucket",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
encryption=s3.BucketEncryption.S3_MANAGED,
enforce_ssl=True,
removal_policy=RemovalPolicy.DESTROY,
)
# 持ち主は読み書きできる
self.bucket.grant_read_write(self.user)
self.bucket.grant_read_write(self.role)
Terraform
# case01/terraform/examples/main.tf より抜粋
# S3 Bucket - 個人用
resource "aws_s3_bucket" "bucket_users" {
for_each = toset(var.user_list)
bucket = "${var.project_name}-${var.environment}-bucket-${each.value}"
tags = {
Name = "${var.project_name}-${var.environment}-bucket-${each.value}"
}
}
# S3 access policy - 個人用
resource "aws_s3_bucket_public_access_block" "bucket_users_public_access_block" {
for_each = toset(var.user_list)
bucket = "${var.project_name}-${var.environment}-bucket-${each.value}"
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# IAM policy - 個人バケットアクセス用
resource "aws_iam_policy" "allow_access_bucket_own" {
for_each = toset(var.user_list)
name = "allow-access-bucket-own-${each.value}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": ["arn:aws:s3:::${var.project_name}-${var.environment}-bucket-${each.value}",
"arn:aws:s3:::${var.project_name}-${var.environment}-bucket-${each.value}/*"]
}
]
}
EOF
}
# IAM policy attach - 個人UserProfile用に個人バケットアクセス用Policyのattache
resource "aws_iam_policy_attachment" "sagemaker_access_own_bucket" {
name = "sagemaker-bucket-own-access"
for_each = toset(var.user_list)
roles = ["sagemaker-execution-role-${each.value}"]
policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/allow-access-bucket-own-${each.value}"
depends_on = [aws_iam_role.sagemaker_execution_role_indivisual, aws_iam_policy.allow_access_bucket_own]
}
# IAM policy attach - 個人Userに個人バケットアクセス用Policyのattache
resource "aws_iam_policy_attachment" "user_access_own_bucket" {
name = "user-bucket-own-access"
for_each = toset(var.user_list)
users = ["${var.project_name}-${each.value}"]
policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/allow-access-bucket-own-${each.value}"
depends_on = [module.user, aws_iam_policy.allow_access_bucket_own]
}
バケットの作成は既出ですが、 s3.Bucket を使います。特徴的なのは、読み書きの権限の付与です。User Profile へのアクセスのようにポリシーを書いてもいいのですが、s3.Bucket クラスは grant_read_write メソッドを持っており、IAM User や IAM Role を引き渡すと読み書きができるようになります。grant_read_write と似たようなメソッドの codecommit.Repository クラスも持っていますが (grand_pull_push)、人ごとの制御は不要で全員が pull/push するので 自作の policy でアタッチしています。
Terraform は grant_read_write 相当のものがないため、地道にポリシーを作成してアタッチする、ということを行っています。
2-5-14. IAM User のパスワード生成と連携
さて、自作の User クラスはほぼ見終わったのですが、一箇所だけIAM User のパスワードをスルーしていました。
ログインパスワードは秘匿情報なので、AWS Secrets Manager という秘匿情報を扱うサービスを利用してセキュアに扱います。そして、CDK を扱う人はログインパスワードをSageMaker Studio を使う人に伝える必要があります。
いくつかやり方がありますが、ここでは Secrets Manager に登録したパスワードの名前を出力し、パスワードの名前をもとに CDK 実行者がパスワードを取得して SageMaker Studio を扱う人に連携することとします。SageMaker Studio を使う人はログインする際に強制的にパスワードを変更させられるため、それをもって CDK でスタックを作成した人の手を離れる、とします。
TypeScript
// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// IAM User 用のパスワードを Secrets Manager で生成
const secret = new secretsmanager.Secret(this, 'Secret')
// Secrets Manager の名前を出力する
new cdk.CfnOutput(this, 'SecretName', {
value: secret.secretName,
})
Python
# case01/cdk-python/chococones/constructs/user.py より抜粋
# IAM User 用のパスワードを Secrets Manager で生成
self.secret = secretsmanager.Secret(self, 'Secret')
# Secrets Manager の名前を出力する
CfnOutput(self, "Password",
value=self.secret.secret_name
)
Terraform
# (再掲)
# case01/terraform/examples/main.tf より抜粋
# IAM Userマネジメントコンソールログイン用プロファイル
resource "aws_iam_user_login_profile" "iam_login_profile" {
for_each = toset(var.user_list)
user = "${var.project_name}-${each.value}"
pgp_key = filebase64("../cert/terraform.public.gpg")
password_reset_required = true
password_length = "20"
depends_on = [module.user]
}
Secrets クラスを使うことでパスワードを生成し Secrets Manager に格納することができます。さらに CfnOutput クラスを使うことで CDK の実行が終わった後、CDK の実行環境や AWS CloudFormation に出力することができます。
Terraform では IAM User を作成するときに gpg を利用してパスワードを格納した暗号化したファイルを出力し、復号化したパスワードをコンソールに出力しています。
CDK を実行したコンソールでの出力例
Outputs:
Chococones.KudoPassword57502016 = KudoSecretE3E97241-toH17eJ00Gdb
Chococones.TsugoPassword14E115E5 = TsugoSecret31EAF625-isqFzPIWR5dH
Terraform を実行したコンソールでの出力例
aws_iam_user_password = {
"chococones-gokazu-kudo" = "XXXX"
"chococones-gokazu-tsugo" = "XXXX"
}
CloudFormation での出力例