【AWS】 (初心者向け) CodePipelineを使ったECSコンテナのデプロイ自動化 その2(CodeCommit, CodeBuild, CodePipeline)

# 概要
CodePipelineでコンテナデプロイを実行するCICD構築をまとめました。
本記事は[【AWS】 (初心者向け) CodePipelineを使ったECSコンテナのデプロイ自動化 その1(ECR, ECS Fargate)](https://qiita.com/ramunauna/items/6417925934441b9fdc78)の続きです。

実行されるパイプラインの流れは下記です。
1. CodeCommitにプッシュ
1. 特定のブランチへのプッシュをトリガーにCodePipelineを起動
1. CodeBuildにてビルドを開始
1. ECRリポジトリにイメージをプッシュ
1. ECS Fargateにデプロイ

コンテナはFlaskを実行するコンテナを構築します。
CICDに焦点をあてるため、簡単なソース更新にてデプロイ実行を確認します。

# 目次
記事を3つに分割しました。
1. コンテナ構築 / 起動
    1-1. Flaskアプリケーションの作成
    1-2. ローカル環境でコンテナの作成 / 起動確認
    1-3. ECRへイメージをプッシュ
    1-4. ECSでコンテナ起動

1. CICDパイプラインの作成 __←本記事はここから__
    2-1. CodeCommitの作成
    2-2. CodeBuildの作成
    2-3. CodePipelineの作成　__←ここまで__

1. 上記のコンテナ / CICDパイプライン構築をコード化
    3-1. CloudFormationテンプレート

前の記事はこちら
[【AWS】 (初心者向け) CodePipelineを使ったECSコンテナのデプロイ自動化 その1(ECR, ECS Fargate)](https://qiita.com/ramunauna/items/6417925934441b9fdc78)
後続の記事はこちら
[【AWS】 (中級者向け) CodePipelineを使ったECSコンテナのデプロイ自動化 その3(CloudFormationでインフラコード化)](https://qiita.com/ramunauna/items/f52cdcaeadedf40e5d22)

# 構成図
最終的な構成はこちら
![aws構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/f4547411-56f7-7d9e-eaca-0422e14b7d9b.png)

# 2. CICDパイプラインの作成
### 2-1. CodeCommitの作成
パイプライン起動のトリガーとするCodeCommitを作成します。
AWSマネジメントコンソールにてCodeCommit管理画面でリポジトリを作成してローカル環境にクローンします。
今回は`develop`という名前のブランチへのプッシュをトリガーにパイプラインを実行するよう設定します。
```bash
git checkout -b develop
git add -A
git commit -m "develop branch"
git push -u origin develop
```

### 2-2. CodeBuildの作成
続いてCodeBuildを作成します。
まずAWS Secret ManagerというAWSサービスを使ってDockerHubの認証情報を保存しておきましょう。

本題から逸れるため、ここでは詳細まで記載しませんが理由は下記です。
参考にどうぞ。
<details>
<summary>DockerHubの認証情報が必要な理由</summary>
<div>

DockerHubは匿名ユーザーでのImageのダウンロード回数に、IPアドレスを基準とした制限を設けています。
匿名ユーザーはCodeBuildのグローバルIPを共有しているため、ダウンロード制限に引っかかってしまいビルド時に`Too Many Requests`エラーが発生することがあります。

参考記事: 
①AWS公式より一次情報
[AWS CodeBuild で Docker イメージを使用する際に発生する「イメージ設定のプルエラー: toomanyrequests」というエラーを解決するにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/codebuild-docker-pull-image-error/)
②Classmethodさんより
[“Too Many Requests.” でビルドが失敗する…。AWS CodeBuild で IP ガチャを回避するために Docker Hub ログインしよう！という話](https://dev.classmethod.jp/articles/codebuild-has-to-use-dockerhub-login-to-avoid-ip-gacha/)
</div>
</details>

#### 2-2-1. Secret ManagerにDockerHub認証情報の保存
AWSマネジメントコンソールにてSecretManager管理画面で`新しいシークレットを保存する`を選択します。
シークレットのタイプで`その他のシークレットのタイプ`を選択して、キー/値のペアにDockerHubのユーザー名、パスワードを保存します。
![secretmanager_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/2d073c39-be19-9c24-235f-a9a6bff09816.png)

#### 2-2-2. buildspec.ymlの作成
それでは前回までの作業フォルダに`buildspec.yml`というビルド時に使用する設定ファイルを作成します。
```yml:buildspec.yml
version: 0.2

env:
  secrets-manager:
    DOCKERHUB_USER: arn:aws:secretsmanager:$AWS_DEFAULT_REGION:$AWS_ACCOUNT_ID:secret:${作成したシークレット名}:username
    DOCKERHUB_PASS: arn:aws:secretsmanager:$AWS_DEFAULT_REGION:$AWS_ACCOUNT_ID:secret:${作成したシークレット名}:password
    # ${作成したシークレット名}は各自の環境のものに置き換え

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION) # ECRへログイン
      - echo Logging in to Docker Hub...
      - echo $DOCKERHUB_PASS | docker login -u $DOCKERHUB_USER --password-stdin # DockerHubへログイン
      - IMAGE_TAG=$CODEBUILD_RESOLVED_SOURCE_VERSION
  build:
    commands:
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG . # イメージの作成とタグ付け
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG # ECRへプッシュ
      - printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG  > imagedefinitions.json

artifacts:
  files:
    - imagedefinitions.json
```
buildspec.ymlを追加したこの時点で一度developブランチへプッシュしておきます。

#### 2-2-3. ビルドプロジェクトの作成
AWSマネジメントコンソールにてCodeBuild管理画面でサイドメニューの`ビルドプロジェクト`のページに移動します。
`ビルドプロジェクトを作成する`から構築を進めます。

ソースに先ほど作成したCodeCommitを指定します。
![codebuild_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/ed99b881-66e6-6f68-47f9-568e74be00c9.png)

環境設定ではdockerの`特権付与`にチェックを入れることに注意しましょう。
サービスロールは新規作成で今回は進めます。
![codebuild_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/bce6f64f-0982-65e6-eca0-6086592fe929.png)
`▼追加設定`を開き、buildspec.ymlに記載した環境変数をここで設定します。
![codebuild_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/6f1bd96f-f43a-fbc6-1428-cc15f5889d56.png)
下記の値を設定します。
- AWS_ACCOUNT_ID: アカウントID
- AWS_DEFAULT_REGION: デフォルトのリージョン名(ap-northeast-1)
- IMAGE_REPO_NAME: イメージのリポジトリ名(ECRで作成したリポジトリ名)
- CONTAINER_NAME: コンテナ名(ECSタスク定義の作成時に`コンテナの定義`で追加したコンテナ名)

Buildspecの項目ではデフォルトでルートディレクトリのbuildspec.ymlが使用されます。
buildspec.ymlではないファイル名やルートディレクトリでない場所を使用している場合は、ファイル名を入力しましょう。
![codebuild_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/51f04c86-4a7d-37c0-e1b3-916194a7148a.png)

ログの項目では`CloudWatchLogs`をチェックしておきましょう。
CloudWatchのロググループが作成されビルドログを確認することができます。

ビルドプロジェクトが完成しましたが、このままではCodeBuildからSecret ManagerとECRへのアクセス権限エラーが発生するため権限を追加します。
サービスロールにアタッチされているポリシーに下記を追記します。
```bash
# CodeBuildからSecret Managerへのアクセスを許可
{
    "Effect": "Allow",
    "Action": "secretsmanager:GetSecretValue",
    "Resource": # 作成したシークレットのARN
}
```
またサービスロールにAWS管理ポリシーの`AmazonEC2ContainerRegistryPowerUser`をアタッチし、CodeBuildからECRへのアクセスを許可します。

ここまで設定した上でビルドプロジェクトから`ビルドの開始`を実行しましょう。

ビルドのステータスが`成功`になりましたら、ECR管理画面でイメージがプッシュされていることも確認しましょう。

### 2-3. CodePipelineの作成
#### 2-3-1. パイプラインの作成
つづいてCodePipelineを作成します。
AWSマネジメントコンソールにてCodePipeline管理画面の`パイプラインを作成する`から構築を進めます。
CodeBuildと同様にサービスロールは新規作成します。

ソースステージの追加で、先ほど作成したCodeCommitとdevelopブランチを選択します。
検出オプションで`Amazon CloudWatch Events (推奨)`を選択することでブランチ更新をトリガーにするイベントルールが作成されます。(次章で説明)
![codepipeline_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/6857a0be-a916-5aeb-b4d0-084166e00af0.png)

ビルドステージの追加で、先ほど作成したビルドプロジェクトを選択します。
ビルドタイプは`単一ビルド`を選択します。
![codepipeline_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/65d3ccff-8b89-af39-d71d-d449ecac2137.png)

デプロイステージの追加で、作成したECSクラスター、サービスを選択します。
イメージ定義ファイルにbuildspec.ymlで記載した`imagedefinitions.json`を指定します。
![codepipeline_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/60bd1f19-66e3-35c8-cad0-6a7981d910df.png)

`パイプラインを作成`すると初回実行が開始されます。
デプロイステージ成功まで少し待ちます。
※必要に応じて自動テストや手動承認等のステージも含めるとより実運用に近いパイプラインになると思います。

#### 2-3-2. developブランチの更新
developブランチの更新からCodePipelineの自動起動を確認します。

AWSマネジメントコンソールにてAmazon EventBridge管理画面でCodePipeline構築時にイベントルールが同時に作成されていることを確認します。
イベントパターンは下記のようになっているはずです。
```json
{
  "source": ["aws.codecommit"],
  "detail-type": ["CodeCommit Repository State Change"],
  "resources": ["arn:aws:codecommit:${リージョン名}:${アカウントID}:${CodeCommitリポジトリ名}"],
  "detail": {
    "event": ["referenceCreated", "referenceUpdated"],
    "referenceType": ["branch"],
    "referenceName": ["develop"]
  }
}
```
`"CodeCommit Repository State Change"`
指定したCodeCommitリポジトリ(`resources`)のdevelopブランチ(`referenceName`)の作成、更新(`event`)をトリガーにCodePipelineを実行するといったイベントパターンです。

ちょこっとFlaskの勉強がてらBlueprintを使ってアプリを分割してみます。
※内容はなんでもいいです。今回はヨーロッパの都市と経度緯度を表示します。(Google Mapより)
- cities.py (新規作成)
```python:cities.py
from flask import Blueprint, render_template, request

api = Blueprint('cities', __name__)

@api.route('/', methods=["POST"])
def cities_europe():
    data = {
        "london": {
            "name": "London",
            "longitude": "-0.10661404281468599",
            "latitude": "51.520022345497104",
        },
        "paris": {
            "name": "Paris",
            "longitude": "2.383285523499959",
            "latitude": "48.885820046457034",
        },
        "amsterdam": {
            "name": "Amsterdam",
            "longitude": "4.7347868083578",
            "latitude": "52.48872238283727",
        }
    }
    name = request.form["europe"]
    city = data[name]
    return render_template('index.html', city=city)
```
- app.py
```python:app.py
from flask import Flask, render_template
from cities import api # この行を追加

app = Flask(__name__)
app.register_blueprint(api) # この行を追加

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=80)
```
- index.html
```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Flask</title>
</head>
<body>
    <p>Hello from flask</p>

    <!-- 下記を追加 -->
    <form action="/" method="POST">
        <label for="europe">
            <div>
                <input type="radio" name="europe" value="london" id="london" checked>
                <label for="london">London</label>
            </div>
            <div>
                <input type="radio" name="europe" value="paris" id="paris">
                <label for="paris">Paris</label>
            </div>
            <div>
                <input type="radio" name="europe" value="amsterdam" id="amsterdam">
                <label for="amsterdam">Amsterdam</label>
            </div>
        </label>
        <input type="submit" value="Submit">
    </form>
    {% if city %}
    <p>welcome to {{city["name"]}}</p>
    <p>longitude: {{city["longitude"]}}</p>
    <p>latitude: {{city["latitude"]}}</p>
    {% endif %}
    <!-- ここまで -->

</body>
</html>
```
上記の変更をdevelopブランチにプッシュします。

CodePipelineの管理画面に移動して対象のパイプラインが自動起動し`進行中`になっていることを確認します。
デプロイまで完了したらブラウザでパブリックIPにアクセスしてみます。
※今回はEIPやドメイン設定をしていないためIPアドレスが可変のため注意です。
最新化されたindex.htmlがデプロイされていたら成功です。

![citiespy_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1813991/6afd11e8-e825-2832-e2a8-50c53db1f835.png)
こんな感じです。

# まとめ
今回はCodeCommitのdevelopブランチの更新をトリガーに、CodePipelineを実行する流れを簡単にまとめました。
デプロイまでの流れに重点をおいたため、ソースの更新やbuildspec.ymlの内容をより作り込むこともできると思いますが、それは状況に応じて置き換えてもらえればと思います。
余談ですが、ビルドログが更新されていくの眺めているの気持ちいいですよね。(病気)

パイプライン構築は扱うサービスも設定項目も多いので複数環境作成する際に手間がかかりますし、意図せぬ環境差分を作ってしまう恐れがあります。
そのため次記事ではECRの作成からCodePipelineの作成までをコード化してCloudFormationで構築する流れをまとめます。

後続の記事はこちら
[【AWS】 (中級者向け) CodePipelineを使ったECSコンテナのデプロイ自動化 その3(CloudFormationでインフラコード化)](https://qiita.com/ramunauna/items/f52cdcaeadedf40e5d22)