CDK Pipelines を導入した複数人開発のススメ

みなさん、こんにちは。horsewinです。

[AWS CDK Advent Calendar 2021](https://qiita.com/advent-calendar/2021/aws-cdk) 9日目の記事となります。

CDKのCI/CD化を手助けしてくれるConstructの1つである、CDK Pipelines（[@aws-cdk/pipelines module](https://docs.aws.amazon.com/cdk/api/latest/docs/pipelines-readme.html)）に触れていきます。
複数人でCDKを利用したCI/CDを導入する上でぜひ検討の1つにしてみてください。

# 今回触れること、触れないこと
本記事では次の内容について触れていきます。

 * CDK開発にCI/CDを組み込むモチベーション
 * CDK Pipelinesとはなにか
 * CDK Pipelinesを利用する上でのTips

次の内容には触れません。ただしAWS公式のドキュメントやリンクをリファーはしますので適宜参考にしてください。

* CDK Pipelinesの作成の流れ（ハンズオンなど）
* CDK/CFnで悩むリソース参照方式（クロススタック参照をするか、ARN参照をするか等）
    * 個人的にはCDK開発で一番悩む箇所

また、すでにCDKのバージョンは`2.x.x`がStableバージョンとしてローンチされていますが、本記事では`1.x.x`系を利用しています。

# CDK開発をチームで進めるうえでのデプロイの悩み
筆者は普段、CDKやPulumiといったInfrastructure as Code(IaC)と触れ合っています。
IaCで環境を作っていく初期段階ではお一人様であることが多いです。いずれのIaCツールにおいてもCLIが用意されているため、`cdk deploy`のようなコマンドでガンガン環境にデプロイをかけていけます。

しかし、いざチーム開発となった際、複数の課題が発生します。

## デプロイの競合

各自にCDKコマンドを利用できるIAMユーザを発行し、それぞれが `cdk deploy`を実行するケースを考えます。

Aさん「S3バケットなら作れそう。名前はAWSアカウントIDを利用したらグローバル
でもかぶらないはずだし、 `my-cdk-12345689012`にしよう」
Bさん「よっしゃ。S3バケットをサクッと作ったろ。名前はAWSアカウントIDを利用したらグローバルでもかぶらんやろ。 `my-cdk-12345689012`でええかな。」

S3の場合は片方のデプロイが失敗するだけになりますが、セキュリティグループなどでAさんが適切なセキュリティグループを設定した後、Bさんがコードを引っぱってきて別の設定でセキュリティグループを作ろうとするとReplaceされてAさんの設定が消えたりもします。

このように各自がデプロイをできる状態になっている場合、高速開発をしているケースで問題が起きがちです[^1]。

[^1]: なお、Terraformといった他のIaCツールではDynamoDBとS3を使って排他ロックをかけるなどの仕組みもありますが、本筋とそれるので割愛します。

## デプロイの実行環境

次に課題になるのは「誰がどこで `cdk deploy`を動かすか」という課題です。
ソースコードをGitHubで管理している場合、GitHub SecretにIAMのアクセスキーなどを登録してGitHub Actionsを実行する方法がシンプルです。また、最近ではGitHub ActionsにIAMロールを渡すという方法もあります。

AWSのマネージドサービスである[CodeCommit](https://aws.amazon.com/codecommit/)を利用してソースコードを管理している場合、Code Buildで `cdk deploy`をしたり、はたまたEC2上で実行するケースもあるでしょう。

いずれにせよ、何かしらの実行環境を用意し、**誰かがいつか**が `cdk deploy`を実行しなければいけません。

デプロイの競合を避けるためにリーダーを決めて、デプロイ可能な人を決め、**メインとなるブランチに修正が取り込まれたタイミングで担当者が手動で実行**が最もわかりやすくシンプルです。
しかし、この担当者に負荷が集中する単一障害点[^2]になりかねません。
**競合を避けつつ、単一障害点を産まないように仕組みでカバーすべき**です。

[^2]: 1つの人やモノに仕組みが依存しているような状態。今回の例示の場合、デプロイ権限者が体調不良などで休んだ場合、だれもデプロイができないなどの状態。

# CI/CDを実現したら解決するのでは？
ここで述べた2つの課題である「デプロイの競合」と「デプロイの実行環境」は、継続的インテグレーション/継続的デリバリ（CI/CD）を実現することで解決します。

AWSでCI/CDを実現するためのサービス郡といえば、AWS Codeシリーズ[^3]です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123578/e57c5c8b-4500-c78c-6e60-1f53e8e39cff.png)

CI/CDの肝となるサービスが、**CodePipeline**です（今回紹介する、CDK Pipelinesとは別モノです）[^3]。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123578/7084bd3e-9007-0393-2e5e-ccb7701f0ca4.png)

[^3]: 引用）https://www2.slideshare.net/AmazonWebServicesJapan/20201111-aws-black-belt-online-seminar-aws-codestar-aws-codepipeline

このCI/CDのワークフローを自前で実装することで、GitHubやCodeCommitにCDKのコードがプッシュされたら、自動で処理を実行します。そして、CodePipelineで定義したアクションから `cdk deploy`が実行できます。

しかし、これを**自前で実装すること**が意外と大変です。
CDK Pipelinesは、このワークフロー生成を非常にシンプルに実現してくれます。

# CDK Pipelinesについて
ようやくタイトルの本題となっている、CDK Pipelinesのはなしです。
[CDK Pipelines](https://docs.aws.amazon.com/cdk/api/latest/docs/pipelines-readme.html)とは、CodePipelineを利用したCI/CDのワークフローを構築してくれる、CDK Constructです。2021年7月28日にGAしました[^cdk-ga]。

[^cdk-ga]: 引用）https://aws.amazon.com/about-aws/whats-new/2021/07/announcing-cdk-pipelines-ga-ci-cd-cdk-apps/

CDK PipelinesはCDKのConstructとして、次のような形でCDKアプリケーションに組み込まれます。

```ts:bin/cdk.ts
const pipeline = new CodePipeline(this, "pipeline", {
  pipelineName: "cdkCICDPipeline",
  synth: new CodeBuildStep("Synth", {
    projectName: "cdkPipelineBuild",
    input: CodePipelineSource.gitHub(
      "OWNER/REPOS",
      "main",
      {
        authentication: SecretValue.secretsManager(githubToken)
      }
    ),
    // Install dependencies, build and run cdk synth
    commands: ["npm ci", "npm run build", "npx cdk synth"],
  })
});

// 開発環境構築ステージ
const development = new DevStage(this, "Development", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION
  }
});
pipeline.addStage(development);
```

CDK Pipelinesを組み込んだ後、`cdk deploy`をするとCodePipeline上にワークフローが自動生成されます（初回のみ`cdk deploy`によってCDK Pipelinesを展開する必要があります）。

![cdkpipelines.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123578/bd60f74f-3087-540a-545c-eb2b6869c5f7.png)

## 構成要素
次に展開されたCDK Pipelinesの構成要素についてかんたんに触れます。
CDK Pipelinesは次のコンポーネントで構成されます[^cdk-blog]。
それぞれのコンポーネントはCodePipelineのステージで実現されるため、「ステージ」といったほうが馴染みがいいのですが、「Stage1」「Stage2」といった表現をされるステージがありややこしいため、本記事ではコンポーネントと呼んでいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123578/5224dd21-0623-70a7-fabc-2113e0fb5020.png)

[^cdk-blog]: 引用）https://aws.amazon.com/blogs/developer/cdk-pipelines-continuous-delivery-for-aws-cdk-applications/

### Source
こちらはイメージがしやすいコンポーネントかと思います。
CDKのソースコードを格納するGitHubやCodeCommitからコードを取得するコンポーネントです。

### Build
こちらのコンポーネントでは、CDKのソースコードを必要に応じてビルドします。また、CDKを展開する元となるCloudFormationのコードを生み出すために`cdk synth`を実行するコンポーネントとなります。

ここで生成されたCloudFormationのコードを元に、Stageコンポーネント（後述しますが、前述したキャプチャにおける"Development"ステージなど）で処理が実行されます。


### Stage
順番が前後しますが、説明の関係上先にこちらのStageコンポーネントの説明をします。
全体図のStage1/Stage2の箇所となります。
Stageコンポーネントは、CDKで作成したいAWS環境を作成するコンポーネントです。
CDK PipelinesにStageコンポーネントを追加するごとに新しいCodePipelineのステージが自動生成されます。
StageコンポーネントもCDKのモジュールとして用意されています。

```ts:Developmentステージの定義
import { Construct, Stage, StageProps } from "@aws-cdk/core";

export class DevStage extends Stage {
  constructor(scope: Construct, id: string, props?: StageProps) {
    // PREPARE
    super(scope, id, props);

    // CREATE
    const service = new SampleServiceStack(this, "APIService");
  }
}
```

作成されたステージでは、`Prepare`アクションと`Deploy`アクションが追加されます。
`Prepare`アクションでは、Buildコンポーネントで生成されたCloudFormationを元に`CHANGE_SET_REPLACE`が実行され、新しいChangeSetが生成されます。
`Deploy`アクションでは、`Prepare`アクションで生成されたChangeSetを元に`CHANGE_SET_EXECUTE`が実行され、CloudFormationの更新処理が開始されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123578/b927e135-1c45-f705-7855-80448eb6fed1.png)

また、`Prepare`と`Deploy`以外にも展開されたリソースに対してのHealcheckをするアクションを追加するなどもできます[^cdk-healthcheck]。

[^cdk-healthcheck]: 引用）https://cdkworkshop.com/20-typescript/70-advanced-topics/200-pipelines/5000-test-actions.html

### UpdatePipeline
このコンポーネントがCDK Pipelinesのコンポーネント郡の特徴と言えます。UpdatePipelineは、CDK Pipelinesが自身のパイプラインを作り変えるコンポーネントです。

たとえば、前述したTypeScriptのコードでは「開発環境構築ステージ」というStageコンポーネントを追加しています。次に、「本番環境構築ステージ」を追加したい場合、「Development」というStageコンポーネントとは別に、「Production」というStageコンポーネントを追加したいはずです。
CDK Pipelinesでは、次のようなコードを追加するだけで、これが実現できます。

```ts:Productionステージの追加
// 本番環境構築ステージ
const production = new ProdStage(this, "Production", {
  env: {
    account: process.env.CDK_PROD_ACCOUNT,
    region: process.env.CDK_PROD_REGION
  }
});
pipeline.addStage(production);
```

UpdatePipelinesステージにおいて、ワークフローが変更や新しいスタックが追加されたことを検知して、自動でパイプラインやデプロイするコードを作り変えます。そして、再度先頭から処理を実行して、パイプライン実行処理が安定するまで自身を作り変えてくれます（自分で自分を作り変えるって画期的ですよね？）。

### PublishAssets 
Stageコンポーネントで利用されるアセットがS3やECRにアップロードされるコンポーネントです。例えばLambdaを更新する際に、S3にソースコード郡をアップロードして更新する動きを代替してくれます。

## CDK Pipelinesのうれしいところ
最初にCDK Pipelinesを`cdk deploy`するだけで、自動でワークフローを生成/更新するワークフローが出来上がるところです。

CodePipelineに手動/別の処理でステージを追加し、その後にCI/CDを走らせるといったこともせず、CDKのコードとしてステージを追加するだけで自動でワークフローが作り変えられるのはやはり画期的です。

# 開発時のTips
ここまででCDK Pipelinesの概要について触れました。
次に、CDK Pipelinesを利用して実際に開発をした上で嬉しかったポイントや気をつけたポイントを5つ言及します。

* 各開発者のIAM権限を弱くできる
* `cdk diff`だけはとれる権限を用意しておく
* ブランチ保護によるパイプラインの実行制御
* Stageコンポーネントの分離粒度
* 開発速度にあわせた実行オプション

なお、冒頭で述べたとおり、本記事ではCDK Pipelinesの作成の流れ（ハンズオンなど）には触れません。作成の流れについての詳細はドキュメントを参照してください。
https://docs.aws.amazon.com/cdk/latest/guide/cdk_pipeline.html


## 各開発者のIAM権限を弱くできる
CloudFormation実行時には、`AdministratorAccess` に近い強い権限がなければリソース作成がままならないことがあります。そのため、CloudFormationは強い権限を持ったIAMユーザで実行することが数多くあります。

CDKは裏側の仕組みとして、CloudFormationが実行されています。
そのため、`cdk deploy`などのCDKコマンドを実行する際に設定するIAM権限は強権限にすることが多いです。
しかし、複数の開発者に強権限を渡すことはさけたいケースは往々にしてあります。

CDK Pipelinesを利用して、CI/CDパイプラインを整備しておくことで、この問題が解決されます。AWSサービスのリソースを変更する権限がなくとも、コードをプッシュするだけでCI/CDパイプラインが変更を検知します。そして、パイプラインの更新とStageコンポーネントの実行を代替します。

各開発者に必要となる権限は、GitHub/CodeCommitといったソースコードリポジトリにCDKのコードをプッシュする権限のみです。強権限を撒く必要はないことは大きなメリットです。

## `cdk diff`だけはとれる権限を用意しておく
さきほどの「各開発者のIAM権限を弱くできる」でIAM権限におけるメリットについて触れました。極論としては、仮にGitHubをCDKコードのリポジトリとしている場合、IAMユーザを割り当てずにCDK Pipelinesを経由してAWSサービスの構築ができます。

しかし、筆者としては、`ReadOnlyAccess`権限だけは各開発者に与えておいた方がよいと考えています。

実際にCDKを開発していくと、作成途中/作成完了時に`cdk diff`によって、どういった差分変更が発生しているかを確認するケースが多くあります。`cdk diff`は現在のローカルにあるCDKコードをベースとしたリソースの状態とAWSリソースの差分を表示してくれるコマンドです。CloudFormationのChangeSetと近い機能ですね。

```bash:CDKのdiffコマンドによる差分確認
# WAFのリソースを削除したケース
❯ cdk diff
Stack SampleCdkStack
Resources
[-] AWS::WAFv2::RuleGroup WafdevsamplecdkwafgeorulegroupE9244FEE destroy
[-] AWS::WAFv2::WebACL Wafdevsamplecdkwebacl795A4DD7 destroy
[-] AWS::WAFv2::LoggingConfiguration WafdevsamplecdkwafloggingC0D7C2D0 destroy
```

`cdk diff`コマンドによって、修正コードによって想定通りにAWSリソースが作成されるかを確認し、メインブランチへの取り込みリクエスト（Pull RequestやMerge Request）を出すという開発プロセスにすると比較的スムーズにチーム開発をすすめることができます。

あくまで現在のAWSリソースの状態が取得できればいいため、`ReadOnlyAccess`権限だけは付与したほうがよいという想いでした。


## ブランチ保護によるパイプラインの実行制御
次にCDK Pipelinesの実行トリガーのはなしとなります。
CDK Pipelinesは特定ブランチ（例えばmainブランチ）へのプッシュをトリガとしたWebhookを検知して、CI/CDパイプラインが実行されます。

mainブランチへ各開発者が自由にプッシュできる場合、**想定しない変更によってパイプラインが実行される恐れ**があります。
パイプラインを実行するタイミングを決めるために開発プロセスの元となるブランチ戦略が重要になります。
**GitHub flow**や**GitLab Flow**、**Git flow**などいくつかのブランチ戦略があります。
GitHubであればブランチ保護設定、CodeCommitであればIAMの権限設定によって、直接メインとなるブランチへのプッシュを防いでいるケースは多いと筆者は考えています。
メインとなるブランチへ修正を取り込む場合は、別ブランチからの変更取り込み（Pull Request等）で他開発者のレビュー承認を得たもののみ取り込むように保護すべきです。
これにより、予期せぬ修正によるパイプライン実行を防ぐことができます。

CDK Pipelines導入時でもこのようなソフトウェア開発のプラクティスはしっかり取り入れておくべきでしょう。

## Stageコンポーネントの分離粒度
４つ目はCDK Pipelinesの中で実際にAWSリソースを作るStageコンポーネントの粒度についてです。
CDK Pipelinesを利用しない、通常のCDKでも同様の類似した概念である`Stack`の分離粒度は非常に悩みが付きない話題です。

`Stack`はチームごとに分割、ライフサイクルごとに分割など方針は様々ですが、
個人的にはライフサイクルごとの分割が一番やりやすい方針でした（ただし依存関係の管理には注意が必要）。

一方、CDK Pipelinesの`Stage`は複数のStackを内包する形となるため、１つ上のレイヤとして管理します。AWS公式のサンプルでもStageの分離レベルは環境ごと（Development/Productionなど）でした。

筆者としても、この分離レベルで違和感がなかったので普段は環境単位でステージを分けています。
しかし、環境で作成するリソースが多くなるに連れ、悩みが出てきました。
最大の悩みとしては、最後の方でこけたときのダメージが大きいというCloudFormationあるあるネタです。

最近のCloudFormationでは、[コケた時のロールバック防止機能](https://aws.amazon.com/jp/about-aws/whats-new/2021/08/aws-cloudformation-troubleshoot-errors/)が追加されており、これを駆使してダメージを軽減することもあります。ちょうど本Advent Calendarの１日目「[CDK とCloudFormation を繋ぐ話](https://qiita.com/yktko/items/7bcc9df940194ee62d8c)」で大村さんが触れていますので気になった方はそちらも是非参照ください。

なお、CDK Pipelinesでは各ステージごとでCloudFormationが実行されるので、すでに作成済みのステージはRollbackしないため大ダメージは受けないです。
ただし、ステージ内のアクションが多段になっていくと後の方のアクションに到達するまで数十秒〜数分かかることもあります。
そのため、できるだけコケる要素をさけるために、`cdk deploy --no-execute`でChangeSetだけ作るプロセスを設けることや、 一時的に`cdk deploy`ができる強権限を払い出すなどが有効なケースがあります

これらはスピード感やIAM管理方針に合わせて実施するようにしましょう。

## 開発速度にあわせた実行オプション
最後はTipsというより注意事項に近いです。
基本的には、複数人でCDK開発をするときは、CDK PipelinesによるCI/CDパイプラインからの更新を主とすべきです。しかし、CodePipelineのキックや各ステージ間の移動等はどうしても時間がかかります。また、都度リモートブランチへプッシュやレビューが必要となり開発速度が上がらないことがあります。そして、どうしても開発速度が出なくなってきたときは直接`cdk deploy`したくなるケースが多いです。

どこまで`cdk deploy`を許すかはチームのルールや開発プロセスに合わせるべきですが、ソースコードのプッシュを伴わない更新時には注意が必要です。

具体的にはSelf-mutation（新しいステージやスタックをデプロイするように自動的にパイプラインを再構成）はOFFにしておくなどです。Self-mutationをONにした場合、`cdk deploy`を使ってパイプラインを更新後、自動的にリモートのソースコードの状態に戻ってしまいます。
CDK PipelinesのConstructに`selfMutation: false`プロパティを渡すことで、Self-mutationを一時的にオフにできます。

```diff
const pipeline = new CodePipeline(this, "pipeline", {
  pipelineName: "cdkCICDPipeline",
+ selfMutation: false,
  synth: new CodeBuildStep("Synth", {
    projectName: "cdkPipelineBuild",
    input: CodePipelineSource.gitHub(
      "OWNER/REPOS",
      "main",
      {
        authentication: SecretValue.secretsManager(githubToken)
      }
    ),
    // Install dependencies, build and run cdk synth
    commands: ["npm ci", "npm run build", "npx cdk synth"],
  })
});
```


# まとめ

- CDK PipelinesはCDKでCI/CDを組み込むのに非常に便利なConstructです
- 複数人開発時の同時更新といった悩みを防ぎつつサクッとCI/CDを導入できます
- 少しクセがあるところもありますが、ベースはCodePipelineとCloudFormationであり、ブラックボックス感がなく使うことができます

# その他
今回のIaCに関する技術書や、AWSでECS/Fargateのコンテナを構築するノウハウを凝縮した書籍も執筆しています。気になった方は是非下記のリンクよりどうぞ。

- 比べてわかる！IaCの選びかた 〜クラウドネイティブIaCストーリー〜
    - https://booth.pm/ja/items/3045918
- AWSコンテナ設計・構築[本格]入門
    - https://amzn.to/31JItpk


# CDK Pipelinesの参考URL
- @aws-cdk/pipelines module · AWS CDK
    - https://docs.aws.amazon.com/cdk/api/latest/docs/pipelines-readme.html
- CDK Pipelinesのmodern APIを使ってCDKアプリケーションをデプロイする
    - https://aws.amazon.com/jp/blogs/news/deploying-a-cdk-application-using-the-cdk-pipelines-modern-api/
- Continuous integration and delivery (CI/CD) using CDK Pipelines
    - https://docs.aws.amazon.com/cdk/latest/guide/cdk_pipeline.html