こんにちは。ソニーミュージックのしんろくです。
前回は、「CDKをはじめよう」ということで、ざっとAWS CDK(以下、CDK)の概要紹介をさせていただきました。これからは、CDKをどんどん深掘りしていきたいと思います。
はじめに
とそのまえに。 「そもそもCDKってなんですの?」 というお声もちらほら聞こえましたので、そのあたりのお話とCDKのメリットを提示してみたいと思います。その後、タイトルにありますとおり、「スタティックなウェブサイト」をたちあげるスクリプトを例示します。
対象読者
- CDK を始めてみたい人
- CloudFormation テンプレートにイライラさせられたことのある人
- AWS 環境構築で楽をしたい人
- AWS リソースやAWS API をより深く理解したい人
- AWS リソースの変更管理をきちんとやりたい人
- マルチアカウント戦略を実施していて、デプロイ作業に苦痛を感じている人
免責事項
- 個々のAWSリソースの説明はおこないません。
- ソースコードはTypeScriptで記述しますが、TypeScriptそのものの機能には言及しません。
CDKって何?
CDKとは、Cloud Development Kitの略称です。SDKがSoftware Development Kitの略称であることを鑑みると、「あ~。AWSみたいなクラウドを開発するライブラリ群なのか」と感じていただけるかもしれません。
がしかし実のところ全然違います。
CDKの説明の前に簡単ですが、AWS CloudFormation(以下CFn)というプロビジョニングサービスの説明をします。
CFnはYAMLやJSONで定義されたテンプレートをもとに、リソースを定義、プロビジョニングおよび管理するためのサービスです。このテンプレートは再利用可能で何度でもスクラップ・アンド・ビルドできたり、他者(=他のAWSアカウント)と共有できたり便利なことがたくさんありますが、YAMLを記述する難易度が非常に高く、「YAML職人」「テンプレート職人」なる言葉が誕生いたしました。
というのも、YAMLというのはそもそもマークアップ言語であり、JSONはテキストベースのデータ交換用フォーマットであり、どちらも論理構造(※例えば東京リージョンであればAをデプロイするが他のリージョンではデプロイしないなど)を記述する言語ではありません。
AWSさんの努力の結果、CFnテンプレート内にロジックを記述できる仕組み(組み込み関数)が実装されていますが、これがますます職人技を必要とする状況を生んでしまいました。
このような状況を改善するために、CDKの誕生が望まれました。
CDKの誕生により、CFnテンプレートを書く代わりに、一般的なプログラミング言語を使用してクラウドインフラをコードとして表現できるようになりました。つまり、開発者はTypeScript, Python, Java, C#など好きなプログラミング言語でクラウドインフラを定義できるようになりました。これにより、コードの再利用性が高まり、より簡潔かつ効率的な開発ができます。
またさらに、CDKが一般的なプログラミング言語の特徴を有することで、統合開発環境(IDE)の自動補完や静的コード分析などを活用することができるようになります。これにより、開発者はAWSのサービスとリソースをより直感的かつ効率的に利用することができるようになり、またGitのようなソースコード管理システムを用いることで、インフラストラクチャの変更履歴を追跡できるため、チーム開発やバージョン管理が容易になりました。
つまり、CDKは「職人技」であったテンプレート開発業務をより汎用的なプログラミング言語にて一般化したといっても過言ではありません。
以下にChat-GPTに聞いたCDKのメリットをまとめておきます。
- 高レベルの抽象化:CDKは、CFnよりも高レベルの抽象化を提供します。これにより、インフラストラクチャをコードで表現する際により簡潔で読みやすい構文が可能になります。
- 言語の選択肢:CDKは、TypeScript, Python, Java, C#など複数のプログラミング言語をサポートしています。これにより、開発者は既に習熟している言語を使用してインフラを定義できます。
- 再利用可能なコンポーネント:CDKを使用すると、カスタムコンポーネント(コンストラクトと呼ばれる)を作成し、プロジェクト間で再利用できます。これにより、コードの重複を減らし、一貫性を保ちながら迅速に開発できます。
- 統合開発体験:CDKは、AWSサービスとの統合が深く、IDEの自動補完や静的コード分析など、開発者向けのツールが豊富にあります。これにより、エラーの可能性が減り、開発効率が向上します。
- ローカルテストとデバッグ:CDKを使用すると、ローカル環境でのテストとデバッグが容易になります。これにより、本番環境へのデプロイ前に問題を発見し、修正することができます。
- プログラマティックな制御:汎用プログラミング言語を使用することで、ループや条件分岐などのプログラムの制御構造を利用して、より柔軟なインフラの設定が可能になります。
実践
以上、CDKの誕生が必然だったことはおわかりいただけたかと思います。ではさっそく、AWSリソースの作成に進みましょう。
構成図
今回は下記のような構成がゴールです。
ユーザーがCloudFrontにアクセスすると、S3からHTMLを返却するという、シンプルなアーキテクチャを構築してみます。
考え方
CDKを記述する際、どういう順序で定義していくのがよいかは考慮すべきポイントです。CDKに慣れてくると、AWSリソース間の結合について理解が深まり、構築が自然に早くなっていきます。
今回は丁寧に進めてみます。
CloudFront の定義
CloudFront のリソースを管理するcloudfront.service.ts
を作成します。
import { aws_cloudfront as cf } from 'aws-cdk-lib'; import { S3Origin } from 'aws-cdk-lib/aws-cloudfront-origins'; import { IBucket } from 'aws-cdk-lib/aws-s3'; import { environment } from '../environments/environment'; import { ServiceBase } from './service.base'; export class CloudfrontService extends ServiceBase { #counter = 0; public createDistribution(bucket: IBucket): cf.Distribution { const cachePolicy = environment.production ? cf.CachePolicy.CACHING_OPTIMIZED : cf.CachePolicy.CACHING_DISABLED; return new cf.Distribution(this.scope, this.id('Distribution', `${this.#counter++}`), { defaultBehavior: { origin: new S3Origin(bucket, { originPath: '/dist/index' }), allowedMethods: cf.AllowedMethods.ALLOW_GET_HEAD, cachePolicy, viewerProtocolPolicy: cf.ViewerProtocolPolicy.HTTPS_ONLY, }, defaultRootObject: 'index.html', enableIpv6: false, httpVersion: cf.HttpVersion.HTTP2, minimumProtocolVersion: cf.SecurityPolicyProtocol.TLS_V1_2_2021, priceClass: cf.PriceClass.PRICE_CLASS_200, }); } }
CloudFront Distributionを定義するメソッドを記述してみましたが、 ここでCloudFrontの定義前にS3の定義が必要になりそうですね。 なので、素直にS3の定義を進めていきます。
S3 の定義
S3のリソースを管理するs3.service.ts
を作成します。
import { RemovalPolicy, aws_s3 as s3 } from 'aws-cdk-lib'; import { environment } from '../environments/environment'; import { ServiceBase } from './service.base'; export class S3Service extends ServiceBase { #counter = 0; public createBucket(id: string): s3.Bucket { return new s3.Bucket(this.scope, this.id('Bucket', id), { autoDeleteObjects: !environment.production, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, enforceSSL: true, objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED, removalPolicy: environment.production ? RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE : RemovalPolicy.DESTROY, }); } }
準備が整いました。
デプロイ
スタックを定義するstack.ts
とCDKのエントリーポイントmain.ts
を定義します。
import { Construct } from 'constructs/lib'; import { CloudfrontService } from '../services/cloudfront.service'; import { S3Service } from '../services/s3.service'; import { CdkEnv } from '../shared/aws-cdk-consts'; import { StackBase } from './stack.base'; export class TechBlog02Stack extends StackBase { #cf: CloudfrontService; #s3: S3Service; constructor(scope: Construct, id: string, env: CdkEnv) { super(scope, id, env.props); this.#cf = new CloudfrontService(this, env); this.#s3 = new S3Service(this, env); } public run(): void { const bucket = this.#s3.createBucket('techblog02'); const distribution = this.#cf.createDistribution(bucket); this.outputs = [{ id: 'distributionDomainName', value: distribution.distributionDomainName }]; } }
import { App } from 'aws-cdk-lib'; import { StackEnv } from './environments/stack.env'; import { CdkEnv } from './shared/aws-cdk-consts'; import { TechBlog02Stack } from './stacks/tech-blog-02.stack'; class Main extends App { #env!: CdkEnv; get #stackName(): string { return this.#env.props.stackName as string; } public run(): void { this.#env = new StackEnv('TB02').env; new TechBlog02Stack(this, this.#stackName, this.#env).run(); } } new Main().run();
ビルド後、cdk synth
コマンドにてテンプレートを表示してみます。
Description: This stack is One of Shinrock TechBlog Sample. Resources: shinrocktechblogsampleBuckettechblog02ACDE4823: Type: AWS::S3::Bucket Properties: BucketEncryption: ServerSideEncryptionConfiguration: - ServerSideEncryptionByDefault: SSEAlgorithm: AES256 OwnershipControls: Rules: - ObjectOwnership: BucketOwnerEnforced PublicAccessBlockConfiguration: BlockPublicAcls: true BlockPublicPolicy: true IgnorePublicAcls: true RestrictPublicBuckets: true Tags: - Key: aws-cdk:auto-delete-objects Value: "true" - Key: sme:edgetech:version Value: 0.0.0 - Key: smej:cost-management:environment Value: dev - Key: smej:cost-management:service Value: Shinrock TechBlog Sample UpdateReplacePolicy: Delete DeletionPolicy: Delete shinrocktechblogsampleBuckettechblog02Policy5AC59E29: Type: AWS::S3::BucketPolicy Properties: Bucket: Ref: shinrocktechblogsampleBuckettechblog02ACDE4823 PolicyDocument: Statement: - Action: s3:* Condition: Bool: aws:SecureTransport: "false" Effect: Deny Principal: AWS: "*" Resource: - Fn::GetAtt: - shinrocktechblogsampleBuckettechblog02ACDE4823 - Arn - Fn::Join: - "" - - Fn::GetAtt: - shinrocktechblogsampleBuckettechblog02ACDE4823 - Arn - /* - Action: - s3:DeleteObject* - s3:GetBucket* - s3:List* - s3:PutBucketPolicy Effect: Allow Principal: AWS: Fn::GetAtt: - CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092 - Arn Resource: - Fn::GetAtt: - shinrocktechblogsampleBuckettechblog02ACDE4823 - Arn - Fn::Join: - "" - - Fn::GetAtt: - shinrocktechblogsampleBuckettechblog02ACDE4823 - Arn - /* - Action: s3:GetObject Effect: Allow Principal: CanonicalUser: Fn::GetAtt: - shinrocktechblogsampleDistribution0Origin1S3Origin3CB02102 - S3CanonicalUserId Resource: Fn::Join: - "" - - Fn::GetAtt: - shinrocktechblogsampleBuckettechblog02ACDE4823 - Arn - /* Version: "2012-10-17" shinrocktechblogsampleBuckettechblog02AutoDeleteObjectsCustomResourceA7904B92: Type: Custom::S3AutoDeleteObjects Properties: ServiceToken: Fn::GetAtt: - CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F - Arn BucketName: Ref: shinrocktechblogsampleBuckettechblog02ACDE4823 DependsOn: - shinrocktechblogsampleBuckettechblog02Policy5AC59E29 UpdateReplacePolicy: Delete DeletionPolicy: Delete CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092: 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 CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F: Type: AWS::Lambda::Function Properties: Code: S3Bucket: cdk-shinrock-assets-956477940259-ap-northeast-1 S3Key: b7f33614a69548d6bafe224d751a7ef238cde19097415e553fe8b63a4c8fd8a6.zip Timeout: 900 MemorySize: 128 Handler: index.handler Role: Fn::GetAtt: - CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092 - Arn Runtime: nodejs18.x Description: Fn::Join: - "" - - "Lambda function for auto-deleting objects in " - Ref: shinrocktechblogsampleBuckettechblog02ACDE4823 - " S3 bucket." DependsOn: - CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092 shinrocktechblogsampleDistribution0Origin1S3Origin3CB02102: Type: AWS::CloudFront::CloudFrontOriginAccessIdentity Properties: CloudFrontOriginAccessIdentityConfig: Comment: Identity for ShinrockTechBlogSampleStackTB02shinrocktechblogsampleDistribution0Origin1E4E67F79 shinrocktechblogsampleDistribution0CDF6A470: Type: AWS::CloudFront::Distribution Properties: DistributionConfig: DefaultCacheBehavior: AllowedMethods: - GET - HEAD CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad Compress: true TargetOriginId: ShinrockTechBlogSampleStackTB02shinrocktechblogsampleDistribution0Origin1E4E67F79 ViewerProtocolPolicy: https-only DefaultRootObject: index.html Enabled: true HttpVersion: http2 IPV6Enabled: false Origins: - DomainName: Fn::GetAtt: - shinrocktechblogsampleBuckettechblog02ACDE4823 - RegionalDomainName Id: ShinrockTechBlogSampleStackTB02shinrocktechblogsampleDistribution0Origin1E4E67F79 OriginPath: /dist/index S3OriginConfig: OriginAccessIdentity: Fn::Join: - "" - - origin-access-identity/cloudfront/ - Ref: shinrocktechblogsampleDistribution0Origin1S3Origin3CB02102 PriceClass: PriceClass_200 Tags: - Key: sme:edgetech:version Value: 0.0.0 - Key: smej:cost-management:environment Value: dev - Key: smej:cost-management:service Value: Shinrock TechBlog Sample Outputs: distributionDomainName0: Value: Fn::GetAtt: - shinrocktechblogsampleDistribution0CDF6A470 - DomainName Parameters: BootstrapVersion: Type: AWS::SSM::Parameter::Value<String> Default: /cdk-bootstrap/shinrock/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.
いい感じにCFnテンプレートが生成されました。
cdk deploy
を実行すれば、AWSアカウント上にCloudFrontとS3が構築され、構成図に記載されたリソース生成はいったんゴールします。
ですが、このままではコンテンツ(HTMLなど)が表示されることはありません。
次回は、index.htmlの準備やOrigin Access Control (OAC) について言及したいと思います。 それではまた!