実践!AWS CDK #26 Version 2

題字・息子たち
2021.12.10

はじめに

AWS CDK の Version 2 がリリースされました。

今回は v2 の主な機能の紹介と、既存の CDK プロジェクトを v1 から v2 へ移行したいと思います。

前回の記事はこちら。

v1 から v2 の主な変更点

公式のドキュメントによると、主な変更点は以下 7 つです。

  1. ライブラリパッケージの単一化
    • aws-cdk-lib パッケージに統合
    • 個々のパッケージのインストールが不要に
  2. 新しい L2 や L3 の Construct は aws-cdk-lib には含まれず個別のパッケージとして配布される
    • 安定版として指定された後に aws-cdk-lib に移行
  3. 新しい機能が追加中の安定版モジュールでは新しい API に Beta1 というサフィックスが付く
    • サフィックスが付いていないバージョンは、その API が安定版になった時に追加される
  4. Construct クラスは AWS CDK から別のライブラリとして抽出された
    • constructs パッケージに分離
  5. v1 で Deprecated となっていたプロパティやメソッドは v2 で完全に削除された
    • 削除された API のリストは GitHub で公開されている
  6. v1 の機能フラグ(feature flags)によって制限された動作は v2 ではデフォルトで有効になった
    • 古い機能フラグはもはや不要で、殆どの場合はサポートされていない
  7. v2 ではデプロイ先の環境が最新のブートストラップスタックを使用して bootstrap されている必要がある
    • レガシーのブートストラップスタックはサポートされなくなった
    • 既存の環境をアップグレードするには再度 $ cdk bootstrap を実行すれば良い

1 つ目の変更がとても嬉しいですね。
これまでは @aws-cdk/aws-ec2@aws-cdk/aws-iam といったパッケージを必要に応じて次のように個別にインストールしていました。

$ npm install @aws-cdk/aws-ec2

上記コマンドの実行タイミングによっては、既にインストールされているパッケージ内の CDK バージョンとズレが発生してビルドが通らなくなるという問題がしばしば起こりました。今回のアップデートによりその場合のトラブルシューティングが不要となり、よりストレスなく CDK での開発に集中できるようになったということです。

やったぜ!

マイグレーション

それでは本シリーズでこれまで作成してきた CDK プロジェクトを実際に v2 へアップデートしていきましょう。

AWS CDK v2 のインストール

以下のコマンドを実行して CDK のバージョンを v2 にアップグレードします。

$ npm install -g aws-cdk

バージョンを確認して 2.0.0 になっていれば OK です。

$ cdk --version

2.0.0 (build 4b6ce31)

Bootstrap の実行

bootstrap コマンドを実行します。

$ cdk bootstrap

 ⏳  Bootstrapping environment aws://060405383263/ap-northeast-1...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
 ✅  Environment aws://060405383263/ap-northeast-1 bootstrapped (no changes).

変更があれば自動で更新されますし、変更がなければ特に何も起きません。

機能フラグの削除

「機能フラグは不要」と書かれていたので、全て消してしまいましょう。

cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/devio.ts",
  "versionReporting": false,
  "context": {
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:enableStackNameDuplicates": "true",
    "aws-cdk:enableDiffNoFail": "true",
    "@aws-cdk/core:stackRelativeExports": "true",
    "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
    "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
    "@aws-cdk/aws-kms:defaultKeyPolicies": true,
    "@aws-cdk/aws-s3:grantWriteWithoutAcl": true,
    "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-efs:defaultEncryptionAtRest": true,
    "systemName": "devio",
    "envType": "stg",
    "keyName": ""
  }
}

修正後

cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/devio.ts",
  "versionReporting": false,
  "context": {
    "systemName": "devio",
    "envType": "stg",
    "keyName": ""
  }
}

dependencies の更新

package.json の記述を修正します。

package.json

{
  "name": "devio",
  "version": "0.1.0",
  "bin": {
    "devio": "bin/devio.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
    "@aws-cdk/assert": "^1.122.0",
    "@types/jest": "^26.0.23",
    "@types/node": "10.17.27",
    "aws-cdk": "1.122.0",
    "jest": "^26.6.3",
    "ts-jest": "^26.2.0",
    "ts-node": "^9.0.0",
    "typescript": "~3.9.7"
  },
  "dependencies": {
    "@aws-cdk/aws-ec2": "^1.122.0",
    "@aws-cdk/aws-elasticloadbalancingv2": "^1.122.0",
    "@aws-cdk/aws-rds": "^1.122.0",
    "@aws-cdk/aws-secretsmanager": "^1.122.0",
    "@aws-cdk/core": "^1.122.0",
    "source-map-support": "^0.5.16"
  },
  "jest": {
    "moduleFileExtensions": [
      "js"
    ]
  }
}

修正後

package.json

{
  "name": "devio",
  "version": "0.1.0",
  "bin": {
    "devio": "bin/devio.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk"
  },
  "devDependencies": {
    "@types/jest": "^26.0.23",
    "@types/node": "10.17.27",
    "aws-cdk-lib": "^2.0.0",
    "constructs": "^10.0.0",
    "jest": "^26.6.3",
    "ts-jest": "^26.2.0",
    "ts-node": "^9.0.0",
    "typescript": "~3.9.7"
  },
  "dependencies": {
    "aws-cdk-lib": "^2.0.0",
    "constructs": "^10.0.0",
    "source-map-support": "^0.5.16"
  },
  "jest": {
    "moduleFileExtensions": [
      "js"
    ]
  }
}

dependencies 修正後、以下のコマンドを実行してパッケージも更新します。

$ npm install

この時点でビルドが通らなくなります。

import 文の更新

これまで @aws-cdk/core やその他のパッケージを import していた部分を aws-cdk-lib を参照するようにします。
また、Construct クラスは constructs パッケージに移行されたのでその書き換えも行います。

lib/devio-stack.ts

import * as cdk from '@aws-cdk/core';
import { Vpc } from './resource/vpc';
import { Subnet } from './resource/subnet';
import { InternetGateway } from './resource/internetGateway';
import { ElasticIp } from './resource/elasticIp';
import { NatGateway } from './resource/natGateway';
import { RouteTable } from './resource/routeTable';
import { NetworkAcl } from './resource/networkAcl';
import { IamRole } from './resource/iamRole';
import { SecurityGroup } from './resource/securityGroup';
import { Ec2 } from './resource/ec2';
import { Alb } from './resource/alb';
import { SecretsManager } from './resource/secretsManager';
import { Rds } from './resource/rds';

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

    // VPC
    const vpc = new Vpc();
    vpc.createResources(this);

    ~ 省略 ~

修正後

lib/devio-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Vpc } from './resource/vpc';
import { Subnet } from './resource/subnet';
import { InternetGateway } from './resource/internetGateway';
import { ElasticIp } from './resource/elasticIp';
import { NatGateway } from './resource/natGateway';
import { RouteTable } from './resource/routeTable';
import { NetworkAcl } from './resource/networkAcl';
import { IamRole } from './resource/iamRole';
import { SecurityGroup } from './resource/securityGroup';
import { Ec2 } from './resource/ec2';
import { Alb } from './resource/alb';
import { SecretsManager } from './resource/secretsManager';
import { Rds } from './resource/rds';

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

    // VPC
    const vpc = new Vpc();
    vpc.createResources(this);

    ~ 省略 ~

同様の修正をすべてのファイルに対し行います。(この時の commit は コチラ

テストコードも修正します。

test/devio.test.ts

import { expect, haveResource } from '@aws-cdk/assert';
import * as cdk from '@aws-cdk/core';
import * as Devio from '../lib/devio-stack';

test('Context', () => {
  const app = new cdk.App({
    context: {
      'systemName': 'starwars',
      'envType': 'prd'
    }
  });
  const stack = new Devio.DevioStack(app, 'DevioStack');

  expect(stack).to(countResources('AWS::EC2::VPC', 1));
  expect(stack).to(haveResource('AWS::EC2::VPC', {
    Tags: [{ Key: 'Name', Value: 'starwars-prd-vpc' }]
  }));
});

修正後

test/devio.test.ts

import { App } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import * as Devio from '../lib/devio-stack';

test('Context', () => {
  const app = new App({
    context: {
      'systemName': 'starwars',
      'envType': 'prd'
    }
  });
  const stack = new Devio.DevioStack(app, 'DevioStack');
  const template = Template.fromStack(stack);

  template.resourceCountIs('AWS::EC2::VPC', 1);
  template.hasResourceProperties('AWS::EC2::VPC', {
    Tags: [{ Key: 'Name', Value: 'starwars-prd-vpc' }]
  });
});

テストコードは @aws-cdk/assert ではなく aws-cdk-lib/assertions というモジュールを使用するよう修正しました。
Stack オブジェクトを渡して Template オブジェクトを生成し、そのオブジェクトに対してチェックを行うような構成となっています。

以下、従来のテストコードで使用したメソッドとの対応表です。

@aws-cdk/assert aws-cdk-lib/assertions 説明
countResources() resourceCountIs() リソース数のチェック
haveResource() hasResourceProperties() プロパティのチェック
haveResourceLike() hasResourceProperties() プロパティのチェック
countResourcesLike() リソース数とプロパティのチェック
anything() Match.anyValue() 任意の値を表現

countResourcesLike() に関しては対応しているメソッドがなさそうだったので、hasResourceProperties() で代用しています。(この時の commit は コチラ

ここまでできたら一度テストを実行してみましょう。

v1 でのテスト実行コマンドは $ npm run build && npm test でしたが、v2 からは $ tsc && npm test というコマンドが推奨されているようです。

$ tsc && npm test

> devio@0.1.0 test
> jest

 PASS  test/resource/internetGateway.test.ts (12.643 s)
 PASS  test/resource/secretsManager.test.ts (12.695 s)
 PASS  test/resource/securityGroup.test.ts (12.686 s)
 PASS  test/resource/subnet.test.ts (12.718 s)
 PASS  test/resource/networkAcl.test.ts (12.749 s)
 PASS  test/resource/alb.test.ts (12.827 s)
 PASS  test/resource/routeTable.test.ts (12.752 s)
 PASS  test/resource/ec2.test.ts (12.864 s)
 PASS  test/resource/iamRole.test.ts (12.844 s)
 PASS  test/resource/natGateway.test.ts (12.845 s)
 PASS  test/resource/rds.test.ts (12.965 s)
 PASS  test/devio.test.ts
 PASS  test/resource/vpc.test.ts
 PASS  test/resource/elasticIp.test.ts

Test Suites: 14 passed, 14 total
Tests:       14 passed, 14 total
Snapshots:   0 total
Time:        14.749 s, estimated 18 s
Ran all test suites.

すべてのテストが通りました。

CFn テンプレートの出力

テンプレートを出力します。

$ cdk synth

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
        - Key: Name
          Value: devio-stg-vpc

  ~ 省略 ~

  RdsDbInstance1c:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceClass: db.r5.large
      AutoMinorVersionUpgrade: false
      AvailabilityZone: ap-northeast-1c
      DBClusterIdentifier:
        Ref: RdsDbCluster
      DBInstanceIdentifier: devio-stg-rds-instance-1c
      DBParameterGroupName:
        Ref: RdsDbParameterGroup
      DBSubnetGroupName:
        Ref: RdsDbSubnetGroup
      EnablePerformanceInsights: true
      Engine: aurora-mysql
      MonitoringInterval: 60
      MonitoringRoleArn:
        Fn::GetAtt:
          - RoleRds
          - Arn
      PerformanceInsightsRetentionPeriod: 7
      PreferredMaintenanceWindow: sun:20:30-sun:21:00
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
Rules:
  CheckBootstrapVersion:
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - "1"
                  - "2"
                  - "3"
                  - "4"
                  - "5"
                - Ref: BootstrapVersion
        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.

ParametersRules というセクションが追加されています。
これは CDK v2 で自動的に付与されるセクションのようです。

v1 から v2 の主な変更点のうち、以下に関するバリデーションと考えられます。

v2 ではデプロイ先の環境が最新のブートストラップスタックを使用して bootstrap されている必要がある

差分の確認

現在デプロイされているリソースとローカルの CDK プロジェクトの差分を調べます。

$ cdk diff

Stack DevioStack
Parameters
[+] Parameter BootstrapVersion BootstrapVersion: {"Type":"AWS::SSM::Parameter::Value<String>","Default":"/cdk-bootstrap/hnb659fds/version","Description":"Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"}

Other Changes
[+] Unknown Rules: {"CheckBootstrapVersion":{"Assertions":[{"Assert":{"Fn::Not":[{"Fn::Contains":[["1","2","3","4","5"],{"Ref":"BootstrapVersion"}]}]},"AssertDescription":"CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."}]}}

上記のセクション部分のみ追加となっています。
その他のリソースに変更はかからないので意図通りです。

デプロイ

$ cdk deploy

DevioStack: deploying...
[0%] start: Publishing cdd63b4f8ae956987786be6dec1976ce054a108324f3aed5af09ffed9a488054:current_account-current_region
[100%] success: Published cdd63b4f8ae956987786be6dec1976ce054a108324f3aed5af09ffed9a488054:current_account-current_region
DevioStack: creating CloudFormation changeset...

 ✅  DevioStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:060405383263:stack/DevioStack/d745be30-5984-11ec-802f-068875dc9763

デプロイできました。(リソースの変更は無し)

これにてマイグレーション作業完了です!

GitHub

今回のソースコードは コチラ です。

おわりに

v1 から v2 への移行は比較的簡単に実施できました。本シリーズでは L1 の CfnXXXX クラスを利用しているため、影響が少なかったのかもしれません。
私は aws-cdk-lib パッケージへの統一化だけで大満足です。これからも CDK を使って色々構築していきたいと思います。

リンク