CHOCOCONES as Code 爆誕 !?

「たけのこの里」の分析環境をコードで制御する

2022-11-02
日常で楽しむクラウドテクノロジー

Author : 呉 和仁, 工藤 朋哉, 津郷 光明

どうも、機械学習ソリューションアーキテクトの呉 (@kazuneet) です。たけのこの里大好きコンテンツとして、

物理的に分類するデバイスを作ってもらったり

と、いろいろネタを引っ張ってきました。

結果、たけのこの里が好きなソリューションアーキテクト達の布教活動により、お客様にも実際に試してもらい、お客様もたけのこの里が好きになっている、という声が漏れ聞こえており、大変うれしく思っております。まれにきのこの山が好きなお客様から、学習データと称して大量にきのこの山を頂いたこともありましたが、そちらもおいしく頂いたことをこの紙面をお借りして御礼を申し上げます。

一方で、きのこの山とたけのこの里の物体検出モデルを作ったり、デバイスにデプロイするアプリを作る環境として、Amazon SageMaker Studio を使っていましたが、SageMaker Studio の環境立ち上げについては今までノータッチで、お客様から「どうやるんじゃ !!! 🤬🤬」とお叱り ( ? ) を受けることもありました。

そこで今回は SageMaker Studio の環境構築にフォーカスします。実は環境構築をするだけならそんなに難しくなくマネジメントコンソールをポチポチすれば立ち上がる のですが業務利用する場合には、SageMaker Studio を使う人の増減対応や、閉域網で利用したい、などの要件が入ってきて、アーキテクティングをしっかりしないと運用が回らなくなってしまうことがあります。

そこで、今回は SageMaker Studio を中心とした分析環境をコードで管理 (Infrastructure as Code, IaC) して運用を楽にすることにチャレンジしてみましょう。たけのこの里の分析環境なので、CHOCOCONES as Code と言っても過言ではありませんね !

※マメ知識:アメリカではたけのこの里はCHOCOCONESという商品名で売っていたそうです (現在終売)。

しかし、私は環境をコードで管理することには疎いので、強力な助っ人に全部丸投・・・ヘルプを頼みました。プロトタイプエンジニアの工藤さんとソリューションアーキテクトの津郷さんです ! (パチパチパチパチ)

津郷「どうもー」

工藤「よろしくお願いしますー」

「津郷さんから自己紹介をお願いします。」

津郷「製薬企業のお客様向けに AWS を活用した研究・ビジネスの推進をサポートしています。IaC は前職でスマホアプリのバックエンドを AWS で構築した際、自分で意図した構成が自動で構築でき、スクラップ&ビルドできる爽快感がたまらなくてかっ○えび○んみたいにやめられない止まらないんです !」

呉 (いや、今日はたけのこの里推しなんですけどね、という顔をしている)

津郷「AWS だけじゃなく Kubernetes やその他製品も扱える Terraform を使った IaC が得意 & オススメです。よろしくお願いします。」

「ありがとうございます。続いて工藤さんお願いします。」

工藤「私はいろいろな分野のお客様と一緒に、プロトタイプを開発することで、課題を解決したりプロジェクトを加速するサポートをしています。実装したプロトタイプは最終的にお客様の環境で動かしていただく必要があるので IaC が必要で、だいたいいつも CDK で書いてますね。個人的にも CDK は大好きです !」

CDK がなんなのか私にはまだわかりませんが、そのうち詳しい説明をしてくれるでしょう。よろしくお願いします !」


さて、早速やっていきましょう。この記事では実際にお客様から伺った要件を切り出して、ケースごとにどうすれば CHOCOCONES as Code が実現できるのか、フィクションの対談形式で呉が何か余計なことをしてそうな雰囲気は出しつつ、工藤さんに CDK の実装を、津郷さんに Terraform での実装を、手をスリスリしながら作ってもらいました。

この連載記事のその他の記事はこちら

選択
  • 選択
  • たけのこの里が好きな G くんのために、きのこの山を分別する装置を作ってあげた。~モデル作成編~
  • たけのこの里が好きな G くんのために、きのこの山を分別する装置を作ってあげた。~分別装置作成編~
  • CHOCOCONES as Code 爆誕 !?「たけのこの里」の分析環境をコードで制御する
  • 「きのこの山」を閉域に閉じ込めてみた ~Amazon SageMaker Studio をよりセキュアに IaC する方法

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


1. Amazon SageMaker Studio とは ?

さて、対談を始める前に Amazon SageMaker Studio を簡単に紹介いたします。

Amazon SageMaker Studio とは、Jupyter Lab を AWS が独自に拡張させた、機械学習に特化した Web ベースの統合開発環境 (IDE) です。Jupyter Lab を拡張しているので、ノートブック(.ipynb ファイル) を実行するだけでなく、Amazon SageMaker の各種機能 (Amazon SageMaker Pipelines, Amazon SageMaker Model Registry, Amazon SageMaker JumpStart, etc)を Jupyter Lab の UI から利用できる便利なものです。が、ここでは一旦ノートブックを実行することにフォーカスします。

SageMaker Studio はリージョンに対して 1 つのドメインを立ち上げ、ドメインに対して SageMaker 上の User (User Profile といいます。IAM User とは別です) を追加していくことで多人数で利用することができます。User Profile それぞれに IAM ロールをアタッチすることで AWS らしく厳格な権限の運用ができます。

例えば、A さんはたけのこの里の画像だけが入った Amazon S3 バケットの読み書き、B さんはきのこの山の画像だけが入った S3 バケットを読み取りだけできる、といった細やかな制御が可能 (もちろん S3 以外のリソースも同様に制御可能) です。User はそれぞれ専用の Amazon EFS の環境が使え、S3 を介して他の User と協力して作業する、といったこともできます。

ノートブックを動かす際は、ノートブック毎に Jupyter のカーネルやインスタンスタイプを選択することができ、GPU インスタンスで PyTorch を使って学習したいとか、Scikit-Learn を用いて CPU インスタンスで前処理したい、といったことを柔軟に扱うことができます。

さて、ここまで SageMaker Studio は便利で、また細やかなユーザーのアクセス制御ができるといったお話をしてきましたが、一方で便利で多機能なものはどう運用すれば良いのかわからなくなる、という話もあります。ここから SageMaker Studio の使い方をケースごとに紹介していきます。まずはベーシックな使い方について、会話を通して紹介いたします。


2. ケース 1 : Amazon SageMaker Studio の環境をコードで作る

2-1. IaC 実装、の前に IaC の大切さ

「まずは初歩中の初歩、Amazon SageMaker Studio をコードで作る、からお願いします」

津郷「ヘイ」

「つってもこれくらいなら私でもできますよ。これでいいんでしょう ?」

import boto3
sm_client = boto3.client('sagemaker')
sm_client.create_domain(…)
…

AWS SDK for Python (Boto3) でゴリ押ししようとするコード

津郷「・・・それはそうなんですが、それだと冪等性がないですよね ?」(IaC を理解していないなコイツという顔をしている)

「冪等性・・・?(なんて読むんだ ? 箒 (ほうき) とはまたちょっと違うぞ・・・?)」

津郷「語りだすと長いのですが、冪等性 (べきとうせい) とは、ざっくり言うと何回実行しても同じ結果を得られることで、IaC で実現したいことの 1 つです。例えばこのコード、初回実行ならいいですが、二回目はすでにドメインが作られているのでエラーで落ちてしまいます。」

「それならドメインの有無を確認して、なければ作る、あれば作らない的なコードを書きますよ・・・」

津郷「そのように AWS のリソースの状態 (この場合は Studio のドメインの有無) を気にしてコードを書くと複雑になりがちです。リソースの状態だけでなく、途中で失敗したときの再開や、前の状態にロールバックしたり…と考えなければならないことが爆発的に増えます。IaC ツールは、人があるべき状態をコードに宣言するだけで、その複雑な部分を代わりにやってくれます。その結果、人はより本質的なあるべき状態を考えることだけに注力できます。」

「ほぉ・・・、ちなみに冪等性以外で IaC で他に実現したいことって何がありますか ?」

津郷「こちらの図にまとまってます。」

クリックすると拡大します

「コスト削減にある “手順書の作成” なんていいですね。過去にプリントスクリーンを連打しながらエクセルにペタペタ貼っていたあの作業について dis られたのがわかります。」

プリントスクリーン連打の例

津郷「そんなことは言っていません。」

「“同じ構成を何度でもデプロイ” もいいですね。開発、ステージング、プロダクションで統一したコードで実行できると負荷なく頻繁にデプロイできそうで嬉しいです。」

2-2. IaC で構築するアーキテクチャー

津郷「話を戻しますが、単に Amazon SageMaker Studio を立ち上げるだけでなく他に頻出要件ありますよね ?」

「そうでしたそうでした。人の増減とかもいい感じにコードで制御したいんですよね」

津郷「いい感じ・・・(エンジニアとは思えないという顔をしている)。権限とかの話もあるので、ケース 1 は例えば以下のようにして、実装しながら有ったほうが良さそうな機能を追加していきます。」

初めて IaC と SageMaker Studio を使う組織を対象とする。
現状 SageMaker Studio を使うのは工藤、津郷の二人で、それぞれが協力して、
きのこの山とたけのこの里を判別する機械学習のトレーニングコードを作る。
トレーニングコードを管理する Git リポジトリも AWS で用意する。
データは S3 に置くが、S3 についてはお互いが読み書きできるバケットと、
自分だけが書き込める場所(読み取りは可能)をそれぞれ用意したい。
IaC を実行するのは管理者の皮をかぶった津郷。

「(できる人だ・・・!)」

津郷「というわけで必要なアーキテクチャーはこんな感じですかね。」

作りたいアーキテクチャー

津郷「あとは上記を実現するのに必要な AWS リソース (ポリシーなど) は随時追加する感じで。」

「お願いします。」

津郷「というわけで CDK はお願いします、工藤さん。」

「ファッ !?」

工藤「どうも~」

津郷「工藤さんは CDK で、私は Terraform でそれぞれ実装しますので、CDK ユーザーにも Terraform ユーザーにも使える記事にします。まずは TypeScript を使用した CDK で早速作ってもらいます。」

「よくわからないけどスゴイ ! CDKってなんですか !? (もちろん Terraform もわからない顔をしている) CHOCOCONES Development Kit ですか ?」

工藤「たけのこの里開発キットではないです。AWS Cloud Development Kit の略で、プログラミング言語から AWS のリソースを宣言的に操作できます。CDK のインターフェースとして使える言語は、TypeScript や Python などがあります。まずは作ってモノを見てもらうのが早いかと思います。まずは私が得意な TypeScript で作ってみましょう。」

「(TypeScript 読めないけど) オナシャッス。ついでに私も Python で書いてみようかしら。」

(3 分後)

工藤「できました。」

津郷「相変わらずの速さですね。早速実行してみましょう。」

2-3. IaC 実行環境準備

さて、作成した IaC を実行するにあたって、多少の環境セットアップが必要です。CDK ないし Terraform をインストールする必要があるのと、AWS に接続できる環境である必要があります。今回は、CDK については Amazon SageMaker Studio Lab から実行することとします。

Amazon SageMaker Studio Lab は機械学習の勉強に最適な無料で使えるコンピューティング環境です。ペルソナとしては、「機械学習は完全に理解したぜ ! これからうちの会社でみんなで機械学習をやるためにセキュアでスケーラブルな環境を IaC でデプロイしてみんなの役に立ってやるぜ !」という感じでしょうか。SageMaker Studio Lab の詳細は こちらの記事 が詳しいです。

とはいえ、手元のノート PC や、Amazon EC2, AWS Cloud9, Amazon SageMaker Notebook といった環境でもどこでも問題ないので、皆様に合った環境から実行してください。

Terraform については Amazon SageMaker Studio Lab にインストールする方法が見つけられなかったので、諦めて手元の Mac で実行することとします。ただし、手元のWin PC や EC2, Cloud9 などから実行することはできますので、インストール作業は各種読み替えて実行してください。詳細は こちら です。

また前提として、IaC は AWS の様々なサービスやリソースに触る都合上、管理者権限相当の AdministratorAccess ポリシーがアタッチされた IAM User が発行してあり、Access Key と Secret Access Key を入手してあることを前提とします。

さて、実行環境の準備をしてみましょう。まずは、Studio Lab の準備です。

https://studiolab.sagemaker.aws/ にアクセスし、右上の「Sign in」をクリックします。アカウントが無い場合は「Request Account」からアカウント申請をしてください。

クリックすると拡大します

ユーザー名とパスワードを入力して「Sign in」をクリックします。

クリックすると拡大します

Compute type が CPU であることを確認して、「Start runtime」をクリックします。

クリックすると拡大します

しばらくすると「Open Project」ボタンが活性化されるので、クリックします。

クリックすると拡大します

Jupyter の画面が開くので、「Terminal」をクリックします。

クリックすると拡大します

Terminal が開いてコマンド入力待機状態になります。

クリックすると拡大します

以下のコマンドを打ちこみます。

CDK を使う場合

 # conda の仮想環境作成
$ conda create -n cdk -y
$ # 作成した仮想環境をアクティベート
$ conda activate cdk
$ # nodejs をインストール
$ conda install -c conda-forge nodejs -y 
$ # CDK をインストール
$ npm install -g aws-cdk
$ # リポジトリをクローン
$ git clone https://github.com/aws-samples/aws-ml-jp
$ # AWS CLI の設定
$ aws configure
(アクセスキーやシークレットを入力)

Terraform を使う場合

$ # GPG のインストール
$ brew install gpg
$ # Terraform のインストール
$ brew install terraform
$ # リポジトリをクローン
$ git clone https://github.com/aws-samples/aws-ml-jp
$ # AWS CLI の設定
$ aws configure
(アクセスキーやシークレットを入力)

※1 CDK は Studio Lab を使っているので conda の仮想環境を使っていますが、venv など、環境に合わせてご使用ください。
※2 各種 インストール も conda install を使っていますが、環境に合わせて brew や yum, apt, などを使ってインストールしてください。CDK のインストールについての詳細は こちら を参照してください。

CDK TypeScript の場合

$ cd sagemaker/sagemaker-studio/IaC/case01/cdk-typescript
$ # 依存ライブラリインストール
$ npm install
$ # cdk の設定
$ cdk bootstrap
$ # デプロイ
$ cdk deploy
(前略)
✅  ChococonesStack
✨  Deployment time: 279.36s
Outputs:
ChococonesStack.KudoSecretNameXXXXXXXX = KudoSecretXXXXXXXX-XXXXXXXXXXX
ChococonesStack.TsugoSecretNameXXXXXXXX = TsugoSecretXXXXXXXX-XXXXXXXXXXX
Stack ARN:
arn:aws:cloudformation:{REGION}}:{ACCOUNT}:stack/ChococonesStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
✨  Total time: 288.84s

CDK Python の場合

$ cd sagemaker/sagemaker-studio/IaC/case01/cdk-python
$ # 依存ライブラリインストール
$ mkdir lib 
$ pip install -r requirements.txt -t /lib
$ # インストールディレクトリにパスを通す
$ export PYTHONPATH=$PWD/lib
$ # cdk の設定
$ cdk bootstrap
$ # デプロイ
$ cdk deploy 
(前略)
✅  ChococonesStack
✨  Deployment time: 284.47s
Outputs:
ChococonesStack.KudoSecretNameXXXXXXXX = KudoSecretXXXXXXXX-XXXXXXXXXXX
ChococonesStack.TsugoSecretNameXXXXXXXX = TsugoSecretXXXXXXXX-XXXXXXXXXXX
Stack ARN:
arn:aws:cloudformation:{REGION}}:{ACCOUNT}:stack/ChococonesStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
✨  Total time: 293.58s

Terraform の場合

$ cd sagemaker/sagemaker-studio/IaC/case01/terraform
$ # 鍵保管用のディレクトリ作成
$ mkdir ./cert
$ # 鍵生成
$ gpg --gen-key
(名前を問われるので terraform,メールアドレスは任意のアドレスを入力する)
$ # 鍵のエクスポート
$ gpg -o ./cert/terraform.public.gpg  --export terraform
$ gpg -o ./cert/terraform.private.gpg --export-secret-key terraform
$ # Terraform 実行準備
$ cd examples
$ terraform init
$ # Terraform 実行
$ # project name は世界でユニークなものを設定してください。筆者が使っているのでおそらく使えません。
$ terraform apply -var='project_name=chococones' -auto-approve
(前略)
Apply complete! Resources: 65 added, 0 changed, 0 destroyed.
(後略)

「え、これだけの操作でリソースが立ち上がるんですか・・・? またまたご冗談を・・・。」

工藤「いいから確認してください。」

「そんなに言うなら・・・」

クリックすると拡大します

「ほんとだ・・・! これが IaC のチカラ・・・! できることはわかったんですが何が起きたのかわかりません。」

工藤「コードを読んでください。」

2-4. どんなことができるのかを把握する

「コードを読め」と突き放されてしまったので、まずはコードを断片から理解してみます。一番理解しやすそうなところをはどこでしょうか・・・。みんなで利用する共用 バケットと共用リポジトリの記述を見てみましょう。

※TypeScript と Python は極力実装を合わせていますが、実装者が違うのと、言語の違いで多少ずれています。Terraform については全く違うツールで大きく異なりますが、同じ視点からコードと解説を入れていきます。

TypeScript

// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
// 共用 Bucket
const sharedBucket = new s3.Bucket(this, 'SharedBucket', {
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  encryption: s3.BucketEncryption.S3_MANAGED,
  enforceSSL: true,
  removalPolicy,
})
// 共用リポジトリ
const repository = new codecommit.Repository(this, 'Repository', {
    repositoryName,
})

Python

# case01/cdk-python/chococones/chococones.py より抜粋
# 共用 Bucket
shared_bucket = s3.Bucket(self, "shared_bucket",
    block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
    removal_policy=removal_policy,
    enforce_ssl=True,
    removal_policy=removal_policy,
)
# 共用リポジトリ
repository = codecommit.Repository(self, "Repository",
    repository_name=REPOSITORY_NAME,
)

Terraform

# case01/terraform/examples/main.tf より抜粋
# S3 Bucket - 共通  
resource "aws_s3_bucket" "bucket_shared" {
  bucket = "${var.project_name}-${var.environment}-bucket-shared"
}
# CodeCommit
resource "aws_codecommit_repository" "repository" {
  repository_name = "${var.project_name}-${var.environment}-repo"
}

CDK の場合は s3.Bucket , codecommit.Repository を呼び出すだけで津郷と工藤が共用で使う S3 のバケットと CodeCommit の git リポジトリが定義できます。

S3 は引数で、パブリックアクセスの拒否、削除時の挙動なども簡単にコントロールすることができます。CodeCommit のリポジトリについては名前を指定しているだけです。

Terraform の場合は variable.tf で定義された変数を参照しているものの、似たような感覚でバケットやリポジトリを定義しています。

このようにコードで AWS のリソースを定義し、IaC のコードをどこで実行しても、何度実行しても、同じ結果を得られるというのは便利ですね !

2-5. コードを理解する

さて、どんな感じにできるかがわかったので、コードを理解しましょう。

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 での出力例

クリックすると拡大します

2-6. 共用バケットと他人のバケットの権限

さて、最後に共用バケットと他人のバケットに対する権限を設定します。共用バケットについては全員が読み書きでき、他人のバケットは読めるだけ、としました。

CDK ではプログラムっぽくループ処理で書きました。Terraform はもともとユーザーをリストで回しているので、そもそも少しプログラムっぽいですが、grant が使えない分少しコード記述量が多いです。

TypeScript

// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts
users.forEach((user) => {
    // 共用バケットへの読み書き権限
    sharedBucket.grantReadWrite(user.user)
    sharedBucket.grantReadWrite(user.role)

    users.forEach((bucketOwner) => {
        // 他のユーザーのバケットへの読み込み権限
        bucketOwner.bucket.grantRead(user.user)
        bucketOwner.bucket.grantRead(user.role)
    })
})

Python

# case01/cdk-python/chococones/chococones.py より抜粋
for user in users:
    # 共用バケットへの読み書き権限
    shared_bucket.grant_read_write(user.user)
    shared_bucket.grant_read_write(user.role)

    # 他のユーザーのバケットへの読み込み権限
    for bucket_owner in users:
        bucket_owner.bucket.grant_read(user.user)
        bucket_owner.bucket.grant_read(user.role)

Terraform

# IAM policy for access shared bucket
data "aws_iam_policy_document" "bucket_shared_access" {
  statement {
    actions = [
      "s3:*"
    ]
    resources = ["arn:aws:s3:::${var.project_name}-${var.environment}-bucket-shared",
    "arn:aws:s3:::${var.project_name}-${var.environment}-bucket-shared/*"]
    effect = "Allow"
  }
}
# IAM Policy - Sagemaker S3 access
resource "aws_iam_policy" "allow_backet_shared_access" {
  name   = "allow-bucket-shared-access"
  policy = data.aws_iam_policy_document.bucket_shared_access.json
}
# IAM policy attach - 個人Userに共通バケットアクセス用Policyのattache
resource "aws_iam_policy_attachment" "user_access_shared_bucket" {
  name       = "user-bucket-shared-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-bucket-shared-access"

  depends_on = [module.user, aws_iam_policy.allow_backet_shared_access]
}
# 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 - 個人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]
}

既出の grantReadWrite/grant_read_write と、読むことだけができるようになる grantRead/grant_read を使って IAM User, IAM Role の設定をしています。Terraform では grant がないので、少し記述が長くなってしまっていますが、AWS に慣れ親しんでいると IAM の表現と変わらないので書きやすいかもしれません。(一応同じ書き方で CDK も書くこともできます)

このように作成した AWS リソースの解説を行ってきましたが、このような形で宣言的にリソースをプログラムで書くことで簡単に AWS リソースを立ち上げられることがわかったかと思います。

ここからは運用面におけるメリットの一例を紹介します。


3. 人の増減対応

「こんなに簡単に使えるなら私も使いたいんですが。っていうかリソース作成は CDKや Terraform が頑張るんで運用の人の労力、実質ゼロですよね。」

津郷カステラのカロリーみたいなこと言わないでください。」

工藤「俺は会社をやめるぞ Tugoooooooooooooォォォッ !!」

津郷「ナンダッテー! もう作っちゃった Yo !!!!! ・・・とでも言うと思いましたか ? というわけで、運用時に発生しがちな、人の増減対応についても触れましょう。コードを以下のように書き換えて、cdk deploy もしくは terraform apply してマネジメントコンソールを確認してみてください。」

TypeScript

// case01/cdk-typescript/lib/sagemaker-studio-ts-stack.ts の diff
- new User(this, 'Kudo', {
-     name: 'kudo',
+ new User(this, 'Go', {
+     name: 'go',
      domainId: domain.attrDomainId,
      policy,
      removalPolicy,
  }),

Python

# case01/cdk-python/chococones/chococones.py の diff
  User(
      self,
-     "Kudo",
-     name="kudo",
+     "Go",
+     name="go",
      policy=policy,
      domain=domain,
  ),

Terraform

# case01/terraform/examples/variable.tf のdiff
  variable "user_list" {
    type    = list(string)
-   default = ["tsugo", "kudo"]
+   default = ["tsugo", "go"]
  }

工藤の User Profile がなくなり、呉の User Profile が作成されている

「おおおおおお ! 数文字書き換えただけで工藤さんが退社されて (してません) 私が使えるようになってる !! お、私専用のS3 Bucket とかももちろんできてますね !」

津郷「このようにマネコンポチポチだといろいろチェックしながら待機時間含めると 1時間くらいチマチマやらないといけない作業を IaC を使えば数文字書き換えるだけで出来上がるのは大きなメリットの 1 つです。」

「これはもう使わない理由がありませんね ! でもお値段が高いんでしょう・・・?」

津郷「テレフォンショッピングみたいになってきましたが、CDK や Terraform 自体はなんと、無料です !」

「わー !! 津郷さん頑張ってくれたんですね !」

津郷「私は何もしていません。」


4. Chococones な機械学習を動かしてみる

さて、環境ができましたが、本当にきのこの山とたけのこの里検出モデルができるのか、最後に確認しておきましょう。

やり方は こちらの記事 の通りにやります。いきなり SageMaker Studio がすでに立ち上がっているので、3-3 から初められます。

~~ 10 分後 ~~

 

動きました!

これで機械学習を動かす環境が整っていることの確認が取れました。


5. 削除

「とはいえ、ここまで作る話とかろうじて変更の話をしましたが、削除する時はどうするんですか ?」

津郷「削除してくださいよ。」

「そうじゃなくて CDK/Terraform で削除するとなんか便利だったりしないんですか ?」

津郷「ある程度楽になります。まずは以下のコマンドを打ってみてください。」

CDK

cdk destroy

Terraform

terraform destroy
# CDK エラー出力抜粋
15:08:48 | DELETE_FAILED        | AWS::SageMaker::UserProfile           | GoUserProfile910B6B7C
Resource of type 'AWS::SageMaker::UserProfile' with identifier 'd-jmr10kkwp2rg|go' has a conflict. Reason: Unable to delete UserProfile [arn:aws:sagemaker:us-east-1:290000
338583:user-profile/d-jmr10kkwp2rg/go] because App(s) are associated with it. (Service: SageMaker, Status Code: 400, Request ID: 8d822131-9c14-491e-bb9a-e3eedcdb5a8a).

15:10:59 | DELETE_FAILED        | AWS::SageMaker::Domain                | domain
Resource of type 'AWS::SageMaker::Domain' with identifier 'd-jmr10kkwp2rg' has a conflict. Reason: Unable to delete Domain [arn:aws:sagemaker:us-east-1:290000338583:domain
/d-jmr10kkwp2rg] because App(s) are associated with it. (Service: SageMaker, Status Code: 400, Request ID: 3d5e22c8-b684-4a32-8541-8ac0acdec731).

15:26:16 | DELETE_FAILED        | AWS::EC2::Subnet                      | vpcPrivateSubnet2Subnet7031C2BA
Resource handler returned message: "The subnet 'subnet-062fdc79e7938f633' has dependencies and cannot be deleted. (Service: Ec2, Status Code: 400, Request ID: a77b272c-e3b
9-46b8-9f13-bbf6e200788f, Extended Request ID: null)" (RequestToken: 8f200541-8e0a-8d9f-4de9-534c15161858, HandlerErrorCode: InvalidRequest)

15:26:19 | DELETE_FAILED        | AWS::EC2::Subnet                      | vpcPrivateSubnet1Subnet934893E8
 
 ❌  ChococonesStack: destroy failed Error: The stack named ChococonesStack is in a failed state.

津郷「エラーで落ちましたかね ?」

「はい、全てが削除されたわけではないです。」

津郷「出力結果を見てほしいのですが、使用中のリソースについては削除されません。これはどちらかというと使ってるのに削除されると困るケースのほうが多いかと思うので、これはこういう挙動だと認識していただければ。例えば、呉さんの UserProfile の削除に失敗していますが、これは呉さんが SageMaker Studio を使ってきのこの山検出モデルを作って App が残っているためです。」

「なるほど、CDK外で作成したリソース (SageMaker の Appなど) は手動で削除すれば消せますか ?」

津郷「はい、消せます。あと SageMaker Studio で使用する Amazon Elastic File System についても自動では削除されないので、こちらも手動で削除する必要があります。エラーに VPC 関連のエラーが出ていますが、これらは EFS 起因です。これらは destroy する前に事前に削除することでエラーを防ぐこともできます。」

「なるほど。とりあえず手動で削除したあと再度 destory を試してみます。」

SageMaker Studio のコントロールパネルからきのこの山モデルを作成した go という User Profile を選択

クリックすると拡大します

datascience の「Delete app」をクリック

クリックすると拡大します

確認のポップアップで、「Yes, delete app」を選んだあと delete を入力して 「delete」をクリック

クリックすると拡大します

default の「Action」から「Delete」をクリック

クリックすると拡大します

確認のポップアップで、「Yes, delete app」を選んだあと delete を入力して「delete」をクリック

クリックすると拡大します

EFS の画面に写って該当 EFS を選択して「Delete」をクリック

クリックすると拡大します

確認のポップアップで EFS の ID を入力して「Confirm」をクリック

クリックすると拡大します

VPC の画面に行き、ChococonesStack/vpc を選択して「Actions」から「Delete VPC」を選択

クリックすると拡大します

確認のポップアップで delete と入力して「Delete」をクリック

クリックすると拡大します

これで再度、cdk destroy もしくは terraform destroy します。

gokazu@b0be838161ee cdk-python % cdk destroy
Are you sure you want to delete: ChococonesStack (y/n)? y
ChococonesStack: destroying...

 ✅  ChococonesStack: destroyed

無事削除されました。

「これで課金もストップですか ?」

津郷「そうです。」

「よくリソースの停止し忘れで課金され続けることがありますが、CDK だとある程度一括で消せますし、消せなかった場合もリソースの何が残っているかがわかるので、管理面ですごくいいですね ! あと、個人的には 完全に削除 と打ち込まなくて済んだのが一番よかったです。」

津郷「今回は個人の S3 Bucket に何もオブジェクトを置かなかったので入れずに済みましたが、なにかデータがある場合は cdk destroy そのままでは消えませんのでご注意ください。マネジメントコンソールからだったら 完全に削除 と入れる必要がありますね。cdk destroy で消したい場合 、autoDeleteObjects という引数があり、True にすると Lambda 関数が生成され destroy 時にお掃除させることもできますが、ここではデータが自動で消えないようにしています。あとは Boto3 とかから消すと良いのではないでしょうか。」

「あ、そうだったんですね・・・。いずれにしても消し忘れが防げるのが嬉しいですね !」

クリックすると拡大します


6. この記事の終わりに

さて、今回は津郷さん、工藤さんに協力してもらい、 Amazon SageMaker Studio を中心としたデータ分析環境を IaC で立ち上げ、運用することに挑戦してみました。ぜひ皆様もアーキテクティングを参考にしながらコードをカスタマイズして使ってみてください。

今回私 (呉) は TypeScript のコードを初めて読みましたが、CDK に限ったコードであれば、Python 書いたことある人なら読めるし、Python への移植も簡単でした。CDK のコードは TypeScript での実装が公開されていることが多いですが、Python 使いも気後れせずに使えそうです。Terraform も、記述する内容はそこまで大差ないので、あとはやる気の問題ですね ! (やる気欠乏性の私が言うと説得力皆無ですが)

今回は 1 ケースしか触れられませんでしたが、他のケースについてもいずれ設計と実装をしたいですね ! というわけで今回の記事はここまでです。最近私や私が所属するチームの同僚などが YouTube で機械学習のプロジェクトの進め方や Amazon SageMaker の使い方を解説したりしているので、こちらもぜひご覧になってください。私や同僚が泣いて喜びます。

Black Belt AI/ML Light Part »
機械学習モデルをプロダクトで活用するためのプロセスを解説する動画シリーズです。SageMake Studio Labを利用したハンズオンで開発プロセスを体験しながら学びます。

Black Belt AI/ML Dark Part »
Amazon SageMaker を用いて、機械学習プロジェクトをうまく回すためのサービス解説や使い方を紹介しています。

また次回の記事でお会いしましょう !

この連載記事のその他の記事はこちら

選択
  • 選択
  • たけのこの里が好きな G くんのために、きのこの山を分別する装置を作ってあげた。~モデル作成編~
  • たけのこの里が好きな G くんのために、きのこの山を分別する装置を作ってあげた。~分別装置作成編~
  • CHOCOCONES as Code 爆誕 !?「たけのこの里」の分析環境をコードで制御する
  • 「きのこの山」を閉域に閉じ込めてみた ~Amazon SageMaker Studio をよりセキュアに IaC する方法

builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます


筆者プロフィール

呉 和仁 (Go Kazuhito / @kazuneet
アマゾン ウェブ サービス ジャパン合同会社
機械学習ソリューションアーキテクト。

IoT の DWH 開発、データサイエンティスト兼業務コンサルタントを経て現職。
プログラマの三大美徳である怠惰だけを極めてしまい、モデル構築を怠けられる AWS の AI サービスをこよなく愛す。

工藤 朋哉
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト。

AWS Japan の Prototype Enginner として、プロトタイプ開発を通じてお客様の技術支援をしています。AWS CDK が好き。最近のマイブームはいろいろなお店のカヌレを食べること。

津郷光明
アマゾン ウェブ サービス ジャパン合同会社
ソリューションアーキテクト。

SIerにて金融系システム開発、企画を経て AWS Japan へ入社。
Infrastructure as Code や自動化を好み、関連するサービスをよく利用します。

さらに最新記事・デベロッパー向けイベントを検索

下記の項目で絞り込む
1

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する