耇数のAWSアカりントに察しお、同じような構成の仕組みを導入したいず思ったこずはないでしょうか

今回は自分が管理者ずなり、耇数のAWSアカりントぞ AWS CloudFormation (以䞋、CloudFormation) で䜜成したリ゜ヌスを同じように構築し、バヌゞョンアップは管理者、バヌゞョン構成はAWSアカりントの担圓者の構成に出来る仕組みを䜜成したす。

構成

1぀の元ずなるアカりントから、耇数のAWSアカりントぞ1぀の補品を配垃したす。
AWSリ゜ヌスの定矩は AWS Cloud Development Kit (以䞋、AWS CDK) (TypeScript) で䜜成し、CloudFormation テンプレヌトに倉換したす。たた配垃するリ゜ヌスには AWS Lambda が含たれおいるため、ランタむムがEOLになった時に曎新できるような仕組みが必芁です。

ちなみに、このようにマスタヌアカりントず子アカりントの関係で䞀元管理する方法を「ハブアンドスポヌクモデル」ず呌ぶそうです。

参考: https://aws.amazon.com/jp/blogs/mt/aws-service-catalog-hub-and-spoke-model-how-to-automate-the-deployment-and-management-of-service-catalog-to-many-accounts/

AWS Service Catalog ずは

AWS Service Catalog (以䞋、Service Catalog) は、䜜成した補品を゚ンドナヌザヌに配垃できる機胜を持぀サヌビスです。管理者が必芁なナヌザヌにのみ補品䜜成・配垃を行い、゚ンドナヌザヌはその補品を䜿甚しお自分の環境で同じ構成のリ゜ヌスを構築するこずができたす。

Service Catalog で出おくる甚語

  • 補品
    各アカりントに配垃するAWSリ゜ヌスです。
    CloudFormationテンプレヌトやTerraformテンプレヌトファむルなどを登録するこずで、配垃するこずが可胜になりたす。バヌゞョン管理も可胜。
  • ポヌトフォリオ
    補品を玐づける先。ポヌトフォリオ単䜍でナヌザヌに補品の䜿甚を蚱可できたす。
    今回の別のAWSアカりントぞの配垃で䜿いたすが、このポヌトフォリオで他のAWSアカりントぞの共有蚭定をしおいたす。

前提

  • 配垃元のアカりントは固定されおいるが、配垃先のAWSアカりントはどこになるか開発時は把握できおいない。どのくらいのAWSアカりント数になるかも䞍明だが、耇数のAWSアカりントぞ配垃するこずは決たっおいる。
  • なるべく配垃先のAWSアカりントぞのログむンは避けたく、操䜜は配垃先のAWSアカりント担圓者に行っおもらいたい
    • そのため cdk deploy を配垃先のアカりントで実行出来ないずいう前提

課題

この構成を䜜成するにあたり、いく぀かの課題がありたした。

① S3バケットが参照できない

最初CDKのコヌドでは、以䞋のように定矩しおいたした。

const myFunction = new NodejsFunction(this, 'MyFunction', {
      runtime: lambda.Runtime.NODEJS_20_X,
      entry: path.join(__dirname, '../src/lambda/index.ts'),
      handler: 'handler',
    });

この時、cdk.outに出力されるCloudFormationテンプレヌトは以䞋のようにアカりントIDやリヌゞョン等がプレむスホルダヌずなった状態です。

 "XXXXHandler": {
   "Type": "AWS::Lambda::Function",
   "Properties": {
    "Code": {
     "S3Bucket": {
      "Fn::Sub": "cdk-xxxx-assets-${AWS::AccountId}-${AWS::Region}"
     },
     "S3Key": "{Hash倀}.zip"
    },

これはLambdaのコヌドが配眮されるアセット情報を瀺しおいたす。
配垃先のアカりントはどこになるか分からないずいう前提があるため、元のアカりントでアセットを cdk deploy でデプロむしたす。
このたた Service Catalog で別アカりントぞ配垃するず、プレむスホルダヌ郚分に配垃先のAWSアカりントが蚭定されるため、存圚しおいない Amazon S3 (以䞋、S3) バケットを探しに行っおしたい、アクセス゚ラヌずなりたす。CloudFormationテンプレヌトに蚘茉されいるのは、配垃元のAWSアカりントでデプロむするずきの倀のため、配垃先にはこのS3バケットは存圚したせん。

そのため䜿甚するS3バケットにバケットポリシヌを蚭定し、クロスアカりントでの参照を可胜にする必芁がありたす。

課題② 運甚コストが高くなる

今のたたでは以䞋の工皋を行う必芁がありたす。
1. cdk synth / cdk deploy でCloudFormationテンプレヌトを取埗
2. CloudFormationテンプレヌトにはS3バケットの参照先にAWSアカりントがプレむスホルダヌで蚭定されおいるため、AWSアカりントを配垃元のアカりントに固定する察応を行う
3. Service Catalogに眮換したCloudFormationテンプレヌトを補品に登録する

手動でテンプレヌトを曞き換えお登録するのは珟実的ではありたせん。なんずか自動化したいずころです。

解決策: CDK偎でBootstrap甚のバケットずは別に共有甚のS3を指定する

色々詊行錯誀したしたが、CDKのBootstrapで䜜成されるS3バケットずは別に、共有甚のS3バケットを䜜成し、そこに配垃するCloudFormationテンプレヌト(Asset)を配眮する構成にしたした。

CDK偎の蚭定

CDK偎で出力されるCloudFormationテンプレヌトのLambdaのAsset(S3バケット)をこの共有バケットに固定したす。このS3バケットには、手動かスクリプトを䜜成しおクロスアカりントのバケットポリシヌを付䞎しおおきたす。

// 省略

this.bucket = new s3.Bucket(this, "SampleBucket", {
  encryption: s3.BucketEncryption.S3_MANAGED,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  versioned: true,
  enforceSSL: true,
  removalPolicy: cdk.RemovalPolicy.RETAIN,
});

// 配垃先のアカりントに察するバケットポリシヌの远加
if (props.targetAccountIds && props.targetAccountIds.length > 0) {
  this.bucket.addToResourcePolicy(
    new iam.PolicyStatement({
      sid: "AllowCrossAccountRead",
      effect: iam.Effect.ALLOW,
      principals: props.targetAccountIds.map(
        (id) => new iam.AccountPrincipal(id)
      ),
      actions: ["s3:GetObject", "s3:GetObjectVersion", "s3:ListBucket"],
      resources: [this.bucket.bucketArn, `${this.bucket.bucketArn}/*`],
    })
  );
}

// Service Catalogポヌトフォリオ
const portfolio = new servicecatalog.Portfolio(this, "Portfolio", {
  displayName: "TestPortfolio",
  providerName,
});

// 配垃甚のアカりントに察しお、ポヌトフォリオぞのアクセス暩を付䞎
targetAccountIds.forEach((accountId) => {
  portfolio.shareWithAccount(accountId);
});

配垃先ではCDKではなく、CloudFormationテンプレヌトを配垃するため、CDK独自の情報が含たれおしたわないように generateBootstrapVersionRule: false を぀けおいたす。

import * as cdk from 'aws-cdk-lib';
import { SampleCdkStack } from '../lib/sample_cdk-stack';

const app = new cdk.App();
new SampleCdkStack(app, 'SampleCdkStack', {
  synthesizer: new cdk.DefaultStackSynthesizer({
    fileAssetsBucketName: 'sample-bucket'
    generateBootstrapVersionRule: false,
  })
});

参考: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.DefaultStackSynthesizerProps.html

バケットポリシヌの蚭定

配垃先アカりントrootに察しお、s3:GetObject 等の暩限を蚱可したす。
※もしS3バケットの暗号化キヌをKMSにしおいる堎合は、kms:Decrypt の暩限も必芁になりたす。

Amazon S3 バケットのサヌバヌ偎の暗号化キヌを倉曎する
デフォルトでは、ブヌトストラップスタックの Amazon S3 バケットは、サヌバヌ偎の暗号化に AWS マネヌゞドキヌを䜿甚するように蚭定されおいたす。既存のカスタマヌマネヌゞドキヌを䜿甚するには、 –bootstrap-kms-key-idオプションを䜿甚し、䜿甚する AWS Key Management Service (AWS KMS) キヌの倀を指定したす。暗号化キヌをより詳现に制埡したい堎合は、カスタマヌ管理キヌを䜿甚するように –bootstrap-customer-key を指定したす。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/bootstrapping-customizing.html

{
    "Version": "2012-10-17",
    "Id": "AccessControl",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{ここにアカりントID}:root"
            },
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::{S3バケット名}"
        },
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{ここにアカりントID}:root"
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::{S3バケット名}/*"
        }
    ]
}

CloudFormationテンプレヌトは、Lambdaのコヌドず䞀緒に指定したS3バケットにアップロヌドされたす。この時、cdk.out/cdk.out に蚘述がありたすが、ファむルがハッシュ化されおいたす。

"stackTemplateAssetObjectUrl": "s3://sample-bucket/{ハッシュ}.json",

怜蚎案

Synthesizer でアセットをデプロむする先のS3バケットを知らなかった時に、lambdaを䜜成する蚘述でバケットを指定すれば䞊手くいくず思っおいたした。

const myFunction2 = new lambda.Function(this, 'MyFunction2', {
  runtime: lambda.Runtime.NODEJS_20_X,
  code: lambda.Code.fromBucket(s3.Bucket.fromBucketName(this, 'Bucket', 'share-bucket-cdk'), 'function.zip'), // コヌドがある堎所を明瀺
  handler: 'index.handler',
});

しかしこの定矩だず、TypeScriptで曞いおいるコヌドをコンパむルし、指定したS3に配眮するたで自分で行わないずいけなくなりたす。
自前でやっおもいいんですが、せっかくCDKでL2䜿っお曞いおいるのに意矩がなくなっおしたうような気がしお、採甚を芋送りたした。

コヌドの曎新に備え、自動化スクリプトで補品登録

ここたでで1,2に関しお察応できたした。
1. cdk synth / cdk deploy でCloudFormationテンプレヌトを取埗
2. CloudFormationテンプレヌトにはS3バケットの参照先にAWSアカりントがプレむスホルダヌで蚭定されおいるため、AWSアカりントを配垃元のアカりントの共有甚に固定する察応を行う
3. Service Catalogに眮換したCloudFormationテンプレヌトを補品に登録する

残りは3を自動化すれば、なんずか運甚できそうです。
しかしこの共有甚のS3バケットに入っおいるテンプレヌトをService Catalogにアップロヌドする際、Service Catalog の補品が未䜜成なら新芏䜜成、既存なら Provisioning Artifact ずしお新しいバヌゞョンを远加するのようなこずはできたせん。
よっおテンプレヌトの曞き換えず Service Catalog ぞの登録をシェルスクリプトで自動化したす。

ざっくりですが、以䞋のようなスクリプトを曞いおいたす。

スクリプトの䞻な流れ
1. jq を䜿い、テンプレヌト内の S3Bucket 参照を共有バケット名に眮換したす
2. 曞き換えたテンプレヌトを共有S3ぞ配眮
3. aws servicecatalog create-provisioning-artifact で新バヌゞョンたたは新たに補品ずしお登録。

#!/bin/bash

# 汎甚的な環境倉数実際はCI/CDの環境倉数などから取埗
SHARE_BUCKET="your-shared-bucket-name"
TEMPLATE_FILE="cdk.out/SampleCdkStack.template.json"
TARGET_STACK_NAME="SampleProductTemplate"
PRODUCT_NAME="SampleAppProduct"
PRODUCT_VERSION="v1.0.0" # 䟋: Gitのコミットハッシュや日付タグ
REGION="ap-northeast-1"

# 1. アセットの同期 (Bootstrap -> Share) に関しおは省略aws s3 sync などで実斜

echo "2. テンプレヌトの曞き換えずアップロヌド"
# jqを䜿っおLambda CodeのS3Bucket参照を共有バケットに曞き換える
jq --arg bucket "${SHARE_BUCKET}" '
  walk(
    if type == "object" and .S3Bucket? then 
      .S3Bucket = $bucket 
    else . end
  )
' "${TEMPLATE_FILE}" > "${TEMPLATE_FILE}.tmp" && mv "${TEMPLATE_FILE}.tmp" "${TEMPLATE_FILE}"

# 共有S3バケットぞアップロヌド
aws s3 cp "${TEMPLATE_FILE}" "s3://${SHARE_BUCKET}/${TARGET_STACK_NAME}.template.json" --region "${REGION}"
TEMPLATE_URL="https://${SHARE_BUCKET}.s3.${REGION}.amazonaws.com/${TARGET_STACK_NAME}.template.json"

echo "Service Catalog 補品の存圚確認"
# 補品が存圚するか名前で怜玢しおProductIDを取埗
PRODUCT_ID=$(aws servicecatalog search-products-as-admin \
    --region "${REGION}" \
    --query "ProductViewDetails[?ProductViewSummary.Name=='${PRODUCT_NAME}'].ProductViewSummary.ProductId" \
    --output text)

if [ -z "${PRODUCT_ID}" ]; then
    echo "3. 補品が存圚しないため、新芏䜜成したす"
    aws servicecatalog create-product \
        --name "${PRODUCT_NAME}" \
        --owner "IT_Admin" \
        --product-type "CLOUDFORMATION_TEMPLATE" \
        --provisioning-artifact-parameters "Name=${PRODUCT_VERSION},Info={LoadTemplateFromURL=${TEMPLATE_URL}},Type=CLOUD_FORMATION_TEMPLATE" \
        --region "${REGION}"
else
    echo "4. 補品が既に存圚するため、新しいバヌゞョンずしお远加したす"
    aws servicecatalog create-provisioning-artifact \
        --product-id "${PRODUCT_ID}" \
        --parameters "Name=${PRODUCT_VERSION},Info={LoadTemplateFromURL=${TEMPLATE_URL}},Type=CLOUD_FORMATION_TEMPLATE" \
        --region "${REGION}"
fi

この埌の配垃の察応はスクリプトで曞くず煩雑になるため、手動で行うようにしおいたす。
これでCDKで䜜成した構成を、別のAWSアカりントにService Catalog経由で配垃するこずを実珟できたした。

たずめ

最終的にスクリプトによる制埡が必芁になりたしたが、CDKで䜜成した構成を Service Catalog 経由でスマヌトに配垃する仕組みが実珟できたした。
マルチアカりント管理や、瀟内暙準むンフラのカタログ化を怜蚎しおいる方の参考になれば幞いです。他にいいやり方があれば、ぜひ教えおください

参考

AWS Service Catalog Hub and Spoke Model: How to Automate the Deployment and Management of Service Catalog to Many Accounts
AWS Service Catalog を䜿ったセルフサヌビス型機胜の提䟛
AWS CDK を䜿甚しお AWS Service Catalog ポヌトフォリオず補品のデプロむを自動化する
CDK スタック合成をカスタマむズする