AWS CDK に入門する

AWS CDK に入門した時の備忘録です。利用言語は TypeScript です。

参考資料

環境

AWS CDK ツールキットをインストール

グローバル領域に AWS CDK ツールキットをインストールします。

$ npm install -g aws-cdk
$ cdk --version
2.127.0 (build 6c90efc)

AWS CDK の操作 - AWS Cloud Development Kit (AWS CDK) v2

ブートストラップを実行

リソースを作成したい AWS アカウント、リージョンに対して、ブートストラップを実行します。ブートストラップについてはこちら。

以下、aws profile の zunda 定義を利用して、us-west-1 リージョンにブートストラップを実行しています。

$ aws --profile zunda sts get-caller-identity
{
    "UserId": "ABCDEFGHIJKLMNOPQRSTU",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/zunda"
}

$ cdk --profile zunda bootstrap aws://123456789012/us-west-1

ブートストラップが実行されると、CDKToolkit という名前の CloudFormation Stack が実行されます。以下、Stack 内で作成される AWS リソースです。

$ aws --profile zunda cloudformation describe-stack-resources --stack-name CDKToolkit --region us-west-1 |
jq -r '.StackResources[] | [.ResourceType, .LogicalResourceId] | @csv' |
column -t -s ,
"AWS::SSM::Parameter"    "CdkBootstrapVersion"
"AWS::IAM::Role"         "CloudFormationExecutionRole"
"AWS::ECR::Repository"   "ContainerAssetsRepository"
"AWS::IAM::Role"         "DeploymentActionRole"
"AWS::IAM::Role"         "FilePublishingRole"
"AWS::IAM::Policy"       "FilePublishingRoleDefaultPolicy"
"AWS::IAM::Role"         "ImagePublishingRole"
"AWS::IAM::Policy"       "ImagePublishingRoleDefaultPolicy"
"AWS::IAM::Role"         "LookupRole"
"AWS::S3::Bucket"        "StagingBucket"
"AWS::S3::BucketPolicy"  "StagingBucketPolicy"

ブートストラッピング - AWS Cloud Development Kit (AWS CDK) v2

AWS CDK 向けプロジェクトフォルダを作成

hello-cdk という名前のプロジェクトフォルダを作成します。

$ mkdir hello-cdk
$ cd hello-cdk
$ cdk init app --language typescript

ツールキットリファレンス

initi 時には、作成するプロジェクトのテンプレートを下記より指定します。上記コマンドでは app を指定。指定したテンプレートに応じたスケルトンを作成してくれます。

$ cdk init --list
Available templates:
* app: Template for a CDK Application
   └─ cdk init app --language=[csharp|fsharp|go|java|javascript|python|typescript]
* lib: Template for a CDK Construct Library
   └─ cdk init lib --language=typescript
* sample-app: Example CDK Application with some constructs
   └─ cdk init sample-app --language=[csharp|fsharp|go|java|javascript|python|typescript]

フォルダ構造。

$ tree -a -I .git -I node_modules .
.
├── .gitignore
├── .npmignore
├── README.md
├── bin
│   └── hello-cdk.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── hello-cdk-stack.ts
├── package-lock.json
├── package.json
├── test
│   └── hello-cdk.test.ts
└── tsconfig.json

以下のパッケージがローカルインストールされています。

$ cat ./package.json | jq '.devDependencies, .dependencies'
{
  "@types/jest": "^29.5.12",
  "@types/node": "20.11.19",
  "jest": "^29.7.0",
  "ts-jest": "^29.1.2",
  "aws-cdk": "2.130.0",
  "ts-node": "^10.9.2",
  "typescript": "~5.3.3"
}
{
  "aws-cdk-lib": "2.130.0",
  "constructs": "^10.0.0"
}

AWS CDK の基本的な概念

実際のコードを記述する前に、AWS CDK の概念について触れておきます。

AWS CDK のコードは、1 つの App をルートに持ち、配下に複数の Stack を、さらに配下に複数の Construct を持つ木構造となります。

以下、実際のコードを参照しながら、App, Stack, Construct について確認していきます。

コードたち

aws-cdk-lib.Stack を継承して、作成したい Stack のクラスを宣言します。このクラスのコンストラクタで宣言された内容が、CloudFormation で作成されるリソース情報にあたります。

そのリソース情報を指定するため、各 AWS サービスに紐づいた Construct を呼び出して利用します。

lib/hello-cdk-stack.ts

import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import * as sns from 'aws-cdk-lib/aws-sns';
import * as subs from 'aws-cdk-lib/aws-sns-subscriptions';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import { Construct } from 'constructs';

export class HelloCdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const queue = new sqs.Queue(this, 'HelloCdkQueue', {
      visibilityTimeout: Duration.seconds(300)
    });

    const topic = new sns.Topic(this, 'HelloCdkTopic');

    topic.addSubscription(new subs.SqsSubscription(queue));
  }
}

Construct の説明。

Construct のシグネチャ。作成したリソースの情報を第3引数に渡して、Construct 毎に用意されているメソッドやプロパティを利用してプログラミングしていきます。

bin ディレクトリにあるファイルが、エントリーポイントにあたるファイルになります。

App インスタンスを初期化した後、lib ディレクトリ配下で定義されした Stack の定義から、Stack を初期化しています。

bin/hello-cdk.ts

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { HelloCdkStack } from '../lib/hello-cdk-stack';

const app = new cdk.App();

new HelloCdkStack(app, 'HelloCdkStack', {
    env: {
        account: '123456789012',
        region: 'ap-northeast-1',
    },
    description: 'sample',
});

デプロイ

AWS CDK ツールキットに用意されているコマンドを実行することで、デプロイ(CloudFormation の実行)を行なえます。なお、コマンドはグローバルインストールされた AWS CDK ツールキットを使うのではなく、ローカルインストールされたものを使います。

利用している Stack を表示。

$ npx cdk --profile zunda list
HelloCdkStack

作成される CloudFormation のテンプレートを表示。

$ npx cdk --profile zunda synth HelloCdkStack
Description: sample
Resources:
  HelloCdkQueueB56C77B9:
    Type: AWS::SQS::Queue
(略)

実態との差分を表示。

$ npx cdk --profile zunda diff HelloCdkStack
Stack HelloCdkStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Template
[+] Description Description: sample

IAM Statement Changes
┌───┬──────────────────────┬────────┬─────────────────┬───────────────────────────┬──────────────────────────────────────────────────────┐
│   │ Resource             │ Effect │ Action          │ Principal                 │ Condition                                            │
├───┼──────────────────────┼────────┼─────────────────┼───────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${HelloCdkQueue.Arn} │ Allow  │ sqs:SendMessage │ Service:sns.amazonaws.com │ "ArnEquals": {                                       │
│   │                      │        │                 │                           │   "aws:SourceArn": "${HelloCdkTopic}"                │
│   │                      │        │                 │                           │ }                                                    │
└───┴──────────────────────┴────────┴─────────────────┴───────────────────────────┴──────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Parameters
[+] Parameter BootstrapVersion 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]"}

Resources
[+] AWS::SQS::Queue HelloCdkQueue HelloCdkQueueB56C77B9 
[+] AWS::SQS::QueuePolicy HelloCdkQueue/Policy HelloCdkQueuePolicy027FC30A 
[+] AWS::SNS::Subscription HelloCdkQueue/HelloCdkStackHelloCdkTopic850E0FBD HelloCdkQueueHelloCdkStackHelloCdkTopic850E0FBD36A066B9 
[+] AWS::SNS::Topic HelloCdkTopic HelloCdkTopic1F583424 

Other Changes
[+] Unknown 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."}]}}


✨  Number of stacks with differences: 1

デプロイ。

$ npx cdk --profile zunda deploy HelloCdkStack

✨  Synthesis time: 2.85s

This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬──────────────────────┬────────┬─────────────────┬───────────────────────────┬──────────────────────────────────────────────────────┐
│   │ Resource             │ Effect │ Action          │ Principal                 │ Condition                                            │
├───┼──────────────────────┼────────┼─────────────────┼───────────────────────────┼──────────────────────────────────────────────────────┤
│ + │ ${HelloCdkQueue.Arn} │ Allow  │ sqs:SendMessage │ Service:sns.amazonaws.com │ "ArnEquals": {                                       │
│   │                      │        │                 │                           │   "aws:SourceArn": "${HelloCdkTopic}"                │
│   │                      │        │                 │                           │ }                                                    │
└───┴──────────────────────┴────────┴─────────────────┴───────────────────────────┴──────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
HelloCdkStack: deploying... [1/1]
HelloCdkStack: creating CloudFormation changeset...

 ✅  HelloCdkStack

✨  Deployment time: 16.91s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/HelloCdkStack/2a672ac0-d5c2-11ee-af98-0e475b115c21

✨  Total time: 19.76s

削除。

$ npx cdk --profile zunda destroy HelloCdkStack
Are you sure you want to delete: HelloCdkStack (y/n)? y
HelloCdkStack: destroying... [1/1]

 ✅  HelloCdkStack: destroyed

ツールキットリファレンス

その他

命名規則・コーディングスタイル

Naming Conventions

  • Class names: PascalCase
  • Properties: camelCase
  • Methods (static and non-static): camelCase
  • Interfaces (“behavioral interface”): IMyInterface
  • Structs (“data interfaces”): MyDataStruct
  • Enums: PascalCase, Members: SNAKE_UPPER

Coding Style

  • Indentation: 2 spaces
  • Line length: 150
  • String literals: use single-quotes (') or backticks (```)
  • Semicolons: at the end of each code statement and declaration (incl. properties and imports).
  • Comments: start with lower-case, end with a period.

Naming & Style

作成されるリソースの物理名

ロジック。

物理名は、AWS CDK 側の命名に委ねことが良いとのこと。

Names are a precious resource. Each name can only be used once. Therefore, if you hardcode a table name or bucket name into your infrastructure and application, you can't deploy that piece of infrastructure twice in the same account. (The name we're talking about here is the name specified by, for example, the bucketName property on an Amazon S3 bucket construct.)

What's worse, you can't make changes to the resource that require it to be replaced. If a property can only be set at resource creation, such as the KeySchema of an Amazon DynamoDB table, then that property is immutable. Changing this property requires a new resource. However, the new resource must have the same name in order to be a true replacement. But it can't have the same name while the existing resource is still using that name.

A better approach is to specify as few names as possible. If you omit resource names, the AWS CDK will generate them for you in a way that won't cause problems. Suppose you have a table as a resource. You can then pass the generated table name as an environment variable into your AWS Lambda function. In your AWS CDK application, you can reference the table name as table.tableName. Alternatively, you can generate a configuration file on your Amazon EC2 instance on startup, or write the actual table name to the AWS Systems Manager Parameter Store so your application can read it from there.

Use generated resource names, not physical names

ステートフルなリソースの id を変更する時は注意すること(原則しないこと)。

Changing the logical ID of a resource results in the resource being replaced with a new one at the next deployment. For stateful resources like databases and S3 buckets, or persistent infrastructure like an Amazon VPC, this is seldom what you want. Be careful about any refactoring of your AWS CDK code that could cause the ID to change. Write unit tests that assert that the logical IDs of your stateful resources remain static.

Don't change the logical ID of stateful resources

Stack 分割の方針

通常、できるだけ多くのリソースを同じスタックに保持する方が簡単なので、分離したいことがわかっている場合を除き、リソースをまとめておいてください。

ステートフルリソース (データベースなど) は、ステートレスリソースとは別のスタックに保存することを検討してください。その後、ステートフルスタックのターミネーション保護をオンにできます。これにより、データ損失のリスクなしに、ステートレススタックのコピーを自由に破棄または複数作成できます。

ステートフルリソースはコンストラクトの名前変更の影響を受けやすく、名前を変更するとリソースが置き換えられます。したがって、ステートフルリソースは、移動や名前の変更が起こりそうなコンストラクト内にネストしないでください (キャッシュのようにステートが失われた場合に再構築できる場合を除きます)。これは、ステートフルリソースを独自のスタックに配置するもう 1 つの正当な理由です。

デプロイ要件に応じて、アプリケーションを複数のスタックに分離します。

BLEA チームの人たちの意見。Stack は分けずに、独自の Construct を作成して分割する。

環境毎のパラメーターの渡し方

BLEA チームの人たちの意見。TypeScript のオブジェクトとしてパラメーターを用意して利用する。