この記事は、ニフティグループ Advent Calendar 2022 4日目の記事です。
はじめに
こんにちは。ニフティ 会員システムグループ シニアエンジニアの伊達です。
AWS上で稼働するアプリケーションの開発をするにあたってIaC(Infrastructure as Code)を実践することは一般的になっています。ただ、そのツールにはいくつか候補があるでしょう。ニフティではTerraformを使うことが多くCDKは今のところ少数派です。
今回はCDKを使うにあたってのちょっとしたTipsを共有します(特に設定値に関するものをいくつか用意しました)。とはいえ、まだまだCDK初級者ですので、@NIFTYDevelopersへ読者諸賢のTipsも教えていただけると嬉しいです。
なお、伊達はTerraformを通らずにCloudFormationとCDKを使い始めたため、それTerraformでも普通にできるよというものがあると思いますが目を瞑っていただけますと幸いです。
また、本記事はCDK v2を前提とし、CDKのコードの言語はTypeScriptを使っています。
CDKとは
AWS CDKの特徴は既存のプログラミング言語を使ってAWS上でシステムを構築できる点です。
2022年12月現在ではTypeScript、Java、Python、C#、Go言語で記述ができます。開発者はアプリケーションのコードを書くのと同じようにIDEの恩恵を受けながらAWSのリソースのプロビジョニングをすることができます。
この記事ではCDKそのものの解説などはしません。詳しくはAWSの公式ページやGitHubを参照ください。
Tipsその1 Contextで設定値を与える
CDKにはContextという仕組みがあり、CDKのStackなどにkey-value形式のデータを渡すことができます。cdk.jsonのcontext内がデフォルト値となります。
例えば cdk initしたばかりのcdk.jsonは以下のようになっています。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40  | 
						{   "app": "npx ts-node --prefer-ts-exts bin/tmp.ts",   "watch": {     "include": [       "**"     ],     "exclude": [       "README.md",       "cdk*.json",       "**/*.d.ts",       "**/*.js",       "tsconfig.json",       "package*.json",       "yarn.lock",       "node_modules",       "test"     ]   },   "context": {     "@aws-cdk/aws-lambda:recognizeLayerVersion": true,     "@aws-cdk/core:checkSecretUsage": true,     "@aws-cdk/core:target-partitions": [       "aws",       "aws-cn"     ],     "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,     "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,     "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,     "@aws-cdk/aws-iam:minimizePolicies": true,     "@aws-cdk/core:validateSnapshotRemovalPolicy": true,     "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,     "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,     "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,     "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,     "@aws-cdk/core:enablePartitionLiterals": true,     "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,     "@aws-cdk/aws-iam:standardizedServicePrincipals": true,     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true   } }  | 
					
ここに値を追加することでStack内などで参照することができます。例えば、既存のVPCがあり、それを参照したいという場合には以下のように cdk.jsonに記述します。
| 
					 1 2 3 4 5  | 
						...     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,     "vpc_id": "vpc-xxxxxxxxxxxxxxxx"   }  | 
					
コード内では以下のようにして参照します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13  | 
						import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack {   constructor(scope: Construct, id: string, props?: cdk.StackProps) {     super(scope, id, props);     // vpcIdの値は"vpc-xxxxxxxxxxxxxxxx"     const vpcId = scope.node.tryGetContext('vpc_id');     ...   } }  | 
					
Tipsその2 環境を分ける
まず、devlepment、staging、productionなど稼働環境を複数持つ場合には、AWSアカウント自体をわけることをおすすめします。同じアカウント内に複数の環境を作るとリソースの重複などを避ける手間があることと、誤った環境にデプロイするなどのオペミスが起きやすくなります。
その上で環境ごとに設定を分けるには、cdk.json内に”stage”といったキーでデータを追加します。
| 
					 1 2 3 4 5  | 
						...     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,     "stage": "development"   }  | 
					
これは以下のように参照できます。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13  | 
						import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack {   constructor(scope: Construct, id: string, props?: cdk.StackProps) {     super(scope, id, props);     // stageの値は"development"     const stage = scope.node.tryGetContext('stage');     ...   } }  | 
					
例えばグローバルでユニークな必要があるドメイン名やS3のバケット名を以下のようにしたいとします。development環境にはprefixをつけるパターンです。
| 環境 | ドメイン名 | S3バケット名 | 
| development | dev-app.example.com | dev-nifty-engineering-example-bucket | 
| production | app.example.com | nifty-engineering-example-bucket | 
cdk.json内ではstage、domain、bucket_nameを設定します(stageがdevelopmentでほかがproduction用の値なのが不格好ではありますが……)。
| 
					 1 2 3 4 5 6 7 8  | 
						...     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,     "stage": "development",     "domain": "app.example.com",     "bucket_name": "nifty-engineering-example-bucket"   }  | 
					
CDKのコードではstageを参照してドメイン名やバケット名を組み立てます。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30  | 
						import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as s3 from 'aws-cdk-lib/aws-s3'; export class ExampleAppStack extends cdk.Stack {   constructor(scope: Construct, id: string, props?: cdk.StackProps) {     super(scope, id, props);     const stage = scope.node.tryGetContext('stage');     let domainPrefix = '';     if (stage == 'development') {       domainPrefix = 'dev-';     }     const domain = [domainPrefix, scope.node.tryGetContext('domain')].join('');     // 既存のRoute53ホストゾーンを参照     const hostedZone = route53.HostedZone.fromLookup(this, 'ExampleAppHostedZone', {       domainName: domain,     });     const bucketName = [domainPrefix, scope.node.tryGetContext('bucket_name')].join('');     const bucket = new s3.Bucket(this, 'ExampleAppBucket', {       bucketName: bucketName,     });   } }  | 
					
Contextはcdkコマンド実行時に上書き指定ができます。以下のようにすることで、コード内で参照される値を変えることができるため、環境ごとに異なる設定でデプロイができます。
| 
					 1 2 3  | 
						$ cdk synth # 指定がないときにはcdk.jsonの値なので stage: "development" $ cdk synth --context stage=development # stage: "development" $ cdk synth -c stage=production # stage: "production"  | 
					
Tipsその3 さらに環境ごとの設定をする
先ほどの書き方の場合には、 if (stage == 'development') { としてましたので、developmentではないときにはproductionという扱いでした。prefixをロジックで追加できるのは良いですが、cdkコマンド実行時にスペルミスすると惨事になりそうです。また、他システムのAPI Keyなど環境ごとに値が全く異なるものもあるでしょう。
以下のように環境ごとの設定をcdk.jsonに記載します。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16  | 
						...     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,     "stage": "development",     "development": {       "domain": "app.example.com",       "bucket_name": "nifty-engineering-example-bucket",       "foo_system_api_key": "ABCDEF012345"     },     "production": {       "domain": "dev-app.example.com",       "bucket_name": "dev-nifty-engineering-example-bucket",       "foo_system_api_key": "XYZABC789012"     }   }  | 
					
その2で記載したコードは以下のようになります。こちらのほうがだいぶスッキリしますね。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21  | 
						import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as route53 from 'aws-cdk-lib/aws-route53'; import * as s3 from 'aws-cdk-lib/aws-s3'; export class ExampleAppStack extends cdk.Stack {   constructor(scope: Construct, id: string, props?: cdk.StackProps) {     super(scope, id, props);     const stage = scope.node.tryGetContext('stage');     const settings = scope.node.tryGetContext(stage);     const hostedZone = route53.HostedZone.fromLookup(this, 'ExampleAppHostedZone', {       domainName: settings.domain,     });     const bucket = new s3.Bucket(this, 'ExampleAppBucket', {       bucketName: settings.bucket_name,     });   } }  | 
					
Tipsその4 リソースにタグを設定する
コスト分析のためにコスト配分タグを使っていると思います。
CDKではStack内のリソースにまとめてタグを設定できます。 以下のようにタグを付けたいとします。
| タグ名 | 値 | 
| application | example application | 
| system | example system | 
例によってcdk.jsonに以下のように書きます。
| 
					 1 2 3 4 5 6  | 
						...     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,     "application": "example application",     "system": "example system"   }  | 
					
以下のようにしてタグを設定できます。scopeはStackでもConstructでも指定できます。
| 
					 1  | 
						Tags.of(scope).add(key, value)   | 
					
ExampleAppStack内のリソースすべてに同じタグを設定するには以下のように書きます。
| 
					 1 2 3 4 5 6 7 8 9 10 11 12 13  | 
						import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack {   constructor(scope: Construct, id: string, props?: cdk.StackProps) {     super(scope, id, props);     cdk.Tags.of(scope).add('application', scope.node.tryGetContext('application'));     cdk.Tags.of(scope).add('system', scope.node.tryGetContext('system'));     ...   } }  | 
					
Tipsその5 リソースにタグを設定する#2
常日頃から活発に開発をしているアプリケーションであれば良いですが、中には一度リリースした後にはほとんど触らないようなものもあります。1年後に手を入れることになり「ドキュメントやレポジトリはどこだっけ……」と調べて回るようなことになりがちです。
タグで各リソースにドキュメントやレポジトリのURLをつけておくと便利です。
| タグ名 | 値 | 
| document | https://notion.so/barcorporation/xxxxxxxxxx | 
| repository | https://github.com/barcorporation/example-application | 
タグのキーと値が上記の場合には、cdk.jsonには以下のように記述します。
| 
					 1 2 3 4 5 6 7  | 
						...     "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,     "document": "https://notion.so/barcorporation/xxxxxxxxxx",     "repository": "https://github.com/barcorporation/example-application",   }  | 
					
そして、CDKのStackのコードに以下のように書けば、Stack内の各リソースにドキュメントとレポジトリのURLがタグ付けされ、AWS管理コンソールから調査を始めたときにドキュメントに辿り着けるようになります。
| 
					 1 2 3 4 5 6 7 8 9 10 11  | 
						import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class ExampleAppStack extends cdk.Stack {   constructor(scope: Construct, id: string, props?: cdk.StackProps) {     super(scope, id, props);     cdk.Tags.of(scope).add('document', scope.node.tryGetContext('document'));     cdk.Tags.of(scope).add('repository', scope.node.tryGetContext('repository'));   } }  | 
					
明日は、@rubihikoさんのSREでのリスク検討に関する記事です。
お楽しみに!
            

