こんにちは、SaaSプロダクト開発部の村山です。 業務でAWS CDKを使って環境構築を行う機会がありましたので 、今回はAWS CDKの紹介をしていきます。
AWS CDKとは?
TypeScript、JavaScript、Python、Java、C#/.Net、Goのいずれかのプログラミング言語を使用してAWSのインフラストラクチャを定義し、AWSにプロビジョニングできるフレームワークです。
記述したコードはCloudFormationのリソースとしてデプロイされます。
使い慣れたプログラミング言語で記述できるため、自由度が高い記述が可能です。
AWS CDKの構造
アプリケーション
アプリケーションは1つ以上のスタックのコンテナです。
スタック
コンストラクトを使ってAWS リソースを定義します。
デプロイ時にCloudFormationのスタックとしてプロビジョニングされるので、CloudFormationのクォータと制限が適用されます。
コンストラクト
コンストラクトはAWS CDKの基本的な構成要素(クラス)で、1つ以上のCloudFormationリソースと設定を表しています。
コンストラクトはAWSが提供するものの他に、独自のコンストラクトを作成したりサードパーティのデベロッパーが作成したコンストラクトを使用することもできます。
コンストラクトにはL1からL3まで3つのレベルがあります。
- L1: Cfn(CloudFormation)リソース(CfnXxxという名前のコンストラクトはL1)
- L2: AWSによってキュレートされたコンストラクトで、デフォルト値やベストプラクティスのセキュリティポリシーなどが含まれている
- L3: 特定のサービスを構築するパターン(xxxPatternsという名前のコンストラクトはL3)
L1 → L3 に向かって抽象度が高くなっています。
基本はL2を使っていくことが多いと思います。L2やL3では作成できない場合は、L1を使って記述していきます。
やってみる
今回はサンプルとしてVPCとVPC Lambdaを作ってみたいと思います。
前提として
- AWSアカウントの準備
- AWS CLIのインストール
- Node.js(14.15.0以降)のインストール
は済んでいるとします。
準備
AWS CDK CLIをインストールします。
$ npm install -g aws-cdk
この記事の執筆時点のCDK versionは2.134.0
です
$ cdk --version 2.134.0 (build 265d769)
デプロイする予定のAWS環境のBootstrapを実行する必要があります。
これは後で行っても問題ないです。
$ cdk bootstrap aws://ACCOUNT-NUMBER/REGION
ディレクトリを作成してCDKアプリを初期化します。 今回はTypeScriptを使います。
$ mkdir cdk-sample $ cd cdk-sample $ cdk init app --language typescript
作成されたディレクトリとファイルはこのようになっています。
. ├── README.md ├── bin │ └── cdk-sample.ts ├── cdk.json ├── jest.config.js ├── lib │ └── cdk-sample-stack.ts ├── node_modules ├── package-lock.json ├── package.json ├── test │ └── cdk-sample.test.ts └── tsconfig.json
実際にコードを書いていくスタックはlib/cdk-sample-stack.ts
です。
エディタで開くとこうなっています。
lib/cdk-sample-stack.ts
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; // import * as sqs from 'aws-cdk-lib/aws-sqs'; export class CdkSampleStack 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, 'CdkSampleQueue', { // visibilityTimeout: cdk.Duration.seconds(300) // }); } }
constructor
の中にコードを記述していきます。
コードが長くなったり再利用をしたくなったりした場合は、メソッドを作ったり、スタックを分割したりして呼び出す形にすることもできます。
こういった柔軟な記述ができるところが、プログラミング言語で書ける利点ですね。
どのコンストラクトを使うかは公式のAPIリファレンスから探します。
ちなみにCDKのドキュメントはcdk docs
コマンドを実行するとブラウザで開くことができます。
GitHubにサンプルコードもあるので、そちらも参考するといいと思います。 github.com
コンストラクタの記述は基本的に
第一引数にスコープ(コンストラクト)
第二引数にid
第三引数にコンストラクタのprops
という形になっています。
VPCの作成
まずはVPCを作成してみます。
ec2コンストラクトをインポート
lib/cdk-sample-stack.ts
import { aws_ec2 as ec2 } from "aws-cdk-lib";
VPCの作成
lib/cdk-sample-stack.ts
const vpc = new ec2.Vpc(this, "SampleVPC", { ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"), vpcName: "cdk-sample-vpc", });
ec2.Vpc
コンストラクトはL2なので、特に指定しなくてもデフォルトでサブネットの作成などをやってくれます。
自分で指定したい場合は、API リファレンスを確認しながらpropsを記述します。
今回は、ipAddresses
でCIDRを指定、vpcName
でVPCの名前を指定しています。
Tips: コンストラクトのインスタンスからは、コンストラクトを構成する要素を取得することができます。
例えば
ec2.Vpc
コンストラクトで作成されたパブリックサブネットを取得するときはvpc.publicSubnets
このようにします。
実際に何をやっているのか確認するために、CDKのコードからCloudFormationのテンプレートを出力できるcdk synth
コマンドを実行してみます。
$ cdk synth
出力されたCloudFormationテンプレート
Resources: SampleVPC676AFAA6: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true InstanceTenancy: default Tags: - Key: Name Value: cdk-sample-vpc Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/Resource SampleVPCPublicSubnet1SubnetFF189553: 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: CdkSampleStack/SampleVPC/PublicSubnet1 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet1/Subnet SampleVPCPublicSubnet1RouteTableD74013E6: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PublicSubnet1 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet1/RouteTable SampleVPCPublicSubnet1RouteTableAssociation0E1E38CA: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: SampleVPCPublicSubnet1RouteTableD74013E6 SubnetId: Ref: SampleVPCPublicSubnet1SubnetFF189553 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet1/RouteTableAssociation SampleVPCPublicSubnet1DefaultRouteD62243DA: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: SampleVPCIGW11D9AA06 RouteTableId: Ref: SampleVPCPublicSubnet1RouteTableD74013E6 DependsOn: - SampleVPCVPCGW9A31D44E Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet1/DefaultRoute SampleVPCPublicSubnet1EIP7C23471C: Type: AWS::EC2::EIP Properties: Domain: vpc Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PublicSubnet1 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet1/EIP SampleVPCPublicSubnet1NATGatewayB71B54D1: Type: AWS::EC2::NatGateway Properties: AllocationId: Fn::GetAtt: - SampleVPCPublicSubnet1EIP7C23471C - AllocationId SubnetId: Ref: SampleVPCPublicSubnet1SubnetFF189553 Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PublicSubnet1 DependsOn: - SampleVPCPublicSubnet1DefaultRouteD62243DA - SampleVPCPublicSubnet1RouteTableAssociation0E1E38CA Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet1/NATGateway SampleVPCPublicSubnet2SubnetAF202FA8: 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: CdkSampleStack/SampleVPC/PublicSubnet2 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet2/Subnet SampleVPCPublicSubnet2RouteTable63C2EDC5: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PublicSubnet2 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet2/RouteTable SampleVPCPublicSubnet2RouteTableAssociation9C39EBDB: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: SampleVPCPublicSubnet2RouteTable63C2EDC5 SubnetId: Ref: SampleVPCPublicSubnet2SubnetAF202FA8 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet2/RouteTableAssociation SampleVPCPublicSubnet2DefaultRoute45289F69: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: SampleVPCIGW11D9AA06 RouteTableId: Ref: SampleVPCPublicSubnet2RouteTable63C2EDC5 DependsOn: - SampleVPCVPCGW9A31D44E Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet2/DefaultRoute SampleVPCPublicSubnet2EIPA80228F3: Type: AWS::EC2::EIP Properties: Domain: vpc Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PublicSubnet2 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet2/EIP SampleVPCPublicSubnet2NATGateway2775C8CB: Type: AWS::EC2::NatGateway Properties: AllocationId: Fn::GetAtt: - SampleVPCPublicSubnet2EIPA80228F3 - AllocationId SubnetId: Ref: SampleVPCPublicSubnet2SubnetAF202FA8 Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PublicSubnet2 DependsOn: - SampleVPCPublicSubnet2DefaultRoute45289F69 - SampleVPCPublicSubnet2RouteTableAssociation9C39EBDB Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PublicSubnet2/NATGateway SampleVPCPrivateSubnet1SubnetB2AF079C: 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: Private - Key: aws-cdk:subnet-type Value: Private - Key: Name Value: CdkSampleStack/SampleVPC/PrivateSubnet1 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet1/Subnet SampleVPCPrivateSubnet1RouteTable11FF53B5: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PrivateSubnet1 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet1/RouteTable SampleVPCPrivateSubnet1RouteTableAssociation42FCDD78: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: SampleVPCPrivateSubnet1RouteTable11FF53B5 SubnetId: Ref: SampleVPCPrivateSubnet1SubnetB2AF079C Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet1/RouteTableAssociation SampleVPCPrivateSubnet1DefaultRoute8F7F8183: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: Ref: SampleVPCPublicSubnet1NATGatewayB71B54D1 RouteTableId: Ref: SampleVPCPrivateSubnet1RouteTable11FF53B5 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet1/DefaultRoute SampleVPCPrivateSubnet2Subnet1A9D7D61: 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: Private - Key: aws-cdk:subnet-type Value: Private - Key: Name Value: CdkSampleStack/SampleVPC/PrivateSubnet2 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet2/Subnet SampleVPCPrivateSubnet2RouteTable8996254C: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: CdkSampleStack/SampleVPC/PrivateSubnet2 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet2/RouteTable SampleVPCPrivateSubnet2RouteTableAssociation13E6A7CE: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: SampleVPCPrivateSubnet2RouteTable8996254C SubnetId: Ref: SampleVPCPrivateSubnet2Subnet1A9D7D61 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet2/RouteTableAssociation SampleVPCPrivateSubnet2DefaultRouteE89BC1AA: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: Ref: SampleVPCPublicSubnet2NATGateway2775C8CB RouteTableId: Ref: SampleVPCPrivateSubnet2RouteTable8996254C Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/PrivateSubnet2/DefaultRoute SampleVPCIGW11D9AA06: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: cdk-sample-vpc Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/IGW SampleVPCVPCGW9A31D44E: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: Ref: SampleVPCIGW11D9AA06 VpcId: Ref: SampleVPC676AFAA6 Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/VPCGW SampleVPCRestrictDefaultSecurityGroupCustomResourceACB89147: Type: Custom::VpcRestrictDefaultSG Properties: ServiceToken: Fn::GetAtt: - CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E - Arn DefaultSecurityGroupId: Fn::GetAtt: - SampleVPC676AFAA6 - DefaultSecurityGroup Account: Ref: AWS::AccountId UpdateReplacePolicy: Delete DeletionPolicy: Delete Metadata: aws:cdk:path: CdkSampleStack/SampleVPC/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: - SampleVPC676AFAA6 - DefaultSecurityGroup Metadata: aws:cdk:path: CdkSampleStack/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: CdkSampleStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler aws:asset:path: asset.4b996a3e5a083d5c78c6f30a8571a94fb7ec557eecbe54dbc065faba0d9076e6 aws:asset:property: Code CDKMetadata: Type: AWS::CDK::Metadata Properties: Analytics: v2:deflate64:H4sIAAAAAAAA/21Qy24CMQz8Fu7ZlMeB9ohWqOKCoqXiWmWzhhp2HZQ4iyrEv9cRpbn0NOPxyB57rt8Wejqx11i57lz12Orbjq07K5E+wc31bX9xqj7Q3tTKpLZHt0stAWetsMYnhg/b9lD0oq1i9A4to6c/cybrjcmwtfxuGa72W5mAo9AyeEMMQfjT8EjyW61Yon4NQHxXDUSfgpO5KbIfSin7/m+Z4EfsICiJByxXH5GO2V976jCHvSvyHehTfBlnr3opjzpFxCokYhxANw/8AQ4DHeJEAQAA Metadata: aws:cdk:path: CdkSampleStack/CDKMetadata/Default Condition: CDKMetadataAvailable 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.
サブネットやルートテーブルが一緒に作成されているのがわかると思います。
この時点で一度デプロイしてみましょう。
$ cdk deploy
実行が完了したらAWSコンソールを確認してみましょう。
指定したCIDRでVPCが作成されました。
Lambdaの作成
次は、Lambdaの作成です。
必要なコンストラクトをインポートします。
lib/cdk-sample-stack.ts
import { aws_ec2 as ec2, aws_lambda as lambda, aws_lambda_nodejs as lambdaNodejs, Duration, } from "aws-cdk-lib"; import { Runtime } from "aws-cdk-lib/aws-lambda";
続いて、LambdaとLambdaの関数URLの設定です。 先ほど作成したVPCをpropsに設定します。
lib/cdk-sample-stack.ts
const testFunc = new lambdaNodejs.NodejsFunctions(this, "test_func", { vpc: vpc, runtime: Runtime.NODEJS_20_X, functionName: "test_func", timeout: Duration.seconds(25), logRetention: 7, }); testFunc.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE, });
固定のレスポンスを返す関数を作成します。
Lambdaのソースコードの指定の仕方はいろいろあるのですが、特に何も指定しない場合はlib/スタック名.関数名.ts
のファイルが参照されます。
lib/cdk-sample-stack.test_func.ts
export async function handler() { return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: { message: "hello CDK" }, }; }
再度デプロイします。
$ cdk deploy
関数URLが発行されたVPC Lambdaができました。
発行された関数URLにアクセスしてみます。
無事、レスポンスが帰ってきました。
最後に
最終的に完成したコードは以下になります。
lib/cdk-sample-stack.test_func.ts
import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import { aws_ec2 as ec2, aws_lambda as lambda, aws_lambda_nodejs as lambdaNodejs, Duration, } from "aws-cdk-lib"; import { Runtime } from "aws-cdk-lib/aws-lambda"; export class CdkSampleStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, "SampleVPC", { ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"), vpcName: "cdk-sample-vpc", }); const testFunc = new lambdaNodejs.NodejsFunction(this, "test_func", { vpc: vpc, runtime: Runtime.NODEJS_20_X, functionName: "test_func", timeout: Duration.seconds(25), logRetention: 7, }); testFunc.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE, }); } }
使い慣れたプログラミング言語を使って、少ないコード量で記述できるのは魅力的ですよね。
スタックの分割など応用編は、機会があれば書きたいと思います。
最後まで読んでいただきありがとうございました。