【連載企画】極めようAWS CDK 〜#1 CDKを始めよう〜

クラウドデータのイメージ画像
こんにちは。ソニー・ミュージックエンタテインメント しんろくです。

「極めよう AWS CDK」と題しまして、ぼくの経験値における AWS CDK にまつわるエトセトラを連載でご紹介していきたいと思います。

はじめに

ぼくは Linuxキャリア20年ほどのエンジニア兼 プログラマとして業務に従事しております。ですので、本連載は基本的に Linux 環境での作業を前提にしております。Windows や Mac 環境での動作についてはほとんど言及いたしませんのでご了承ください。

またAWS歴は2016年からと浅めですが、AWS CDKについてはバージョン1がGAされたタイミングから利用していますので、CDK界隈ではそれなりに長い経験値があるかもしれません(笑)。

前置きは以上、この記事を読んでAWS CDKに興味をもっていただけたら幸いです。

想定読者

本連載は以下のような読者のみなさんにオススメできると思っています。

  • AWS CDKを始めてみたい人
  • CloudFormation テンプレートにイライラさせられたことのある人
  • AWS環境構築で楽をしたい人
  • AWSリソースやAWS APIをより深く理解したい人
  • AWSリソースの変更管理をきちんとやりたい人
  • マルチアカウント戦略を実施していて、デプロイ作業に苦痛を感じている人

また、AWS CDKは複数のプログラミング言語に対応していますが、 本稿での使用言語は TypeScript で進めます。

AWS CDK とは

公式ドキュメントを読んでいただいたほうが正確ですが ぼくの理解でざっくりいうと、汎用プログラミング言語から CloudFormation テンプレート(YAML or JSON)を生成し CloudFormation スタックを実行してくれるフレームワークのことです。

その他、Lambda 関数のデプロイをおこなってくれたり、Docker ビルドを実行して ECR にプッシュしてくれたり、単純な CloudFormation スタックの実行だけではない便利な機能もたくさんあります。 このあたりは次回以降 にご紹介できればと思います。

さっそく始めよう

作業環境は厳密に一致していなくても大丈夫ですが、できるだけ新しい環境が良いと思います。

  • nodejs 18.x が動く Linux 環境(EC2でもWSLでも大丈夫です)
  • エディタは VSCode をおすすめしますが、これも好きなものを使ってください。
  • のちのち Docker が必要になってきます。

ちなみにぼくの執筆時点の環境です。

$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

$ node -v
v18.15.0

$ npm -v
9.8.1

$ code --version
1.81.1
6c3e3dba23e8fadc360aed75ce363ba185c49794
x64

インストール

AWS CDK の CLI をインストールしましょう。 ルート権限がない人は、自分のローカルで大丈夫です。(適宜 PATHを通してください。)

 $ sudo npm i -g aws-cdk
 $ cdk --version
 2.93.0 (build 724bd01)

プロジェクトディレクトリ作成

好きなところに作業用ディレクトリを作りましょう。

$ cd path/to/work
$ mkdir hello-cdk

AWS CDK CLIを実行しよう

CDKの初期設定を行います。 これを行うと、骨組みのファイルがすべて生成されます。便利! 言語は TypeScript を指定します。

$ cd hello-cdk
$ cdk init -l typescript

# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

# Useful commands

* `npm run build`   compile typescript to js
* `npm run watch`   watch for changes and compile
* `npm run test`    perform the jest unit tests
* `cdk deploy`      deploy this stack to your default AWS account/region
* `cdk diff`        compare deployed stack with current state
* `cdk synth`       emits the synthesized CloudFormation template

Executing npm install...
✅ All done!

これでプロジェクトディレクトリができあがりました。

$ tree -L 1
.
├── README.md
├── bin
├── cdk.json
├── jest.config.js
├── lib
├── node_modules
├── package.json
├── test
└── tsconfig.json

4 directories, 5 files

細かいファイルの内容は今後の記事で深掘りしていきますので 今回はみんな大好き VPC を作ってみます。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';

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

    // The code that defines your stack goes here

    // example resource
    // const queue = new sqs.Queue(this, 'HelloCdkQueue', {
    //   visibilityTimeout: cdk.Duration.seconds(300)
    // });
  }
}

上記ファイルができあがっているので、下記のように改造します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

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

    const vpc = new cdk.aws_ec2.Vpc(this, 'vpc', {
      maxAzs: 99, // おまじない
      natGateways: 0,
    });

    new cdk.CfnOutput(this, 'vpcId', { value: vpc.vpcId });
  }
}

なんとなくVPCを作っているんだな~と思っていただければ大丈夫です。 こまかいオプションの説明は次回以降掘り下げていきます。

ではこれをビルド(正確にはTSからJSへのトランスパイル)します。

$ npm run build

> hello-cdk@0.1.0 build
> tsc

エラーが出なければ下記のようなディレクトリになっているはずです。

.
├── README.md
├── bin
│   ├── hello-cdk.d.ts
│   ├── hello-cdk.js
│   └── hello-cdk.ts
├── cdk.json
├── jest.config.js
├── lib
│   ├── hello-cdk-stack.d.ts
│   ├── hello-cdk-stack.js
│   └── hello-cdk-stack.ts
├── node_modules
├── package.json
├── test
│   ├── hello-cdk.test.d.ts
│   ├── hello-cdk.test.js
│   └── hello-cdk.test.ts
└── tsconfig.json

218 directories, 14 files

では、cdk synth コマンドから CloudFormation テンプレートを作成してみましょう。

$ cdk synth > hello-cdk.yml
Resources:
  vpcA2121C38:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: HelloCdkStack/vpc
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/Resource
  vpcPublicSubnet1Subnet2E65531E:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      CidrBlock: 10.0.0.0/18
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: HelloCdkStack/vpc/PublicSubnet1
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet1/Subnet
  vpcPublicSubnet1RouteTable48A2DF9B:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: HelloCdkStack/vpc/PublicSubnet1
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet1/RouteTable
  vpcPublicSubnet1RouteTableAssociation5D3F4579:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: vpcPublicSubnet1RouteTable48A2DF9B
      SubnetId:
        Ref: vpcPublicSubnet1Subnet2E65531E
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet1/RouteTableAssociation
  vpcPublicSubnet1DefaultRoute10708846:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: vpcIGWE57CBDCA
      RouteTableId:
        Ref: vpcPublicSubnet1RouteTable48A2DF9B
    DependsOn:
      - vpcVPCGW7984C166
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet1/DefaultRoute
  vpcPublicSubnet2Subnet009B674F:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      CidrBlock: 10.0.64.0/18
      MapPublicIpOnLaunch: true
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Public
        - Key: aws-cdk:subnet-type
          Value: Public
        - Key: Name
          Value: HelloCdkStack/vpc/PublicSubnet2
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet2/Subnet
  vpcPublicSubnet2RouteTableEB40D4CB:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: HelloCdkStack/vpc/PublicSubnet2
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet2/RouteTable
  vpcPublicSubnet2RouteTableAssociation21F81B59:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: vpcPublicSubnet2RouteTableEB40D4CB
      SubnetId:
        Ref: vpcPublicSubnet2Subnet009B674F
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet2/RouteTableAssociation
  vpcPublicSubnet2DefaultRouteA1EC0F60:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: vpcIGWE57CBDCA
      RouteTableId:
        Ref: vpcPublicSubnet2RouteTableEB40D4CB
    DependsOn:
      - vpcVPCGW7984C166
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/PublicSubnet2/DefaultRoute
  vpcIsolatedSubnet1Subnet8B28CEB3:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 0
          - Fn::GetAZs: ""
      CidrBlock: 10.0.128.0/18
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Isolated
        - Key: aws-cdk:subnet-type
          Value: Isolated
        - Key: Name
          Value: HelloCdkStack/vpc/IsolatedSubnet1
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IsolatedSubnet1/Subnet
  vpcIsolatedSubnet1RouteTable0D6B2D3D:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: HelloCdkStack/vpc/IsolatedSubnet1
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IsolatedSubnet1/RouteTable
  vpcIsolatedSubnet1RouteTableAssociation172210D4:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: vpcIsolatedSubnet1RouteTable0D6B2D3D
      SubnetId:
        Ref: vpcIsolatedSubnet1Subnet8B28CEB3
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IsolatedSubnet1/RouteTableAssociation
  vpcIsolatedSubnet2Subnet2C6B375C:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone:
        Fn::Select:
          - 1
          - Fn::GetAZs: ""
      CidrBlock: 10.0.192.0/18
      MapPublicIpOnLaunch: false
      Tags:
        - Key: aws-cdk:subnet-name
          Value: Isolated
        - Key: aws-cdk:subnet-type
          Value: Isolated
        - Key: Name
          Value: HelloCdkStack/vpc/IsolatedSubnet2
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IsolatedSubnet2/Subnet
  vpcIsolatedSubnet2RouteTable3455CBFC:
    Type: AWS::EC2::RouteTable
    Properties:
      Tags:
        - Key: Name
          Value: HelloCdkStack/vpc/IsolatedSubnet2
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IsolatedSubnet2/RouteTable
  vpcIsolatedSubnet2RouteTableAssociation8A8FAF70:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: vpcIsolatedSubnet2RouteTable3455CBFC
      SubnetId:
        Ref: vpcIsolatedSubnet2Subnet2C6B375C
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IsolatedSubnet2/RouteTableAssociation
  vpcIGWE57CBDCA:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: HelloCdkStack/vpc
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/IGW
  vpcVPCGW7984C166:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId:
        Ref: vpcIGWE57CBDCA
      VpcId:
        Ref: vpcA2121C38
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/VPCGW
  vpcRestrictDefaultSecurityGroupCustomResourceA6EBC6D0:
    Type: Custom::VpcRestrictDefaultSG
    Properties:
      ServiceToken:
        Fn::GetAtt:
          - CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E
          - Arn
      DefaultSecurityGroupId:
        Fn::GetAtt:
          - vpcA2121C38
          - DefaultSecurityGroup
      Account:
        Ref: AWS::AccountId
    UpdateReplacePolicy: Delete
    DeletionPolicy: Delete
    Metadata:
      aws:cdk:path: HelloCdkStack/vpc/RestrictDefaultSecurityGroupCustomResource/Default
  CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
      ManagedPolicyArns:
        - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: Inline
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - ec2:AuthorizeSecurityGroupIngress
                  - ec2:AuthorizeSecurityGroupEgress
                  - ec2:RevokeSecurityGroupIngress
                  - ec2:RevokeSecurityGroupEgress
                Resource:
                  - Fn::Join:
                      - ""
                      - - "arn:"
                        - Ref: AWS::Partition
                        - ":ec2:"
                        - Ref: AWS::Region
                        - ":"
                        - Ref: AWS::AccountId
                        - :security-group/
                        - Fn::GetAtt:
                            - vpcA2121C38
                            - DefaultSecurityGroup
    Metadata:
      aws:cdk:path: HelloCdkStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Role
  CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}
        S3Key: 4b996a3e5a083d5c78c6f30a8571a94fb7ec557eecbe54dbc065faba0d9076e6.zip
      Timeout: 900
      MemorySize: 128
      Handler: __entrypoint__.handler
      Role:
        Fn::GetAtt:
          - CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0
          - Arn
      Runtime: nodejs18.x
      Description: Lambda function for removing all inbound/outbound rules from the VPC default security group
    DependsOn:
      - CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0
    Metadata:
      aws:cdk:path: HelloCdkStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler
      aws:asset:path: asset.4b996a3e5a083d5c78c6f30a8571a94fb7ec557eecbe54dbc065faba0d9076e6
      aws:asset:property: Code
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/22Qva7CMAyFn4U9DX9CghF1QHe6VUGsKE0NGFoHJU4RQrw7LiCyMNn+bPkce6IXUz0amGvIbH3OGqz0fc3GnpWgHdiJvm8vVuV72ha5KmLVoF3HioB7lrLSRYaNqRpIPLFlCM6iYXT0HVaFx84wpB1/xOAlXwm9mttH9FMtWVwdWyB+qBKCi96KVAzs2lTK6t+twrsOa/BKnADLgQekQz//H/kSX+q5oxp7hw9FrgZ9CsNuPNfjmbznFBAzH4mxBV2+4xO5ekmdOgEAAA==
    Metadata:
      aws:cdk:path: HelloCdkStack/CDKMetadata/Default
    Condition: CDKMetadataAvailable
Outputs:
  vpcId:
    Value:
      Ref: vpcA2121C38
Conditions:
  CDKMetadataAvailable:
    Fn::Or:
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - af-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-northeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-1
          - Fn::Equals:
              - Ref: AWS::Region
              - ap-southeast-2
          - Fn::Equals:
              - Ref: AWS::Region
              - ca-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - cn-northwest-1
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-central-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-north-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-2
          - Fn::Equals:
              - Ref: AWS::Region
              - eu-west-3
          - Fn::Equals:
              - Ref: AWS::Region
              - me-south-1
          - Fn::Equals:
              - Ref: AWS::Region
              - sa-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-east-2
      - Fn::Or:
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-1
          - Fn::Equals:
              - Ref: AWS::Region
              - us-west-2
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.

たった数行のTypeScriptから なんと389行もあるYAMLが生成されました。

このテンプレートの中には

  • VPCがひとつ
  • Subnet / RouteTable / SubnetRouteTableAssociation / Route が public / isolate で 2つずつ
  • InternetGateway がひとつ
  • VpcRestrictDefaultSG というカスタムリソースがひとつ
  • Role がひとつ
  • VpcRestrictDefaultSG を動かすためのLambda関数がひとつ
  • メタデータがひとつ
  • Parameter がひとつ

という構成が記述されています。 このYAMLを人が記述しようとすると大変なことになるのは一目瞭然かと思います。

YAMLに記述されている構成のイメージ
図1 構成図

本日は「CDKをはじめよう」と題して AWS CDK の紹介をいたしました。 今後は CDK の深掘りやアーキテクチャを構築するときの考え方 ソニーミュージックグループならではの使い方などを ご紹介していければと考えています。

ではまた次回お会いしましょう!