「きのこの山」を閉域に閉じ込めてみた

~ Amazon SageMaker Studio をよりセキュアに IaC する方法

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

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

どうも、機械学習ソリューションアーキテクトの呉 (@kazuneet) です。

前回の記事 では Chococones as Code と称し、IaC を用いてたけのこの里を分析する環境として Amazon SageMaker Studio をデプロイしました。IaC を使うことのメリットとしてユーザー (分析環境を利用する人) とその権限 (ユーザーが何をしていいのか) 、そして分析環境をコードで管理することを実現し、人の出入りや権限の管理が楽になることを紹介してきました。

基本的には上記で運用すればよいのですが、お客様によっては厳しいセキュリティ要件を満たさないといけない場合があり、前回の記事だと不足する、というケースも散見されます。そこで、今まで我々ソリューションアーキテクトが直面してきたお客様のセキュリティ要件の中で最も遭遇頻度が高い、インターネットに出ないアーキテクチャーの最大公約数的なアーキテクチャーとその実装をこの記事では紹介します。

※ 前回の記事 (case01) よりセキュリティを強固にしていますが、あくまで「インターネットに出られないようにする」に主眼を置いているので、皆様のセキュリティ要件を満たしているかどうかはまた別のお話です。お客様の個々の要件ごとにアーキテクティングが必要なことにご留意ください。特に SageMaker Studio の User Profile に AmazonSageMakerFullAccess がついているので、Studio の中から他人が発行した Training Job を止めたり、他人の User Profile を削除できたりします。もちろんそういった操作が必要な場合もあるので、業務を鑑みてどんな操作の権限が必要なのかを検討する必要があります。

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

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

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

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

1. ・・・の前に最近の Amazon SageMaker Studio のアップデート

本題に行く前に、 2022 年末に Amazon SageMaker のアップデートが多数ありました。その中で記事に関連するアップデートを 2 つ最初に紹介いたします。

1 つ目は SageMaker Studio の UI が新しくなりました。

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

黒を基調としたところに vivid なカラーが映えていて私は好きです。

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

今までは 1 つのリージョンに対してドメインが 1 つしか作れなかったため、部署ごとにドメインを用意して協力作業をしていく、といったことができず、すべてのユーザーがリージョン内の 1 つのドメインに紐付いている必要がありました。今回のアップデートで部署ごとにドメインを作成などができるようになりました。これは IaC を使うことで尚更管理が簡単になるので、この記事がさらに有用になるアップデートで嬉しい限りです。

さて、話を今回のアーキテクチャーに戻します。


2. よく遭遇するセキュリティ要件とアーキテクチャー

ここからは前回コードを書いてくれた、そして今回もコードを書いてくれた、工藤さんと津郷さんで会話形式 (フィクション) でよくあるセキュリティ要件と我々の思いを語りながらアーキテクチャーを固めていきます。

呉・津郷・工藤「というわけでよろしくおねがいします。」

「津郷さんはよく堅めのセキュリティ要件にエンカウントするとか ?」

津郷「そうですね、製薬や金融のお客様を担当するときはセキュリティが厳しいイメージがあります。」

「具体的にはどんなセキュリティ要件があるでしょうか ?」

津郷「よくあるのは SageMaker Studio の環境からインターネットに出ないようにすることですね。ネットワークの経路としてインターネットに出さないというのが必須というのはよくあります。」

「インターネットを使えない ? こんなエラーが起きたらどうするんですか」

  File "/tmp/ipykernel_15236/3574047905.py", line 1
    print('hello)
          ^
SyntaxError: unterminated string literal (detected at line 1)

津郷それはググるまでもなくシングルクォートを閉じてください。ってインターネットで検索できない、とかそういうことではなく、お客様が持っているお客さんの情報や実験などの機密情報などが、そもそも流出しないようにインターネットに出られない閉じた環境なら大丈夫だよね、といった発想です。」

「(でもやっぱりその環境から検索できないじゃん) なるほど。セキュリティとして、Amazon SageMaker Studio の通信はすべて SSL/TLS で暗号化されているので十分なはずですが、性悪説に基づき悪意のある内部の人間がデータをインターネットから持ち出せてしまうことを考えると閉じざるを得ないケースというのはありそうですね。」

津郷トゲトゲしい言い方が気になりますが、そういうのを防ぐためには、ってことですね。」

ゼロトラストォォォォ !

津郷必殺技みたいに言わないでください。今回の記事のタイトルの話ですが、きのこの山の画像が外に流出したら困るでしょう。だから閉域にするんです。」

「絶対に許せません。たけのこの里の画像はたくさん流通して欲しいですが。」

津郷「あと、ライブラリのインストールを自由にできなくしたりしますね。」

PyTorch を自作しろってことですか ?

津郷作れるなら作っていただいて構わないですが、どちらかというと野良ライブラリなどセキュリティ的に危ういものを防ぐ意味で、使えるライブラリを制限します。AWS CodeArtifact というサービスを使えば、インターネットにつながらなくても pip でインストールができ、かつ、インストールできるライブラリやバージョンを制限することができます。」

「なるほど、AWS CodeArtifact を使えばインターネットに出られなくても pip install できるんですね。ただライブラリやバージョンの制限は開発者目線だと辛いですね。確かにセキュアにはなりますが、ライブラリって試して使える使えないを判断することも多いので、開発速度とのトレードオフですねぇ・・・。開発者だけでなく制限する側も OK/NG の判断難しそうです。」

工藤「インターネットアクセスと合わせて、マネジメントコンソールを禁じる場合もありますよね。そもそもマネジメントコンソール自体がインターネットアクセスというのもありますし、IAM でコントロールされるとはいえ、GUI でいろいろ簡単にできたり、他のサービスへアクセスできちゃうのもありますからね。」

「マネジメントコンソール便利なのに。」

工藤「あとはマネジメントコンソールや API をコールする環境の IP アドレスの制限はかけたりしますよね。この IP アドレスからしかアクセスできない、API 叩けない、みたいに制限します。」

「クラウドの便利さが…とは思いつつそこはガバナンスなのでいろいろ天秤にかけた結果ですね。会社以外で仕事したくない人の言い訳にも使えますからね。これは私も賛成です。」

工藤「インターネットに出さ (せ) ないともつながるのですが、AWS 内の通信はすべて閉域網にする、というのもよくありますね。例えば SageMaker Studio から Amazon S3 にアクセスするときは通常インターネットからアクセスするのですが、そもそもインターネットに出られないので、Amazon VPC Endpoint 経由にしてアクセスできるようにします。」

津郷「というのを踏まえて、今回実装するのは以下の要件としました。」

# 前回と同じ
初めて IaC と SageMaker Studio を使う組織を対象とする。 
現状 SageMaker Studio を使うのは工藤、津郷の二人で、それぞれが協力して、
きのこの山とたけのこの里を判別する機械学習のトレーニングコードを作る。
トレーニングコードを管理する Git リポジトリも AWS で用意する。
データは S3 に置くが、S3 についてはお互いが読み書きできるバケットと、
自分だけが書き込める場所 (読み取りは可能) をそれぞれ用意したい。
IaC を実行するのは管理者の皮をかぶった津郷。
# 今回の追加要件
ユーザーはマネジメントコンソールにログインし、
フリートマネージャーからリモートデスクトップで Amazon EC2 にアクセスし、
署名付き URL を発行して SageMaker Studio にアクセスする。
前述の EC2 や SageMaker Studio の環境からインターネットにアクセスはできない。
SageMaker Studio の環境でライブラリを追加インストールしたい場合は、AWS CodeArtifact を利用する。
AWS 内の通信はすべて閉域網(インターネットに出ない)ように VPC Endpoint を利用する。

津郷「各自の SageMaker Studio の S3 や AWS CodeCommit へのアクセス権限については前回と一緒なので割愛して、図に起こすとこんな感じです。」

今回のアーキテクチャー

津郷「ユーザーは、マネジメントコンソールに自分の IAM User でログインして、Fleet Manager からリモートデスクトップで EC2 にアクセスします。EC2 で SageMaker Studio の署名付き URL を発行して、EC2 のブラウザから SageMaker Studio にアクセスします。」

先程の会話を踏まえて、といいながら踏まえていないところがありますね ? 」

津郷「はい、マネジメントコンソールを使っているところですね。本来、ここはお客様の環境 (ここでは津郷と工藤が接続している場所) と AWS を AWS Direct Connect(DX) か AWS VPN でつなぐことでインターネットを介さずにリモートデスクトップで EC2 までアクセスします。」

本来のアーキテクチャー

津郷「しかし、すでに DX や VPN を引いているお客様はさておき、個人がお試しで引くのは大変で、DX や VPN を前提とした IaC を紹介したところで再現できる人は少ないので、このようなアーキテクチャーにしています。DX や VPN を引いている場合はマネジメントコンソールの部分を置き換えてください。同様に IAM ユーザーに対する IP アドレスの制限もかけていません。」

「もちろん、EC2 は専用で、そこから発行できる SageMaker Studio の 署名付き URL も自分の User Profile しか使えないんですよね ? 」

津郷「もちろんです。津郷の EC2 に工藤さんの IAM User ではアクセスできないですし、津郷の EC2 から工藤さんの SageMaker Studio の User Profile を用いた署名付き URL は発行させません。」


3. デプロイと各ユーザーによる利用イメージ

実装は長いので実装の説明に行く前に、どんな感じで SageMaker Studio を使えるのか、デプロイから利用までの流れを紹介します。

3-1. デプロイ

今回も同様にAWS Chococones Development Kit ・・・じゃなかった、 AWS Cloud Development Kit(CDK) は SageMaker Studio Lab から、Terraform は手元の Mac から実行します。ここではデプロイに必要なコマンドだけ記載しますので、詳細は前回の記事の2-3. IaC 実行環境準備を参照してください。

前回から IaC が入っているリポジトリが更新されています。CDK も Terraform もまずは以下のコマンドを打ち込んでリポジトリを更新しましょう。

リポジトリが残っている場合

# 前回クローンしたリポジトリに cd する
cd aws-ml-jp/
# リポジトリの最新化
git pull origin main

リポジトリが残っていない場合

# リポジトリをクローン
git clone https://github.com/aws-samples/aws-ml-jp
cd aws-ml-jp/

※設定などが残っていない場合は前回の記事の2-3. IaC 実行環境準備を参照して事前準備してください。

CDK TypeScript の場合

$ cd sagemaker/sagemaker-studio/IaC/case02/cdk-typescript
$ # 依存ライブラリインストール
$ npm install
$ # cdk の設定(前回実行している場合は不要)
$ # cdk bootstrap
$ # デプロイ
$ cdk deploy
(前略)
✅  ChococonesStack
✨  Deployment time: 446.38s
Outputs:
ChococonesStack.KudoEC2PasswordXXXXXXXXX = KudoEC2SecretXXXXXXXX-XXXXXXXXXXXX
ChococonesStack.KudoPasswordXXXXXXXXX = KudoSecretXXXXXXXX-XXXXXXXXXXXX
ChococonesStack.TsugoEC2PasswordXXXXXXXX = TsugoEC2SecretXXXXXXXX-XXXXXXXXXXXX
ChococonesStack.TsugoPasswordXXXXXXXXX = TsugoSecretXXXXXXXX-XXXXXXXXXXXXStack ARN:
arn:aws:cloudformation:{REGION}}:{ACCOUNT}:stack/ChococonesStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
✨  Total time: 450.95s

CDK Python の場合

$ cd sagemaker/sagemaker-studio/IaC/case02/cdk-python
$ # 依存ライブラリインストール
$ mkdir lib 
$ pip install -r requirements.txt -t /lib
$ # インストールディレクトリにパスを通す
$ export PYTHONPATH=$PWD/lib
$ # cdk の設定(前回実行している場合は不要)
$ # cdk bootstrap
$ # デプロイ
$ cdk deploy 
(前略)
✅  ChococonesStack
✨  Deployment time: 448.68s
Outputs:
ChococonesStack.KudoEC2PasswordXXXXXXXXX = KudoEC2SecretXXXXXXXX-XXXXXXXXXXXX
ChococonesStack.KudoPasswordXXXXXXXXX = KudoSecretXXXXXXXX-XXXXXXXXXXXX
ChococonesStack.TsugoEC2PasswordXXXXXXXX = TsugoEC2SecretXXXXXXXX-XXXXXXXXXXXX
ChococonesStack.TsugoPasswordXXXXXXXXX = TsugoSecretXXXXXXXX-XXXXXXXXXXXXStack ARN:
arn:aws:cloudformation:{REGION}}:{ACCOUNT}:stack/ChococonesStack/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
✨  Total time: 453.82s

Terraform の場合

$ cd sagemaker/sagemaker-studio/IaC/case02/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
$ 
$ # Lambda 関数のアセットを zip 圧縮
$ cd examples
$ zip rotate.py.zip rotate.py
$ 
$ # Terraform 実行準備 
$ terraform init
$ # Terraform 実行
$ # project name は世界でユニークなものを設定してください。筆者が使っているのでおそらく使えません。
$ terraform apply -var='project_name=chococones' -auto-approve
(前略)
Plan: 114 to add, 0 to change, 0 to destroy.
(後略)

さて、これで各種リソースが出来上がりました。管理者は以下の情報を各ユーザーに配ります。

Secrets Manager の画面に行きます。

{ユーザー名}{8桁の数字}-{ランダムな12文字} が マネジメントコンソールにログインするためのパスワード、{ユーザー名}EC2{8桁の数字}-{ランダムな12文字} の命名規則がそれぞれの人がアクセスする EC2 にログインするための OS のパスワードです。

シークレットの名前のリンクをクリックし、

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

シークレットの値を取得する」、をクリックして表示される文字列を各ユーザーに配ってください。

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

3-2. ユーザーの利用イメージ

ここからはユーザー (閉域で SageMaker Studio を使う人) の気持ちでどう使うのかを紹介します。

まずはマネジメントコンソールにログインします。管理者から送られてきたパスワードを元にログインします。

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

初回ログイン時はパスワード変更を求められるので変更します。

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

AWS Systems Manager の画面に移動します。

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

フリートマネージャー」の画面に行きます。

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

自身の EC2 を選択して、「リモートデスクトップ(RDP)との接続」を選択します。

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

ユーザー名に user-1 を入力し (実装で変更可能) 、管理者からもらった EC2 用のパスワードを入力し、「Connect」をクリックします。

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

Windows の画面が出るので、Explorer を開きます。

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

C:\ 直下に GetPresignedUrl というファイルがあるので、右クリックして、「Run with PowerShell」をクリックします。

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

初めて実行した時は Do you want to change the execution policy? と聞かれるので、[A] Yes to All である、 A を入力します。

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

ブラウザが開きます。初回起動時のみ聞かれる質問がいくつか出ます。「Start without your data」を選択するのが楽でいいでしょう。

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

チェックを外して 「Confirm and start browsing」をクリックします。

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

すると、SageMaker Studio の起動画面に遷移するので待ちます。

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

起動しました。

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

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

念の為インターネットに出られないことと、CodeArtifact 経由で pip install できることを確認します。「File」 → 「New」 → 「Terminal」をクリックし、ターミナルを起動します。

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

試しに curlhttps://amazon.co.jp にアクセスしてもなにも起きません。

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

ちなみに手元の Mac だと ステータスコード 301 を返します。

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

numpy をインストールしてみてもインターネットに出られないのでインストールできません。

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

しかし、以下のコマンドで CodeArtifact の認証を通したあとに pip install numpy -U をすると numpy がインストールできていることがわかります。

# 認証コマンド
aws codeartifact login --tool pip --repository chococones --domain chocones --domain-owner {ACCOUND_ID} --region {region}

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

これらがインターネットに接続せずに、ライブラリを追加できる分析環境の使い方です。


4. 実装

使い方がわかったところでどのように実現したのか実装を紹介します。
前回との差分をメインに紹介していきます。diff を取りながら見てみてください。

4-1. 閉域を実現するために

今回はインターネットに出ない閉域で SageMaker Studio を始めとしたリソースを構築していきます。AWS で閉域といえば、VPC と Subnet ですね !
前回の記事であまり詳しく解説しませんでしたが、VPC 作成部分のコード差を見てみましょう。

前回 (閉域でない) の CDK TypeScirpt の場合

lib/chococones-stack.ts より抜粋

// Domain 用の VPC
const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 2 })

今回 (閉域) の CDK TypeScript の場合

lib/chococones-stack.ts より抜粋

// Domain 用の VPC
const vpc = new ec2.Vpc(this, 'Vpc', {
  maxAzs: 2,
  natGateways: 0,
  subnetConfiguration: [
    {
      name: 'isolated-subnet',
      subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
      cidrMask: 24,
    },
  ],
})

前回(閉域でない)の CDK Python の場合

chococones/chococones.py より抜粋

# Domain 用の VPC
vpc = ec2.Vpc(self, "vpc", max_azs=2)

今回 (閉域) の CDK Python の場合

chococones/chococones.py より抜粋

# Domain 用の VPC
vpc = ec2.Vpc(
    self,
    "vpc",
    max_azs=2,
    nat_gateways=0,
    subnet_configuration=[
        {
            "name": "isolated-subnet",
            "subnetType": ec2.SubnetType.PRIVATE_ISOLATED,
            "cidrMask": 24,
        },
    ],
)

前回(閉域でない)の Terrafrom の場合

# 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"
  }
}

今回 (閉域)の Terraform の場合

# VPC
module "vpc_for_sagemaker" {
  source = "terraform-aws-modules/vpc/aws"

  name            = "${var.project_name}-${var.environment}-vpc"
  cidr            = "10.0.0.0/16"
  azs             = ["${var.aws_region}a", "${var.aws_region}c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]

  enable_nat_gateway     = false
  single_nat_gateway     = false
  one_nat_gateway_per_az = false
  enable_dns_support     = true
  enable_dns_hostnames   = true

  tags = {
    Name = "${var.project_name}-${var.environment}-vpc"
  }
}

前回は CDK の場合1行書いただけなのに、今回はいろいろ増えました。重要なのは、ec2.SubnetType.PRIVATE_ISOLATED と書かれた部分です。PRIVATE_ISOLATED と記載することで、インターネットにルーティングしない Subnet ができあがります。この Subnet に SageMaker Studio のドメインだったり、EC2 だったりを配置することで、中のリソースからインターネットに出られないようにします。
一方 Terraform はもともと事細かに設定していたのでほとんど変更はないです。大きな違いは一箇所だけ、enable_nat_gateway が False になっています。これはインターネットに出ていく必要がないためです。private_subnets に記載した Subnet に各リソースを作成します。

4-2. VPC Endpoint

いくらインターネットに出さないからといって、AWS の各リソースにアクセスできないと AWS を使っている意味があまりありません。AWS の各リソースにアクセスするときはデフォルトでインターネット経由でアクセスしようとしてしまうため、閉域からアクセスしようとすると到達できません(なのでそもそも SageMaker Studio にすらアクセスできません)。閉域から AWS のリソースにアクセスするためには、使用するリソースごとに VPC Endpoint を立てます。VPC Endpoint を立てると、インターネットを経由せずに AWS の各リソースにアクセスできます。VPC Endpoint のリソースを立ち上げる際にどこに立てるのかという Subnet を指定するだけで OK です。

CDK TypeScript の場合

lib/chococones-stack.ts より抜

// エンドポイントを設定
new ec2.InterfaceVpcEndpoint(this, `SsmVpcEndpoint`, {
  service: ec2.InterfaceVpcEndpointAwsService.SSM,
  privateDnsEnabled: true,
  vpc: vpc,
  subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
})
…
(中略)
…
new ec2.InterfaceVpcEndpoint(this, 'SecretsManagerApiVpcEndpoint', {
  service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
  privateDnsEnabled: true,
  vpc: vpc,
  subnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED },
})
vpc.addGatewayEndpoint('S3GatewayEndpoint', {
  service: ec2.GatewayVpcEndpointAwsService.S3,
  subnets: [{ subnetType: ec2.SubnetType.PRIVATE_ISOLATED }],
})

CDK Python の場合

chococones/chococones.py より抜粋

# エンドポイントを設定
ec2.InterfaceVpcEndpoint(
    self,
    "SsmVpcEndpoint",
    service=ec2.InterfaceVpcEndpointAwsService.SSM,
    private_dns_enabled=True,
    vpc=vpc,
    subnets={"subnet_type": ec2.SubnetType.PRIVATE_ISOLATED},
)
…
(中略)
…
ec2.InterfaceVpcEndpoint(
    self,
    "SecretsManagerApiVpcEndpoint",
    service=ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
    private_dns_enabled=True,
    vpc=vpc,
    subnets={"subnet_type": ec2.SubnetType.PRIVATE_ISOLATED},
)

vpc.add_gateway_endpoint(
    "S3GatewayEndpoint",
    service=ec2.GatewayVpcEndpointAwsService.S3,
    subnets=[
        {"subnet_type": ec2.SubnetType.PRIVATE_ISOLATED},
    ],
)

Terraform の場合

examples/main.tf より抜粋

# Security Group for VPC endpoint
resource "aws_security_group" "allow_local_https" {
  name        = "allow_local_https"
  description = "Allow HTTPS inbound traffic"
  vpc_id      = module.vpc_for_sagemaker.vpc_id

  ingress {
    description = "TLS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [module.vpc_for_sagemaker.vpc_cidr_block]
  }

  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

  tags = {
    Name = "allow_local_https"
  }
}

resource "aws_vpc_endpoint" "s3_endpoint" {
  vpc_id            = module.vpc_for_sagemaker.vpc_id
  service_name      = "com.amazonaws.${data.aws_region.current.name}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = module.vpc_for_sagemaker.private_route_table_ids
  policy            = <<POLICY
    {
        "Statement": [
            {
                "Action": "*",
                "Effect": "Allow",
                "Resource": "*",
                "Principal": "*"
            }
        ]
    }
    POLICY
}

# VPCエンドポイント
module "endpoints" {
  source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"

  vpc_id             = module.vpc_for_sagemaker.vpc_id
  security_group_ids = [aws_security_group.allow_local_https.id]

  endpoints = {

    ssm = {
      service             = "ssm"
      private_dns_enabled = true
      subnet_ids          = module.vpc_for_sagemaker.private_subnets
      security_group_ids  = [aws_security_group.allow_local_https.id]
    },
    …
    (中略)
    …
    secretsmanager = {
      service             = "secretsmanager"
      private_dns_enabled = true
      subnet_ids          = module.vpc_for_sagemaker.private_subnets
  } }
}

長過ぎるので、中略してしまいましたが、Inteface VPC Endpoint で、今回使用する AWS のサービスである、EC2, S3, CodeCommit, CodeArtifact などの VPC Endpoint を作ります。S3 のみ Gateway VPC Endpoint を作成しています。
Terraform もほとんど同じで、VPC エンドポイント用のセキュリティグループを設定している以外は、粛々と VPC エンドポイントを設定しています。

これでこの Subnet からインターネットを介さず、使用する AWS の各リソースにアクセスできるようになりました。

4-3. EC2 周りの設定

今回は EC2 を踏み台にして、 SageMaker Studio にアクセスしますので、EC2 周りの設定を見ていきましょう。

4-3-1. EC2 と 署名付き URL の発行

ともかく、EC2 を作成する必要があります。EC2 の作成は以下の通りです。

CDK TypeScript の場合

lib/constructs/user.ts より抜粋

// EC2 用のパスワードを Secrets Manager で生成
const ec2Secret = new secretsmanager.Secret(this, 'Ec2Secret')

// EC2 インスタンス
const instance = new ec2.Instance(this, 'Instance', {
  vpc: props.vpc,
  vpcSubnets: props.vpc.selectSubnets({
    subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
  }),
  instanceType: ec2.InstanceType.of(
    ec2.InstanceClass.T3,
    ec2.InstanceSize.LARGE
  ),
  machineImage: new ec2.WindowsImage(
    ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE
  ),
  role: instanceRole,
  userData: ec2.UserData.custom(
    `
<powershell>
$password = (Get-SECSecretValue -SecretId ${ec2Secret.secretArn} -Region ${region}).SecretString
New-LocalUser -Name user-1 -Password (ConvertTo-SecureString $password -AsPlainText -Force)
Add-LocalGroupMember -Group "Remote Desktop Users" -Member user-1
Add-LocalGroupMember -Group Administrators -Member user-1
Write-Output '$url = New-SMPresignedDomainUrl -DomainId ${props.domainId} -UserProfileName ${this.userProfile.userProfileName} -Region ${region}' | Set-Content -Encoding Default 'C:\\GetPresignedUrl.ps1'
Write-Output 'start microsoft-edge:$url' | Add-Content -Encoding Default 'C:\\GetPresignedUrl.ps1' 
</powershell>
<persist>true</persist>
`
  ),
})

CDK Python の場合

chococones/constructs/user.py より抜粋

# EC2 へのログイン用パスワードを Secrets Manager で生成
ec2_secret = secretsmanager.Secret(self, "EC2Secret")
(中略)

# EC2 インスタンス
instance = ec2.Instance(
    self,
    "Instance",
    vpc=vpc,
    vpc_subnets=ec2.SubnetSelection(
        subnet_type=ec2.SubnetType.PRIVATE_ISOLATED
    ),
    instance_type=ec2.InstanceType.of(
        ec2.InstanceClass.T3,
        ec2.InstanceSize.LARGE,
    ),
    machine_image=ec2.WindowsImage(
        ec2.WindowsVersion.WINDOWS_SERVER_2022_ENGLISH_FULL_BASE
    ),
    role=instance_role,
    user_data=ec2.UserData.custom(
       f"""
<powershell>
$password = (Get-SECSecretValue -SecretId {self.ec2_secret.secret_arn} -Region {region}).SecretString
New-LocalUser -Name user-1 -Password (ConvertTo-SecureString $password  -AsPlainText -Force)
Add-LocalGroupMember -Group "Remote Desktop Users" -Member user-1
Add-LocalGroupMember -Group Administrators -Member user-1
Write-Output '$url = New-SMPresignedDomainUrl -DomainId {domain.attr_domain_id} -UserProfileName {self.user_profile.user_profile_name} -Region {region}' | Set-Content -Encoding Default 'C:\\GetPresignedUrl.ps1'
Write-Output 'start microsoft-edge:$url' | Add-Content -Encoding Default 'C:\\GetPresignedUrl.ps1'
</powershell>
<persist>true</persist>
        """
    ),
)

Terraform の場合

examples/main.tf より抜粋

# Secret
resource "aws_secretsmanager_secret" "ec2_login_password" {
  for_each = toset(var.user_list)
  name     = "${var.project_name}-ec2-login-password-${each.value}"
}

resource "aws_secretsmanager_secret_version" "secret_version" {
  for_each      = toset(var.user_list)
  secret_id     = "${var.project_name}-ec2-login-password-${each.value}"
  secret_string = jsonencode("dummypassword")

  lifecycle {
    ignore_changes = [
      secret_string
    ]
  }
  depends_on = [
    aws_secretsmanager_secret.ec2_login_password
  ]
}

# EC2
resource "aws_instance" "ec2-indivisual" {
  for_each               = toset(var.user_list)
  ami                    = data.aws_ssm_parameter.windows_latest_ami.value
  instance_type          = "m5.xlarge"
  iam_instance_profile   = "ec2-instanceprofile-${each.value}"
  subnet_id              = module.vpc_for_sagemaker.private_subnets[0]
  vpc_security_group_ids = [aws_security_group.private_ec2.id]
  user_data              = <<EOF
<powershell>
$password = (Get-SECSecretValue -SecretId ${var.project_name}-ec2-login-password-${each.value}).SecretString
New-LocalUser -Name user-1 -Password (ConvertTo-SecureString $password -AsPlainText -Force)
Add-LocalGroupMember -Group "Remote Desktop Users" -Member user-1
Add-LocalGroupMember -Group Administrators -Member user-1
Write-Output '$url = New-SMPresignedDomainUrl -DomainId ${aws_sagemaker_domain.domain.id} -UserProfileName sagemaker-user-${each.value} -Region us-east-1' | Set-Content -Encoding Default 'C:\GetPresignedUrl.ps1'
Write-Output 'start microsoft-edge:$url' | Add-Content -Encoding Default 'C:\GetPresignedUrl.ps1' 
</powershell>
<persist>true</persist>
EOF
  tags = {
    Name = "ec2-indivisual-${each.value}"
  }
  depends_on = [
    aws_secretsmanager_secret.ec2_login_password,
    aws_secretsmanager_secret_rotation.ec2_login_password_secret_rotation,
    aws_iam_policy_attachment.ec2_get_secret,
    aws_secretsmanager_secret_rotation.ec2_login_password_secret_rotation
  ]

}

最初に Secrets Manager で OS にログインするためのパスワードを生成しています。このパスワードを用いて OS のユーザーを作成します。

EC2 から SageMaker Studio にアクセスすることもあり、OS は慣れている方が多い Windows を利用しています。
引数で、作成した VPC / Private Isolated Subnet を指定しています。role は後述の別途作成したロールをアタッチしています。

また、長め(?)の User Data を設定しています。User Data は EC2 初回起動時のみ (※) 動くスクリプトを設定できます。ここは今回の肝の 1 つなのですが、以下のことを行っています。

※例外はありますが今回は割愛します。

  • Windows OS 内のユーザー作成
    工藤さんや津郷さんが IAM User を用いてマネジメントコンソールにログイン後、 Fleet Manager から Windows OS にリモートデスクトップでログインする際に利用するユーザーを作成します。また、ユーザーのパスワードは秘匿情報なので、Secrets Manager から設定するパスワードを取得します。
    パスワードの取得方法は、AWS Tools for Powershell の Get-SECSecretValue を用いています。(EC2 の Windows AMI では、AWS Tools for Powershell が最初から使えるため)
  • 署名付き URL を発行して、ブラウザでその URL にアクセスするためのスクリプトを設置
    津郷さんや工藤さんが EC2 から SageMaker Studio にアクセスするためには、署名付き URL を発行する必要があります。署名付き URL は今後何度も発行するので、New-SMPresignedDomainUrl を用いて署名付き URL を発行する ps スクリプトを配置しています。また、署名付き URL を発行する、ということはそのままブラウザでアクセスする、ということなので、自動で Edge を起動して取得した URL にアクセスするようにしています。ユーザー (津郷さんや工藤さん) はこの ps スクリプトを実行して SageMaker Studio にアクセスします。

また、Terraform では、Secrets Manager にパスワードを登録する際、何も考慮しないと tfstate にパスワードが平文で記載されてしまうため、Secret Manager でダミーのパスワードを登録すると同時にパスワードローテーション用の Lambda を実装することでパスワードが tfstate に記載されないような実装を行なっています。

4-3-2. EC2 にアタッチするロールと必要なポリシー

EC2 で (Windows OS 内のユーザー作成に必要な) Secrets Manager へのアクセスと、SageMaker Studio の署名付き URL 発行するためのロールとポリシーが必要です。Fleet Manager でアクセスするためのポリシーをアタッチします。

CDK TypeScript の場合

lib/constructs/user.ts より抜粋

// インスタンス用 Role
const instanceRole = new iam.Role(this, `InstanceRole`, {
  assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName(
      'AmazonSSMManagedInstanceCore'
    ),
  ],
})

const instancePolicy = new iam.ManagedPolicy(this, 'InstancePolicy')

instancePolicy.addStatements(
    new iam.PolicyStatement(
        {
          effect: iam.Effect.ALLOW,
          actions: ["sagemaker:CreatePresignedDomainUrl"],
          resources: [
            userProfile.attrUserProfileArn,
          ],
        }
    )
)

instancePolicy.addStatements(
    new iam.PolicyStatement(
        {
          effect: iam.Effect.ALLOW,
          actions: ['secretsmanager:GetSecretValue'],
          resources: [
            ec2Secret.secretArn
          ]
        }
    )
)

instanceRole.addManagedPolicy(instancePolicy)

CDK Python の場合

chococones/constructs/user.py より抜粋

# インスタンス用 Role
instance_role = iam.Role(
    self,
    "InstanceRole",
    assumed_by=iam.ServicePrincipal("ec2.amazonaws.com"),
    managed_policies=[
        iam.ManagedPolicy.from_aws_managed_policy_name(
            "AmazonSSMManagedInstanceCore"
        )
    ],
)

instance_policy = iam.ManagedPolicy(self, "InstancePolicy")

instance_policy.add_statements(
    iam.PolicyStatement(
        effect=iam.Effect.ALLOW,
        actions=["sagemaker:CreatePresignedDomainUrl"],
        resources=[
            user_profile.attr_user_profile_arn,
        ],
    )
)

instance_policy.add_statements(
    iam.PolicyStatement(
        effect=iam.Effect.ALLOW,
        actions=['secretsmanager:GetSecretValue'],
        resources=[
            ec2_secret.secret_arn
        ]
    )
)

instance_role.add_managed_policy(instance_policy)

Terraform の場合

examples/main.tf より抜粋

# IAM role - 個人EC2用
resource "aws_iam_role" "ec2_role_indivisual" {
  for_each           = toset(var.user_list)
  name               = "ec2-role-${each.value}"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json
}

data "aws_iam_policy_document" "ec2_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

# IAM policy - SecretsManagerの値を取得
resource "aws_iam_policy" "ec2_get_secret" {
  for_each = toset(var.user_list)
  name     = "allow-get-${var.project_name}-ec2-login-password-${each.value}"
  policy   = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "secretsmanager:GetSecretValue"
      ],
      "Effect": "Allow",
      "Resource": ["arn:aws:secretsmanager:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:secret:${var.project_name}-ec2-login-password-${each.value}-??????"]
    }
  ]
}
EOF
}

resource "aws_iam_instance_profile" "ec2_isntanceprofile_indivisual" {
  for_each = toset(var.user_list)
  name     = "ec2-instanceprofile-${each.value}"
  role     = "ec2-role-${each.value}"
}

以上で User Data 実行時の Secrets Manager からの Windows に設定するパスワードの取得と、署名付き URL の発行と、EC2 に Fleet Manager からアクセスができるようになります。

4-4. EC2 にアクセスするための IAM User 周りの設定

前回は IAM User を使ってマネジメントコンソールから SageMaker Studio, S3, CodeCommit などにアクセスしましたが、今回はそもそも基本的にマネジメントコンソールを使用しない前提で、DX や VPN を引いている場合は直接リモートデスクトップで接続するため不要なものです。フリートマネージャーから EC2 にリモートデスクトップでアクセスするために必要最低限のポリシーだけをアタッチします。

CDK TypeScript の場合

lib/constructs/user.ts より抜粋

// IAM ユーザー
this.user = new iam.User(this, 'User', {
  userName: props.name,
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName('IAMUserChangePassword'),
    props.stdioCommonPolicy,
  ],
  password: secret.secretValue,
  passwordResetRequired: true,
})

// IAM ユーザーにアタッチする Fleet Manager を使うためのポリシー
const startSessionPolicyJson = {
  Version: '2012-10-17',
  Statement: [
    {
      Sid: 'EC2',
      Effect: 'Allow',
      Action: ['ec2:DescribeInstances', 'ec2:GetPasswordData'],
      Resource: '*',
    },
    {
      Sid: 'SSM',
      Effect: 'Allow',
      Action: [
        'ssm:DescribeInstanceProperties',
        'ssm:GetCommandInvocation',
        'ssm:GetInventorySchema',
      ],
      Resource: '*',
    },
    {
      Sid: 'SSMStartSession',
      Effect: 'Allow',
      Action: ['ssm:StartSession'],
      Resource: ['arn:aws:ssm:*::document/AWS-StartPortForwardingSession'],
      Condition: {
        BoolIfExists: {
          'ssm:SessionDocumentAccessCheck': 'true',
        },
      },
    },
    {
      Sid: 'AccessTaggedInstances',
      Effect: 'Allow',
      Action: ['ssm:StartSession'],
      Resource: [
        `arn:aws:ec2:*:${accountId}:instance/*`,
        `arn:aws:ssm:*:${accountId}:managed-instance/*`,
      ],
      Condition: {
        StringLike: {
          'ssm:resourceTag/Name': [nameTagValue],
        },
      },
    },
    {
      Sid: 'GuiConnect',
      Effect: 'Allow',
      Action: [
        'ssm-guiconnect:CancelConnection',
        'ssm-guiconnect:GetConnection',
        'ssm-guiconnect:StartConnection',
      ],
      Resource: '*',
    },
  ],
}

const startSessionPolicyDocument = iam.PolicyDocument.fromJson(
    startSessionPolicyJson
)
const startSessionManagedPolicy = new iam.ManagedPolicy(
    this,
    'StartSessionManagedPolicy',
    {
      document: startSessionPolicyDocument,
    }
)
startSessionManagedPolicy.attachToUser(this.user)

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("IAMUserChangePassword"),
    ],
    password=secret.secret_value,
    password_reset_required=True,
)
# IAM ユーザーにアタッチする Fleet Manager を使うためのポリシー
START_SESSION_POLICY_JSON = {
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EC2",
            "Effect": "Allow",
            "Action": ["ec2:DescribeInstances", "ec2:GetPasswordData"],
            "Resource": "*",
        },
        {
            "Sid": "SSM",
            "Effect": "Allow",
            "Action": [
                "ssm:DescribeInstanceProperties",
                "ssm:GetCommandInvocation",
                "ssm:GetInventorySchema",
            ],
            "Resource": "*",
        },
        {
            "Sid": "SSMStartSession",
            "Effect": "Allow",
            "Action": ["ssm:StartSession"],
            "Resource": [
                "arn:aws:ssm:*::document/AWS-StartPortForwardingSession"
            ],
            "Condition": {
                "BoolIfExists": {
                    "ssm:SessionDocumentAccessCheck": "true",
                }
            },
        },
        {
            "Sid": "AccessTaggedInstances",
            "Effect": "Allow",
            "Action": ["ssm:StartSession"],
            "Resource": [
                f"arn:aws:ec2:*:{account_id}:instance/*",
                f"arn:aws:ssm:*:{account_id}:managed-instance/*",
            ],
            "Condition": {
                "StringLike": {
                    "ssm:resourceTag/Name": [NAME_TAG_VALUE],
                }
            },
        },
        {
            "Sid": "GuiConnect",
            "Effect": "Allow",
            "Action": [
                "ssm-guiconnect:CancelConnection",
                "ssm-guiconnect:GetConnection",
                "ssm-guiconnect:StartConnection",
            ],
            "Resource": "*",
        },
    ],
}
start_session_policy_document = iam.PolicyDocument.from_json(
    START_SESSION_POLICY_JSON
)
start_session_managed_policy = iam.ManagedPolicy(
    self,
    "StartSessionManagedPolicy",
    document=start_session_policy_document,
)
start_session_managed_policy.attach_to_user(self.user)

Terraform の場合

examples/main.tf より抜粋

# IAM User
module "user" {
  source            = "../modules/iam/user"
  for_each          = toset(var.user_list)
  aws_iam_username  = "${var.project_name}-${each.value}"
  aws_iam_grouppath = "/users/"

}

# 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 Role SSM利用権限
resource "aws_iam_policy_attachment" "access_session_manager" {
  name       = "access-own-profile"
  for_each   = toset(var.user_list)
  roles      = ["ec2-role-${each.value}"]
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  depends_on = [aws_iam_role.ec2_role_indivisual]
}
resource "aws_iam_policy_attachment" "ec2_get_secret" {
  name       = "get-login-password"
  for_each   = toset(var.user_list)
  roles      = ["ec2-role-${each.value}"]
  policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/allow-get-${var.project_name}-ec2-login-password-${each.value}"
  depends_on = [aws_iam_role.ec2_role_indivisual]
}
# IAM policy - 個人StartSession用
resource "aws_iam_policy" "user_access_only_own_instance" {
  for_each = toset(var.user_list)
  name     = "user-access-only-own-ec2-${each.value}"
  policy   = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "EC2",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:GetPasswordData"
            ],
            "Resource": "*"
        },
        {
            "Sid": "SSM",
            "Effect": "Allow",
            "Action": [
                "ssm:DescribeInstanceProperties",
                "ssm:GetCommandInvocation",
                "ssm:GetInventorySchema"
            ],
            "Resource": "*"
        },
        {
            "Sid": "SSMStartSession",
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession"
            ],
            "Resource": [
                "arn:aws:ssm:*::document/AWS-StartPortForwardingSession"
            ],
            "Condition": {
                "BoolIfExists": {
                    "ssm:SessionDocumentAccessCheck": "true"
                }
            }
        },
        {
            "Sid": "AccessTaggedInstances",
            "Effect": "Allow",
            "Action": [
                "ssm:StartSession"
            ],
            "Resource": [
                "arn:aws:ec2:*:${data.aws_caller_identity.current.account_id}:instance/*",
                "arn:aws:ssm:*:${data.aws_caller_identity.current.account_id}:managed-instance/*"
            ],
            "Condition": {
                "StringLike": {
                    "ssm:resourceTag/Name": [
                        "ec2-indivisual-${each.value}"
                    ]
                }
            }
        },
        {
            "Sid": "GuiConnect",
            "Effect": "Allow",
            "Action": [
                "ssm-guiconnect:CancelConnection",
                "ssm-guiconnect:GetConnection",
                "ssm-guiconnect:StartConnection"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

これで、IAM User のパスワード変更 (初回ログイン時にパスワード変更を強制するため) と、フリートマネージャー経由で EC2 にリモートデスクトップするのに必要なポリシーだけをアタッチできました。実装は少し長くなってしまっていますが、ほとんどはフリートマネージャーで自分用の EC2 にアクセスするためのポリシー設定です。

大事なことなので 2 回書きますが、DX や VPN を引いている場合はこれらのリソースは不要です。

4-5. AWS CodeArtifact 周りの設定

今回のアーキテクチャーではインターネットから pip install することを禁じており、ライブラリを追加したい場合は CodeArtifact 経由でインストールします。

4-5-1. CodeArtifact のドメインとリポジトリの作成

まずは CodeArtifact のリソースを作成します。

CDK TypeScript の場合

lib/chococones-stack.ts より抜粋

const codeartifactDomain = new codeartifact.CfnDomain(
  this,
  'codeartifactDomain',
  {
    domainName: codeartifactRepositoryName,
  }
)

const codeartifactRepository = new codeartifact.CfnRepository(
  this,
  'codeartifactRepository',
  {
    domainName: codeartifactDomain.domainName,
    repositoryName: codeartifactRepositoryName,
    externalConnections: ['public:pypi'],
  }
)

codeartifactRepository.addDependency(codeartifactDomain)

CDK Python の場合

chococones/chococones.py より抜粋

code_artifact_domain = codeartifact.CfnDomain(
    self,
    "codeartifactDomain",
    domain_name=CODE_ARTIFACT_DOMAIN_NAME,
)

code_artifact_repository = codeartifact.CfnRepository(
    self,
    "codeartifactRepository",
    domain_name=code_artifact_domain.domain_name,
    repository_name=CODE_ARTIFACT_REPOSITORY_NAME,
    external_connections=["public:pypi"],
)

code_artifact_repository.add_dependency(code_artifact_domain)

Terraform の場合

examples/main.tf より抜粋

# CodeArtifact
resource "aws_codeartifact_domain" "codeartifact_domain" {
  domain = var.project_name
}

resource "aws_codeartifact_repository" "codeartifact_repository" {
  repository = var.project_name
  domain     = aws_codeartifact_domain.codeartifact_domain.domain
  external_connections {
    external_connection_name = "public:pypi"
  }
}

今回は Python を使う前提なので pypi を指定していますが、npmmaven などを使う場合はリストに記載してください。
CDK では、CodeArtifact の ドメインができる前にリポジトリを作ろうとするとエラーで落ちてしまうため、add_dependency でドメイン作成が先になるよう強制しています。

4-5-2. SageMaker Studio から CodeArtifact にアクセスするためのポリシー

最後に CodeArtifact へアクセスするためのポリシーを設定します。ここで設定したポリシーを SageMaker Studio の User Profile のロールにアタッチします。SageMaker Studio から CodeArtifact には pip install (と事前に必要な認証) するのに必要な権限に絞っています。

CDK TypeScript の場合

lib/chococones-stack.ts より抜粋

// 全員の SageMaker Studio Profile につける共用 Policy
const studioCommonPolicy = new iam.ManagedPolicy(this, 'CommonPolicy')
// 共用 CodeArtifact リポジトリへのアクセス許可
studioCommonPolicy.addStatements(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
      'codeartifact:DescribeDomain',
      'codeartifact:DescribeRepository',
      'codeartifact:GetAuthorizationToken',
      'codeartifact:GetRepositoryEndpoint',
      'codeartifact:GetRepositoryPermissionsPolicy',
      'codeartifact:ListPackages',
      'codeartifact:ListRepositories',
      'codeartifact:ListTagsForResource',
      'codeartifact:ReadFromRepository',
    ],
    resources: [codeartifactDomain.attrArn, codeartifactRepository.attrArn],
  })
)
studioCommonPolicy.addStatements(
  new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: ['sts:GetServiceBearerToken'],
    resources: ['*'],
    conditions: {
      StringEquals: {
        'sts:AWSServiceName': 'codeartifact.amazonaws.com',
      },
    },
  })
)

CDK Python の場合

chococones/chococones.py より抜粋

# 全員の SageMaker Studio Profile につける共用 Policy
studio_common_policy = iam.ManagedPolicy(self, "StudioCommonPolicy")
# 共用 CodeArtifact リポジトリへのアクセス許可
studio_common_policy.add_statements(
    iam.PolicyStatement(
        effect=iam.Effect.ALLOW,
        actions=[
            "codeartifact:DescribeDomain",
            "codeartifact:DescribeRepository",
            "codeartifact:GetAuthorizationToken",
            "codeartifact:GetRepositoryEndpoint",
            "codeartifact:GetRepositoryPermissionsPolicy",
            "codeartifact:ListPackages",
            "codeartifact:ListRepositories",
            "codeartifact:ListTagsForResource",
            "codeartifact:ReadFromRepository",
        ],
        resources=[
            code_artifact_domain.attr_arn,
            code_artifact_repository.attr_arn,
        ],
    )
)
studio_common_policy.add_statements(
    iam.PolicyStatement(
        effect=iam.Effect.ALLOW,
        actions=["sts:GetServiceBearerToken"],
        resources=["*"],
        conditions={
            "StringEquals": {"sts:AWSServiceName": "codeartifact.amazonaws.com"}
        },
    )
)

Terraform の場合

examples/main.tf より抜粋

# IAM policy document for access shared CodeArtifact
data "aws_iam_policy_document" "codecartifact_access" {
  statement {
    actions=[
        "codeartifact:DescribeDomain",
        "codeartifact:DescribeRepository",
        "codeartifact:GetAuthorizationToken",
        "codeartifact:GetRepositoryEndpoint",
        "codeartifact:GetRepositoryPermissionsPolicy",
        "codeartifact:ListPackages",
        "codeartifact:ListRepositories",
        "codeartifact:ListTagsForResource",
        "codeartifact:ReadFromRepository",
    ]
    resources = [aws_codeartifact_domain.codeartifact_domain.arn, aws_codeartifact_repository.codeartifact_repository.arn]
    effect    = "Allow"
  }
  statement {
    actions   = ["sts:GetServiceBearerToken"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "sts:AWSServiceName"
      values   = ["codeartifact.amazonaws.com"]
    }
    effect = "Allow"
  }
}

# IAM policy for access shared CodeArtifact
resource "aws_iam_policy" "allow_codeartifact_access" {
  name   = "allow-access-codeartifact-repository"
  policy = data.aws_iam_policy_document.codecartifact_access.json
}

5. おわりに

「いやーきのこの山を閉域に閉じ込めることに成功しました。」

津郷「これでセキュアに SageMaker Studio を使えますね。」

工藤「この SageMaker Studio IaC シリーズは一旦おしまいの予定ですが、また面白いセキュリティ要件とか出てきたら紹介したいですね。」

「さて、まとめましょう。」

前回の記事 (case01) では、IaC を用いて SageMaker Studio とその周辺の AWS リソースを管理することで、運用が楽になりスケールできることを紹介しました。それに加え、今回 (case02) はより強固なセキュリティを求めて、(DX や VPN を用いれば) インターネット外に通信が出ていかないアーキテクチャーを IaC で実装しました。

実際に利用する際は濃淡があり、case01 と case02 の間 (あるいは case02 より更に強固な)に皆様にフィットするアーキテクチャーがあるかと思いますので、今回紹介した記事 / コードを参考に切った貼ったして最適化していただけると幸いです。

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

選択
  • 選択
  • たけのこの里が好きな 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 や自動化を好み、関連するサービスをよく利用します。

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

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