AWS CDKをプロダクションで使うための私見プラクティス

AWS CDK (Cloud Development Kit)の登場によって、ようやく理想的なInfrastructure as Codeが実現できるようになってきたなと感じています。
素で書くCloudFormationテンプレートやAnsible等と比べて、特に以下のあたりが便利ですね。

  • 型があり、補完が効く
  • ロール、ポリシー周りの記述がgrantXxxだけで直感的に記述できる
  • 変更に伴う変更セット周りの面倒くささが解消される

CDKをプロダクションで使っていくために、「こうするといいんじゃないかな」と思う個人的なプラクティスをまとめてみました。

(注) CDKのバージョンは執筆時点での最新バージョンである1.19.0を対象としています。
また、言語は問わない内容にしたつもりですが、TypeScriptしか触っていませんので、TypeScriptが前提です。

書き方はサンプルリポジトリを参考にする

まず、プロジェクトの構成は cdk init app で作られたものを土台にし、下手に弄らないほうが無難だと思います。
この構成だと、lib 以下にスタックの定義を記述していくことになると思います。

公式のサンプルリポジトリに豊富なサンプルがあるので、近しい構成のものを参考にしながら、膨らませていくとよいと思います。

github.com

書けるものはdescriptionを書く

Stack, Lambda, API等、説明(description)が記載できるものについては横着せずに、書いておくのがよいと思います。

const sampleStack = new SampleStack(app, 'SampleStack', {
  description: 'はじめてのAWS CDKスタックです'
});

マネージメントコンソールで一覧で見た時に用途が明確になりますし、コードのコメントのかわりとしても使えます。

~/.aws/credentialsのデフォルトプロファイルに注意

cdk deploy とすると、あまりに簡単にデプロイができてしまいますが、オプション未指定時は ~/.aws/credentials のデフォルトプロファイルが使われることに注意しましょう(事故の元になります)。

常にプロファイルを指定してデプロイするよう心がけたほうが安全だと思います。

$ cdk deploy --profie staging

コードを短くすることにこだわりすぎない

CFnテンプレートを書くのに比べて短い記述量で済むのがCDKのメリットの一つですが、記述を短くすることを最優先にせずに、バランスを考えた方がよい気がしています。

例えば、3つのLambda関数を作成する場合、以下のようにループを用いて書くことができます。

for (const name of ['fn1', 'fn2', 'fn3']) {
  new lambda.Function(this, name, {
    runtime: lambda.Runtime.NODEJS_12_X,
    code: lambda.Code.asset('src'),
    handler: `${name}.handler`
  });
}

多少冗長でも以下のようにありのままに書いた方がよい気がしています。直感的に内容が分かりますし、例えば、後で一部の関数だけメモリやタイムアウトの値を変えたくなった時に素直に対応ができます。
このあたり、DRYの原則に反しますが、テストコードを書く時の感覚に近いかもしれません。

new lambda.Function(this, 'fn1', {
  runtime: lambda.Runtime.NODEJS_12_X,
  code: lambda.Code.asset('src'),
  handler: 'fn1.handler'
});

new lambda.Function(this, 'fn2', {
  runtime: lambda.Runtime.NODEJS_12_X,
  code: lambda.Code.asset('src'),
  handler: 'fn2.handler'
});

new lambda.Function(this, 'fn3', {
  runtime: lambda.Runtime.NODEJS_12_X,
  code: lambda.Code.asset('src'),
  handler: 'fn3.handler'
});

Stackのコンストラクタだけで記述を済ませず、シンプルにする

独自のStackクラスのコンストラクタ内ですべての処理を記載しようとすると、モノによっては、やはり長くなってしまいます。
1つ1つのリソース定義はメソッドに抽出するなどして、コンストラクタでは、全体像がひと目で把握できるシンプルな構造になっていると、後のメンテナンスがしやすいと思います。

import cdk = require('@aws-cdk/core');
import sns = require('@aws-cdk/aws-sns');
import subs = require('@aws-cdk/aws-sns-subscriptions');
import sqs = require('@aws-cdk/aws-sqs');

export class SqsSnsStack extends cdk.Stack {

  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const queue = this.createQueue();

    const topic = this.createTopic();

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

  private createQueue(): sqs.Queue {
    return new sqs.Queue(this, 'HelloCdkQueue', {
      visibilityTimeout: cdk.Duration.seconds(300)
    });
  }

  private createTopic(): sns.Topic {
    return new sns.Topic(this, 'HelloCdkTopic');
  }
}

STABLEなものだけで構築できそうならば、CDKを採用する

CDKは新しいツールであり、2018年8月にDeveloper Previewとして発表された頃からGitHubをウォッチしていますが、アップデートが頻繁にあります。
まぁ、GAになってからは落ち着いた感じもしますが、それでも、今後、破壊的な変更がある可能性はゼロではないと思います。

公式のAPIリファレンスを見ると、各サービスごとに、CDKでの安定性(stability)ステータスが確認できます。例えば、Lambdaの場合は下記URLです。

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-lambda-readme.html

ちょっと乱暴ですが、ステータスが実験的(EXPERIMENTAL)になっているものの利用は避け、安定(STABLE)なものだけで実現可能な場合にのみ、CDKを採用する、というのも手です。
使いたいオプションがサポートされていなかったり、という罠にハマることが少ないと思うので…(まぁIssueなりPullRequestを送ればよい話ではありますが…)。

まぁ、サーバレスなアプリケーションを作る定番サービス(DynamoDB, Lambda, API Gateway, SNS, SQS, etc)はいずれもSTABLEなので、多くのケースでは実戦で使えると思ってます。

Runtime Contextをうまく活用する

CDKにはContextという形で、パラメータ値をスタックに流し込むことができます。

https://docs.aws.amazon.com/cdk/latest/guide/context.html

うまく活用することで、デプロイ時の不測の変更を防いだり、環境(ステージ)間の差をコードから分離して管理することができるようになります。

参考記事

CloudFormationのベストプラクティスに基づいて使う

CDKは新しいAWSのサービスというわけではなく、実態はCloudFormationであり、CFnをプログラム的に扱えるようにうまく抽象化したものです。
そのため、CDKを使っていく上でも、CFnの知識は不可欠であり、CFnには成熟したベストプラクティスが存在するので、まずはそれを身につけた上で使っていきましょう。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html