LESSON 40分

「TerraformのHCLも良いが、TypeScriptやPythonでインフラを書けると、条件分岐やループ、型チェック、テストがプログラミング言語のフルパワーで使える。CDKはAWSに最適化されていて、Pulumiはマルチクラウドに対応している」と佐藤CTOが説明した。

推定読了時間: 40分


AWS CDK の Construct レベル

L1 / L2 / L3 Construct

レベル名称特徴
L1CFn ResourcesCloudFormation リソースの1
CfnBucket
L2Curatedベストプラクティス付きの抽象化Bucket
L3Patterns複数リソースの組み合わせパターンLambdaRestApi
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";

// ──── L1: 低レベル(CloudFormation そのまま)────
const l1Bucket = new s3.CfnBucket(this, "L1Bucket", {
  bucketName: "my-l1-bucket",
  versioningConfiguration: {
    status: "Enabled",
  },
  bucketEncryption: {
    serverSideEncryptionConfiguration: [
      {
        serverSideEncryptionByDefault: {
          sseAlgorithm: "AES256",
        },
      },
    ],
  },
});

// ──── L2: ベストプラクティス付き抽象化 ────
const l2Bucket = new s3.Bucket(this, "L2Bucket", {
  versioned: true,
  encryption: s3.BucketEncryption.S3_MANAGED,
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,  // デフォルトで安全
  removalPolicy: cdk.RemovalPolicy.RETAIN,
  lifecycleRules: [
    {
      transitions: [
        {
          storageClass: s3.StorageClass.INTELLIGENT_TIERING,
          transitionAfter: cdk.Duration.days(90),
        },
      ],
    },
  ],
});

// ──── L3: パターン(複数リソースの組み合わせ)────
const handler = new lambda.Function(this, "ApiHandler", {
  runtime: lambda.Runtime.NODEJS_20_X,
  code: lambda.Code.fromAsset("lambda"),
  handler: "index.handler",
});

// LambdaRestApi は API Gateway + Lambda + IAM 権限を一括設定
const api = new apigateway.LambdaRestApi(this, "Api", {
  handler,
  proxy: false,
});

const items = api.root.addResource("items");
items.addMethod("GET");
items.addMethod("POST");

CDK による実践的なスタック設計

マイクロサービスの Construct

// lib/constructs/microservice.ts
import { Construct } from "constructs";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as logs from "aws-cdk-lib/aws-logs";
import * as cdk from "aws-cdk-lib";

export interface MicroserviceProps {
  serviceName: string;
  image: ecs.ContainerImage;
  cluster: ecs.ICluster;
  vpc: ec2.IVpc;
  listener: elbv2.IApplicationListener;
  path: string;
  desiredCount?: number;
  cpu?: number;
  memoryLimitMiB?: number;
  environment?: Record<string, string>;
  healthCheckPath?: string;
}

export class Microservice extends Construct {
  public readonly service: ecs.FargateService;

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

    const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef", {
      cpu: props.cpu ?? 256,
      memoryLimitMiB: props.memoryLimitMiB ?? 512,
    });

    const container = taskDefinition.addContainer("app", {
      image: props.image,
      logging: ecs.LogDrivers.awsLogs({
        streamPrefix: props.serviceName,
        logRetention: logs.RetentionDays.ONE_MONTH,
      }),
      environment: props.environment,
      healthCheck: {
        command: [
          "CMD-SHELL",
          `curl -f http://localhost:${props.cpu ?? 3000}${props.healthCheckPath ?? "/health"} || exit 1`,
        ],
        interval: cdk.Duration.seconds(30),
        timeout: cdk.Duration.seconds(5),
        retries: 3,
      },
    });

    container.addPortMappings({ containerPort: 3000 });

    this.service = new ecs.FargateService(this, "Service", {
      cluster: props.cluster,
      taskDefinition,
      desiredCount: props.desiredCount ?? 2,
      vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
      circuitBreaker: { enable: true, rollback: true },
    });

    // ALB ターゲットグループ
    const targetGroup = new elbv2.ApplicationTargetGroup(this, "TG", {
      vpc: props.vpc,
      port: 3000,
      protocol: elbv2.ApplicationProtocol.HTTP,
      targets: [this.service],
      healthCheck: {
        path: props.healthCheckPath ?? "/health",
        healthyThresholdCount: 2,
        unhealthyThresholdCount: 3,
      },
    });

    // パスベースルーティング
    new elbv2.ApplicationListenerRule(this, "Rule", {
      listener: props.listener,
      priority: this.node.addr.charCodeAt(0) % 50000 + 1,
      conditions: [elbv2.ListenerCondition.pathPatterns([`${props.path}*`])],
      targetGroups: [targetGroup],
    });

    // Auto Scaling
    const scaling = this.service.autoScaleTaskCount({
      minCapacity: props.desiredCount ?? 2,
      maxCapacity: (props.desiredCount ?? 2) * 5,
    });

    scaling.scaleOnCpuUtilization("CpuScaling", {
      targetUtilizationPercent: 70,
      scaleInCooldown: cdk.Duration.seconds(300),
    });
  }
}

スタックでの使用

// lib/app-stack.ts
import * as cdk from "aws-cdk-lib";
import * as ecs from "aws-cdk-lib/aws-ecs";
import { Microservice } from "./constructs/microservice";

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

    const vpc = new ec2.Vpc(this, "Vpc", { maxAzs: 3 });
    const cluster = new ecs.Cluster(this, "Cluster", { vpc });
    const alb = new elbv2.ApplicationLoadBalancer(this, "ALB", {
      vpc,
      internetFacing: true,
    });
    const listener = alb.addListener("Listener", { port: 443 });

    // 各サービスを統一パターンで定義
    const services = [
      { name: "user-service", path: "/api/users/", image: "user-service:latest" },
      { name: "order-service", path: "/api/orders/", image: "order-service:latest" },
      { name: "product-service", path: "/api/products/", image: "product-service:latest" },
    ];

    for (const svc of services) {
      new Microservice(this, svc.name, {
        serviceName: svc.name,
        image: ecs.ContainerImage.fromEcrRepository(
          ecr.Repository.fromRepositoryName(this, `${svc.name}-repo`, svc.image)
        ),
        cluster,
        vpc,
        listener,
        path: svc.path,
      });
    }
  }
}

Pulumi との比較

同じインフラを Pulumi で記述

// Pulumi (TypeScript)
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const environment = pulumi.getStack();  // dev, staging, prod

// VPC
const vpc = new aws.ec2.Vpc("main", {
  cidrBlock: "10.0.0.0/16",
  enableDnsHostnames: true,
  tags: { Name: `${environment}-vpc` },
});

// S3 Bucket
const bucket = new aws.s3.Bucket("data", {
  bucket: `myapp-${environment}-data`,
  versioning: { enabled: true },
  serverSideEncryptionConfiguration: {
    rule: {
      applyServerSideEncryptionByDefault: {
        sseAlgorithm: "AES256",
      },
    },
  },
});

// Lambda Function
const lambdaRole = new aws.iam.Role("lambdaRole", {
  assumeRolePolicy: JSON.stringify({
    Version: "2012-10-17",
    Statement: [{
      Action: "sts:AssumeRole",
      Effect: "Allow",
      Principal: { Service: "lambda.amazonaws.com" },
    }],
  }),
});

const fn = new aws.lambda.Function("api", {
  runtime: "nodejs20.x",
  handler: "index.handler",
  code: new pulumi.asset.AssetArchive({
    ".": new pulumi.asset.FileArchive("./lambda"),
  }),
  role: lambdaRole.arn,
  environment: {
    variables: {
      BUCKET_NAME: bucket.id,
      ENVIRONMENT: environment,
    },
  },
});

// 出力
export const bucketName = bucket.id;
export const functionArn = fn.arn;

CDK vs Pulumi 比較

項目AWS CDKPulumi
クラウド対応AWS のみAWS, GCP, Azure, K8s 等
状態管理CloudFormation StackPulumi Service / S3
言語TypeScript, Python, Java, Go, C#TypeScript, Python, Go, C#, Java
抽象化レベルL1/L2/L3 ConstructComponent Resources
テストassertions モジュールUnit テスト標準サポート
デプロイcdk deploy → CFnpulumi up → 直接API
ドリフト検出CloudFormation drift detectionpulumi refresh
学習コストAWS知識が活きるマルチクラウド知識が必要

IaC テストの基礎

CDK のスナップショットテスト

// test/app-stack.test.ts
import * as cdk from "aws-cdk-lib";
import { Template } from "aws-cdk-lib/assertions";
import { AppStack } from "../lib/app-stack";

describe("AppStack", () => {
  const app = new cdk.App();
  const stack = new AppStack(app, "TestStack", {
    env: { account: "123456789012", region: "ap-northeast-1" },
  });
  const template = Template.fromStack(stack);

  test("ECS サービスが作成される", () => {
    template.resourceCountIs("AWS::ECS::Service", 3);
  });

  test("ALB が HTTPS リスナーを持つ", () => {
    template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", {
      Port: 443,
      Protocol: "HTTPS",
    });
  });

  test("Fargate タスクに適切な CPU/メモリが設定される", () => {
    template.hasResourceProperties("AWS::ECS::TaskDefinition", {
      Cpu: "256",
      Memory: "512",
      RequiresCompatibilities: ["FARGATE"],
    });
  });

  test("CloudWatch Logs グループが設定される", () => {
    template.hasResourceProperties("AWS::Logs::LogGroup", {
      RetentionInDays: 30,
    });
  });
});

Pulumi のユニットテスト

// test/index.test.ts
import * as pulumi from "@pulumi/pulumi";
import "jest";

// Pulumi のモック
pulumi.runtime.setMocks({
  newResource: (args: pulumi.runtime.MockResourceArgs) => {
    return { id: `${args.name}-id`, state: args.inputs };
  },
  call: (args: pulumi.runtime.MockCallArgs) => {
    return args.inputs;
  },
});

describe("Infrastructure", () => {
  let infra: typeof import("../index");

  beforeAll(async () => {
    infra = await import("../index");
  });

  test("S3 bucket にバージョニングが有効", (done) => {
    pulumi.all([infra.bucketName]).apply(([name]) => {
      expect(name).toContain("myapp");
      done();
    });
  });
});

まとめ

ツール強み適用場面
Terraform (HCL)マルチクラウド、エコシステムインフラ専任チーム
AWS CDKAWS最適化、L3パターンAWS メインのアプリチーム
Pulumiプログラミング言語のフルパワーマルチクラウド+開発者主導

チェックリスト

  • CDK の L1/L2/L3 Construct の違いを説明できる
  • CDK でカスタム Construct を作成できる
  • Pulumi と CDK の違いを比較できる
  • CDK の assertions でテストを書ける
  • プロジェクトに適した IaC ツールを選定できる

次のステップへ

次のレッスンでは、IaC のテストと品質管理をさらに深掘りし、Terratest、policy as code、セキュリティスキャンを学びます。