出典
まとめ
- CFnテンプレートを、各種プログラミング言語のコードから生成するフレームワーク
- TypeScript, JavaScript, Python, Java, C#から好きな言語を選択できる(2019-07-26現在)
- リソースの構造をコードで管理できるようになる
- 繰り返しや参照、構造の再利用やパラメータの注入など、CFnテンプレートでは表現が難しかった記述が柔軟にできるようになる
- 下のレイヤーに潜っているCFnの存在を意識できないと、使うのが辛そう
要点
Constructs
constructは、SDKアプリにおいて基本的な構成単位になる。
- あらゆる構成物を包括するクラウドコンポーネントを意味する
- コンポーネントの中身は1つのリソースでもOK
- 複数のリソースからなる高レベルなコンポーネントであってもOK
AWS Construct Library
- CDKにはConstruct Libraryが含まれていて、各種AWSリソースを表現するconstructsが含まれている
- このライブラリには、AWSで利用可能な全てのリソースが含まれている
- これらのリソースにはいくつかのレベル(constructの粒度)がある
- 最も低レベルなもの: CFN Resources
-
Cfn
という接頭辞が名前についている - 例:
s3.CfnBucket
- CFN Resourcesを使うときには、リソースのプロパティを明示的に設定しなければならない
- そのリソースについて完全な理解が要求される
-
- 次点で低レベルのもの: AWS Resources
- intent-based API
- CFN constructsから要求されるglue logicやboilerplate等
- リソースを素朴に使い祭に便利な各種メソッド
- リソースについて全て知らなくても便利に使える各種デフォルトの設定・・・などを有する
- 例:
s3.Bucket
- 更に高レベルなもの: patterns
- いくつかのリソースを含むconstructs
- 例
- aws-ecs-patterns.LoadBalancedFargateService: ELB + Fargate
- apigateway.LambdaRestApi: Api Gateway + Lambda
- 最も低レベルなもの: CFN Resources
Composition
- 高レベルの抽象的なconstructsを構成する要となるのが_Composition_と呼ばれるパターン
- この入れ子の階層構造は_construct tree_と呼ばれる
- AWS CDKにおいて、この木構造のルートはAWS CDK appになる
- このappの中に、1つ以上のstackを作ることになる
- このstackが、デプロイの1単位になる
- CFnのスタックに類するもの
- _Composition_パターンを踏襲することにより、再利用可能なcomponentsを定義できるようになる
Initialization
- ConstructsはConstruct基底クラスを拡張する形で実装する
- Constructは初期化時に3つの引数を取る
- scope
- 初期化されるConstructを内包する、親のConstruct
- id
- このConstructのスコープにおいて固有の識別子
- このConstructの名前空間として使われ、リソース名やCFnの論理ID等を付与するのに用いられる
- Constructを再利用する際に、スコープ内で識別子がユニークであることを確実にする
- props
- プロパティのキーバリューセット
- 言語依存ではあるが、全く無い場合には無視することもできる
- scope
Interacting with Constructs
- AWS Construct Libraryに含まれるConstructsは、一定のガイドラインに沿ったパターンの実装・APIになっている
-
grantメソッド
- IAMのREADないしWRITE系ポリシーをアタッチするメソッド
- ARNやname、URLのgetter
-
grantメソッド
- 詳しくはこのへん参照
Apps
The App Construct
- Constructは、必ず直接ないし間接的にどこかのStack内で定義されなければならない
- 更に、StackはApp constructの中に定義される必要がある
const app = new App();
new MyFirstStack(app, 'hello-cdk');
app.synth();
- App constructは拡張することもできる
class MyApp extends App {
constructor() {
new MyFirstStack(this, 'hello-cdk');
}
}
new MyApp().synth();
App Lifecycle
- Construction
- CDKのコードが実行されて定義されたConstructのインスタンスが生成され、各Constructがリンクされる。
- Preparation
- prepareメソッドが実装されたConstructの全てが追加される。ユーザー的にあまり意識する必要はなさそう。
- Validation
- validateメソッドが実装されたConstructのバリデーションが実行される。
- このフックに通知とか追加しておける。
- なるべくバリデーションは入れたほうが良いらしい。
- Synthesis
- CDK app実行の最終段階。
app.synth()
の呼び出しによりトリガーされる。 - construct treeをなぞって、全てのconstructのsynthesizeメソッドが呼び出される。
- CFnテンプレート、Lambda bundle、Docker image assets等のデプロイに必要な生成物をここで作る。
- CDK app実行の最終段階。
- Deployment
- Synthesisフェーズで生成したものをここでAWSにデプロイする。
- CFnのスタックもここでデプロイされる。
Stacks
- AWS CDKにおいてデプロイのひとかたまりになるもので、CFnのスタックとほぼ同義
-
cdk synth
が実行されると、cloud assemblyはスタックのインスタンス毎に(同じクラスのインスタンスであっても)templateを分けて生成する - このCFnテンプレートにおけるCFnパラメータは、AWS CDK内で定義され、デプロイ時にのみ利用可能になる
- 下記の例では、合計6つのCFnスタックがデプロイされる
class ControlPlane extends Stack { ... }
class DataPlane extends Stack { ... }
class Monitoring extends Stack { ... }
class MyService extends Construct {
constructor(...) {
new ControlPlane(this, ...);
new DataPlane(this, ...);
new Monitoring(this, ...);
}
}
const app = new App();
new MyService(app, 'beta');
new MyService(app, 'prod', { prod: true });
app.run();
- Stackの名前は明示的に決めることもできるが、デフォルトではStackオブジェクトのconstruct IDから決定される
new MyStack(this, 'not:a:stack:name', { stackName: 'this-is-stack-name' });
他にも色々APIがある。
Environments
- 各々のスタックは、明示的ないし暗黙的にenvironmentと関連付けられる
- environmentが未定義の場合、デプロイ時(
cdk deploy
実行時)の環境に準じて属性が決められる- stack.account
- stack.region
- stack.availablityZones
- など
- productionで使うときは、それぞれのstackについてenvironmentを明示的にコーディングするのが推奨される
- 複数の環境にスタックをデプロイするときは、正しくクレデンシャルを設定して
cdk deploy STACK
すること
const envEU = { account: '2383838383', region: 'eu-west-1' };
const envUSA = { account: '8373873873', region: 'us-west-2' };
new MyFirstStack(this, 'first-stack-us', { env: envUSA, encryption: false });
new MyFirstStack(this, 'first-stack-eu', { env: envEU, encryption: true });
Resources
Referencing Resources
- CDKのコード上では、各種CDK resourceのインターフェースを備えたオブジェクトを、それらを引数やパラメータとしてとるメソッドやオブジェクトに渡すことができる
- 例
- ECS serviceにclusterを渡す
- CloudFront distributionにsourceBucketを渡す など
- 例
const cluster = new ecs.Cluster(this, 'Cluster', { /* ... */ });
const service = new ecs.Service(this, 'Service', {
cluster: cluster,
/* ... */
});
Accessing Resources in a Different Stack
- 同一アカウント、同一リージョン内であれば、他のスタックにあるリソースもCDK内では参照可能
const prod = { account: '123456789012', region: 'us-east-1' };
const stack1 = new StackThatProvidesABucket(app, 'Stack1' , { env: prod });
// stack2 will take a property { bucket: IBucket }
const stack2 = new StackThatExpectsABucket(app, 'Stack2', {
bucket: stack1.bucket,
env: prod
});
Physical Names
- CDKを通して作成されたスタックのリソースは、物理名がCFnの論理名とは異なる
- 論理名の後ろに、ランダムな接尾辞がつく
- 例
- 論理名: Stack2MyBucket4DD88B4F
- 物理名: tack2mybucket4dd88b4f-iuv1rbv9z3to
- 例
- プロパティ等で物理名を指定することも可
- ただし、冪等性のないCDKのコードになる可能性がある
- 作成後に変更不可なプロパティを変更してリソースが再作成される場合など
- ただし、冪等性のないCDKのコードになる可能性がある
const bucket = new s3.Bucket(this, 'MyBucket', {
bucketName: 'my-bucket-name',
});
Passing Unique Identifiers
- 基本的にresourceへの参照はオブジェクトとして渡されるべきだが、やむなく必要な場合にはidentifierを渡すこともできる
const bucket = new s3.Bucket(this, 'Bucket');
new lambda.Function(this, 'MyLambda', {
/* ... */,
environment: {
BUCKET_NAME: bucket.bucketName,
},
});
Importing Existing External Resources
- 既に作成済みのリソースを、そのARNを用いて生成したCDKオブジェクトとしてスタックに加えることができる
// Construct a resource (bucket) just by its name (must be same account)
Bucket.fromName(this, 'Bucket', 'my-bucket-name');
// Construct a resource (bucket) by its full ARN (can be cross account)
Bucket.fromArn(this, 'Bucket', 'arn:aws:s3:::my-bucket-name');
// Construct a resource by giving more than one attribute (complex resources)
Resource.fromAttributes(this, 'MyResource', {
resourceName: 'booh',
vpc: vpc
});
Permission Grants
- 殆どのAWS constructsは扱いやすいgrantメソッドを実装している
- 例: 以下のコードでは、lambda関数にs3バケットへの読み書きを許可し、それがKMSによって暗号化されていれば、その復号もlambdaに許可する
bucket.grantReadWrite(lambda);
- grantメソッドは
iam.Grant
オブジェクトを返す- このオブジェクトのsuccess属性が返す値によって、許可が適切に適用されたかどうか確認できる
- 特定の権限についてgrantメソッドが使えない場合には、より一般的なgrantメソッドを使って設定できる
table.grant(lambda, 'dynamodb:CreateBackup');
- resourceを作成する際のプロパティとして、このresourceにアタッチするroleを指定できる
- 指定しなければ、CDKのコード上ではこのresouceのオブジェクトに直接grantメソッドを使えばよい
- また、roleに直接policyをアタッチすることもできる(
addToRolePolicy
メソッド)
Metrics and Alarms
- resouceはCloudWatchでモニタリングできるメトリクスを生成する
- 以下の例ではSQSの
ApproximateNumberOfMessagesNotVisible
を監視するalermを設定している
import cw = require('@aws-cdk/aws-cloudwatch');
import sqs = require('@aws-cdk/aws-sqs');
import { Duration } from '@aws-cdk/core';
const queue = new sqs.Queue(this, 'MyQueue');
const metric = queue.metricApproximateNumberOfMessagesNotVisible({
label: 'Messages Visible (Approx)',
period: Duration.minutes(5),
// ...
});
metric.createAlarm(this, 'TooManyMessagesAlarm', {
comparisonOperator: cw.ComparisonOperator.GreaterThan,
threshold: 100,
// ...
});
Network Traffic
ACLの設定とか。
Amazon CloudWatch Events
- イベントソースのresouceで
addXxxNotification
を使ってイベントリスナーを登録すると、イベントを発行できる
Import targets = require('@aws-cdk/aws-events-targets');
const handler = new lambda.Function(this, 'Handler', { /*…*/ });
const bucket = new s3.Bucket(this, 'Bucket');
bucket.addObjectCreatedNotification(new targets.LambdaFunction(handler));
Identifiers
- CDKでは様々なidentifiersを用いており、それらの種類を知っておく必要がある
- identifierはそれらが存在するスコープ内で一意であればよい
- グローバルスコープで一意である必要はない
- 然るべきスコープ内で重複すると、CDKはエラーを出す
Construct IDs
- CDKにおいて最も一般的なidentifier
- constructをインスタンス化する時の2番目の引数
- この最初の引数になるconstructのスコープ内でユニークであればOK
- 以下の例では、
Stack1
とStack2
の2つのスタック内でそれぞれMyBucket
が作成されている
import { App, Construct, Stack, StackProps } from '@aws-cdk/cdk';
import s3 = require('@aws-cdk/aws-s3');
class MyStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
new s3.Bucket(this, 'MyBucket');
}
}
const app = new App();
new MyStack(app, 'Stack1');
new MyStack(app, 'Stack2');
Paths
- construct treeにおける、リソースのpath
-
/
で区切られた文字列で表現される- 例
Stack1/MyBucket
Stack2/MyBucket
- 例
- CDKのプログラムにおいては、下記のようにpathを取得できる
- CDKアプリにおいて、pathは常にユニークになる
const path: string = myConstruct.node.path;
Unique IDs
- pathを連結し、8-digit hashを加えた文字列
- 下記の要領で、全てのconstructのunique idを取得できる
- CDKによって生成されるCFnテンプレートにおける論理名になる
const uid: string = myConstruct.node.uniqueId;
Logical IDs
- 前述のUnique IDが、CFnの_logical identifiers_として使われている
- CFnが管理しているリソースはこのLogical IDで識別している
- そのため、CDKによって生成されたIDが同一アカウント・リージョン内で重複すると、他のデプロイのリソースを破壊する可能性が生じる
- 故に、Logical IDを変更することは極力避けなければならない
Tokens
- CDKのコード上では扱われるものの、CFnスタックのdeploy時まで未定となる各種値のプレースホルダの文字列を_tokens_と呼ぶ
- 下記の要領で確認できる
${TOKEN[Bucket.Name.1234]}
- 例えば、下記のコードにおいて
bucket.bucketName
はdeploy時まで確定できない - そのため、内部的には_tokens_を使っている
const bucket = new s3.Bucket(this, 'MyBucket');
const fn = new lambda.Function(stack, 'MyLambda', {
// ...
environment: {
BUCKET_NAME: bucket.bucketName,
}
});
- メタプログラミング感が出てくるので、これを直接使って凝ったことはしない方がよさそう
Tagging
-
Tag
クラスが2つのメソッドを持っていて、コレで付与と削除を行う-
Tag.add()
: constructとその子供の全てにタグを付与する -
Tag.remove()
: constructとその子供全てに付与されている指定のタグを全て除去する
-
- TaggingはAspectと呼ばれる特定のスコープに対する操作の方法を用いて実現されている
Tag.add(myConstruct, 'key', 'value');
Tag.remove(myConstruct, 'key');
Tag.add()
Tag.add(myConstruct, 'tagname', 'value', {
applyToLaunchedInstances: false,
includeResourceTypes: ['AWS::Xxx::Yyy'],
excludeResourceTypes: ['AWS::Xxx::Zzz'],
priority: 100,
});
上記の例におけるその他オプション
- applyToLaunchedInstances
- オートスケーリング機能等によって自動的に作成されたリソースにも同様のタグを振るかどうか
- デフォルトは
true
なので、これをfalse
にすると、前述の挙動が止まる
- includeResourceTypes/excludeResourceTypes
- 特定タイプのリソースにのみ、タグ付与・削除を行う場合に使う
- priority
- priorityが同じタグ付が指定された場合、construct treeの葉に近い方から優先される
Tag.remove()
もほぼ同様。
Assets
- _Assets_とは、CDK appにバンドルするファイル、ディレクトリ、Dockerイメージ等のこと
- 例えばLambdaのハンドラ関数もこれに含まれる
- CDKのコードにおいては、この_Assets_をconstructsから参照できる
- CDKは各assetsについてsource hashを生成していて、変更があったか否かを確認して、必要があればデプロイする
- CDK CLIによってデプロイ時に指定されたパラメータ等もassetsとして扱われる
Amazon S3 Assets
- aws-s3-assetsモジュールを用いて、ローカルにあるファイルやディレクトリをCDKがS3にアップロードできる
import { Asset } from '@aws-cdk/aws-s3-assets';
// Archived and uploaded to Amazon S3 as a .zip file
const directoryAsset = new Asset(this, "SampleZippedDirAsset", {
path: path.join(__dirname, "sample-asset-directory")
});
// Uploaded to Amazon S3 as-is
const fileAsset = new Asset(this, 'SampleSingleFileAsset', {
path: path.join(__dirname, 'file-asset.txt')
});
- Lambdaのハンドラ関数もS3 Assetsとして扱われる
- S3 assetsはデプロイ時にURLや実際のオブジェクト名を解決してCDKコード内で参照できる
const imageAsset = new Asset(this, "SampleAsset", {
path: path.join(__dirname, "images/my-image.png")
});
new lambda.Function(this, "myLambdaFunction", {
code: lambda.Code.asset(path.join(__dirname, "handler")),
runtime: lambda.Runtime.PYTHON_3_6,
handler: "index.lambda_handler",
environment: {
'S3_BUCKET_NAME': imageAsset.s3BucketName,
'S3_OBJECT_KEY': imageAsset.s3ObjectKey,
'S3_URL': imageAsset.s3Url
}
});
- grantメソッドでassetsに対するIAMパーミッションを設定することもできる
const asset = new Asset(this, 'MyFile', {
path: path.join(__dirname, 'my-image.png')
});
const group = new iam.Group(this, 'MyUserGroup');
asset.grantRead(group);
Docker Image Assets
- CDKはaws-ecr-assetsプラグインを使うことで、Dockerイメージをassetsとして扱うことができる
- 下記のコードで、ローカルビルド(Dockerファイル等を配置しておく)したイメージをECRにアップロードする
-
my-image
ディレクトリにDockerfileを配置する
-
import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets';
const asset = new DockerImageAsset(this, 'MyBuildImage', {
directory: path.join(__dirname, 'my-image')
});
- 下記の要領で、ローカルビルドしたECRリポジトリのイメージをFargateで使う
import ecs = require('@aws-cdk/aws-ecs');
import path = require('path');
import { DockerImageAsset } from '@aws-cdk/aws-ecr-assets';
const asset = new DockerImageAsset(this, 'my-image', {
directory: path.join(__dirname, "..", "demo-image")
});
const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef", {
memoryLimitMiB: 1024,
cpu: 512
});
taskDefinition.addContainer("my-other-container", {
// 下記のコードも可
// image: ecs.ContainerImage.fromAsset(path.join(__dirname, "..", "demo-image"))
image: ecs.ContainerImage.fromEcrRepository(asset.repository, asset.imageUri)
});
Permissions
- AWS Construct Libraryでよく用いられるイディオムがある
- IAMモジュールにそれらが実装されている
Grants
- 他からのアクセスを受け付けるタイプのリソースには、アクセスを許可するgrantメソッドが用意されている
- S3 bucketとか DynamoDB tableとか
- 名前も大概決まってる
- grantRead
- grantReadWrite とか
- grantを受けるオブジェクトはIGrantableを実装している必要がある
- IAM RoleとかUserとかGroupなんかが該当
- 他にもgrantできるオブジェクトもある(後述)
Roles
-
Role
オブジェクトにはassumedBy
プロパティでService Principalを設定する
import iam = require('@aws-cdk/aws-iam');
const role = new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), // required
});
-
addToPolicy
メソッドでポリシーを追加する -
PolicyStatement
のインスタンスを渡す
role.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.DENY,
resources: [bucket.bucketArn, otherRole.roleArn],
actions: ['ec2:SomeAction', 's3:AnotherAction'],
conditions: {StringEquals: {
'ec2:AuthorizedService': 'codebuild.amazonaws.com',
}}}));
- IAM Roleを設定しないでインスタンスを作ると、自動的にRoleを生成して使ってくれるサービスもある
import codebuild = require('@aws-cdk/aws-codebuild');
// imagine roleOrUndefined is a function that might return a Role object
// under some conditions, and undefined under other conditions
const someRole: iam.IRole | undefined = roleOrUndefined();
const project = new codebuild.Project(this, 'Project', {
// if someRole is undefined, the Project creates a new default role,
// trusting the codebuild.amazonaws.com service principal
role: someRole,
});
- ただし、明示的にRoleを設定しない場合には、外部からimportされた場合、属性値としてRoleを参照された時に何も返さないので注意する
- なるべく明示的に指定する
// project is imported into the CDK application
const project = codebuild.Project.fromProjectName(this, 'Project', 'ProjectName');
// project is imported, so project.role is undefined, and this call has no effect
project.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW, // ... and so on defining the policy
}));
Resource Policies
- Resource Policyとして、Policy Statementを設定する
bucket.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:SomeAction'],
resources: [bucket.bucketArn],
principals: [role]
}));
Context
Construct Context
- CDKは、AMIやAZ等、CDKを前回デプロイした時の各種パラメータを
cdk.context.json
に保持している - それらのアップデートがあった後のCDKの実行時に意図しない変更、アップデートが入らないようにしている
- CDKユーザーは、主に3つの方法でContextを指定できる
- CDKが自動で確認、収集する
- CDK CLIを
--context
オプション付で実行する - CDKのコード中で
construct.node.setContext
メソッドを使う
- CDKのコード中では
construct.node.tryGetContext
で値を取得できる
Viewing and Managing Context
-
cdk context
コマンドで、コンテキストの内容を確認・管理できる -
cdk context --reset
で、コンテキストを削除できる-
cdk context --clear
で、全てのコンテキストを削除
-
- これらの内容を最新にしたければ、
cdk synth
コマンドを使う
Aspects
- _Aspects_は、オペレーションの範囲を設定したスコープ内に限定する方法
- 色々範囲は設定できる
- constructs
- tag
- constructsの状態 など
- 以下の要領で指定する
myConstruct.node.applyAspect(new SomeAspect(...));
- これを実行すると、construct内部のリストにaspectが追加され、任意の順番で実行される。
- エラーハンドリングなんかに使うと便利そう
class BucketVersioningChecker implements IAspect {
public visit(node: IConstruct): void {
// See that we're dealing with a CfnBucket
if (node instanceof s3.CfnBucket) {
// Check for versioning property, exclude the case where the property
// can be a token (IResolvable).
if (!node.versioningConfiguration
|| (!Tokenization.isResolvable(node.versioningConfiguration)
&& node.versioningConfiguration.status !== 'Enabled')) {
node.node.addError('Bucket versioning is not enabled');
}
}
}
}
// Apply to the stack
stack.node.applyAspect(new BucketVersioningChecker());
Escape Hatches
逃げ手についての補足。機能が足りない、痒いところに手が届かなかった時は・・・
- CFn Constructsを直接使う
- AWS Constructs内のCFn Resourceを直接修正する
- CFnテンプレートと実際のリソースに不整合が生じた時等に、それらを無視する形でとりあえず同期・削除等操作できる
- 足りないリソースをカスタムリソースとして作る
あるいは、今後この辺りの仕様も充実してくるのかもしれない。
ポイントとかやってみて思うこと
あとで書く。