1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NIJIBOXAdvent Calendar 2022

Day 23

AWS CDKについて学ぶ→TypeScriptでTDDを意識して実践→CDKプロジェクトの環境別のデプロイを考えてみた

Last updated at Posted at 2023-01-11

はじめに

AWS CDKは、AWS リソース(インフラやサーバーなど)をコードから構築できる開発キットで、このAWS CDKを使ってAWS リソースを構築したくて勉強しました。

個人的にAWS公式サイトは迷子になるので、勉強したことを残しておきたくて記事にしました。
(ただのやってみた系の記事です)

やったこと

  • AWS CDKについて理解する
  • AWS CDKを使った開発方法を理解して、実践する
    • テストコードも書く(TDDを意識)
  • 環境(開発・検証など)が分かれている場合の実装方法を考えて実践する

※ 開発方法の理解が目的なので、記事内で出現するコードは簡単な内容で実装しています。
CDK Pipelinesを使った自動デプロイはやっていません。

前提知識

AWS CloudFormationの知識
AWS CloudFormationについてはこちらの記事にまとめています。

筆者の動作環境

  • Node.js 18.12.1
  • AWS CDK 2.53.0
  • AWS CLI 2.9.1
  • MacBook Pro M1チップ Monterey(12.0.1)

AWS CDKについて学ぶ

  • AWS CDKは、AWS リソースをコードから構築できる開発キット
  • TypeScriptやPythonなどの言語を使って開発できる
  • 実装したコードはAWS環境にデプロイする前にテストできる

開発の流れ

  1. CDK プロジェクトを作成(cdk init)
  2. AWS リソースのコードを実装
  3. CloudFormationのテンプレートに変換(cdk synth)
  4. CloudFormationにデプロイ(cdk deploy)
    → デプロイするとCloudFormationのスタックが作成され、AWS環境にリソースを作る

CDK プロジェクトの3つの要素

  • App(CDK アプリケーション)

    • スタック同士の依存関係などスタックの管理をしている
  • Stack(スタック)

    • AWS環境へのデプロイの単位で、CloudFormationのスタック(テンプレート)に相当する
  • Construct(コンストラクト)

    • デプロイするAWS リソースの定義こと
    • スタック内に、Construct Library を使ってAWS リソースを定義していく

こちらの記事がとてもわかりやすかったです。

Construct Library(コンストラクト ライブラリ)とは?

AWS リソースを実装するためのライブラリで、AWS リソースごとに用意されている
(例)Lambda コンストラクト ライブラリ

  • 使い方はAWS CDKのAPIリファレンス に定義されている

  • コンストラクト ライブラリは3種類ある

    • L1 コンストラクト (Low Level Construct)

      • AWS リソースのプロパティ値を明示的に設定する必要がある
      • コードが長くなるが細かい設定ができる
      • クラス名にCfnプレフィックスがついている
    • L2 コンストラクト (High Level Construct)

      • 便利なメソッドが提供されていて、直感的にコーディングしやすい
      • デフォルトの設定がされていて、細かい設定が省略できる
        → よしなにAWS リソースをベストプラクティスで作成してくれる
      • サポートされていないAWS リソースがある
    • L3 コンストラクト (Patterns)

      • 複数のAWS リソースを組み合わせてパターン化されたAWS環境を作れる

基本的にL2を使い、細かい設定をするならL1のライブラリを選ぶだったような・・(たしか)

AWS CDKの自動テストについて

テストは2種類ある。

アサーションテスト

実装したコードのAWS リソースの設定が、期待値と一致するかなどテストする。

詳細には「CDK スタック インスタンスのプロパティ値」と「変換されるCloudFormationテンプレートのプロパティ値」を比較しているようである。

テストの実装はCDK assertionsモジュールを使って行う。
AWS CDKのAPIリファレンスのassertionsモジュールのページに定義されている。

スナップショットテスト

以前に作成したCloudFormationテンプレートと比較して差分があるかテストする。

テストについての公式ドキュメント

AWS CDK コマンド

CDK コマンドを使ってCDKの実行を行う。
CDK コマンドはcdk --helpで確認できる。

よく使いそうなコマンド一覧

CDK コマンド 説明
cdk init [TEMPLATE] CDK プロジェクトを作成(テンプレートを省略可)
cdk bootstrap [ENVIRONMENTS..] デプロイ設定(アカウント、リージョンごとに1回設定)
cdk list [STACKS..] CDKのスタック一覧を表示
cdk synthesize [STACKS..] CDKのスタックをCloudFormationのテンプレートファイルに変換(cdk synthに省略可)
cdk deploy [STACKS..] CDKのスタックをCloudFormationへデプロイ
cdk destroy [STACKS..] 指定したスタックをCloudFormationから削除
cdk diff [STACKS..] CDKのスタックとCloudFormationのスタックの差分を表示
  • CDKのスタックが1つの場合は、スタック名の省略が可能
  • こちらの記事に全てのコマンドについて詳細に書かれていました


[実践]AWS CDK のインストール

事前準備

AWS CDKを使用するには、以下のインストールが必要(2022年12月時点)

  • Node.js 10.13.0 以降
    長期サポートが有効な最新バージョンがおすすめとのこと
    ※バージョン 13.0.0 〜 13.6.0 は互換性の問題があるためサポートしていない

  • AWS CLI
    設定手順はこちらの記事にまとめています。

AWS CDK をインストールする

インストール
$ npm install -g aws-cdk
バージョン確認
$ cdk --version
2.53.0 (build 7690f43)

AWS CDK のバージョンについて

推奨のバージョンについては公式サイトで確認できる。
2022年12月時点ではv2を使うことを勧められている。

インストール可能なバージョンの確認
$ npm info aws-cdk versions

[
  '0.8.0',          '0.8.1',          '0.8.2',          '0.9.0',
     〜〜略〜〜
  '2.51.0',         '2.51.1',         '2.52.0',         '2.52.1',
  '2.53.0'
]

[実践]CDK プロジェクト作成〜デプロイ (VPC作成)

やること
CDK プロジェクトを作成 → VPCを構築するコードを書く → AWS環境にリソースデプロイ

1. CDK プロジェクト作成(cdk init)

  • 任意のフォルダを作成し、cdk init --language [言語]を実行する
  • フォルダ名がデフォルトのCDKスタック名などに使われていたので、適当すぎない名前が良いかも
プロジェクト作成
$ mkdir sample-app

$ cd sample-app/

$ cdk init --language typescript

Applying project template sample-app for typescript
# Welcome to your CDK TypeScript project

You should explore the contents of this project. It demonstrates a CDK app with an instance of a stack (`SampleAppStack`)
which contains an Amazon SQS queue that is subscribed to an Amazon SNS topic.
# →SQS キューを含み、SNS トピックにサブスクライブしているスタック をデモとして実装しているよ!

✅ All done!

作成されたプロジェクト構成を詳しく見てみる

プロジェクト構成
sample-app/
├── README.md
├── bin
│   └── sample-app.ts  # 【実装ファイル】Appクラス定義Stackクラスのインスタンスを作成している
├── cdk.json           # [設定ファイル]CDKの設定
├── jest.config.js     # [設定ファイル]JSのテストプラットフォーム「jest」の設定ファイル
├── lib
│   └── sample-app-stack.ts # 【実装ファイル】StackとConstructクラスを定義(リソースを実装)
│   
├── node_modules        # [設定ファイル]依存パッケージのファイル群
├── package-lock.json
├── package.json        # [設定ファイル]プロジェクト設定ファイル
├── test
│   └── sample-app.test.ts # 【実装ファイル】自動テスト用ファイル
└── tsconfig.json          # [設定ファイル]TypeScriptの設定ファイル

もっと詳細を見る!
  • TypeScriptのインストールと設定がされていた

  • gitの設定もされていて、パッケージの初期の状態でコミットされていた

    gitコミットログ
    $ git log 
    commit 51d266b634b626e28cb63b9a1957ed8683d4b9a9 (HEAD -> main)
    Author: sari <sari@xxxxxx.co.jp>
    Date:   Tue Dec 6 23:55:14 2022 +0900
    
    Initial commit
    
  • --generate-onlyオプションをつけてCDK プロジェクトを作成すれば、パッケージのインストールやgit リポジトリ作成は行われない

    helpでオプション確認
      $ cdk init --help
      --generate-only      If true, only generates project files, without
                           executing additional operations such as setting up a
                           git repo, installing dependencies or compiling the
                           project                    [boolean] [default: false]
    

【実装ファイル】をもう少し詳細に説明

  • lib/sample-app-stack.tsでStackクラスを定義

    • Stackクラスはデプロイの単位(CloudFormationのテンプレートになる)
    • Stackクラスは複数作ることが可能
  • bin/sample-app.tsでStackクラスのインスタンスを作成

    • Stackクラスを追加した場合、このファイルにインスタンス化するコードを追加する
    • アプリケーション実行時に最初に呼びされる
      sample-app/cdk.json
       "app": "npx ts-node --prefer-ts-exts bin/sample-app.ts",
      

      ts-nodeを利用するとJavaScriptにコンパイルせずにTypeScriptのファイルのまま実行することができる。

プロジェクト設定(好み) - Node.jsのバージョン固定

現時点で最新のバージョンの18.12.1を使うので、package.jsonに設定しておく。
Node.jsのバージョン管理ツールはVoltaを使っているので以下で設定した。

$ volta pin node@v18.12.1
package.json
 "volta": {
    "node": "18.12.1"
  }

2. デプロイ設定(cdk bootstrap)

AWS CDKでデプロイするときは、デプロイ用のS3などの準備が必要である。
AWSアカウントとリージョン毎にcdk bootstrapコマンドを実行して準備する。
(1回設定した次から設定は不要)

名前付きプロファイルを指定して設定する

(1)名前付きプロファイルを設定する

手順はこちら

設定したプロファイル
$ cat ~/.aws/config
[profile hoge-system_dev]
region = ap-northeast-1
output = json

$ cat ~/.aws/credentials 
[hoge-system_dev]
aws_access_key_id = XXXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXXX

(2)cdk bootstrapコマンドを実行

設定したプロファイルを指定してcdk bootstrapコマンドを実行

$  cdk bootstrap --profile hoge-system_dev

プロファイルで[default]に設定しなかったので、
cdk deployなどのAWS環境と接続するコマンドは--profileで対象の環境を指定する

3. 実装 (VPCを作成する)

TDDを意識してコーディングしました。

(1)テストコードを書く

  • APIリファレンスのCDK assertionsモジュールのページを見ながらテストを書く

  • テストの期待値は、CloudFormationのテンプレートのプロパティ値を書いていくので、Cloud Fromationの公式リファレンスを見るのが、個人的には効率よくかけた

  • ひとまず、VPCにサブネットを2つを作るコードを最低限で書いてみる

test/sample-app.test.ts
import * as cdk from "aws-cdk-lib";
import { Template, Match } from "aws-cdk-lib/assertions";
import * as SampleApp from "../lib/sample-app-stack";

// スタック作成〜テンプレート取得
const app = new cdk.App();
const stack = new SampleApp.SampleAppStack(app, "MyTestStack");
const template = Template.fromStack(stack);

// TEST
test("VPCがcidr192.168.0.0/16で1つ作成されること", () => {
  template.resourceCountIs("AWS::EC2::VPC", 1);
  template.hasResourceProperties("AWS::EC2::VPC", {
    CidrBlock: "192.168.0.0/16",
  });
});

test("サブネットが2つ作成されること", () => {
  template.resourceCountIs("AWS::EC2::Subnet", 2);
});

(2)AWS リソースを構築するコードを書く

lib/sample-app-stack.ts
import { Stack, StackProps } from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";

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

    const vpc = new ec2.Vpc(this, "SariVpc", {
      ipAddresses: ec2.IpAddresses.cidr("192.168.0.0/16"),

      // サブネット構成(特に指定なければマルチAZ構成で2つ作成される)
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "SariVpc_PublicSubnet",
          subnetType: ec2.SubnetType.PUBLIC,
        },
      ],
    });
  }
}

(3)自動テスト実行

テスト実行
$ npm run build && npm run test

> sample-app@0.1.0 build
> tsc

> sample-app@0.1.0 test
> jest

 PASS  test/sample-app.test.ts (9.376 s)
  ✓ VPCがcidr192.168.0.0/16で1つ作成されること (1 ms)
  ✓ サブネットが2つ作成されること

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        9.628 s
Ran all test suites.

※(例)テスト失敗した時の結果

> sample-app@0.1.0 test
> jest

 FAIL  test/sample-app.test.ts
  ✓ VPCがcidr192.168.0.0/16で1つ作成されること (1 ms)
  ✕ サブネットが1つ作成されること

  ● サブネットが1つ作成されること

    Expected 1 resources of type AWS::EC2::Subnet but found 2

      17 |
      18 | test("サブネットが1つ作成されること", () => {
    > 19 |   template.resourceCountIs("AWS::EC2::Subnet", 1);
         |            ^
      20 | });
      21 |

      at Template.resourceCountIs (node_modules/aws-cdk-lib/assertions/lib/template.js:1:2087)
      at Object.<anonymous> (test/sample-app.test.ts:19:12)

デプロイする前に、間違いや想定との乖離に気がつけるので効率が良い!

[補足]TypeScriptコンパイル
TypeScriptで記述されたコードをJavaScriptにコンパイルする。

  • リアルタイムでコンパイルする場合npm run watch
  • コンパイルを都度実行したい場合 npm run build

(tscコマンドはプロジェクト作成時に自動でpackage.jsonに設定されている)

package.json
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },

(4)(1)〜(3)を繰り返す

(1)〜(3)を繰り返して開発する。
こちらのサイトのテストコードがとても分かりやすかったです。参考にさせていただきました。

4. CloudFormationのテンプレートに変換(cdk synth)

前章で貼り付けたコードを変換する

$ cdk synth
出力されたファイル
cdk.out/
├── SampleAppStack.assets.json
├── SampleAppStack.template.json
├── cdk.out
├── manifest.json
└── tree.json

5. CloudFormationにデプロイ(cdk deploy)

  • プロファイル(AWS環境)を指定して、デプロイを実行する
  • CDKのスタックが1つしかないので、cdk deployコマンド実行時にスタック名の指定は省略した
デプロイ
$ cdk deploy --profile hoge-system_dev

✨  Synthesis time: 3.8s

SampleAppStack: building assets...
SampleAppStack: assets built
SampleAppStack: deploying...
SampleAppStack: creating CloudFormation changeset...

 ✅  SampleAppStack

✨  Deployment time: 70s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:xxxxxxxx:stack/SampleAppStack/xxxxxxxxx

✨  Total time: 73.8s

6. デプロイされたリソースを確認

AWS マネジメントコンソールから確認する。

  • CloudFormationのスタックが作成されていた!

  • 作成されたリソース

    • 指定したリソースが作成されていた(VPC 1つとサブネット2つ)
    • 指定していないリソースも作成されていた(これがよしなに作成してくれるやつ)

リソースの詳細確認

  • パブリックサブネット
    作るAZは指定してないが、それぞれ別のAZに作られマルチAZ構成になっていた!
    すごい!

  • IGW
    IGWの作成は明示的に指定していないが自動で作ってくれていた!
    subnetType: ec2.SubnetType.PUBLICの設定から判断してくれたと思う。

  • ルート設定
    メインルートテーブル(1つ)とカスタムルートテーブル(2つ)が作られていた!

    カスタムルートテーブル(2つ)に、それぞれのサブネットとIGWのルート設定がされていた!
    ルートはメインルートテーブルに設定せず、カスタムルートテーブルに設定するのがベストプラクティス。指定しなくてもここまで設定してくれるとは・・・!

[実践]CDK プロジェクト修正〜デプロイ (Lambda)

やること
CDK プロジェクトを作成 → Lambdaを構築するコードを書く → AWS環境にリソースデプロイ(スタック作成) → Lambdaを構築したコードを修正 → AWS環境にリソースデプロイ(スタック更新)

1. CDK プロジェクトを作成(cdk init)

プロジェクト作成
$ mkdir sample-app-lambda

$ cd sample-app-lambda/

$ cdk init --language typescript

同じAWSアカウントとリージョンなので、cdk bootstrap はしなかった

2. 実装 (Lambdaを作成する)

Lambdaのコードも一緒にデプロイしたいので、プロジェクトに手動でLambdaのコードを追加する。
LambdaのコードはPythonで記載した。(特に理由なし)

プロジェクト構成
sample-app-lambda
├── README.md
├── bin
│   └── sample-app-lambda.ts
├── cdk.json
├── jest.config.js
├── lib
│   ├── lambda       ←追加
│   │   └── hello.py ←追加
│   └── sample-app-lambda-stack.ts  ←修正
├── node_modules
├── package-lock.json
├── package.json
├── test
│   └── sample-app-lambda.test.ts   ←修正
└── tsconfig.json

こちらを参考に実装した

test/sample-app-lambda.test.ts
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import * as SampleApp from "../lib/sample-app-lambda-stack";

// スタック作成〜テンプレート取得
const app = new cdk.App();
const stack = new SampleApp.SampleAppLambdaStack(app, "TestStack");
const template = Template.fromStack(stack);

// TEST
test("作成するLambdaは1つであること", () => {
  template.resourceCountIs("AWS::Lambda::Function", 1);
});

describe("「Sari-getHellowWorld」のテスト", (): void => {
  test("Runtime(言語)の設定が「python3.9」であること", () => {
    template.hasResourceProperties("AWS::Lambda::Function", {
      Runtime: "python3.9",
    });
  });

  test("handler(実行コード)の設定が「lambda_function.lambda_handler」であること", () => {
    template.hasResourceProperties("AWS::Lambda::Function", {
      Handler: "lambda_function.lambda_handler",
    });
  });

  test("Description(説明)の設定が「HellowWorldを返却するAPI」であること", () => {
    template.hasResourceProperties("AWS::Lambda::Function", {
      Description: "HellowWorldを返却するAPI",
    });
  });

  test("Lambda名が「Sari-getHellowWorld」であること", () => {
    template.hasResourceProperties("AWS::Lambda::Function", {
      FunctionName: "Sari-getHellowWorld",
    });
  });
});

lib/sample-app-lambda-stack.ts
import { Stack, StackProps, aws_lambda } from "aws-cdk-lib";
import { Construct } from "constructs";

const path = require("path");
export class SampleAppLambdaStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // 第2引数はリソースの論理IDになる
    new aws_lambda.Function(this, "Sari-getHellowWorld", {
      functionName: "Sari-getHellowWorld",
      description: "HellowWorldを返却するAPI",
      runtime: aws_lambda.Runtime.PYTHON_3_9, // Lambda言語
      handler: "lambda_function.lambda_handler", //実行する関数
      code: aws_lambda.Code.fromAsset(path.join(__dirname, "lambda")), // Lambdaに設定するコード(展開前に圧縮されて S3 にアップロードされる)
    });
  }
}

lib/lambda/hello.py
import json

def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "body": json.dumps('Hello from Lambda!')
    }

3. CloudFormationのテンプレートに変換(cdk synth)

$ cdk synth

4. CloudFormationにデプロイ(cdk deploy)

Lambda用のIAMロール作るよって聞かれたのでYesにした。
(IAMロール必要だけどコードに書いてなかった)

デプロイ
$ cdk deploy --profile hoge-system_dev

SampleAppLambdaStack: assets built

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 │
├───┼────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤
│ + │ ${Sari-getHellowWorld/ServiceRole.Arn} │ Allow  │ sts:AssumeRole │ Service:lambda.amazonaws.com │           │
└───┴────────────────────────────────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│   │ Resource                           │ Managed Policy ARN                                                             │
├───┼────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${Sari-getHellowWorld/ServiceRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(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
SampleAppLambdaStack: deploying...
SampleAppLambdaStack: creating CloudFormation changeset...

 ✅  SampleAppLambdaStack

✨  Deployment time: 61.28s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1

✨  Total time: 69.9s

5. デプロイされたリソースを確認

AWS マネジメントコンソールから確認する。

  • CloudFormationのスタックが作成されていた!

作成されたリソースの詳細を見る

  • Lambda用のIAMロール
    IAMロールにアタッチされたポリシーは「CloudWatch Logs」の書き込み権限のみで、Lambdaのログを「CloudWatch Logs」に書き込む用と思われる。
    必要最低限の権限が付与されたロールが自動で作られていた!
    すごい..

  • Lambda
    一緒にデプロイしたコードもアップされていた!

    自動で作成されたIAMロールもアタッチされていた。。!

6. デプロイしたLambdaをテスト実行してみる

Lambdaを画面からテスト実行すると以下のエラーになった。
「lambda_functionがない」とのことで、これは単純なhandlerの設定ミス・・なので修正してデプロイし直す。

7. コード修正

handlerの設定を「hello.lambda_handler」に修正

lib/sample-app-lambda-stack.ts
handler: "hello.lambda_handler",

せっかくなので、Lambdaのレスポンスも変えてみる

lib/lambda/hello.py
import json

def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "body": json.dumps('Hello world!Hello world!') #ここを修正
    }

もちろんテストも修正済(テストコードは省略)

8. スタックの差分を確認(cdk diff)

  • CloudFormationの変更セットにあたる確認
  • 修正されるリソースの設定を確認することができる
$ cdk diff --profile hoge-system_dev

Stack SampleAppLambdaStack
Resources
[~] AWS::Lambda::Function Sari-getHellowWorld SarigetHellowWorldxxxxx 
 ├─ [~] Code
 │   └─ [~] .S3Key:
 │       ├─ [-] 1c5e3cd55bb87bfa6xxxxxxxxxxx.zip
 │       └─ [+] 2637a990f446715ddxxxxxxxxxxx.zip
 ├─ [~] Description
 │   ├─ [-] HellowWorld?????API
 │   └─ [+] HellowWorldを返却するAPI
 ├─ [~] Handler
 │   ├─ [-] lambda_function.lambda_handler
 │   └─ [+] hello.lambda_handler
 └─ [~] Metadata
     └─ [~] .aws:asset:path:
         ├─ [-] asset.1c5e3cd55bb87bfa6xxxxxxxxxxx
         └─ [+] asset.2637a990f446715ddxxxxxxxxxxx
  • Description
    変えてないけど、文字化けしているせい。日本語だめ?

  • Metadata
    Lambdaのコードの差分
    (cdk.out 配下にアップロードするソースが出力されている)

9. CloudFormationにデプロイ(cdk deploy) 

CloudFormationのスタックを更新する。

デプロイ
 cdk deploy --profile hoge-system_dev

✨  Synthesis time: 9.36s

SampleAppLambdaStack: building assets...
SampleAppLambdaStack: assets built
SampleAppLambdaStack: deploying...
SampleAppLambdaStack: creating CloudFormation changeset...

 ✅  SampleAppLambdaStack

✨  Deployment time: 56.07s

Stack ARN:
arn:aws:cloudformation:
✨  Total time: 65.43s
  • デプロイの途中でCloudFormationの変更セットを作成している
  • cdk deploy --change-set-nameで、変更セットの名前を指定できる
    ※変更セットについては「変更セットを使用したスタックの更新」に記載している。
  • 変更セットを作成した後、そのままスタックを更新していた
    変更セットだけ作成のCDK コマンドはなさそうでした。あれば知りたい・・・

10. デプロイされたリソースを確認

AWS マネジメントコンソールから確認する。

  • Lambdaのソースが更新されていた

  • 「handler」の設定も変更されていたので、Lambdaをテスト実行すると成功した

[実践]スタックを削除(cdk destroy

cdk destroyで、以下が全て削除される。

  • CloudFormationのスタック
  • スタックから作成されたAWS リソース
$ cdk destroy --profile hoge-system_dev
Are you sure you want to delete: SampleAppLambdaStack (y/n)? y
SampleAppLambdaStack: destroying...

 ✅  SampleAppLambdaStack: destroyed

[実践]1つのCDKプロジェクトで開発・検証・本番の各環境にデプロイしたい

実案件では1つのCDK プロジェクトで開発環境 (dev)、ステージング環境 (stg)、本番環境 (prod)などの複数環境にデプロイすることになる。AWS リソースによっては、世界中で一意の名前にしないといけないものがあり、開発・検証・本番でそれぞれ名前を変えないといけない。

CloudFormationを使った時に「リソース名の一部をプレフィックスにして、デプロイ時に外部パラメータで受け取った値を埋め込む」などの対応していたので、同じようなことがしたい。

まず方法を調べる

context変数を使って、環境を切切り替える記事を見つけた。

context変数とは?

◇コマンドラインのcontext変数(--contextオプション)

--contextオプションで変数を渡すことができる。変数はキー&バリューの形式。

$ cdk deploy --context Env=prod # 本番環境用スタックの生成

cdk.jsonに定義

cdk.jsonのcontextにcontext変数をキー&バリューで記述するして、アプリオブジェクトからcontext変数にアクセスできる。

cdk.json
{
  "context": {
    "bucket_name": "myotherbucket"
  }
}
呼び出し側のコード(bin/*.ts)
const app = new cdk.App();
const bucket_name = app.node.tryGetContext('bucket_name')

実践

やること

  • --contextオプションで実行環境を指定して、環境毎の設定でリソースをデプロイする
  • 各環境の設定は専用のクラスを作成して定義する(cdk.jsonの肥大化を避けるため)
  • 手軽に試したいので、開発環境と検証環境の2つで行う
  • AWS リソースはLambdaを作成する
    (全世界で名前を一意にする必要があり、前章でやったので)
ゴール(デプロイのコマンド)
$ cdk deploy --context Env=stg --profile hoge-system_stg # ステージング用
$ cdk deploy --context Env=dev --profile hoge-system_dev  # 開発環境用

調査の時に見つけたサイトのコードを参考にさせていただきました。

1. CDK プロジェクトを作成(cdk init)

手順などは省略

2. contextを扱うクラスを作成

lib/context.tsを手動で作成して、定義を記載する。

lib/context.ts
export type EnvironmentType = "dev" | "stg";

export type Config = {
  envSystemName: string;
  envType: EnvironmentType;
  envName: string;
};

export function getConfig(environmentType: EnvironmentType): Config {
  const systemName = "SampleApp";

  switch (environmentType) {
    case "stg":
      return {
        envSystemName: `${systemName}-${environmentType}`,
        envType: environmentType,
        envName: "staging",
      };
    case "dev":
      return {
        envSystemName: `${systemName}-${environmentType}`,
        envType: environmentType,
        envName: "develop",
      };
    default:
      throw new Error("Context value [Env] is invalid (use [stg] or [dev]).");
  }
}

3. 実装(環境ごとにスタックを分けてLambdaを作成)

実装したコードのみ貼り付け

◇環境ごとにスタックを作成する

bin/sample-app-lambda.ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { SampleAppLambdaStack } from "../lib/sample-app-lambda-stack";

import { EnvironmentType, getConfig } from "../lib/context";

const app = new cdk.App();

// 環境ごとの定義を取得してStackインスタンス作成
const env = app.node.tryGetContext("Env") as EnvironmentType;
const config = getConfig(env);

// id(=スタック名)を環境ごとに変えることでスタックを分ける
new SampleAppLambdaStack(app, config.envSystemName, { config });

// app単位で作成するリソース全てにタグをつける
cdk.Tags.of(app).add("ENV", config.envName);
cdk.Tags.of(app).add("name", config.envSystemName);

[補足]CDKで作成するリソースに一括タグ付けできる
「SCOPE」にapp、stack、constructを指定できる。

Tags.of(SCOPE).add('key', 'value');

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/tagging.html

◇Lambdaを作成する

Lambdaの論理IDと名前も環境ごと変える

lib/sample-app-lambda-stack.ts
import { Stack, StackProps, Duration, aws_lambda } from "aws-cdk-lib";
import { Construct } from "constructs";
import { Config } from "../lib/context";

interface SampleAppLambdaStackProps extends StackProps {
  config: Config;
}

const path = require("path");
export class SampleAppLambdaStack extends Stack {
  constructor(scope: Construct, id: string, props: SampleAppLambdaStackProps) {
    super(scope, id, props);

    const { envType } = props.config;

    // 第2引数はリソースの論理IDになる
    new aws_lambda.Function(this, `Sari-getHellowWorld-${envType}`, {
      functionName: `Sari-getHellowWorld-${envType}`,
      description: "HellowWorldを返却するAPI",
      runtime: aws_lambda.Runtime.PYTHON_3_9,
      handler: "hello.lambda_handler",
      code: aws_lambda.Code.fromAsset(path.join(__dirname, "lambda")),
      timeout: Duration.seconds(30),
    });
  }
}

◇自動テストは全環境分を実行する

実行結果

テストコード

test/sample-app-lambda.test.ts
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import * as SampleApp from "../lib/sample-app-lambda-stack";
import { EnvironmentType, getConfig } from "../lib/context";

// 開発スタックとテンプレート作成
const getTestTemplate = (env: EnvironmentType): cdk.assertions.Template => {
  const app = new cdk.App();
  const config = getConfig(env);

  const stack = new SampleApp.SampleAppLambdaStack(app, `testStac${env}`, {
    config,
  });

  return Template.fromStack(stack);
};

//全環境で同じテストを定義
const testSameInAllEnv = (template: cdk.assertions.Template) => {
  test("作成するLambdaは1つであること", () => {
    template.resourceCountIs("AWS::Lambda::Function", 1);
  });

  describe("「Sari-getHellowWorld-xxx」のテスト", (): void => {
    test("Runtime(言語)の設定が「python3.9」であること", () => {
      template.hasResourceProperties("AWS::Lambda::Function", {
        Runtime: "python3.9",
      });
    });

    test("handler(実行コード)の設定が「hello.lambda_handler」であること", () => {
      template.hasResourceProperties("AWS::Lambda::Function", {
        Handler: "hello.lambda_handler",
      });
    });

    test("Description(説明)の設定が「HellowWorldを返却するAPI」であること", () => {
      template.hasResourceProperties("AWS::Lambda::Function", {
        Description: "HellowWorldを返却するAPI",
      });
    });

    test("Lambdaの関数実行タイムアウト時間が「30秒」であること", () => {
      template.hasResourceProperties("AWS::Lambda::Function", {
        Timeout: 30,
      });
    });
  });
};

/** テスト実行 */
const templateDev = getTestTemplate("dev");
describe("開発環境のテスト", (): void => {
  // 共通のテスト
  testSameInAllEnv(templateDev);

  // 環境ごとのテストを定義
  test("開発環境はLambda名が「Sari-getHellowWorld-dev」であること", () => {
    templateDev.hasResourceProperties("AWS::Lambda::Function", {
      FunctionName: "Sari-getHellowWorld-dev",
    });
  });
});

const templateStg = getTestTemplate("stg");

describe("検証環境のテスト", (): void => {
  // 共通のテスト
  testSameInAllEnv(templateStg);

  // 環境ごとのテストを定義
  test("検証環境はLambda名が「Sari-getHellowWorld-stg」であること", () => {
    templateStg.hasResourceProperties("AWS::Lambda::Function", {
      FunctionName: "Sari-getHellowWorld-stg",
    });
  });
});

lib/context.tsのコードは書けてないです・・・書かなきゃ・・・・

4. CloudFormationにデプロイ

CloudFormationのテンプレートファイルに変換して、各環境にデプロイする。

開発環境にデプロイ

$ cdk synth --context Env=dev

$ cdk deploy --context Env=dev --profile hoge-system_dev
  • 実際にLambdaが開発環境にデプロイできた!
  • Lambdaの名前は--contextオプションで指定した環境の定義が埋め込まれて「Sari-getHellowWorld-dev」になった!
  • テスト実行も成功!

検証環境にデプロイ

$ cdk synth --context Env=stg

$ cdk deploy --context Env=stg --profile hoge-system_stg
  • 実際にLambdaが検証環境にデプロイできた!
  • Lambdaの名前は--contextオプションで指定した環境の定義が埋め込まれて「Sari-getHellowWorld-stg」になった!
  • テスト実行も成功!

開発環境と検証環境が同じAWSアカウントでもうまくいくのか?

同じAWSアカウントでも、それぞれの環境用にスタックが作成されてデプロイできるのかを確認した。

$ cdk synth --context Env=dev
$ cdk deploy --context Env=dev --profile hoge-system

$ cdk synth --context Env=stg
$ cdk deploy --context Env=stg --profile hoge-system

問題なくデプロイできた!

スタック削除の確認

削除も指定した環境のスタックのみが消えていることも確認した!

$ cdk destroy cdk destroy --context Env=dev --profile hoge-system_dev
$ cdk destroy cdk destroy --context Env=stg --profile hoge-system_stg

[おまけ]コンテキストキャッシュクリア(cdk context --clear)

cdk コマンド実行時にキャッシュファイル (cdk.context.json) が生成される。
消したい場合は以下のコマンドを実行する。

cdk context --clear

以上


まとめ(ただの感想)

AWS構成を考える(図) → 詳細設計(テスト期待値で記載) → リソースを実装
の流れで効率的に開発だできそうな印象を持てました。

ワークショップのページにLambdaがあったので試しにやってみましたが、サーバーレスアプリケーションはAWS SAMを使うとローカルで実行もできるので、適宜使い分けがいるのかなと思いました。

今後やっていきたいこと

  • 複雑な構成で練習する
  • 自動テストの期待値をもっとかけるようにする(=設計力)
  • CDK Pipelinesを使って自動的にビルド、テスト、デプロイする
  • AWS SSOで認証情報を管理して、AWS CDKを使う

参考サイト

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?