新しいことにはウェルカム

技術 | 電子工作 | ガジェット | ゲーム のメモ書き

CloudFormationは、AWS CDKから使うのが正解な気がしてきた

以前、AWS CloudFormationテンプレートを手書きしようとして挫折しました…。

www.kwbtblog.com

その時学んだのは、CloudFormationテンプレートは人の手で書く類のものではなく、何かしらのツールを使って生成するものだなと。

そして、そのためのツールが、AmazonからAWS CDKという形で提供されていたので使ってみたのですが、めちゃ便利でした!!

もうこれからは、AWSのリソース作成はコンソールやCLIで行わず、全部AWS CDKで作ろうかと思うくらい便利でした!

必要な時にサクッとAWS CDKを使えるよう、ここにAWS CDKの簡単な説明と基本的な使い方をメモしておこうと思います。

AWS CDKとは?

AWS CDK(Cloud Development Kit)とは一言で言うと、CloudFormationテンプレートジェネレーターです。

TypeScript等のプロブラム言語で、CloudFormationテンプレートを出力するプログラムが書けます。

また、出力したテンプレートを使ってCloudFormationへのデプロイもAWS CDK上で行うこともできます。

テンプレートをプログラムで書くとは?

言葉だけだとイメージしづらいんですが、プログラミングは下記のような流れになっています。

  1. AWSの各種リソースがクラスとして定義されていて、デプロイしたいAWSリソースのクラスをnewでオブジェクトとして生成していきます。
  2. オブジェクトは親子関係を持っていて、CloudFormationのスタックを頂点とした、リソースのオブジェクトツリーを構築していきます。

ユーザーが書くプロブラムはここまでで、このプロブラムを実行すると、構築されたオブジェクトツリー全体が、CloudFormationテンプレートとして出力されます。

AWS CDKのここが便利

簡潔に書ける

CloudFormationテンプレートを手で作成しようとすると、論理名の紐付けや、関数の利用など、色々面倒な作業が多いのですが、プログラム言語の柔軟性により、いい感じに簡潔に記述できるようになります。

ミスがなくなる

リソース名やパラメータ名の入力時に、エディタの補完機能が使えるのでタイプミスがなくなります。

また、誤ったリソースどうしを参照した場合、プログラムの入力・コンパイル時にエラーがでるので、論理ミスもなくなります。

コンソール・CLI感覚でリソースが作れる

コンソールやAWS CLIでリソースを作成した場合、表には見えていなが、実は裏で必要なパラメータの設定や、権限の付与、他の必要リソースの作成などが行われています。

テンプレートを手で書いた場合、それらを全て自分で記述しなければならず、このコンソール・CLIコマンドから見えている部分と、実際のテンプレートとのギャップがめちゃめちゃ大きいんです。

しかし、AWS CDKを使うと、プログラムでは指定されていないが実は必要なものを、裏でよしなにテンプレートに加筆してくれるので、コンソール・CLIで作成する時と同じ手軽さで、CloudFormationを使ってリソースを作成することができちゃいます。

続いて、使い方を見ていきます。

AWS CDKインストール

npmでインストールするので、Node.jsをインストールしておく必要があります。

npm install -g aws-cdk

AWS CDKのCLIコマンドはcdkです。

作業の流れ

アプリ作成

まずフォルダを作成してアプリを作成します。

1アプリ1フォルダで、フォルダ名がアプリ名になります。

作成したフォルダ内でAWS CDKのCLIコマンドでアプリの雛形をセットアップします。

mkdir <app-name>
cd <app-name>
cdk init app --language tyescript

開発サイクル

上記で作成した雛形を元にプログラムを記述し、下記のコマンドで開発サイクルを回していきます。

npm run build # TypeScriptのコンパイル
cdk diff # テンプレート出力および、CloudFormationへデプロイ済みテンプレートとの差分表示
cdk synth # テンプレート出力
cdk deploy # CloudFormationへスタックをデプロイ
cdk destory # CloudFormationにデプロイしたスタックを削除

cdk diffおよびcdk synthコマンド時にプログラムが実行され、テンプレート出力が行われます。プログラムにコンソール出力が記載されていた場合、出力もこの時行われます。

テンプレートファイルは

./cdk.out/<スタック名>.template.json

ファイルとして出力されます。

サンプル

例えば「ファイルがアップされるとAWS SNSでメール通知するS3バケット」も、下記のように、やりたいことを記載するだけでできてしまいます。

実際に出力されるテンプレートは数百行に及ぶのですが、必要に応じて適切な加筆を裏で自動で行ってくれるので、プログラムの記述量はほんの少しで済みます。

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as s3n from '@aws-cdk/aws-s3-notifications';
import * as sns from '@aws-cdk/aws-sns';
import * as snss from '@aws-cdk/aws-sns-subscriptions';

class MyFirstCdkStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        const bucket = new s3.Bucket(this, 'Bucket');
        const topic = new sns.Topic(this, 'Topic');
        topic.addSubscription(new snss.EmailSubscription('foo@bar.com'));
        bucket.addObjectCreatedNotification(new s3n.SnsDestination(topic));
    }
}

const app = new cdk.App();
new MyFirstCdkStack(app, 'dev-MyFirstCdkStack');

プログラム構成

サンプルプログラムを見ると分かるように、まずアプリを作成し、次にスタックを作成し、スタックのコンストラクタの中で、AWSの各種リソースを生成していきます。

後述しますが、オブジェクト生成時に、第1引数に親を渡していて、アプリ->スタック->リソースと、ツリー構造になっています。

construct

テンプレートへの出力対象となるオブジェクトは、Constructクラスを継承していて、constructと呼ばれます。

constructはnewで作成し、作成時に、左から順に「scope」「id」「props」の3つの引数を取ります。

引数説明

  • scope
    • そのconstructを作成した、親のconstructを示します。ほとんど場合、ここにはthisをセットします
  • id
    • constructの識別名を指定します
  • props
    • そのリソースを作成するのに必要なパラメータをセットします。どんなパラメータが必要かは、リソースのクラスによって異なります

論理名

CloudFormationテンプレートでは、リソースに論理名を割り振ってリソースを識別するのですが、AWS CDKでは<scopeのid>+<id>+<ハッシュ値>が、その論理名になります。

「scope」にはthisを渡すので、constructが入れ子状(ツリー状)に作成されると、論理名もフォルダパスのように階層上に定義され、論理名が重複しないようになっています。

ただし、同じscope内で同じidだと、同じ論理名になり重複してしまいます。(当たり前ですね…)

また、CloudFormationテンプレートでは、論理名が変わると別のリソースとして扱われるため、一度CloudFormationへデプロイした後に、プログラムのリソースの生成位置を、別のscope配下に移動した場合、デプロイ済みのリソースとは全く別のリソース扱いになってしまうので注意が必要です。

スタック

1つのアプリから複数のスタックを生成することができ、テンプレートはスタック毎に出力されます。

テンプレートファイルは./cdk.out/<スタックのid名>.template.jsonで、CloudFormatinにデプロイした時のスタック名も<スタックのid名>となります。

スタックの第1引数にアプリを渡していることから分かるように、リソースはアプリ->スタック->リソースと、スタックの子にあたるのですが、リソースの論理名にはアプリとスタックのidは含まれません。

複数スタック

前述のとおり、アプリからは複数のスタックを生成することができます。

const app = new cdk.App();
new MyFirstCdkStack(app, 'MyFirstCdkStack');
new MySecondCdkStack(app, 'MySecondCdkStack');

cdkコマンドで、プログラムで作成されるスタック一覧を見ることができます。

cdk ls

cdkコマンドでスタック名を指定すると、指定したスタックだけに適用することができます。

cdk deploy MyFirstCdkStack

リファレンス

プログラミンは、AWS CDK APIリファレンスを片手に作業することになるかと思います。

「Constructs」に記載されているクラスが、いわゆるnewで追加していく各種AWSリソースのconstructです。

constructをクリックすると、必要なパラメータ(props)や、そのconstructのプロパティ・メソッドの詳細説明を見ることができます。

constructはCloudFormationテンプレートのリソースをラップしたものと言えるのですが、リソースに行う大抵の操作はconstructのクラスに移植されているので、ほぼAWS CDK APIリファレンスのみでリソース構築ができてしまいます。

ただ、後述するCloudFormationテンプレートをそのまま扱う際は、CloudFormationのテンプレートリファレンスを見ながらの作業になります。

生のCloudFormationテンプレート記述を行う

多くの場合、s3.Bucketなど、ハイレベルなconstructを使用しますが、CloudFormationテンプレートと1対1対応する、「Cfn<リソース種類>」のconstructもあり、それを使って直接CloudFormationテンプレートを記載することもできます。

「Cfn<リソース種類>」は、そのリソースが使用するプロパティが予め用意されていますが、更に、CfnResourceを使えば、リソースの種類・プロパティ名までも自分で指定して、完全に生のCloudFormationテンプレートを記載することもできます。

const cfnBucket = new s3.CfnBucket(this, 'Bucket01', {});
const cfnResouce = new cdk.CfnResource(this, 'Bucket02', {
    type: 'AWS::S3::Bucket',
    properties: {},
});

ちなみに、AWS CDKでは

  • 「Cfn<リソース種類>」や「CfnResource」など、CloudFormationテンプレートと1対1対応で、CloudFormationテンプレートを直接扱う、ローレベルのconstructを、L1(Level 1)constructと呼びます
  • 「s3.Bucket」など、CloudFormationテンプレートをカプセル化して、テンプレートを直接扱わない、ハイレベルのconstructをL2(Level 2)constructと呼びます

既存のリソースを参照する

スタック外で作成された既存リソースは、スタックの中から変更することはできませんが、参照したい時はあります。

いくつかのconstructには、from...()という名前のstatic関数が用意されているので、それを使えば、既存リソースの参照を作成することができます。

例えば、新規ユーザーに既存のグループを割り当てるには下記のようになります。

const group = iam.Group.fromGroupArn(this, 'group', 'arn:aws:iam::1234:group/group_name')
const user = new iam.User(this, 'user');
user.addToGroup(group);

出力されたテンプレートは下記のようになっているのですが、AWS CDKが良しなにARNからグループ名を設定してくれているのが分かります。

{
  "Resources": {
    "user2C2B57AE": {
      "Type": "AWS::IAM::User",
      "Properties": {
        "Groups": [
          "group_name"
        ]
      }
    }
  }
}

逆に、static関数が用意されていないconstructは、参照する手段がないことを意味しています。

AWS CDKコードの再利用

cdk.Constructクラスを継承して、オリジナルconstructを作り、一連のリソース作成をまとめることができます。

export interface customProps {
    custom?: string;
}

export class customConstruct extends cdk.Construct {
    constructor(scope: Construct, id: string, props: customProps = {}){
        super(scope, id);

        const bucket1 = new s3.Bucket(this, 'bucket1');
        const bucket2 = new s3.Bucket(this, 'bucket2');
        const bucket3 = new s3.Bucket(this, 'bucket3');
    }
}

スタックもconstructなので、スタックもオリジナルconstructとも言えます。

それらにより、一連のリソース作成を、オリジナルconstructにまとめてモジュール化し、他のAWS CDKプログラムから再利用することができます。

本番・開発に分ける

本番・開発でリソースを分けるには、出力するスタックで分けてしまうのが、AWS CDK流のようです。

プログラムなので、スタックに分けるコードはいかようにも書けるのですが、ポータビリティーを考えると、環境値をハードコーディングせず、propsに値を渡す形がいいのかなぁと思います。

propsを使ったサンプルを記載しました。追加したpropsのパラメーターが親クラスに渡らないように、パラメーターを除いたpropsを親クラスのコンストラクタに渡します。

interface stackProps extends cdk.StackProps {
    stage?: string;
}

class MyFirstCdkStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props: stackProps) {
        const { stage, ..._props } = props;
        super(scope, id, _props);

        const bucket = new s3.Bucket(this, 'Bucket', {
            bucketName: `${stage}-MyFirstCdkBucket`,
        });
    }
}

let stage;
const app = new cdk.App();

// dev stack
stage = 'dev';
new MyFirstCdkStack(app, `${stage}-MyFirstCdkStack`, { stage });

// prod stack
stage = 'prod';
new MyFirstCdkStack(app, `${stage}-MyFirstCdkStack`, { stage });

ファイルアップロード

AWSのリソースをデプロイするにあたり、ローカルファイルのアップロードが必要なケースがあったりしますが、そんな時も、ユーザーが別途自分でアップロードする必要はなく、AWS CDKが代わりにやってくれるので便利です。

Lambda

Lambda関数のソースコードが置かれているフォルダを指定すれば、自動でフォルダ内をまるっとアップロードしてくれます。

const lambda = new lambda.Function(this, "lambdaTest", {
     runtime: lambda.Runtime.NODEJS_12_X,
     code: lambda.Code.fromAsset("./src"),
     handler: "app.handler",
});

S3

S3にアップしたいファイルが置かれているフォルダを指定すれば、自動でフォルダ内をまるっとアップロードしてくれます。

const bucket = new s3.Bucket(this, 'bucketTest');
const s3file = new s3deploy.BucketDeployment(this, 's3FileTest', {
    sources: [s3deploy.Source.asset('./upload')],
    destinationBucket: bucket,
});

その他注意点

バージョンを揃える

AWS CDKは頻繁にバージョンアップが行われていて、CLIとモジュールおよび、モジュールどうしのバージョンが違ってしまうことが結構あります。バージョンが異なるとエラーになることが多いので、こまめにチェックして、全てのバージョンを揃えます。

CLIバージョン確認

cdk --version

モジュールバージョン確認

cat package.json

# 古いものだけチェック
npm outdated

CLIバージョンアップ

npm update -g aws-cdk

aws-cdk関連モジュールバージョンアップ

npm install <モジュール名>

# 「aws-cdk」関連モジュール一括処理処理
npm ls --depth=0 --json --dev --prod \
    | jq -r '.dependencies|keys|.[]' \
    | grep aws-cdk \
    | xargs -L 1 npm install

変えちゃいけないもの

リソースのユニークは「スタック名+論理名」で決まります。

そして、論理名は「scope+id」で決まります。

なので、一度デプロイした後で

  • スタック名(スタックのid)
  • リソースのツリー階層位置(scope)
  • リソースの識別名(id)

を変更すると、同じリソースとはみなされなくなるので、変更する際は十分に注意が必要です。

CloudFormationのパラメーターは使わない

AWS CDKではパラメーターを使わず、静的なテンプレートを作成することが推奨されています。

In general, we recommend against using AWS CloudFormation parameters with the AWS CDK. Unlike context values or environment variables, the usual way to pass values into your AWS CDK apps without hard-coding them, parameter values are not available at synthesis time, and thus cannot be easily used in other parts of your AWS CDK app, particularly for control flow.

CloudFormationは再利用の単位がテンプレートだったため、それに値を与える手段としてパラメーターがありました。

AWS CDKではそれらが、constructとpropsになり、値はconstruct利用時に与え、値設定済みのFIXしたテンプレートを個別に出力するというスタイルに変わったので、CloudFormationのパラメーターを使う必要性が薄まっています。

既存のCloudFormationテンプレートを読み込む

AWS CDKに魅了されると、既存のCloudFormatinテンプレートもAWS CDKで扱いたくなってきます。

cloudformation-include.CfnIncludeを使うと、既存のCloudFormatinテンプレートを読み込んで、中で記載されているリソースをL1 constructとして生成してくれます。

そのconstructを編集することにより、既存のCloudFormationテンプレートを、AWS CDKのコード上で編集することを可能としています。

利用はイマイチ?

cloudformation-include.CfnIncludeを実際に試してみましたが、生成されるのはL1 constructなので、生のCloudFormationテンプレートを扱っているのと変わらず、利用するメリットはあまり感じませんでした。

恐らく自分が求めているのは、既存のCloudFormationテンプレートを、L2 constructを使ってリライトしたAWS CDKコードなんだと思います。

ただ、既存のリソースからCloudFormationテンプレートを生成するCloudFormerが、結局正式リリースされなかったのと同様、既存のCloudFormatinテンプレートからL2 constructコードを生成する機能が、今後出てくることは無いんじゃないかと思っています…。

なので、既存のCloudFormatinテンプレートを見ながら、自分で同じものをAWS CDKで作り直した方が早そうです。

感想など

AWS CDKは、ほんとよくできてますね。

欲を言えば、CloudFormationと一緒にリリースして欲しかった…。

CloudFormationをリリースした当時は、まさかテンプレートがこんなにも複雑になるとは思ってなかったのかも知れませんね。

Infrastructure as Code(IaC)も、リソースが複雑になりすぎて、単にリソースを宣言的に記述するだけでは対応しきれず、高度なプログラミング機能が必要になってきたということでしょうか。

そして、Amazon的には、独自にプログラミング機能を追加するより、いっそのこと、プログラミング言語そのものでIaCを記述すればいいじゃんとなったんじゃないかと憶測しています。