AWS CDK × TypeScript:YAMLを1行も書かずにクラウドインフラを構築する

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

CloudFormationの問題点

AWSインフラを管理した経験があれば、その辛さはよく分かるはずだ。シンプルなAPI Gateway + Lambda + DynamoDBの構成でも、500行超えのYAMLに膨れ上がることがある。同じVPC設定を10個のスタックにコピペして、あとで3箇所のCIDRブロックを更新し忘れたことに気づく。リファクタリングはもはや遺跡発掘作業だ。

CloudFormationは機能する。だが、インフラをコードではなく「ドキュメント」として扱う。ループもなく、関数もなく、型チェックもない。何かが壊れたとき、エラーメッセージは意味不明だ。そこそこ複雑なスタックをロールバックする?最低でも20分は覚悟しよう。

問題はCloudFormation自体ではなく、そのパラダイムにある。宣言的YAMLは人間が読めるコンフィグのために設計されたのであって、3つの環境で動く15個のマイクロサービスのためではない。ある規模を超えると、もはやインフラを書いているのではなく、YAMLをフルタイムで保守しているだけになる。

コアコンセプト:AWS CDKが実際に何をするのか

AWS CDK(Cloud Development Kit)はそのモデルを逆転させる。書くのはTypeScriptコードだけ——CDKがデプロイ時にCloudFormationテンプレートを自動的に生成してくれる。

この転換は大きな意味を持つ。IDEの補完機能とコンパイル時の型チェックが使える。本物のループと条件分岐が書ける。インフラをバージョン管理されたnpmパッケージとして共有できる。そして重要なのは、エラーがデプロイ前に表面化すること——20分のロールアウトの途中ではなく。

CDKはリソースを3つのレベルのコンストラクトとして整理する:

  • L1(Cfn*クラス):CloudFormationリソースを直接ラップしたもの。完全なコントロールが可能だが冗長。CloudFormation仕様のすべてのプロパティに1対1でマッピングされる。
  • L2:デフォルト値とヘルパーメソッドを持つ高レベルの抽象化。作業の90%はこれを使う。
  • L3(パターン):ALBの背後のECSサービスなど、一般的な複数リソース構成向けの意見付き定義済みソリューション。

cdk deployを実行すると、CDKがTypeScriptをCloudFormationテンプレートに変換し、デプロイする。YAMLは内部に存在し続けるが、触る必要はまったくない。

生のテンプレートからCDKへ移行したことで、12スタックにわたる約8,000行のYAMLの保守から、どの開発者でも午後一つで読めるTypeScriptコードベースへと変わった。この転換は、チーム全体のインフラに対する考え方を変えた。

実践

前提条件とインストール

Node.js 18以上、認証情報が設定済みのAWS CLI、そして有効なAWSアカウントが必要だ。

npm install -g aws-cdk
cdk --version

アカウント/リージョンのペアごとに一度だけブートストラップを実行する——これでCDKの動作に必要なS3バケットとIAMロールが作成される:

aws configure  # アクセスキー、シークレット、デフォルトリージョンを設定
cdk bootstrap aws://YOUR_ACCOUNT_ID/ap-northeast-1

最初のCDKプロジェクトを作成する

mkdir my-infra && cd my-infra
cdk init app --language typescript

実際の作業はこの2つのファイルが担う:

my-infra/
├── bin/
│   └── my-infra.ts        # アプリのエントリーポイント — スタックをインスタンス化する
├── lib/
│   └── my-infra-stack.ts  # スタック定義 — リソースが定義される場所
├── cdk.json
└── package.json

実践的なスタックの構築:S3 + Lambda + API Gateway

lib/my-infra-stack.tsを開き、デフォルトの内容を実用的なパターンに置き換えよう:

import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { Construct } from 'constructs';

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

    // バージョニングとサーバーサイド暗号化を有効にしたS3バケット
    const bucket = new s3.Bucket(this, 'StorageBucket', {
      versioned: true,
      encryption: s3.BucketEncryption.S3_MANAGED,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // 本番環境ではRETAINに変更すること
    });

    // Lambdaハンドラー
    const handler = new lambda.Function(this, 'ApiHandler', {
      runtime: lambda.Runtime.NODEJS_20_X,
      code: lambda.Code.fromAsset('src/lambda'),
      handler: 'index.handler',
      environment: {
        BUCKET_NAME: bucket.bucketName,
      },
    });

    // 1行で適切なIAMポリシーを自動的に付与する
    bucket.grantReadWrite(handler);

    // LambdaにつないだAPI Gateway
    const api = new apigateway.RestApi(this, 'MyApi', {
      restApiName: 'マイサービスAPI',
    });
    api.root.addMethod('GET', new apigateway.LambdaIntegration(handler));

    new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
  }
}

bucket.grantReadWrite(handler)の呼び出しは実際に重要な処理をしている。適切なIAMポリシーを構築し、Lambda実行ロールに自動でアタッチするのだ。生のCloudFormationで同じことをすると15〜20行のYAMLが必要になり、バケット名やロールのARNが変わるたびに手動で更新しなければならない。

Lambdaハンドラーを作成する:

mkdir -p src/lambda
// src/lambda/index.ts
export const handler = async (event: any) => {
  return {
    statusCode: 200,
    body: JSON.stringify({
      message: 'CDKからこんにちは!',
      bucket: process.env.BUCKET_NAME,
    }),
  };
};

デプロイ前にプレビューする

闇雲にデプロイしてはいけない。まずdiffを確認しよう:

cdk diff    # リソースの変更を表示する — インフラにおけるgit diffのようなもの
cdk synth   # 生のCloudFormationテンプレートを生成して確認できる

diffに問題がなければ、デプロイしよう:

cdk deploy

CDKはIAMパーミッションの変更があれば確認を求める——意図的な安全策だ。その後、デプロイの進捗がターミナルに直接ストリーミングされる。

重複なしのマルチ環境サポート

ここでCDKが手書きテンプレートより明らかに優れている点が分かる。アプリレベルでスタックをパラメータ化しよう:

// bin/my-infra.ts
import * as cdk from 'aws-cdk-lib';
import { MyInfraStack } from '../lib/my-infra-stack';

const app = new cdk.App();
const env = app.node.tryGetContext('env') || 'dev';

new MyInfraStack(app, `MyInfra-${env}`, {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

フラグ1つでどの環境にもデプロイできる:

cdk deploy -c env=staging
cdk deploy -c env=prod

再利用可能なコンストラクト:CDKの真骨頂

複数のスタックを構築し始めると、同じパターンがあちこちに現れることに気づく。CDKではそれらをチームの標準を組み込んだ合成可能なコンストラクトに抽出できる:

// lib/constructs/monitored-lambda.ts
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import { Construct } from 'constructs';

interface MonitoredLambdaProps extends lambda.FunctionProps {}

export class MonitoredLambda extends Construct {
  public readonly function: lambda.Function;

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

    this.function = new lambda.Function(this, 'Function', {
      ...props,
      tracing: lambda.Tracing.ACTIVE, // X-Rayを常時有効化
    });

    // すべてのLambdaに自動でエラーレートアラームを設定
    new cloudwatch.Alarm(this, 'ErrorAlarm', {
      metric: this.function.metricErrors(),
      threshold: 5,
      evaluationPeriods: 1,
    });
  }
}

これ以降、チームが起動するすべてのLambdaにはデフォルトでX-RayトレーシングとCloudWatchエラーアラームが設定される。誰も忘れない。スタック間の設定のずれも発生しない。

リソースの削除

cdk destroy

CDKがプロビジョニングしたものをすべて削除する。RemovalPolicy.RETAINでマークされたリソース——たとえば本番データベース——は削除されず、警告とともにスキップされる。

CDKが有効な場合と有効でない場合

CDKがその価値を発揮するのは、インフラの複雑さが抽象化を正当化するときだ。環境をまたぐ繰り返しパターン、TypeScriptやPythonを既に知っているチーム、インフラをバージョン管理されたパッケージとして配布する必要性——このうちどれか一つでも当てはまれば傾く。三つすべて?迷う必要はない。

ほとんど変更のない少数の静的リソースには、生のCloudFormationやAWSコンソールの方が本当にシンプルだ。余計な手続きは不要。チームがTerraformに慣れているなら、そのHCL構文と豊富なモジュールレジストリの方が合うかもしれない——CDKはAWS専用なので、特にマルチクラウドの構成では。

しかし、AWSで本格的なスケールでサービスを展開しているチームにとって、CDKには積み重なる優位性がある:インフラが本物のコードベースになるのだ。aws-cdk-lib/assertionsでユニットテストを書ける。プルリクエストでインフラの変更をレビューできる。本番に当たる前にリグレッションを検出できる。

CDKを最大限に活用しているチームは、lib/をアプリケーションコードと同じように扱う——合成可能なコンストラクト、テストカバレッジ、意味のあるコミット履歴。dev、staging、prodにわたる30以上のスタックを管理するとき、その規律はすぐに報われる。新しいエンジニアが一度のセッションでアーキテクチャ全体を把握できるようになるのだ。

Share: