AWS CDKを書く！（更新中）

# はじめに

- AWS CDK勉強中の私です。
- 理解のために、手習としてCDKを書いてみます。
- 書くことにとよって要素を管理しやすいファイル配置を考えたいと思います。

# サンプル

以下の構成のCDKを書いてみます。

- 1.CodeCommitリポジトリ：ソースコードの保存場所
- 2.CodeArtifactドメインとリポジトリ：プライベートな成果物リポジトリ
- 3.CodeBuildプロジェクト：ビルド、テスト、パッケージングのプロセス
- 4.CodeDeployアプリケーションとデプロイメントグループ：デプロイ管理
- 5.CodePipeline：全体のCI/CDプロセスの調整

デプロイは各ステージ（dev,prod）へのデプロイを含みます。

記述は、TypeScriptで行います。テストコードは今回は割愛します。

## 全体イメージ

```mermaid

graph TD
    A[Developer] -->|Push Code| B[CodeCommit Repository]
    B --> C[CodePipeline]
    C -->|Source Stage| D[Source Artifact]
    D --> E[Build Stage]
    E -->|CodeBuild Project| F[Build Artifact]
    F --> G[Dev Deploy Stage]
    F --> H[Prod Deploy Stage]
    G -->|CodeDeploy| I[Dev EC2 Instances]
    H -->|CodeDeploy| J[Prod EC2 Instances]
    
    K[CodeArtifact Domain] --> L[CodeArtifact Repository]
    E -.->|Fetch Dependencies| L
    
    M[IAM Roles and Policies] -.->|Permissions| C
    M -.->|Permissions| E
    M -.->|Permissions| G
    M -.->|Permissions| H
    M -.->|Permissions| I
    M -.->|Permissions| J
    
    subgraph "CI/CD Pipeline"
    C
    D
    E
    F
    G
    H
    end
    
    subgraph "Artifact Management"
    K
    L
    end
    
    subgraph "Dev Environment"
    I
    end
    
    subgraph "Prod Environment"
    J
    end
    
    classDef aws fill:#FF9900,stroke:#232F3E,stroke-width:2px,color:white;
    class B,C,E,G,H,I,J,K,L,M aws;
    
```

## ディレクトリ構造

管理しやすいファイル配置を考えます。
考慮する点は以下です。

- スタック定義を、ある程度のかたまりにまとめる
    - 今回は、CI/CDパイプラインスタックとしてまとめている

- 環境ごとの設定ファイルは、config/ に外出しする
    - 例. 使用するEC2インスタンスタイプなど


```
my-cdk-project/
│
├── bin/
│   └── my-cdk-project.ts       # エントリーポイント：アプリケーションとスタックの定義
│
├── lib/
│   ├── constructs/             # カスタムコンストラクト
│   │   ├── database-construct.ts
│   │   └── web-server-construct.ts
│   │
│   ├── stacks/                 # スタック定義
│   │   ├── database-stack.ts
│   │   ├── web-server-stack.ts
│   │   └── cicd-pipeline-stack.ts  # CI/CDパイプラインスタック
│   │
│   └── my-cdk-project-stack.ts # メインスタック（他のスタックを組み合わせる）
│
├── config/                     # 設定ファイルディレクトリ
│   ├── dev.ts                  # 開発環境の設定
│   └── prod.ts                 # 本番環境の設定
│
├── test/
│   └── my-cdk-project.test.ts  # テストファイル
│
├── assets/                     # プロジェクトアセット（Lambda関数、設定ファイルなど）
│   ├── lambda/
│   │   └── hello-world.js
│   └── configs/
│       └── app-config.json
│
├── cdk.json                    # CDK設定ファイル
├── package.json                # プロジェクト依存関係とスクリプト
├── tsconfig.json               # TypeScript設定
├── .gitignore
└── README.md
```

### cdk.json ファイル
プロジェクトの設定を一元管理する役割。CDKの動作をプロジェクトの要件に合わせてカスタマイズし、開発プロセスを効率化することができます。異なる環境（開発、ステージング、本番など）に対する設定を管理するのにも役立ちます。

```json:cdk.json

{
  // アプリケーションのエントリーポイントを指定
  // TypeScriptを使用する場合、ts-nodeを通じてエントリーポイントファイルを実行
  "app": "npx ts-node --prefer-ts-exts bin/my-cdk-project.ts",

  // ファイル変更の監視設定（cdk watchコマンド用）
  "watch": {
    // 監視対象のファイルパターン
    "include": [
      "**"
    ],
    // 監視から除外するファイルパターン
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "tsconfig.json",
      "package*.json",
      "yarn.lock",
      "node_modules",
      "test"
    ]
  },

  // プロジェクト全体で使用される設定値
  "context": {
    // AWS CDKの特定の機能のフラグ
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:stackRelativeExports": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-lambda:recognizeVersionProps": true,
    "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,

    // 環境ごとの設定
    "dev": {
      "account": "123456789012",
      "region": "us-west-2"
    },
    "prod": {
      "account": "987654321098",
      "region": "us-east-1"
    }
  },

  // オプトインが必要な特定のCDK機能を有効化
  "requiresOptIn": [
    "@aws-cdk/aws-apigateway:disableCloudWatchRole"
  ],

  // スタック合成プロセスのカスタマイズ設定
  "synthesizer": {
    // CDKブートストラップスタックのバージョンを格納するSSMパラメータ
    "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
  }
}
```

###  bin/my-cdk-project.ts

```typescript

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { CicdPipelineStack } from '../lib/stacks/cicd-pipeline-stack';
import { devConfig } from '../config/dev';
import { prodConfig } from '../config/prod';

// CDKアプリケーションを初期化
const app = new cdk.App();

// 開発環境用のCICD パイプラインスタックを作成
// 'DevCicdPipelineStack' という名前で、環境を 'dev' に設定し、開発用の設定を適用
new CicdPipelineStack(app, 'DevCicdPipelineStack', { 
  environment: 'dev', 
  config: devConfig 
});

// 本番環境用のCICD パイプラインスタックを作成
// 'ProdCicdPipelineStack' という名前で、環境を 'prod' に設定し、本番用の設定を適用
new CicdPipelineStack(app, 'ProdCicdPipelineStack', { 
  environment: 'prod', 
  config: prodConfig 
});

// アプリケーションを合成（CloudFormationテンプレートを生成）
app.synth();
```

### config/dev.ts

```typescript

import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { EnvironmentConfig } from '../lib/types';

// 開発環境の設定
export const devConfig: EnvironmentConfig = {
  // デプロイ対象のGitブランチ名
  branchName: 'develop',
  
  // CodeDeployのデプロイ設定
  // 開発環境では全てのインスタンスを一度にデプロイ
  deploymentConfig: codedeploy.ServerDeploymentConfig.ALL_AT_ONCE,
  
  // EC2インスタンスタイプ
  // 開発環境では小さめのインスタンスを使用
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
  
  // その他の開発環境固有の設定をここに追加
};
```

## stack/

```typescript

import * as cdk from 'aws-cdk-lib';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as codeartifact from 'aws-cdk-lib/aws-codeartifact';
import * as codedeploy from 'aws-cdk-lib/aws-codedeploy';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

// 環境ごとの設定を定義するインターフェース
interface EnvironmentConfig {
  readonly branchName: string;
  readonly deploymentConfig: codedeploy.ServerDeploymentConfig;
  readonly instanceType: ec2.InstanceType;
}

// スタックのプロパティを定義するインターフェース
interface CicdPipelineStackProps extends cdk.StackProps {
  readonly environment: string;
  readonly config: EnvironmentConfig;
}

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

    const { environment, config } = props;

    // CodeCommit リポジトリ
    // アプリケーションのソースコードを格納するリポジトリを作成
    const repo = new codecommit.Repository(this, 'CodeCommitRepo', {
      repositoryName: `my-app-repo-${environment}`,
      description: `Repository for my application (${environment})`,
    });

    // CodeArtifact ドメインとリポジトリ
    // プライベートな成果物リポジトリを提供
    const artifactDomain = new codeartifact.CfnDomain(this, 'ArtifactDomain', {
      domainName: `my-artifact-domain-${environment}`,
    });

    const artifactRepo = new codeartifact.CfnRepository(this, 'ArtifactRepo', {
      repositoryName: `my-artifact-repo-${environment}`,
      domainName: artifactDomain.domainName,
    });

    // CodeBuild プロジェクト
    // ソースコードのビルド、テスト、パッケージングを行う
    const buildProject = new codebuild.PipelineProject(this, 'BuildProject', {
      projectName: `my-app-build-${environment}`,
      environment: {
        buildImage: codebuild.LinuxBuildImage.AMAZON_LINUX_2_4,
      },
      environmentVariables: {
        CODEARTIFACT_DOMAIN: { value: artifactDomain.domainName },
        CODEARTIFACT_REPO: { value: artifactRepo.repositoryName },
        ENVIRONMENT: { value: environment },
      },
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        phases: {
          install: {
            commands: [
              // CodeArtifact にログインして npm の設定を行う
              'aws codeartifact login --tool npm --domain $CODEARTIFACT_DOMAIN --repository $CODEARTIFACT_REPO',
            ],
          },
          build: {
            commands: [
              'npm ci', 
              'npm run build',
              'npm test',
            ],
          },
        },
        artifacts: {
          'base-directory': 'dist', // 成果物のベースディレクトリを 'dist' に設定
          files: ['**/*'],
        },
        cache: {
          paths: ['/root/.npm/**/*'], // npm キャッシュを保存してビルド時間を短縮
        },
      }),
    });

    // CodeDeploy アプリケーションとデプロイメントグループ
    // アプリケーションのデプロイを管理する
    const application = new codedeploy.ServerApplication(this, 'CodeDeployApplication', {
      applicationName: `my-app-${environment}`,
    });

    const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'CodeDeployDeploymentGroup', {
      application,
      deploymentGroupName: `my-app-deployment-group-${environment}`,
      installAgent: true,
      ec2InstanceTags: new codedeploy.InstanceTagSet({
        'Environment': [environment],
      }),
      deploymentConfig: config.deploymentConfig, // 環境ごとの設定を使用
    });

    // CodePipeline
    // 全体の CI/CD プロセスを調整する
    const pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
      pipelineName: `my-app-pipeline-${environment}`,
    });

    // ソースステージ
    // CodeCommit リポジトリからソースコードを取得する
    const sourceOutput = new codepipeline.Artifact();
    const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
      actionName: 'CodeCommit',
      repository: repo,
      output: sourceOutput,
      branch: config.branchName, // 環境ごとの設定を使用
    });
    pipeline.addStage({
      stageName: 'Source',
      actions: [sourceAction],
    });

    // ビルドステージ
    // CodeBuild プロジェクトを使用してアプリケーションをビルドする
    const buildOutput = new codepipeline.Artifact();
    const buildAction = new codepipeline_actions.CodeBuildAction({
      actionName: 'CodeBuild',
      project: buildProject,
      input: sourceOutput,
      outputs: [buildOutput],
    });
    pipeline.addStage({
      stageName: 'Build',
      actions: [buildAction],
    });

    // デプロイステージ
    // CodeDeploy を使用してアプリケーションをデプロイする
    const deployAction = new codepipeline_actions.CodeDeployServerDeployAction({
      actionName: 'CodeDeploy',
      input: buildOutput,
      deploymentGroup,
    });
    pipeline.addStage({
      stageName: 'Deploy',
      actions: [deployAction],
    });

    // 必要な権限を付与する
    artifactRepo.addDependency(artifactDomain);
    buildProject.addToRolePolicy(new iam.PolicyStatement({
      actions: ['codeartifact:GetAuthorizationToken', 'codeartifact:GetRepositoryEndpoint', 'codeartifact:ReadFromRepository'],
      resources: ['*'],
    }));

    // CodeCommit リポジトリのクローン URL を出力として提供する
    new cdk.CfnOutput(this, 'RepositoryCloneUrlHttp', {
      value: repo.repositoryCloneUrlHttp,
      description: `CodeCommit Repository Clone URL (HTTPS) for ${environment}`,
    });
  }
}

```


# コードレビュー観点

AWS CDK/CloudFormation コードレビューチェックリストを以下に示します。

## 1. セキュリティ

- [ ] IAMロールは最小権限の原則に従っているか
- [ ] セキュリティグループのインバウンド/アウトバウンドルールは適切に制限されているか
- [ ] 機密情報（パスワード、APIキーなど）がハードコードされていないか
- [ ] 保存データの暗号化が適用されているか（例：S3バケット、RDSインスタンス）
- [ ] 転送中のデータの暗号化が設定されているか（例：HTTPS、VPN）
- [ ] パブリックアクセス可能なリソースは意図的かつ必要最小限か

## 2. 可読性とメンテナンス性

- [ ] リソース名、変数名は明確で一貫性があるか
- [ ] 複雑な構成やロジックに適切なコメントが付けられているか
- [ ] コードは論理的に構造化され、適切に分割されているか
- [ ] 再利用可能なコンポーネントやモジュールが適切に作成されているか
- [ ] プロジェクトのREADMEファイルは十分な情報を提供しているか

## 3. ベストプラクティスの遵守

- [ ] AWS Well-Architected Frameworkの5つの柱が考慮されているか
- [ ] CDKの場合、適切なレベルのコンストラクト（L2推奨）が使用されているか
- [ ] リソースのプロビジョニングに一貫性のあるアプローチが取られているか
- [ ] 環境間（開発、ステージング、本番）で再利用可能な設計になっているか

## 4. パフォーマンスとスケーラビリティ

- [ ] リソース（EC2インスタンス、RDSなど）は適切にサイジングされているか
- [ ] オートスケーリングが必要に応じて設定されているか
- [ ] パフォーマンスを考慮したアーキテクチャ（キャッシュ、CDNなど）が採用されているか
- [ ] 大規模なデータ処理や高トラフィックに対応できる設計になっているか

## 5. コスト最適化

- [ ] コスト効率の良いインスタンスタイプやストレージクラスが選択されているか
- [ ] リザーブドインスタンスやSavings Plansの使用が検討されているか
- [ ] 不要なリソースを自動的に停止または削除する仕組みがあるか
- [ ] データ転送コストを最小限に抑える設計になっているか

## 6. 耐障害性と可用性

- [ ] クリティカルなリソースはマルチAZ構成になっているか
- [ ] バックアップと復旧のメカニズムが実装されているか
- [ ] 適切なヘルスチェックとモニタリングが設定されているか
- [ ] 障害時の自動復旧メカニズムが考慮されているか

## 7. コンプライアンスと監査

- [ ] 必要なタグ（コスト配分、所有者、環境など）が全リソースに適用されているか
- [ ] 業界固有のコンプライアンス要件（GDPR、HIPAA等）が満たされているか
- [ ] ログ記録とモニタリングが適切に設定されているか
- [ ] リソースの変更履歴を追跡する仕組みがあるか

## 8. テストと検証

- [ ] CDKの場合、ユニットテストが実装されているか
- [ ] スタック間の依存関係が適切に管理されているか
- [ ] デプロイ前の静的解析やバリデーションが行われているか
- [ ] テスト環境でのデプロイテストが計画されているか

## 9. バージョン管理とデプロイメント戦略

- [ ] コードは適切にバージョン管理されているか
- [ ] 環境ごとの設定の違いが適切に管理されているか
- [ ] ブルー/グリーンデプロイメントなどの無停止更新戦略が検討されているか
- [ ] ロールバック手順が定義されているか

## 10. リソース間の整合性

- [ ] リソース間の依存関係が適切に定義されているか
- [ ] クロススタック参照が正しく使用されているか
- [ ] 循環参照が無いことを確認したか

## 11. CDK固有の考慮事項

- [ ] 最新の安定版CDKバージョンが使用されているか
- [ ] カスタムコンストラクトは適切に実装され、テストされているか
- [ ] CDKシンセサイズの出力を確認し、意図したCloudFormationテンプレートが生成されているか
- [ ] アセットのパッケージングと配布が適切に行われているか
- [ ] `cdk.json`ファイルが適切に設定されているか
- [ ] コンテキスト値が適切に使用されているか
- [ ] スタック間の依存関係が正しく定義されているか
- [ ] 環境に応じた条件付きロジックが適切に実装されているか
- [ ] CDKのアスペクトを使用してクロスカッティングの懸念事項に対処しているか
- [ ] `cdk diff`コマンドを使用して変更内容を確認しているか
- [ ] カスタムリソースが必要に応じて適切に実装されているか
- [ ] CDKパイプラインを使用している場合、適切に設定されているか
- [ ] プロップの型安全性が確保されているか
- [ ] 削除保護やスタック更新ポリシーが適切に設定されているか
- [ ] ECRイメージやLambdaコードなどのアセットが適切に管理されているか

## 12. CloudFormation固有の考慮事項

- [ ] テンプレートは論理的に構造化され、読みやすくなっているか
- [ ] パラメータとマッピングが適切に使用されているか
- [ ] 条件付きリソース作成が効果的に実装されているか
- [ ] スタックの出力が適切に定義されているか
- [ ] テンプレートのフォーマットが一貫しているか（JSONまたはYAML）
- [ ] 重要なリソースに対してDeletionPolicy属性が設定されているか
- [ ] UpdateReplacePolicy属性が適切に使用されているか
- [ ] ネストされたスタックが効果的に使用されているか
- [ ] カスタムリソースが必要に応じて適切に実装されているか
- [ ] テンプレートの各セクション（Parameters, Resources, Outputs等）が論理的に整理されているか
- [ ] 疑似パラメータ（例：AWS::Region, AWS::StackName）が適切に使用されているか
- [ ] 組み込み関数（Fn::Join, Fn::Sub等）が効果的に使用されているか
- [ ] テンプレートのバージョン管理が行われているか
- [ ] スタックポリシーが適切に定義されているか
- [ ] ChangeSetを使用して変更内容を確認しているか
- [ ] テンプレートの再利用性と保守性が考慮されているか


## 用語まとめ

| 用語 | 説明 |
|------|------|
| CDK (Cloud Development Kit) | AWSリソースをプログラミング言語で定義するためのフレームワーク |
| CloudFormation | AWSリソースを宣言的に定義するためのサービス |
| コンストラクト | CDKにおける再利用可能なコンポーネント |
| L1コンストラクト | CloudFormationリソースを直接表すCDKの低レベルコンストラクト |
| L2コンストラクト | デフォルト値とベストプラクティスが組み込まれたCDKの中レベルコンストラクト |
| L3コンストラクト | 複数のAWSサービスを組み合わせた高レベルのパターンを表すCDKコンストラクト |
| スタック | デプロイ可能な最小単位のリソース集合 |
| シンセサイズ | CDKコードからCloudFormationテンプレートを生成するプロセス |
| アセット | CDKプロジェクトに含まれるファイル（Lambdaコード、Dockerイメージなど） |
| cdk.json | CDKアプリケーションの設定ファイル |
| コンテキスト値 | CDKアプリケーションで使用される環境依存の値 |
| アスペクト | 複数のコンストラクトに横断的な変更を適用するための機能 |
| プロップ | コンストラクトの設定オプション |
| DeletionPolicy | リソースの削除動作を制御するCloudFormation属性 |
| UpdateReplacePolicy | リソースの置換動作を制御するCloudFormation属性 |
| ネストされたスタック | 他のスタック内で使用される再利用可能なスタック |
| カスタムリソース | CloudFormationでネイティブにサポートされていないリソースを作成する機能 |
| 疑似パラメータ | CloudFormationが提供する事前定義されたパラメータ |
| 組み込み関数 | CloudFormationテンプレート内で使用できる特殊な関数 |
| ChangeSet | CloudFormationスタックの変更内容をプレビューする機能 |
| スタックポリシー | CloudFormationスタック内のリソース更新を制御するポリシー |
| cdk diff | 既存のスタックとCDKコードの差分を表示するCDKコマンド |
| CDKパイプライン | CDKを使用したCI/CDパイプラインの自動化機能 |