【連載企画】極めようAWS CDK 〜#2 スタティックなウェブサイトを立ち上げよう(前編)〜

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

前回は、「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リソースの作成に進みましょう。

構成図

今回は下記のような構成がゴールです。

今回の取り組みで目指すゴールを示す構成図
図1 構成図

ユーザーが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) について言及したいと思います。 それではまた!