はじめに
皆さん初めまして。長谷川です。
1本目の投稿になります。
突然ですが、皆さんはAWSのリソース構築をどのように行っていますか?
AWS CloudFormationをはじめ様々なIaCツールが活用できるAWSですが、上手く活用できておりますでしょうか?
かくいう私も細かな検証作業などではマネコンからポチポチっとやってしまうことが多く、中々活用できていないと感じています。
今回は案件での活用も増えている「AWS CDK」を利用したリソース構築について着目したいと思います。
具体的にはCDKコンストラクトの特徴について焦点を当てて見ていきます。
AWS CDKとは?
AWS公式サイトで確認すると以下のような記述になっています。
AWS Cloud Development Kit (AWS CDK) は、コードでクラウドインフラストラクチャを定義して AWS CloudFormation を通じてプロビジョニングするオープンソースのソフトウェア開発フレームワークです。
—AWS CDK とはより引用
TypeScript、JavaScript、Python、Java、C#/.Net、Goといったプログラミング言語でクラウドコンポーネントを定義できることがAWS CDKの大きな特徴ですが、実際に裏で動くのはAWS CloudFormationであることも大事なポイントですね。
リソース作成失敗時の自動ロールバックやリソース設定の不整合を防ぐドリフト検出といったCloudFormationの強みをそのまま活用することができます。
AWS CDK コンストラクト
本記事ではAWS CDKを利用する強みの一つである「抽象化」を支えるAWS CDK コンストラクトについて注目したいと思います。
こちらについてもAWS公式サイトの定義を紹介します。
コンストラクトは、AWS Cloud Development Kit (AWS CDK) アプリケーションの基本的な構成要素です。コンストラクトは、1 つ以上の AWS CloudFormation リソースとその設定を表すアプリケーション内のコンポーネントです。コンストラクトをインポートして設定することで、アプリケーションを 1 つずつ構築していきます。
—AWS CDK コンストラクトより引用
AWS CDKではコンストラクトを基にコードの記述を行うことで、リソースの定義をしていきます。
そして、コンストラクトのレベルとして次の3段階が用意されています。
コンストラクトレベル | 説明(AWS CDK コンストラクトより引用) |
レベル1(L1) | L1 コンストラクトは、CFN リソース とも呼ばれ、最下位のコンストラクトであり、抽象化は行いません。各 L1 コンストラクトは、単一の AWS CloudFormation リソースに直接マッピングされます。 |
レベル2(L2) | L1 コンストラクトと比較して、L2 コンストラクトは直感的なインテントベースの API を通してより高度な抽象化を提供します。L2 コンストラクトには、適切なデフォルトプロパティ設定、ベストプラクティスのセキュリティポリシー、および多くのボイラープレートコードやグルーロジックを生成する機能が含まれています。 |
レベル3(L3) | L3 コンストラクトは、パターンとも呼ばれ、最も高度な抽象レベルです。各 L3 コンストラクトには、アプリケーション内の特定のタスクまたはサービスを達成するために連携するように設定された、複数リソースの集合を含めることができます。L3 コンストラクトは、アプリケーション内の特定のユースケースの AWS アーキテクチャ全体を作成するために使用されます。 |
表内で強調している「抽象化の有無」について気になりました。
AWS CDKというと必要なパラメータのみ設定し、残りはベストプラクティスに基づき「よしなに」やってくれるものと捉えていましたが、そうではないのでしょうか?
実際にレベル1、レベル2コンストラクトのそれぞれを使用してリソース作成(S3,VPC)を試してみました。
上記のコマンドを使用することで、実際にリソースをデプロイする前にCloudFormationのテンプレートに落とし込んだ結果を確認することができます。
S3
まずはS3バケットの作成について、両者の結果の違いを示します。
実行環境は以下の通りです。
Node.js | 10.9.2 |
TypeScript | 5.7.3 |
AWS CDK | 2.178.0 |
また、実行環境の構築やコードの記載には「TypeScript の基礎から始める AWS CDK 開発入門」を参考にしました。
まだまだTypeScriptを使用した経験が浅いので、ここで紹介されているハンズオンについても一度じっくりと試してみたいと考えています。
L1コンストラクト
L1コンストラクトを使用したS3作成のTypeScriptコードは以下の通りです。
class CfnBucket (construct)
import { Duration, Stack, StackProps } from 'aws-cdk-lib'; import * as s3 from "aws-cdk-lib/aws-s3"; import { Construct } from 'constructs'; export class WorkStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // s3 を宣言 const s3Bucket = new s3.CfnBucket(this, "s3-L1-Construct", { }); } }
cdk synthコマンドを使用し、CloudFormationテンプレートを出力すると以下の通りです。
[Properties]の設定等は特になく、真っ新なS3バケットが一つ作成されました。
L2コンストラクト
続いてL2コンストラクトを使用してS3バケットを作成する場合です。
class Bucket (construct)
import { Duration, Stack, StackProps } from 'aws-cdk-lib'; import * as s3 from "aws-cdk-lib/aws-s3"; import { Construct } from 'constructs'; export class WorkStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // s3 を宣言 const s3Bucket = new s3.Bucket(this, "s3-L2-Construct", { }); } }
cdk synthコマンドを使用し、CloudFormationテンプレートを出力すると以下の通りです。
[Properties]の設定等は同じくありませんが、以下の項目が追加されているようです。
- UpdateReplacePolicy: Retain
- DeletionPolicy: Retain
これらはスタックの変更・削除時に対象のリソースを保持させるポリシーです。
誤った変更などをしてしまった際の保険になりますね。
しかし、L1とL2の違いとしてはそれほど大きくないですね。
続いてVPCの例を見てみましょう。
VPC
L1コンストラクト
L1コンストラクトを使用したVPC作成のTypeScriptコードは以下の通りです。
パラメータとしてCIDR情報が必須のため、その設定のみ記載しています。
import { Duration, Stack, StackProps } from 'aws-cdk-lib'; import * as ec2 from "aws-cdk-lib/aws-ec2"; import { Construct } from 'constructs'; export class WorkStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // vpc を宣言 const vpc = new ec2.CfnVPC(this, "VPC-L1-Construct", { cidrBlock: '10.0.0.0/16', }); } }
cdk synthコマンドを使用し、CloudFormationテンプレートを出力すると以下の通りです。
[Properties]について確認すると、[CidrBlock]の情報のみが保持されていることが分かります。
ただVPCとしての枠だけが用意される形ですね。
結局何か作業をするには、サブネットやゲートウェイなどまだまだ要素が足りていません。
L2コンストラクト
次に、L2コンストラクトです。
なんとパラメータとして必須のものが存在しません!
コードとして実行可能な最小限の記載をしています。
import { Duration, Stack, StackProps } from 'aws-cdk-lib'; import * as ec2 from "aws-cdk-lib/aws-ec2"; import { Construct } from 'constructs'; export class WorkStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // vpc を宣言 const vpc = new ec2.Vpc(this, "VPC-L2-Construct", { }); } }
cdk synthコマンドを使用し、CloudFormationテンプレートを出力すると以下の通りです。
かなり長いですので、飛ばしていただいて大丈夫です。
Resources: VPCL2ConstructEE410864: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true EnableDnsSupport: true InstanceTenancy: default Tags: - Key: Name Value: WorkStack/VPC-L2-Construct Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/Resource VPCL2ConstructPublicSubnet1SubnetCC74A439: 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: WorkStack/VPC-L2-Construct/PublicSubnet1 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet1/Subnet VPCL2ConstructPublicSubnet1RouteTable88DC6194: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PublicSubnet1 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet1/RouteTable VPCL2ConstructPublicSubnet1RouteTableAssociation731F896E: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: VPCL2ConstructPublicSubnet1RouteTable88DC6194 SubnetId: Ref: VPCL2ConstructPublicSubnet1SubnetCC74A439 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet1/RouteTableAssociation VPCL2ConstructPublicSubnet1DefaultRoute5581F2A1: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: VPCL2ConstructIGW1021B62D RouteTableId: Ref: VPCL2ConstructPublicSubnet1RouteTable88DC6194 DependsOn: - VPCL2ConstructVPCGW6FBE15DE Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet1/DefaultRoute VPCL2ConstructPublicSubnet1EIP1D9D41B6: Type: AWS::EC2::EIP Properties: Domain: vpc Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PublicSubnet1 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet1/EIP VPCL2ConstructPublicSubnet1NATGateway9B0E92D1: Type: AWS::EC2::NatGateway Properties: AllocationId: Fn::GetAtt: - VPCL2ConstructPublicSubnet1EIP1D9D41B6 - AllocationId SubnetId: Ref: VPCL2ConstructPublicSubnet1SubnetCC74A439 Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PublicSubnet1 DependsOn: - VPCL2ConstructPublicSubnet1DefaultRoute5581F2A1 - VPCL2ConstructPublicSubnet1RouteTableAssociation731F896E Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet1/NATGateway VPCL2ConstructPublicSubnet2Subnet9562D1FC: 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: WorkStack/VPC-L2-Construct/PublicSubnet2 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet2/Subnet VPCL2ConstructPublicSubnet2RouteTable24B41773: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PublicSubnet2 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet2/RouteTable VPCL2ConstructPublicSubnet2RouteTableAssociation8AEA9A7C: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: VPCL2ConstructPublicSubnet2RouteTable24B41773 SubnetId: Ref: VPCL2ConstructPublicSubnet2Subnet9562D1FC Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet2/RouteTableAssociation VPCL2ConstructPublicSubnet2DefaultRouteC8976C2A: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: VPCL2ConstructIGW1021B62D RouteTableId: Ref: VPCL2ConstructPublicSubnet2RouteTable24B41773 DependsOn: - VPCL2ConstructVPCGW6FBE15DE Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet2/DefaultRoute VPCL2ConstructPublicSubnet2EIPB8510F82: Type: AWS::EC2::EIP Properties: Domain: vpc Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PublicSubnet2 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet2/EIP VPCL2ConstructPublicSubnet2NATGatewayEAF78599: Type: AWS::EC2::NatGateway Properties: AllocationId: Fn::GetAtt: - VPCL2ConstructPublicSubnet2EIPB8510F82 - AllocationId SubnetId: Ref: VPCL2ConstructPublicSubnet2Subnet9562D1FC Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PublicSubnet2 DependsOn: - VPCL2ConstructPublicSubnet2DefaultRouteC8976C2A - VPCL2ConstructPublicSubnet2RouteTableAssociation8AEA9A7C Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PublicSubnet2/NATGateway VPCL2ConstructPrivateSubnet1SubnetE5C7047B: 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: WorkStack/VPC-L2-Construct/PrivateSubnet1 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet1/Subnet VPCL2ConstructPrivateSubnet1RouteTable9ABEBB2E: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PrivateSubnet1 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet1/RouteTable VPCL2ConstructPrivateSubnet1RouteTableAssociationAFAD682E: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: VPCL2ConstructPrivateSubnet1RouteTable9ABEBB2E SubnetId: Ref: VPCL2ConstructPrivateSubnet1SubnetE5C7047B Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet1/RouteTableAssociation VPCL2ConstructPrivateSubnet1DefaultRoute592FE75C: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: Ref: VPCL2ConstructPublicSubnet1NATGateway9B0E92D1 RouteTableId: Ref: VPCL2ConstructPrivateSubnet1RouteTable9ABEBB2E Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet1/DefaultRoute VPCL2ConstructPrivateSubnet2SubnetD1EAC9D8: 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: WorkStack/VPC-L2-Construct/PrivateSubnet2 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet2/Subnet VPCL2ConstructPrivateSubnet2RouteTable0C21B8AE: Type: AWS::EC2::RouteTable Properties: Tags: - Key: Name Value: WorkStack/VPC-L2-Construct/PrivateSubnet2 VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet2/RouteTable VPCL2ConstructPrivateSubnet2RouteTableAssociation91E37B6F: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: Ref: VPCL2ConstructPrivateSubnet2RouteTable0C21B8AE SubnetId: Ref: VPCL2ConstructPrivateSubnet2SubnetD1EAC9D8 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet2/RouteTableAssociation VPCL2ConstructPrivateSubnet2DefaultRoute99DCB01B: Type: AWS::EC2::Route Properties: DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: Ref: VPCL2ConstructPublicSubnet2NATGatewayEAF78599 RouteTableId: Ref: VPCL2ConstructPrivateSubnet2RouteTable0C21B8AE Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/PrivateSubnet2/DefaultRoute VPCL2ConstructIGW1021B62D: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: WorkStack/VPC-L2-Construct Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/IGW VPCL2ConstructVPCGW6FBE15DE: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: Ref: VPCL2ConstructIGW1021B62D VpcId: Ref: VPCL2ConstructEE410864 Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/VPCGW VPCL2ConstructRestrictDefaultSecurityGroupCustomResource76DD52E2: Type: Custom::VpcRestrictDefaultSG Properties: ServiceToken: Fn::GetAtt: - CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E - Arn DefaultSecurityGroupId: Fn::GetAtt: - VPCL2ConstructEE410864 - DefaultSecurityGroup Account: Ref: AWS::AccountId UpdateReplacePolicy: Delete DeletionPolicy: Delete Metadata: aws:cdk:path: WorkStack/VPC-L2-Construct/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: - VPCL2ConstructEE410864 - DefaultSecurityGroup Metadata: aws:cdk:path: WorkStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Role CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E: Type: AWS::Lambda::Function Properties: Code: S3Bucket: Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region} S3Key: 7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200.zip Timeout: 900 MemorySize: 128 Handler: __entrypoint__.handler Role: Fn::GetAtt: - CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0 - Arn Runtime: nodejs20.x Description: Lambda function for removing all inbound/outbound rules from the VPC default security group DependsOn: - CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0 Metadata: aws:cdk:path: WorkStack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler aws:asset:path: asset.7fa1e366ee8a9ded01fc355f704cff92bfd179574e6f9cfee800a3541df1b200 aws:asset:property: Code CDKMetadata: Type: AWS::CDK::Metadata Properties: Analytics: v2:deflate64:H4sIAAAAAAAA/82UTWvCQBCGf4t7lHVbU7DFWwylBEoNWjxUpEx2R11NdsPuJCLify8mfoDQU3vIaWbeHYaH2ZcJRP/5RTx2YOd7Um17mU7FYUogtxx2/htlIA6zQs5Zly14tDSzJOJJmWZaTsvUIM0PDCrQGaQ607T/sgbZkHUZZ1UhY3XOpVZulFm5Pdc5FM2QuBibdyiNXLMhuRI500U1iO7awXu9MnFRDUKlHHo/NpFDIG1N3XHkLaGoQZT6AHoDwh3s2bDeXGsA20Hx65pqizXGOmUTWxJ+QprhTb9pofdW6nrwtfmUvMbJKdym88TpCgj/3bFLyHwL/uJvGO0haQlG48LYEDqDVw81t+9chUQg1zkaOvIJels6ifMFj0pPNr8Kl5t5Ee7eE2crrdCNwCMPvUeaEqy0WR25sQrFxj9UQSD6T6Lf2Xite640pHMUkyb+AI/mB4K7BQAA Metadata: aws:cdk:path: WorkStack/CDKMetadata/Default Condition: CDKMetadataAvailable Conditions: --以下省略--
作成されたリソースについてまとめると以下の通りです。
-
- VPC(10.0.0.0/16)
-IGWの紐付け
-
- PublicSubnet1(10.0.0.0/18)
-RouteTable,EIP,NAT Gatewayの紐付け
-
- PublicSubnet2(10.0.64.0/18)
-RouteTable,EIP,NAT Gatewayの紐付け
-
- PrivateSubnet1(10.0.128.0/18)
-RouteTableの紐付け
-
- PrivateSubnet2(10.0.192.0/18)
-RouteTableの紐付け
- デフォルトセキュリティグループ
-
- IAMロール
-Lambda実行
-
- Lambda Function
-デフォルトセキュリティグループからインバウンド・アウトバウンドルールを削除
詳細な設定をしなくても一気にリソースが作成されるのはすごいですね!
ネットワークへの接続経路が確保され、Public/Privateサブネットも用意されたため、すぐにサーバ等を立てて作業を開始することが出来そうです。
L2コンストラクトを使用することで「よしなに」色々とリソースがされることが分かりました。
まとめ
AWS公式サイトにて「L1コンストラクトは、単一のAWS CloudFormationリソースに直接マッピングされます。」と記載があるように、L1コンストラクトはあくまで設定されたパラメータに忠実にリソースを作成することが分かりました。
また、L2コンストラクトを使用することで、あまりAWSに詳しくなくても簡単に必要なリソースが作成されるのは面白い点だと思いました。
しかし、今回のVPCの場合のように、「そこまで作らなくても...」と感じることもあるかもしれません。
「よしなに」やってくれるのは大変便利ですが、最低限必要なパラメータを設定するスキルは必要ですね。
ただ、やはりリソースの作成負担の軽減や変更のしやすさは大きなメリットになるかと思いますので、上手く使っていきたいですね。
最後まで読んでいただきありがとうございます。
もし参考になりましたら幸いです。