CodePipelineでCI/CDパイプラインを組もうとして苦労した話

## はじめに
今年の10月に株式会社LITALICOに入社しました@i-kです。SREチームに所属しています。
この記事は『[LITALICO Advent Calendar 2020](https://qiita.com/advent-calendar/2020/litalico)』2日目の記事です。

前職でCI/CDパイプラインを設計・構築した際にCodePipelineを使おうとして苦労した話を書いていこうと思います。

## CodePipelineとは
フルマネージドのCI/CDサービスです。
リポジトリサービスのCodeCommitやビルドサービスのCodeBuild、デプロイサービスのCodeDeployやAWS以外のCIツール等と連携してデプロイフローを構築することができます。ソースにはGitHubやCodeCommit以外にECRやS3、Bitbucketを指定できます。
AWSのサービスなので当たり前なのですが他のAWSサービスと親和性が高いです。
手動承認ステージが作れたり、CloudWatch+SNSでパイプラインの状態を通知出来たり、最近だとStepfunctionsを呼べるようになったので複雑なワークフローを組めたりもできます。
AWSコンソールから見るとこんなイメージです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283016/73d5593e-68e0-5c76-232c-93ee0123e214.png)

## 前提
元々はCircleCI＋サードパーティ製のスクリプトでビルド・テスト・デプロイ（BlueGreen）までを行う作りになっていました。しかしステージング環境でたまにターゲットがいなくなるバグがあったりロールバックが出来なかったりと色々改善点がありました。
丁度そのときプロダクトの基盤をECS（EC2）からECS（Fargate）に移行しようという時期だったので合わせてデプロイフローも見直すことにしました。
最初に挙がった要件は以下のようなものだったと思います。

- pushをデプロイのトリガーにしたい
- ビルド・テスト・デプロイまでを一連を自動化
- マネージドサービスに寄せて保守コストを減らしたい
- デプロイ後に問題があったら簡単にロールバックできるようにしたい
- ECRのlatest運用をやめてコミットIDをタグとして付与したい

設計を進める中でこのような要望も上がってきました。

- やっぱりpushではなく環境によって以下のイベントをデプロイのトリガーにしたい
    - 本番: releaseタグ
    - ステージング: stagingブランチへのpullrequest merge
    - 開発: developブランチへのpush
    - テストまで(デプロイはしない): featureブランチへのpush
- パイプラインの成功/失敗ステータスをGitHubのコミット履歴に反映したい
- ゆくゆくは他プロダクトに横展開させたい

## どうしてCodePipelineを選んだか
以下のように当初の要件は満たせていたのとマネージドサービスというキーワードに引っ張られすぎたのと当時の部署でインフラ専任は私一人で他の意見が出てくることもなかったので結構安易に選んでしまった気がします。。

- pushをデプロイのトリガーにしたい
=>CodePipelineで出来る
- ビルド・テスト・デプロイまでを一連を自動化=>CodePipeline＋CodeBuild＋CodeDeployで出来る
- マネージドサービスに寄せて保守コストを減らしたい=>全てAWSマネージドサービスになる
- デプロイ後に問題があったら簡単にロールバックできるようにしたい=>2800分以内ならCodeDeployの機能でボタン一つで出来る
- ECRのlatest運用をやめてコミットIDをタグとして付与したい=>CodeBuild内で出来る

## CodePipelineで辛かったこと
試行錯誤する中で以下のような辛い部分が出てきました。
[1. パイプラインの構成ファイルが多くなる](#1-パイプラインの構成ファイルが多くなる)
[2. プッシュ以外をトリガーにするには少し工夫が必要](#2-プッシュ以外をトリガーにするには少し工夫が必要)
[3. パイプラインの成功/失敗ステータスをGitHubのコミット履歴に反映するのにも少し工夫が必要](#3-パイプラインの成功失敗ステータスをgithubのコミット履歴に反映するのにも少し工夫が必要)
[4. 変数の受け渡しが大変](#4-変数の受け渡しが大変)
[5. パイプラインの数が増えすぎる](#5-パイプラインの数が増えすぎる)
[6. 横展開するべきでない形になった](#6-横展開するべきでない形になった)


### 1. パイプラインの構成ファイルが多くなる
まずCodePipeline＋CodeBuild＋CodeDeployを作るためにCloudFormationやTerraformが必要になります。
IAMロールやartifactを置くS3も合わせて用意する必要があります。
CodeBuildではbuildspec.yml、CodeDeployではappspec.ymlを使用してそれぞれビルド/デプロイの内容を定義するのでこちらも必須です。
詳しくは後述しますがCLIを使用しないとプッシュ以外のイベントをトリガーに出来なかったのでコードで管理出来ない部分も出てきました。
個人で作る分には着々と作り上げていく感じがむしろ楽しかったのですが複数人で管理していくことを考えるとデメリットが大きいです。

### 2. プッシュ以外をトリガーにするには少し工夫が必要
CodePipelineでプッシュ以外をトリガーにしたい場合はCLIによるパイプラインの`filters`という設定の変更とGitHubのリポジトリのWebhookの設定の変更が必要でした。webhookはパイプラインを作ると自動的に登録されます。ここではreleaseタグをトリガーにする設定例を書きます。

- GitHub側の設定
リポジトリの`Settings`=>`Webhooks`=>`対象のWebhook`と進み、Webhookの編集画面から`Let me select individual events`を選択するとWebhookのトリガーとなるイベントを選択できます。この中の`Releases`にチェックを入れるとreleaseタグをトリガーに出来ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283016/d44e55cc-ab6b-5802-b310-a4a68724be23.png)

- CodePipeline側の設定
`filters`で飛んできたWebhookをフィルターしているのでこれを変える必要があります。
最初に以下のコマンドで現在のパイプラインのfilter設定を取得します。

```
$ aws codepipeline list-webhooks --max-items 1
```

このような結果が返ってくると思います。

```result.json
{
    "webhooks": [
        {
            "url": "https://ap-northeast-1.webhooks.aws/trigger?xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
            "definition": {
                "authenticationConfiguration": {
                    "SecretToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
                },
                "name": "ishiitest--Source--kishii4726xxxxxxxx",
                "authentication": "GITHUB_HMAC",
                "targetPipeline": "ishii-test",
                "targetAction": "Source",
                "filters": [
                    {
                        "jsonPath": "$.ref",
                        "matchEquals": "refs/heads/{Branch}"
                    }
                ]
            },
            "lastTriggered": 1578623394.926,
            "arn": "arn:aws:codepipeline:ap-northeast-1:AccountId:webhook:ishiitest--Source--kishii4726xxxxxxxx",
            "tags": []
        }
    ],
    "NextToken": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

これをもとに設定用のjsonファイルを作成します。

```modify_filters.json
{"webhook":
    {"name": "ishiitest--Source--kishii4726xxxxxxxx",
    "targetPipeline": "ishii-test",
    "targetAction": "Source",
    "filters": [
        {
        "jsonPath": "$.action",
        "matchEquals": "released"
        },
        {
        "jsonPath": "$.release.target_commitish",
        "matchEquals": "{Branch}"
        }
    ],
    "authentication": "GITHUB_HMAC",
    "authenticationConfiguration": {"SecretToken":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}
    }
}
```
最後に作成したファイルを使用して`filters`の設定を更新します。

```
aws codepipeline put-webhook --cli-input-json file://modify_filters.json --region "ap-northeast-1"
```


### 3. パイプラインの成功/失敗ステータスをGitHubのコミット履歴に反映するのにも少し工夫が必要
コミット履歴のパイプラインの成功/失敗ステータス反映はCodePipelineでは行えないのでLambdaを使用する必要があります。
![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283016/54786087-0705-cab2-881f-23d6e54bfe39.png)

検証には[こちら](https://github.com/jenseickmeyer/github-commit-status-bot)を使わせてもらいました。


### 4. 変数の受け渡しが大変
事前に定義が必要だったりステージによって指定の仕方が変わったりどこでどうすれば変数を呼び出せるのかに苦労した記憶があります。
CodeBuildはCodeBuild単体で使う場合とCodePipelineの中で使う場合で変数の指定の仕方が変わったりもします。

### 5. パイプラインの数が増えすぎる
構築予定のシステムには3つのサービス（リポジトリ）が存在しました。
各サービスごとに本番/ステージング/開発へのデプロイパイプライン＋テスト用パイプラインが必要です。
ですので3 * 4 = 12のパイプラインが出来上がることになります。
CodeBuildで条件分岐させれば3つのパイプラインでいけるかなとぼんやり考えてたのですが、詰めていくと分岐処理を作ることや
そもそもCodePipelineで複数ブランチをソースとすることもそれなりに手間がかかりそう(filtersをいじれば出来るかも？)でした。
1~4の検証でパイプライン1つでも中々複雑になることが分かっていたので、このあたりでいよいよ厳しいなという雰囲気になってきました。

### 6. 横展開するべきでない形になった
共通化出来る部分は共通化して構築/管理コストを減らすための横展開なのにそこが達成出来ないので。

## 作りながら思ったこと
大体頑張れば出来そうだったのですがCircleCIかGitHubActionsなら頑張らなくても解決出来るのにという気持ちが日に日に大きくなりました。
ざっくり表にまとめました。

| 課題 | CodePipeline | CircleCI | GitHubActions |
| ---- | ---- | ---- | ---- |
| パイプラインの構成ファイルが増える | Cfnテンプレート等<br>appspec.yml<br>buildspec.yml<br>CLI | .circleci/config.yml | .github/workflows/*.yml |
| プッシュ以外をトリガーにしたい | CLIとGitHubから設定 | .circleci/config.ymlで定義 | .github/workflows/*.ymlで定義 |
| パイプラインの成功/失敗ステータスをGitHubのコミット履歴に反映したい | Lambda | 自動 | 自動 |
| 変数の受け渡しが大変 | 事前定義が必要な場合がある。<br>ステージによって呼び出し方が変わる。<br>自分で設定する場合はステージごとに定義が必要 | デフォルトの環境変数である程度拾える。<br>自分で設定する場合はプロジェクト・ジョブなどのレベルで設定可能 | デフォルトの環境変数である程度拾える。<br>自分で設定する場合は.github/workflows/*.ymlで定義する |
| パイプラインの数が増えて管理が煩雑になりすぎる | サービスの数×ブランチの数=パイプラインの数 | サービスの数=パイプラインの数に出来る | サービスの数×ブランチの数=パイプラインの数(≒ワークフローファイルの数) |
| 横展開 | AWS環境構築<br>GitHubWebHook設定<br> | 定義ファイル<br>UIからGitHub連携 | 定義ファイルのみ |

## ボツ案
CodePipelineをやめてCodeBuild＋CodeDeployにしようかと考えたりもしました。
ソースを取得するときに.gitをcloneするかしないかの違いだったと思うのですがCodeBuildの方がCodePipelineよりソースの指定や変数周りで融通が効きます。
ただCodeBuild＋CodeDeployにしたとしても構成ファイルの多さなど課題は残るのでボツになりました。

## 結果とその後
他の業務の合間を縫いながら2週間くらい悪戦苦闘したあと、一緒に作ってくれていた開発の方の**CircleCIにしませんか、、？**の一言でCodePipeline/CodeBuildは諦めてCircleCI＋CodeDeployにすることにしました。※当時はまだGitHuActionsは正式リリースされていなかったので選択肢になかったです
引き際が分からなくなってたのですごいありがたかったです。
CircleCIにすると決めてからは1日ちょっとでCI/CDフローが完成しました。StepFunctionsを使ってDBマイグレーション等の処理をいい感じにフローの中に組み込めたりも出来ました。
GitHubActionsが正式リリースされてからはGitHubActionsを使う機会も増えました。

## さいごに
最初のサービス選定から多々反省点はありますが、ブランチ運用やパイプラインの構成など1からCI/CDパイプラインを設計・構築出来たことはすごく良い経験になりました。
今回の話とはあんまり関係ないですがStepFunctionsはたくさん使い道がありそうなので使いこなしていきたいです。

明日は @tom-ock さんの記事です！

## 参考
- Codepipeline Variables
    - https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-variables.html
- CodeBuild Environment variables in build environments
    - https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html
- GitHub Releases API
    - https://developer.github.com/v3/repos/releases/
- CircleCI Using Environment Variables
    - https://circleci.com/docs/2.0/env-vars/#adding-environment-variables-to-a-job