🌀

CDKでFargateをデプロイしようとしたら大変だった記憶

2024/08/15に公開

始めに

IaCやってますか。僕は最近知りました。
CloudFormationを触る機会があったのですが、YAMLファイルを書くのが辛かった記憶があります。
とにかく長い。CDKならコードベースで(比較的)短くインフラを定義できるよ!なる言説を見たので、チャレンジしてみました。
AWS初学者、CDK初学者の備忘録として、誰かの役に立てれば幸いです。

開発環境

開発環境にはEC2インスタンスを使用しました。
OSはUbuntuで、Dockerをルートレスモードでインストールして、VSCodeからSSH接続して開発コンテナを起動しました。

成果物

わけあってリポジトリは公開していないですが、最終的な成果物は下記の通りです。

cdk-sandbox-fargate
|-- .devcontainer
|   `-- devcontainer.json
|-- .gitignore
|-- README.md
|-- bin
|   `-- cdk-sandbox-fargate.ts
|-- biome.json
|-- bun.lockb
|-- bunfig.toml
|-- cdk.json
|-- jest.config.js
|-- lib
|   |-- construct
|   |   |-- cdk-sandbox-fargate-service.ts
|   |   `-- cdk-sandbox-fargate-vpc.ts
|   `-- stack
|       `-- cdk-sandbox-fargate-stack.ts
|-- package.json
|-- property
|   |-- common.ts
|   `-- development.ts
|-- test
|   `-- cdk-sandbox-fargate.test.ts
|-- toolkit-template.yml
|-- tsconfig.json
`-- yarn.lock

また、AWSリソースの構成は下図の通りです。
VPCとインターネットゲートウェイは手動で作成しました。

AWSリソースの構成

0. 開発環境を構築する

0.1. 開発コンテナを起動する

開発コンテナを使用するため、devcontainer.jsonを作成します。

devcontainer.json
{
  "image": "node:22.5.1",
  "runArgs": ["--name=csf-cdk"],
  "containerEnv": {
    "TZ": "Asia/Tokyo"
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "eamodio.gitlens",
        "ms-azuretools.vscode-docker",
        "MS-CEINTL.vscode-language-pack-ja",
        "biomejs.biome",
        "tamasfe.even-better-toml",
        "kennylong.kubernetes-yaml-formatter",
        "DavidAnson.vscode-markdownlint"
      ],
      "settings": {
        "editor.renderWhitespace": "all",
        "editor.tabSize": 2,
        "editor.formatOnSave": true,
        "files.trimFinalNewlines": true,
        "files.trimTrailingWhitespace": true,
        "[dockerfile]": {
          "editor.defaultFormatter": "ms-azuretools.vscode-docker"
        },
        "[json]": {
          "editor.defaultFormatter": "biomejs.biome"
        },
        "[jsonc]": {
          "editor.defaultFormatter": "biomejs.biome"
        },
        "[toml]": {
          "editor.defaultFormatter": "tamasfe.even-better-toml"
        },
        "[yaml]": {
          "editor.defaultFormatter": "kennylong.kubernetes-yaml-formatter"
        },
        "[markdown]": {
          "editor.defaultFormatter": "DavidAnson.vscode-markdownlint",
          "files.trimTrailingWhitespace": false
        },
        "[javascript]": {
          "editor.defaultFormatter": "biomejs.biome"
        },
        "[typescript]": {
          "editor.defaultFormatter": "biomejs.biome"
        }
      }
    }
  },
  "postStartCommand": "npm install -g bun"
}

開発コンテナの詳細については省略します。
Node.jsが使える環境を準備できれば問題ないと思います。
devcontainer.jsonを作成したら、開発コンテナを起動します。

https://containers.dev/implementors/json_reference/

0.2. CDKプロジェクトを作成する

CDKプロジェクトを作成するため、下記コマンドを実行します。

$ bunx cdk init app --language typescript

素直にディレクトリを作成して階層を変えれば良いのですが、devcontainer.jsonがあるとコマンドが失敗します(CDKプロジェクトは空のディレクトリにしか作成できないため)。
僕はdevcontainer.jsonを一度削除してから、CDKプロジェクトを作成しました😂

https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/hello_world.html

コマンドを実行すると、下記のようなディレクトリ構成になります。

cdk-sandbox-fargate
|-- .devcontainer
|   `-- devcontainer.json
|-- .gitignore
|-- .npmignore
|-- README.md
|-- bin
|   `-- cdk-sandbox-fargate.ts
|-- cdk.json
|-- jest.config.js
|-- lib
|   `-- cdk-sandbox-fargate-stack.ts
|-- package-lock.json
|-- package.json
|-- test
|   `-- cdk-sandbox-fargate.test.ts
`-- tsconfig.json

0.3. Bunを設定する

パッケージ管理ツールとしてbunを使用するため、package-lock.jsonを削除します。
また、Node.jsのパッケージとして公開することもないため、.npmignoreを削除します。
削除した後、bunfig.tomlを作成します。

bunfig.toml
[install.lockfile]

# whether to save the lockfile to disk
save = true

# whether to save a non-Bun lockfile alongside bun.lockb
# only "yarn" is supported
print = "yarn"

こうすることで、yarn.lockとしてlockfileが作成されるようになります。

https://bun.sh/docs/install/lockfile

ここで一旦node_modulesを削除して、パッケージを再インストールします。

$ bun install

0.4. Biomeを設定する

リンター/フォーマッターとしてBiomeを使用するため、インストールします。

$ bun add -DE biome

インストールしたら、設定ファイルであるbiome.jsonを作成します。

biome.json
{
  "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
  "formatter": {
    "indentStyle": "space"
  },
  "javascript": {
    "formatter": {
      "trailingCommas": "none",
      "semicolons": "asNeeded",
      "indentStyle": "space"
    }
  },
  "json": {
    "parser": {
      "allowComments": true
    },
    "formatter": {
      "indentStyle": "space"
    }
  }
}

https://biomejs.dev/ja/reference/configuration/

0.5. npmスクリプトを追加する

CDKでよく使用するコマンドをnpmスクリプトに追加します。
最終的なpackage.jsonは下記の通りです。

package.json
{
  "name": "cdk-sandbox-fargate",
  "bin": {
    "cdk-sandbox-fargate": "bin/cdk-sandbox-fargate.js"
  },
  "scripts": {
    "refresh": "rm -rf cdk.out node_modules && bun install",   ★パッケージをクリーンインストールする
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk",
    "bootstrap": "cdk bootstrap --qualifier cdk --template toolkit-template.yml",   ★bootstrapする
    "deploy:D": "cdk deploy --qualifier cdk -c environment=development",   ★開発環境にデプロイする
    "deploy:S": "cdk deploy --qualifier cdk -c environment=staging",   ★検証環境にデプロイする
    "deploy:P": "cdk deploy --qualifier cdk -c environment=production",   ★本番環境にデプロイする
    "destroy:D": "cdk destroy --qualifier cdk -c environment=development",   ★開発環境のスタックを削除する
    "destroy:S": "cdk destroy --qualifier cdk -c environment=staging",   ★検証環境のスタックを削除する
    "destroy:P": "cdk destroy --qualifier cdk -c environment=production",   ★本番環境のスタックを削除する
    "ncu": "bunx npm-check-updates"   ★パッケージの更新を確認する
  },
  "dependencies": {
    "aws-cdk": "2.151.0",
    "aws-cdk-lib": "2.151.0",
    "constructs": "10.3.0",
    "source-map-support": "0.5.21"
  },
  "devDependencies": {
    "@biomejs/biome": "1.8.3",
    "@types/jest": "29.5.12",
    "@types/node": "22.3.0",
    "jest": "29.7.0",
    "ts-jest": "29.2.4",
    "ts-node": "10.9.2",
    "typescript": "5.5.4"
  }
}

★が追加したコマンドです。

1. デプロイに必要なリソースを作成する

デプロイに必要なリソースを作成するため、下記コマンドを実行します。

$ bun run cdk bootstrap

実行するのですが、僕が使用するAWS環境は共用環境という都合があり、下記の問題があります。
・他の開発者が既にデプロイに必要なリソースを作成している
CDKToolkitStackの名称、qualifierが重複する。
・作成されるロールに許可の境界が設定されない
許可の境界が設定されていないロールの作成自体が許可の境界で拒否されているので、そもそもロールを作成できない。

これらの問題を解消するため、下記の手順を実施しました。

1.1. CDKToolkitStackの名称を設定する

CDKToolkitStackの名称の重複を回避するため、cdk.jsonに設定を追加します。

cdk.json
{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-sandbox-fargate.ts",
  "watch": {
    "include": ["**"],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "tsconfig.json",
      "package*.json",
      "yarn.lock",
      "node_modules",
      "test"
    ]
  },
  "context": {
    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
    "@aws-cdk/core:checkSecretUsage": true,
    "@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
    "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
    "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
    "@aws-cdk/aws-iam:minimizePolicies": true,
    "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
    "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
    "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
    "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
    "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
    "@aws-cdk/core:enablePartitionLiterals": true,
    "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
    "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
    "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
    "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
    "@aws-cdk/aws-route53-patters:useCertificate": true,
    "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
    "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
    "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
    "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
    "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
    "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
    "@aws-cdk/aws-redshift:columnId": true,
    "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
    "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
    "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
    "@aws-cdk/aws-kms:aliasNameRef": true,
    "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
    "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
    "@aws-cdk/aws-efs:denyAnonymousAccess": true,
    "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
    "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
    "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
    "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
    "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
    "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
    "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
    "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
    "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
    "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
    "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
    "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
    "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
    "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
    "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false
  },
  "toolkitStackName": "<toolkit-stack-name>"   ★CDKToolkitStackの名称
}

★が追加した設定です。
cdk.jsonにqualifierを追加することもできるのですが、僕の環境では上手く機能しない(設定が無視される)ため、コマンドラインで指定することで回避しました。

https://dev.classmethod.jp/articles/changing-the-aws-cdk-bootstrap-environment-from-the-default-qualifier-hnb659fds/

1.2. 許可の境界を設定する

デプロイに必要なリソースに含まれるロールに許可の境界を設定するため、下記コマンドを実行します。

$ bun run cdk bootstrap --show-template > toolkit-template.yml

作成したtoolkit-template.ymlを編集して、許可の境界を設定します。
長いため省略しますが、下記のように設定します。

toolkit-template.yml
<省略>
  FilePublishingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            AWS:
              Ref: AWS::AccountId
        - Fn::If:
          - HasTrustedAccounts
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              AWS:
                Ref: TrustedAccounts
          - Ref: AWS::NoValue
      RoleName:
        Fn::Sub: ${GenericRolePrefix}-${Qualifier}-file-publishing-role   ★ロールの名称
      PermissionsBoundary:
        Fn::Sub: arn:aws:iam::${AWS::AccountId}:policy/${PermissionsBoundaryName}   ★許可の境界
      Tags:
      - Key: aws-cdk:bootstrap-role
        Value: file-publishing
<省略>

★が追加した設定です。
これを作成するロールの数だけ追加します。
また、リソースの命名規則が決められている場合は、ついでに設定します。

https://zenn.dev/yumemi_inc/articles/cdk-with-permissions-boundary

1.3. リソースを作成する

デプロイに必要なリソースを作成するため、下記コマンドを実行します。

$ bunx cdk bootstrap --qualifier cdk --template toolkit-template.yml

qualifierは10文字以内で他の開発者と重複しなければ何でも良いと思います。
実行した結果、作成されるリソースは下図の通りです。
スタックがCREATE_COMPLETEになっていればOKです。

CDKToolkitStack

https://www.cloudbuilders.jp/articles/3511/

2. 環境ごとにPropsを分離する

開発環境/検証環境/本番環境ごとにPropsを分離できるように、外部ファイルにPropsを記述します。

2.1. 共通のPropsを分離する

まず、共通のPropsをcommon.tsに記述します。

common.ts
import * as cdk from "aws-cdk-lib"

export interface CdkSandboxFargateProps extends CommonProps {
  envName?: string
  vpcId?: string
  vpcName?: string
  gatewayId?: string
  image?: string
}

export interface CommonProps extends cdk.StackProps {
  genericRolePrefix?: string
  genericResourcePrefix?: string
}

export const commonProps: CommonProps = {
  env: { account: "<account>", region: "<region>" },
  permissionsBoundary: cdk.PermissionsBoundary.fromArn(
    "<permissions-boundary-arn>"
  ),
  genericRolePrefix: "<generic-role-prefix>",
  genericResourcePrefix: "<generic-resource-prefix>"
}

(可能な限り)リソースの命名規則を守るため、genericRolePrefixとgenericResourcePrefixを記述します。
また、デプロイするリソースに含まれるロールに許可の境界が設定されるよう、permissionsBoundaryを記述します。

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.StackProps.html

2.2. 開発環境のPropsを分離する

次に、開発環境のPropsをdevelopment.tsに記述します。

development.ts
import type { CdkSandboxFargateProps } from "./common"
import { commonProps } from "./common"

export const developmentProps: CdkSandboxFargateProps = {
  ...commonProps,
  envName: "<envName>",
  vpcId: "<vpcId>",
  vpcName: "<vpcName>",
  gatewayId: "<gatewayId>",
  image: "nginx:1.27.0"
}

環境名や既存VPCのID/名称、インターネットゲートウェイのID、Fargateで動作させるコンテナのイメージを追加します。

3. Fargateのリソースを作成する

CDKはApp/Stack/Constructの3要素で構成されています。
それぞれ順番に作成します。

https://qiita.com/Brutus/items/6c8d9bfaab7af53d154a

3.1. App要素を作成する

まず、App要素を作成します。リソースをデプロイするときのエントリーポイントになります。
bin配下のcdk-sandbox-fargate.tsを編集します。

cdk-sandbox-fargate.ts
#! /usr/bin/env node
import * as cdk from "aws-cdk-lib"
import { CdkSandboxFargateStack } from "../lib/stack/cdk-sandbox-fargate-stack"
import { developmentProps } from "../property/development"

const getProps = (environmentContextArgumentValue: string) => {
  switch (environmentContextArgumentValue) {
    case "development":
      return developmentProps
    case "staging":
      return {}
    case "production":
      return {}
    default:
      return {}
  }
}

const app = new cdk.App()

const environmentContextArgumentKey = "environment"
const environmentContextArgumentValue = app.node.tryGetContext(
  environmentContextArgumentKey
)

const props = getProps(environmentContextArgumentValue)
console.dir("props:")
console.dir(props)

const cdkSandboxFargateStack = new CdkSandboxFargateStack(
  app,
  `${props.genericResourcePrefix}-cdk-sandbox-fargate-stack-${props.envName}`,
  { ...props }
)

コマンドラインで指定されたコンテキスト引数(環境名)をapp.node.tryGetContextで取得します。
取得した環境名によってPropsを切り替えられるよう、getPropsを定義します。
環境ごとのPropsをStack要素に渡します。

https://dev.classmethod.jp/articles/aws-cdk-multi-environment-config/

3.2. Stack要素を作成する

次に、Stack要素を作成します。
bin/stack配下のcdk-sandbox-fargate-stack.tsを編集します。
CDKプロジェクトを作成したときよりディレクトリが1階層深くなっているのは、Construct要素とディレクトリを分けたかったためです。

https://github.com/aws-samples/baseline-environment-on-aws

cdk-sandbox-fargate-stack.ts
import * as cdk from "aws-cdk-lib"
import type { Construct } from "constructs"
import type { CdkSandboxFargateProps } from "../../property/common"
import { CdkSandboxFargateVpc } from "../construct/cdk-sandbox-fargate-vpc"
import { CdkSandboxFargateService } from "../construct/cdk-sandbox-fargate-service"

export interface CdkSandboxFargateStackProps extends CdkSandboxFargateProps {}

export class CdkSandboxFargateStack extends cdk.Stack {
  constructor(
    scope: Construct,
    id: string,
    props: CdkSandboxFargateStackProps
  ) {
    super(scope, id, props)

    // VPC
    const cdkSandboxFargateVpc = new CdkSandboxFargateVpc(this, "Vpc", {
      ...props
    })

    // Fargate
    const cdkSandboxFargateService = new CdkSandboxFargateService(
      this,
      "Service",
      {
        ...props,
        subnetIds: cdkSandboxFargateVpc.subnetIds,
        routeTableIds: cdkSandboxFargateVpc.routeTableIds
      }
    )
  }
}

Stack要素の内部で呼び出すConstruct要素をVPCとFargateに分離します。
こうすることで、ネットワーク関連のリソースはVPCで、サービス関連のリソースはFargateで、というように、コードの見通しが良くなります。
App要素から受け取ったPropsは基本的にそのままConstruct要素に渡しますが、ネットワーク関連のリソースを作成した後でないと決まらない値については、追加で渡します。

3.3. Construct要素を作成する

最後に、Construct要素を作成します。

3.3.1. ネットワーク関連のリソースを作成する

まず、ネットワーク関連のリソースである、bin/construct配下のcdk-sandbox-fargate-vpc.tsを編集します。

cdk-sandbox-fargate-vpc.ts
import * as cdk from "aws-cdk-lib"
import { Construct } from "constructs"
import type { CdkSandboxFargateProps } from "../../property/common"

export interface CdkSandboxFargateVpcProps extends CdkSandboxFargateProps {}

export class CdkSandboxFargateVpc extends Construct {
  public readonly subnetIds: string[]
  public readonly routeTableIds: string[]

  constructor(scope: Construct, id: string, props: CdkSandboxFargateVpcProps) {
    super(scope, id)

    // パブリックサブネットを追加
    const cdkSandboxFargatePublicSubnet1 = new cdk.aws_ec2.PublicSubnet(
      this,
      "PublicSubnet1",
      {
        availabilityZone: `${props.env?.region}c`,
        cidrBlock: "10.0.1.0/25",
        vpcId: props.vpcId ?? ""
      }
    )
    // インターネットゲートウェイをルートに追加
    cdkSandboxFargatePublicSubnet1.addRoute("PublicRoute1", {
      routerId: props.gatewayId ?? "",
      routerType: cdk.aws_ec2.RouterType.GATEWAY,
      enablesInternetConnectivity: true
    })
    // タグを追加
    cdk.Tags.of(cdkSandboxFargatePublicSubnet1).add(
      "Name",
      `${props.genericResourcePrefix}-csf-public-subnet-1-${props.envName}`
    )
    cdk.Tags.of(
      cdkSandboxFargatePublicSubnet1.node.findChild("RouteTable")
    ).add(
      "Name",
      `${props.genericResourcePrefix}-csf-public-route-table-1-${props.envName}`
    )

    // パブリックサブネットを追加
    const cdkSandboxFargatePublicSubnet2 = new cdk.aws_ec2.PublicSubnet(
      this,
      "PublicSubnet2",
      {
        availabilityZone: `${props.env?.region}d`,
        cidrBlock: "10.0.1.128/25",
        vpcId: props.vpcId ?? ""
      }
    )
    // インターネットゲートウェイをルートに追加
    cdkSandboxFargatePublicSubnet2.addRoute("PublicRoute2", {
      routerId: props.gatewayId ?? "",
      routerType: cdk.aws_ec2.RouterType.GATEWAY,
      enablesInternetConnectivity: true
    })
    // タグを追加
    cdk.Tags.of(cdkSandboxFargatePublicSubnet2).add(
      "Name",
      `${props.genericResourcePrefix}-csf-public-subnet-2-${props.envName}`
    )
    cdk.Tags.of(
      cdkSandboxFargatePublicSubnet2.node.findChild("RouteTable")
    ).add(
      "Name",
      `${props.genericResourcePrefix}-csf-public-route-table-2-${props.envName}`
    )

    // subnetIds
    this.subnetIds = [
      cdkSandboxFargatePublicSubnet1.subnetId,
      cdkSandboxFargatePublicSubnet2.subnetId
    ]

    // routeTableIds
    this.routeTableIds = [
      cdkSandboxFargatePublicSubnet1.routeTable.routeTableId,
      cdkSandboxFargatePublicSubnet2.routeTable.routeTableId
    ]
  }
}

既存のVPCにサブネットを2つ追加します。
サブネットからインターネットに通信するために、インターネットゲートウェイをルートテーブルのルートに追加します。
(可能な限り)リソースの命名規則を守るため、タグも追加します。
追加したサブネットやルートテーブルのIDは、後述するサービス関連のリソースで使用するため、readonlyなpublicプロパティにします。

3.3.2. サービス関連のリソースを作成する

次に、サービス関連のリソースである、bin/construct配下のcdk-sandbox-fargate-service.tsを編集します。

cdk-sandbox-fargate-service.ts
import * as cdk from "aws-cdk-lib"
import { Construct } from "constructs"
import type { CdkSandboxFargateProps } from "../../property/common"

export interface CdkSandboxFargateServiceProps extends CdkSandboxFargateProps {
  subnetIds: string[]
  routeTableIds: string[]
}

export class CdkSandboxFargateService extends Construct {
  constructor(
    scope: Construct,
    id: string,
    props: CdkSandboxFargateServiceProps
  ) {
    super(scope, id)

    // クラスターを追加するVPCを定義
    const cdkSandboxFargateVpc = cdk.aws_ec2.Vpc.fromVpcAttributes(
      this,
      "Vpc",
      {
        availabilityZones: [`${props.env?.region}c`, `${props.env?.region}d`],
        vpcId: props.vpcId ?? "",
        publicSubnetIds: props.subnetIds,
        publicSubnetRouteTableIds: props.routeTableIds,
        region: props.env?.region
      }
    )

    // クラスターを追加
    const cdkSandboxFargateCluster = new cdk.aws_ecs.Cluster(this, "Cluster", {
      clusterName: `${props.genericResourcePrefix}-csf-cluster-${props.envName}`,
      vpc: cdkSandboxFargateVpc
    })

    // ロググループを追加
    const cdkSandboxFargateLogGroup = new cdk.aws_logs.LogGroup(
      this,
      "LogGroup",
      {
        logGroupName: `${props.genericResourcePrefix}-csf-log-group-${props.envName}`,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
        retention: cdk.aws_logs.RetentionDays.ONE_MONTH
      }
    )

    // サービスを追加
    const cdkSandboxFargateService =
      new cdk.aws_ecs_patterns.ApplicationLoadBalancedFargateService(
        this,
        "Service",
        {
          assignPublicIp: true,
          cluster: cdkSandboxFargateCluster,
          desiredCount: 2,
          loadBalancerName: `kuroda-csf-lb-${props.envName}`,
          serviceName: `${props.genericResourcePrefix}-csf-service-${props.envName}`,
          taskImageOptions: {
            image: cdk.aws_ecs.ContainerImage.fromRegistry(props.image ?? ""),
            containerName: `${props.genericResourcePrefix}-csf-web-server-${props.envName}`,
            family: `${props.genericResourcePrefix}-csf-task-definition-${props.envName}`,
            logDriver: cdk.aws_ecs.LogDriver.awsLogs({
              streamPrefix: "web-server",
              logGroup: cdkSandboxFargateLogGroup
            })
          }
        }
      )

    // タグを追加
    cdk.Tags.of(cdkSandboxFargateService).add("AutoStop", "Disabled")
  }
}

Fargateのクラスター/ロググループ/サービスを追加します。
クラスターを追加するVPCを定義するときに、ネットワーク関連のリソースから受け取ったサブネットやルートテーブルのIDを使用します。
僕が使用するAWS環境では、定期的にECSタスクが自動停止するため、それを回避するためにタグも追加します。

3.3.3. リソースをデプロイする

リソースをデプロイするため、下記コマンドを実行します。

$ bunx cdk deploy --qualifier cdk -c environment=development

実行した結果、作成されるリソースは下図の通りです。
スタックがCREATE_COMPLETEになっていればOKです。

CDKSandboxFargateStack

ALBのURLにアクセスすると、Fargateで動作するNginxの画面が表示されます。

お疲れ様でした。

終わりに

CDKに軽く触ってみよう、というつもりが、自分史上最長のポエムになってしまいました…😶‍🌫️
とはいえ、CDKでFargateをデプロイするという目的は達成できましたし、CDKを使用したことがあります、と言えるくらいにはなったと思います。
最後に、CDKを使用して感じたことをまとめます。

CloudFormationとの比較

メリット/デメリットはこんな感じかなと思います。

CDKのメリット
・短く書ける
・TypeScriptで書ける

CDKのデメリット
・学習コストが高い
・抽象度が高い

短く書ける、については、同等のリソースをCloudFormationでデプロイして比較したわけではないので説得力に欠けますが、CDKではL2/L3レイヤーのConstruct要素を使用することで、複数のリソースを簡単に作成できます。
例えば、サブネットを作成すればルートテーブルが作成されるように、Fargateサービスを作成すればALBが作成されるように、開発者が細かく意識しなくても必要なリソースを作成できる反面、よく知らないリソースが作成されている、なんてことも起こり得ます。
抽象度が高いゆえに短く書けるが、落とし穴も存在すると感じました。

学習コストが高い、については、bootstrapって何?とか、App/Stack/Constructって何?を理解して、簡単なリソースをデプロイするまでに時間がかかるという感想です。
AWS環境次第ではbootstrapさえ失敗するなど、CloudFormationと比較すると事前準備に苦労する印象です。

TypeScriptで書ける、については、もはや個人の好みかもしれませんが、リソースをコードで管理するうえでTypeScriptの強力な型推論が使用できることは、開発者のストレス軽減に寄与すると思います。
正直これだけでCDKを使用する理由になると思います。Pythonとかで書いてる人は…分かりません。

リソースの命名規則が守れない

そもそもCDKではリソースの名称を明示的に指定しないことがベストプラクティスですが、命名をCDK任せにすると、往々にしてエグい名称になります。
開発者が一目見て何のリソースであるか判断できない状況が発生しますし、顧客もリソースを見るということであれば尚更頭が痛いと思います。
とはいえ、CDKでデプロイするリソースの全てに命名規則を適用しようとすると、短く書けるメリットを潰してしまうことにもなるので、妥協点を探ることが必要だと感じました。

https://logmi.jp/tech/articles/326696

いつCDKを導入するか

IaCツール全般に言えることですが、人間の手作業によるインフラ構築よりも、自動的なインフラ構築の方が正確で早いです。
悩ましいのは、どのタイミングでCDKを導入するか、だと思います。
僕の個人的な考えですが、早ければ早いほど良いと思います。
当然、プロジェクト開始直後やAWSの知識が浅い段階で焦って導入する必要はない(サービスの理解を深めてからCDKを導入すれば良い)ですが、少なくとも本格的な開発作業に着手する前に導入しておくのがベターだと考えます。
開発作業では、インフラ構築よりもアプリ開発に注力しがちで、ともすればインフラ構築の自動化はリリース直前に時間の余裕があったらやる、になりがちです。
でも考えてみればリリース直前にそんな余裕はありません。開発作業の段階で自動化しなかったインフラ構築は永遠に自動化されません。
リリースは勿論のこと、機能追加でインフラが増えるたび、非効率なエクセルファイルの作成と手作業による構築が引き継がれていきます…。

Discussion