40
42

More than 3 years have passed since last update.

【備忘録・まとめ】AWS Cloud Development Kit (AWS CDK)

Posted at

出典

まとめ

  • 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

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
      • プロパティのキーバリューセット
      • 言語依存ではあるが、全く無い場合には無視することもできる

Interacting with Constructs

  • AWS Construct Libraryに含まれるConstructsは、一定のガイドラインに沿ったパターンの実装・APIになっている
    • grantメソッド
      • IAMのREADないしWRITE系ポリシーをアタッチするメソッド
    • ARNやname、URLのgetter
  • 詳しくはこのへん参照

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

App Lifecycle

  1. Construction
    • CDKのコードが実行されて定義されたConstructのインスタンスが生成され、各Constructがリンクされる。
  2. Preparation
    • prepareメソッドが実装されたConstructの全てが追加される。ユーザー的にあまり意識する必要はなさそう。
  3. Validation
    • validateメソッドが実装されたConstructのバリデーションが実行される。
    • このフックに通知とか追加しておける。
    • なるべくバリデーションは入れたほうが良いらしい。
  4. Synthesis
    • CDK app実行の最終段階。app.synth()の呼び出しによりトリガーされる。
    • construct treeをなぞって、全てのconstructのsynthesizeメソッドが呼び出される。
    • CFnテンプレート、Lambda bundle、Docker image assets等のデプロイに必要な生成物をここで作る。
  5. 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のコードになる可能性がある
      • 作成後に変更不可なプロパティを変更してリソースが再作成される場合など
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
  • 以下の例では、Stack1Stack2の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

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テンプレートと実際のリソースに不整合が生じた時等に、それらを無視する形でとりあえず同期・削除等操作できる
  • 足りないリソースをカスタムリソースとして作る

あるいは、今後この辺りの仕様も充実してくるのかもしれない。

ポイントとかやってみて思うこと

あとで書く。

40
42
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
42