CDKで作成したスタックをCloudFormation StackSetsで展開する
こんにちは。たかやまです。
こちらのブログにある通り、CDKを使ってCloudFormation(CFn) StackSetsを展開することができます。
ブログでは事前に準備したCFnテンプレートをCfnStackSet
で展開する方法をご紹介しています。
CDKを使っている方にはCDKで作成したスタックをCFn StackSetsで展開したいという方もいると思うので、今回はCDKで作成したスタックをCFn StackSetsで展開する方法ご紹介したいと思います。
さきにまとめ
- 展開したいCDKスタックを
stage.synth().stacks[0].template
でCFnテンプレート変換することでL1コンストラクトCfnStackSet
で展開可能 - CDK Bootstrapを利用するL2コンストラクトの拡張機能は展開先アカウントで利用できない
- S3のautoDeleteObjectsオプションなど
- 複数のCFn StackSetsを管理する場合はテンプレートスタックにプレフィックスをつけて配列制御する
やってみた
サンプルコードはこちらです。
CDKコード紹介
ディレクトリ構成はこちらです。
. ├── bin │ └── stacksets.ts ├── lib │ ├── template │ │ └── cloudtrail-stack.ts │ └── stacksets.ts ├── test │ └── stacksets.test.ts ├── README.md ├── cdk.json ├── jest.config.js ├── package-lock.json ├── package.json └── tsconfig.json
lib/stacksets.ts
lib配下のスタックについてご紹介します。
lib/stacksets.tsはCFn StackSetsを作成するスタックです。
import * as cdk from 'aws-cdk-lib'; import * as cfn from 'aws-cdk-lib/aws-cloudformation'; import { Construct } from 'constructs'; export interface props extends cdk.StackProps { stackSetsName: string; regions: string[]; accounts?: string[]; organizationalUnitIds?: string[]; templateBody: string; } export class StackSets extends cdk.Stack { constructor(scope: Construct, id: string, props: props) { super(scope, id, props); new cfn.CfnStackSet(this, 'Stackset', { permissionModel: props.accounts !== undefined ? 'SELF_MANAGED' : 'SERVICE_MANAGED', stackSetName: `${props.stackSetsName}`, autoDeployment: props.accounts !== undefined ? undefined : { enabled: true, retainStacksOnAccountRemoval: false, }, capabilities: ['CAPABILITY_NAMED_IAM'], stackInstancesGroup: [ { regions: props.regions, deploymentTargets: props.accounts !== undefined ? { accounts: props.accounts } : { organizationalUnitIds: props.organizationalUnitIds }, }, ], templateBody: props.templateBody, }); } }
CFn StackSetsはOrganizations配下以外にデプロイするSELF_MANAGED
と、Organizations配下にデプロイするSERVICE_MANAGED
があります。SERVICE_MANAGEDとSELF_MANAGEDで指定する必須パラメーターが変わります。ここではaccounts
パラメータの指定によってSELF_MANAGEDかSERVICE_MANAGEDかを切り替えています。
lib/template/cloudtrail-stack.ts
lib/tmplate配下にはCFn StackSetsで展開するスタックのテンプレートを定義しています。
ここでは各アカウントにCloudTrailとS3を作成するテンプレートを定義しています。
import * as cdk from 'aws-cdk-lib'; import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; export class OrganizationsCloudtrailStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); /** * Create S3 */ const bucket = new s3.Bucket(this, 'Bucket', { blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, bucketName: `cloudtrail-${cdk.Aws.ACCOUNT_ID}-${cdk.Aws.REGION}`, encryption: s3.BucketEncryption.S3_MANAGED, removalPolicy: cdk.RemovalPolicy.RETAIN, }); /** * Create CloudTrail */ new cloudtrail.Trail(this, 'CloudTrail', { bucket: bucket, enableFileValidation: true, isMultiRegionTrail: true, sendToCloudWatchLogs: false, trailName: `cloudtrail-${cdk.Aws.ACCOUNT_ID}`, }); } }
CFn StackSetsでは展開するテンプレートのリソース名に重複を避けるため、アカウントIDやリージョン名をリソース名に含めることがあると思います。CDKではcdk.Aws.ACCOUNT_ID
やcdk.Aws.REGION
を使うことで、デプロイ時に動的に値を設定することができます。
生成されるテンプレートにCFnでいう疑似パラメータが設定されます。
一点注意点として、CDKを使っているとコンストラクトで拡張された機能を使いたくなると思いますがこちらは利用できません。(例えばS3のオブジェクトを自動削除するautoDeleteObjects
オプションなど)
コンストラクトで拡張された機能は管理アカウントのCDK Bootstrapのリソースを利用する必要があるため、CFn StackSetsで展開するアカウントのようにCDK Bootstrapを利用できないアカウントには適用できないためご注意ください。
bin/stacksets.ts
bin配下に実際にデプロイするCDKアプリを定義していきます。
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import 'source-map-support/register'; import { StackSets } from '../lib/stacksets'; import { CloudtrailStack } from '../lib/template/cloudtrail-stack'; const app = new cdk.App(); const stage = new cdk.Stage(app, 'template'); const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; new CloudtrailStack(stage, 'CloudTrailStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); const template1 = stage.synth().stacks[0].template; new StackSets(app, 'CloudTrailStackSet', { stackSetsName: 'cloudtrail-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template1), });
CFn StackSetsのCDKアプリケーションを定義する前に、14-17行目でCDKでコーディングしたlib/template/cloudtrail-stack.ts
をCDKのStage環境として定義し、CFnテンプレートに変換(synth)しています。
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import 'source-map-support/register'; import { StackSets } from '../lib/stacksets'; import { CloudtrailStack } from '../lib/template/cloudtrail-stack'; const app = new cdk.App(); const stage = new cdk.Stage(app, 'template'); const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; new CloudtrailStack(stage, 'CloudTrailStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); const template1 = stage.synth().stacks[0].template; new StackSets(app, 'CloudTrailStackSet', { stackSetsName: 'cloudtrail-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template1), });
また、CFn StackSetsで展開するテンプレートはgenerateBootstrapVersionRule
をfalseに設定してください。こちらを設定しないとCFnテンプレート変換時にCDK BootstrapのバージョンチェックのRulesが追加されます。
CFn StackSetsの展開先アカウントはCDK Bootstrapを利用できないため、バージョンチェックを行うとデプロイが失敗するので、こちらのオプションを追加してバージョンチェックのRulesが追加されないようにしておきます。
CDK Bootstrapのバージョンチェックを行うRulesと設定したときのエラーメッセージ
Parameters: BootstrapVersion: Type: AWS::SSM::Parameter::Value<String> Default: /cdk-bootstrap/hnb659fds/version Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip] Rules: CheckBootstrapVersion: Assertions: - Assert: Fn::Not: - Fn::Contains: - - "1" - "2" - "3" - "4" - "5" - Ref: BootstrapVersion AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.
エラーメッセージ
Resource handler returned message: "Resource of type 'Stack set operation [50c1c093-936b-4715-a7da-032bab5a20ee] was unexpectedly stopped or failed. status reason(s): [Unable to fetch parameters [/cdk-bootstrap/hnb659fds/version] from parameter store for this account.]' with identifier 'cloudtrail-stack:e8c388fb-d905-4000-a13d-78532277d1eb' did not stabilize." (RequestToken: ba151e54-f653-c612-13dc-35815c21d9fa, HandlerErrorCode: NotStabilized)
展開するテンプレートの準備ができたら、CFn StackSetsのtemplateBody
に値を渡すことで、CFn StackSetsのテンプレートとして利用できます。
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import 'source-map-support/register'; import { StackSets } from '../lib/stacksets'; import { CloudtrailStack } from '../lib/template/cloudtrail-stack'; const app = new cdk.App(); const stage = new cdk.Stage(app, 'template'); const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; new CloudtrailStack(stage, 'CloudTrailStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); const template1 = stage.synth().stacks[0].template; new StackSets(app, 'CloudTrailStackSet', { stackSetsName: 'cloudtrail-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template1), });
デプロイ
こちらのCDKアプリケーションをデプロイしてみます。
cdk ls
をするとテンプレートとして利用されるStageのスタックとCFn StackSetsとして機能するAppのスタックが表示されます。
> npx cdk ls CloudTrailStackSet template/CloudTrailStack
デプロイはCFn StackSetsとして機能するAppのスタックをデプロイします。
npx cdk deploy CloudTrailStackSet
デプロイすると管理アカウントに、CFn StackSetsを展開するためのCFnが展開されます。
StackSetsコンソールにデプロイしたいスタックが展開されていればデプロイ完了です。
複数のCFn StackSetsをデプロイ
CFn StackSetsを複数展開する場合は、bin/stacksets.ts
に複数のStackSetsを定義します。
ここではVPCを展開するテンプレートをlib/template
に追加してみます。(お金はかけたくないのでNAT Gatwayは0で...)
import * as cdk from 'aws-cdk-lib'; import * as vpc from 'aws-cdk-lib/aws-ec2'; import { Construct } from 'constructs'; export class VpcStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); /** * Create VPC */ new vpc.Vpc(this, 'VPC', { natGateways: 0, }); } }
bin/stacksets.ts
に追加したVPCテンプレートを展開するCFn StackSetsの定義を追加していきます。
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import 'source-map-support/register'; import { StackSets } from '../lib/stacksets'; import { CloudtrailStack } from '../lib/template/cloudtrail-stack'; import { VpcStack } from '../lib/template/vpc-stack'; const app = new cdk.App(); const stage = new cdk.Stage(app, 'template'); const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; new CloudtrailStack(stage, 'CloudTrailStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); new VpcStack(stage, 'VpcStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); const template1 = stage.synth().stacks[0].template; new StackSets(app, 'CloudTrailStackSet', { stackSetsName: 'cloudtrail-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template1), }); const template2 = stage.synth().stacks[1].template; new StackSets(app, 'VpcStackSet', { stackSetsName: 'vpc-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template2), });
定義を見ていただくとわかる通り、CFn StackSetsに渡すテンプレート内容はstage.synth().stacks
の配列を指定して制御しています。
ただ、この配列の順番ですが、bin/stacksets.ts
の定義順に格納されているかというとそうではなく、Stage環境のスタック名の辞書式順序に格納されています。ここでは都合よくCloudTrailStack
とVpcStack
の定義順序と辞書式順序が一致しているので問題ありませんが、なるべくStageのスタック名はプレフィックスでナンバリングしておくと良いと思います。
#!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; import 'source-map-support/register'; import { StackSets } from '../lib/stacksets'; import { CloudtrailStack } from '../lib/template/cloudtrail-stack'; import { VpcStack } from '../lib/template/vpc-stack'; const app = new cdk.App(); const stage = new cdk.Stage(app, 'template'); const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }; new CloudtrailStack(stage, '01-CloudTrailStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); new VpcStack(stage, '02-VpcStack', { synthesizer: new cdk.DefaultStackSynthesizer({ generateBootstrapVersionRule: false }), }); const template1 = stage.synth().stacks[0].template; new StackSets(app, 'CloudTrailStackSet', { stackSetsName: 'cloudtrail-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template1), }); const template2 = stage.synth().stacks[1].template; new StackSets(app, 'VpcStackSet', { stackSetsName: 'vpc-stack', env: env, organizationalUnitIds: ['ou-omcc-xxxxxxxx'], regions: ['ap-northeast-1'], templateBody: JSON.stringify(template2), });
stage.synth().stacks
の配列サンプルを載せておくので、気になる方はご覧ください。
スタック名01-CloudTrailStack/02-VpcStackのときのstage.synth().stacks
の配列
[ CloudFormationStackArtifact { assembly: CloudAssembly { directory: 'cdk.out/assembly-template', manifest: [Object], version: '31.0.0', artifacts: [Array], runtime: [Object] }, id: 'template01CloudTrailStack6486F08A', manifest: { type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', properties: [Object], dependencies: [Array], metadata: [Object], displayName: 'template/01-CloudTrailStack' }, messages: [], _dependencyIDs: [ 'template01CloudTrailStack6486F08A.assets' ], environment: { account: 'unknown-account', region: 'unknown-region', name: 'aws://unknown-account/unknown-region' }, templateFile: 'template01CloudTrailStack6486F08A.template.json', parameters: {}, tags: {}, assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}', assumeRoleExternalId: undefined, cloudFormationExecutionRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}', stackTemplateAssetObjectUrl: 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/b2c6ad7f1f26184db44be45c55d65a916b0c6cf35ed157f1282eacae24a68aef.json', requiresBootstrapStackVersion: 6, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version', terminationProtection: undefined, validateOnSynth: false, lookupRole: { arn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}', requiresBootstrapStackVersion: 8, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version' }, stackName: 'template-01-CloudTrailStack', assets: [], displayName: 'template/01-CloudTrailStack (template-01-CloudTrailStack)', name: 'template-01-CloudTrailStack', originalName: 'template-01-CloudTrailStack', _deps: [ [AssetManifestArtifact] ] }, CloudFormationStackArtifact { assembly: CloudAssembly { directory: 'cdk.out/assembly-template', manifest: [Object], version: '31.0.0', artifacts: [Array], runtime: [Object] }, id: 'template02VpcStack20E16860', manifest: { type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', properties: [Object], dependencies: [Array], metadata: [Object], displayName: 'template/02-VpcStack' }, messages: [], _dependencyIDs: [ 'template02VpcStack20E16860.assets' ], environment: { account: 'unknown-account', region: 'unknown-region', name: 'aws://unknown-account/unknown-region' }, templateFile: 'template02VpcStack20E16860.template.json', parameters: {}, tags: {}, assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}', assumeRoleExternalId: undefined, cloudFormationExecutionRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}', stackTemplateAssetObjectUrl: 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/687f13f0cd648f71831293dc979635b3392b87adc4359a290f2cf8a9c720bec5.json', requiresBootstrapStackVersion: 6, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version', terminationProtection: undefined, validateOnSynth: false, lookupRole: { arn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}', requiresBootstrapStackVersion: 8, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version' }, stackName: 'template-02-VpcStack', assets: [], displayName: 'template/02-VpcStack (template-02-VpcStack)', name: 'template-02-VpcStack', originalName: 'template-02-VpcStack', _deps: [ [AssetManifestArtifact] ] } ]
スタック名01-VpcStack/02-CloudTrailStackのときのstage.synth().stacks
の配列
[ CloudFormationStackArtifact { assembly: CloudAssembly { directory: 'cdk.out/assembly-template', manifest: [Object], version: '31.0.0', artifacts: [Array], runtime: [Object] }, id: 'template01VpcStack40FB5329', manifest: { type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', properties: [Object], dependencies: [Array], metadata: [Object], displayName: 'template/01-VpcStack' }, messages: [], _dependencyIDs: [ 'template01VpcStack40FB5329.assets' ], environment: { account: 'unknown-account', region: 'unknown-region', name: 'aws://unknown-account/unknown-region' }, templateFile: 'template01VpcStack40FB5329.template.json', parameters: {}, tags: {}, assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}', assumeRoleExternalId: undefined, cloudFormationExecutionRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}', stackTemplateAssetObjectUrl: 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/be394ee471b065d48e13675e069a147352dcf222e7f4185c7dcee67852f1f464.json', requiresBootstrapStackVersion: 6, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version', terminationProtection: undefined, validateOnSynth: false, lookupRole: { arn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}', requiresBootstrapStackVersion: 8, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version' }, stackName: 'template-01-VpcStack', assets: [], displayName: 'template/01-VpcStack (template-01-VpcStack)', name: 'template-01-VpcStack', originalName: 'template-01-VpcStack', _deps: [ [AssetManifestArtifact] ] }, CloudFormationStackArtifact { assembly: CloudAssembly { directory: 'cdk.out/assembly-template', manifest: [Object], version: '31.0.0', artifacts: [Array], runtime: [Object] }, id: 'template02CloudTrailStackA2668286', manifest: { type: 'aws:cloudformation:stack', environment: 'aws://unknown-account/unknown-region', properties: [Object], dependencies: [Array], metadata: [Object], displayName: 'template/02-CloudTrailStack' }, messages: [], _dependencyIDs: [ 'template02CloudTrailStackA2668286.assets' ], environment: { account: 'unknown-account', region: 'unknown-region', name: 'aws://unknown-account/unknown-region' }, templateFile: 'template02CloudTrailStackA2668286.template.json', parameters: {}, tags: {}, assumeRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}', assumeRoleExternalId: undefined, cloudFormationExecutionRoleArn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}', stackTemplateAssetObjectUrl: 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/da4a7edb538f1f6fa31d42f718b0b040b925253321763a2cabb50985ea6b6c90.json', requiresBootstrapStackVersion: 6, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version', terminationProtection: undefined, validateOnSynth: false, lookupRole: { arn: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}', requiresBootstrapStackVersion: 8, bootstrapStackVersionSsmParameter: '/cdk-bootstrap/hnb659fds/version' }, stackName: 'template-02-CloudTrailStack', assets: [], displayName: 'template/02-CloudTrailStack (template-02-CloudTrailStack)', name: 'template-02-CloudTrailStack', originalName: 'template-02-CloudTrailStack', _deps: [ [AssetManifestArtifact] ] } ]
cdk ls
をしてみるとこのようになります。
> npx cdk ls CloudTrailStackSet VpcStackSet template/01-CloudTrailStack template/02-VpcStack
あとはデプロイしたいAppのスタックをデプロイしていきます。
npx cdk deploy VpcStackSet
最後に
CFn StackSetsを展開する場合、L1コンストラクトのCfnStackSet
しか用意されておらず、ぱっとみCDKで定義したコードの展開はできないように思えますが、今回ご紹介した手法を用いることで、CDKで定義したコードを展開することができました。
展開元のCFnもCDKで定義することでコーディング工数を大幅に削減できると思いますので、ぜひStackSetsの管理する手法としてCDKご活用いただければと思います。
以上、たかやま(@nyan_kotaroo)でした。