🕌

CDKのローカル開発環境を整える

に公開

はじめに

AWS CDKを使って開発を行ってきましたが、ローカル環境の構築ではcdk-localがあるのを知りつつも既存プロジェクトが巨大だったので導入を諦めていました。

ただ、新しいプロジェクトが始まりそうで、新プロジェクトではcdk-localを使用して本環境とローカル環境の構築を一本化したいなぁと考えてます。今回はCDKのローカル開発環境を整えるための手順をまとめて、実際にcdk-localを使ってローカル環境へのデプロイを試してみたいと思います。

まだ検討・学習の段階なので今回はLocalStackは無料の範囲で確認していきます。

開発環境と言語

  • 言語: TypeScript
  • IDE: Visual Studio Code

CDKおよびLocalStack

CDKとLocalStackの説明は公式ドキュメントや他の方の解説の方が丁寧と思うので割愛します。。。

LocalStack

インストール

LocalStackのインストール方法はOSによって異なるので、公式サイトをご確認ください。

LocalStackのインストール

LocalStackの開始

localstack start -d

LocalStackの終了

localstack stop

使用可能なサービス

以下のコマンドで確認ができます。

localstack status services

CDKのインストール

今回はグローバルにインストールします。

npm install -g aws-cdk-local aws-cdk

プロジェクトの初期化

プロジェクトの初期化は以下のコマンドになります。cdkコマンドでも作成できますが、別途Local用のプロファイルを作成しなければいけないようなので、cdklocalで初期化します。

言語はtypescriptを採用します。

cdklocal init --language typescript

Visual Studio Codeの設定

コードスニペットの設定

Stackファイルのテンプレート

cdk initを実行した際にのサンプルコードとして以下のコードが出力されます。

import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // The code that defines your stack goes here
  }
}

Stackを新規で生成する際にこちらをコピペしていくことになると思うので、スニペットに登録しておきます。共同開発を想定して、プロジェクトにスニペットを登録しておきます。

スニペットの登録は、コマンドパレットから Snippets: Configure Snipetts -> New snippets file for ${projectname} -> ファイル名入力でts-cdkと入力します。テンプレートが出力されるので、以下の内容を記載します。

{
  "Stack file template": {
    "scope": "javascript,typescript",
    "prefix": "stack",
    "body": [
      "import * as cdk from 'aws-cdk-lib'",
      "import { Construct } from 'constructs'",
      "",
      "// FIXME: Change the stack name",
      "export class MyCdkStack extends cdk.Stack {",
      "  constructor(scope: Construct, id: string, props?: cdk.StackProps) {",
      "  super(scope, id, props)",
      "",
      "  // The code that defines your stack goes here",
      "  }",
      "}",
      ""
    ],
    "description": "Stack file template for AWS CDK"
  }
}

Constructのテンプレート

Constructは適切に分割していきたいので、Constructのテンプレートも用意しておきます。こちらも同様にスニペットに登録します。

{
  "Construct template": {
    "scope": "javascript,typescript",
    "prefix": "construct",
    "body": [
      "// import { Construct } from 'constructs'",
      "",
      "// FIXME: Change the construct name",
      "export class MyConstruct extends Construct {",
      "  constructor(scope: Construct, id: string) {",
      "    super(scope, id);",
      "  }",
      "}"
    ],
    "description": "Construct template for AWS CDK"
  }
}

実装してみる

実際のAWSでのSI業務ではVPCを構築することが多いので、VPCを構築してみます。

import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'

export class VpcStack extends cdk.Stack {
  public readonly vpc: cdk.aws_ec2.Vpc
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    this.vpc = new cdk.aws_ec2.Vpc(this, 'MyVpc', {
      maxAzs: 3, // Default is all AZs in the region
      natGateways: 1, // Number of NAT gateways to create
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public',
          subnetType: cdk.aws_ec2.SubnetType.PUBLIC
        },
        {
          cidrMask: 24,
          name: 'private',
          subnetType: cdk.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS
        }
      ]
    })
  }
}

bin/cdk.tsには以下のように記載します。

import * as cdk from 'aws-cdk-lib'
import { VpcStack } from '../lib/vpc-stack'
const app = new cdk.App()
new VpcStack(app, 'VpcStack')

ローカルへのデプロイ

Bootstrap

初回デプロイ実行前に以下のコマンドでbootstrapを実行します。

cdklocal bootstrap

デバッグの設定

VSCode設定の追加

デバッグを行うために、launch.jsonを以下のように設定します。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug CDKLocal Synth",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/node_modules/aws-cdk-local/bin/cdklocal",
      "cwd": "${workspaceFolder}",
      "args": ["synth"],
      "outFiles": ["${workspaceFolder}/**/*.js"]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug CDK Synth",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}/node_modules/aws-cdk/bin/cdk",
      "cwd": "${workspaceFolder}",
      "args": ["synth"],
      "outFiles": ["${workspaceFolder}/**/*.js"]
    }
  ]
}

Debug CDKLocal SynthはLocalStack向けのデバッグ設定、Debug CDK Synthは本番環境向けのデバッグ設定です。

プロジェクトへcdklocalの追加

デフォルトではcdklocalはプロジェクトに含まれていないため、依存関係に追加しておきます。

npm install --save-dev aws-cdk-local

これでデバッグが可能になります。

デプロイ

デバッグしてみて問題なさそうだったので実際にデプロイしてみましょう。

cdklocal deploy

エラーになりました。

❌  VpcStack failed: _ToolkitError: The stack named VpcStack failed to deploy: CREATE_FAILED (An error occurred (InvalidAllocationID.NotFound) when calling the CreateNatGateway operation: Allocation ID '['unknown']' not found.): An error occurred (InvalidAllocationID.NotFound) when calling the CreateNatGateway operation: Allocation ID '['unknown']' not found.

GitHub Copilotに聞いてみると、以下のように指摘されました。

このエラーの原因は、cdklocal(LocalStack)でNAT Gatewayを作成しようとした際に、Elastic IP(EIP)の割り当てIDが見つからないためです。
LocalStackはNAT GatewayやEIPの完全なエミュレーションをサポートしていないため、natGatewaysを1以上に設定するとこのエラーが発生します。

LocalStackではNAT Gatewayのエミュレーションができなそうですが、実際のAWS環境ではNAT Gatewayを使用したいです。そのため、cdklocalによる実行時のみNAT Gatewayを使用しないように、natGateways: 0と設定します。

LocalStackを使うときはAWS_ENDPOINT_URLやLOCALSTACK_HOSTNAMEなどの環境変数がセットされているようなので、以下のソースを追加します。(GitHub Copilotによる提案で、裏付けは取ってません)

export function isLocalStack(): boolean {
  // Check if the environment variable LOCALSTACK_HOST is set
  return (
    process.env.AWS_ENDPOINT_URL !== undefined ||
    process.env.LOCALSTACK_HOSTNAME !== undefined ||
    process.env.LOCALSTACK_HOST !== undefined
  )
}
function buildVpcConfig(): cdk.aws_ec2.VpcProps {
  if (isLocalStack()) {
    return {
      maxAzs: 1, // Use a single AZ for LocalStack
      natGateways: 0, // No NAT gateways in LocalStack
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'public',
          subnetType: cdk.aws_ec2.SubnetType.PUBLIC
        }
      ]
    }
  }
  return {
    maxAzs: 3, // Default is all AZs in the region
    natGateways: 1, // Number of NAT gateways to create
    subnetConfiguration: [
      {
        cidrMask: 24,
        name: 'public',
        subnetType: cdk.aws_ec2.SubnetType.PUBLIC
      },
      {
        cidrMask: 24,
        name: 'private',
        subnetType: cdk.aws_ec2.SubnetType.PRIVATE_WITH_EGRESS
      }
    ]
  }
}

再度実行してみます。

VpcStack: deploying... [1/1]
VpcStack: creating CloudFormation changeset...

 ✅  VpcStack

✨  Deployment time: 10.11s

Stack ARN:
arn:aws:cloudformation:us-east-1:000000000000:stack/VpcStack/b4100619

✨  Total time: 12.36s

成功しました!LocalStack向けに実行されているかを判定して、ローカル向けの設定を別途定義すればソースは一本化できそうです。ただ、isLocalStackの判定が増えるとコードが複雑になりそうですね。今後改善を検討していきたいと思います。

まとめ

今回初めてcdklocalを使ってローカル環境の構築を行いました。LocalStackを使うことで、AWSのサービスをローカルでエミュレートできるため、cdklocalで構築したDBをそのままローカル環境で利用できれば別途ローカル向けのdocker-composeやローカル環境構築用のスクリプト作成が不要になりそうです。今後は実際にDBやS3などのサービスを使った開発も行って、ローカルにデプロイしたアプリケーションから実際にLocalStackのサービスを利用できるかを確認していきたいと思います。

参考

Discussion