AWS上で動くKubernetesクラスタのためのCI/CDパイプライン

- いろいろと苦戦したのでまとめます

# 前提
- クラウドはAWS
- ソース管理はCodeCommit
- ビルド環境はCodeBuild
- 同じAWSのアカウント上で稼働しているKubernetesクラスタがデプロイターゲット
- マニフェストファイルはHelmチャートで管理
- アプリケーション毎にHelmチャートのリポジトリが存在する
- Spinnakerが使える状態にある


# 却下案1(CodePipelineですべて管理)
- CodeCommit
- CodeBuild(ビルド用)
- CodeBuild(デプロイ用)

<img width="271" alt="_無題.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/1bb8a72a-594f-3739-bfff-8e3a44647a74.png">



## 概要
- 大前提として、AWSにはKubernetesにデプロイするサービスが無い
- そのため、デプロイの実装 or デプロイツールの構築が必要になる
- ここではデプロイ用のCodeBuildを作成し、BuildSpecにデプロイコマンドを記載(Helmを使用しているのでHelmコマンド)
- デプロイ用のBuildSpecもアプリ毎に作る必要がある
- `helm install`(初回のデプロイ)と`helm upgrade`(2回目以降のデプロイ)を使い分けないといけない
    - これはHelmを使わなければ回避できる
- CodeBuildの本来の使用用途に合ってない感がすごい

# 却下案2(全てSpinnakerで管理)
- CodeCommit
- Spinnaker
    - CodeCommitへのPushをトリガーにAPIでSpinnakerのパイプラインをスタート
    - API(POST)→LambdaでCodeBuildを叩き、随時API(GET)→LambdaでCodeBuildのStatusを取得
        - Spinnakerのステージとしては「Webhook」で作成する
    - StatusがSucceededになったらのデプロイ
        - Spinnakerのステージとしては「Bake」と「Deploy」


<img width="613" alt="無題.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/7aeafe2f-bc54-6a10-ee4c-a88e71c5fd0c.png">


## 概要
- CDツールのSpinnakerだが、こちらでBuildの状況なども管理するやり方(こちらも本来の使い方とは違うと思いますが)

- アプリ毎にAPI GatewayとLambda3つ(パイプライン開始用、CodeBuild開始用、CodeBuildのStatus取得用)を作成する必要がある、、、管理したくない

# 採用案
- CodePipeline(CI)
    - CodeCommit
    - CodeBuild
- Spinnaker(CD)

## 概要
- 却下案1と却下案2の良いところを合わせたような構成
- CodePipelineでCodeBuildでのビルドまでを管理し、CDはSpinnakerで実行
- SpinnakerのパイプラインでトリガーをWebhookに設定しておき、CodeBuildの最後にそのURLを叩く
- 開発環境・本場環境で別々のCodeBuildを作成し、それぞれのCodePipelineを定義する

<img width="403" alt="__無題.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/853dc4ac-504b-43f0-687d-fbf9966d760a.png" align="center">

#CI(開発環境)
## CodeBuildの作成
- CodeBuildでやりたいことは以下の通り
    - ソースコードのビルド、テスト
    - Dockerイメージのビルド、プッシュ
    - S3へHelmChartをアップロード
        - S3経由でSpinnakerに渡すため
    - Spinnakerのパイプライン開始

- CodeBuildの設定
    - 対象のソースコードと、Helmチャートが格納されているリポジトリのdevelopブランチを「Source」に指定する
    - 公式で提供されている[CodeBuild用のDockerイメージ](https://github.com/aws/aws-codebuild-docker-images) を基に、以下のパッケージを追加
        - helm
        - yq
            - yamlを操作するツール
    - NAT Gatewayが作成されているSubnetを含むVPC内で稼働させる
        - Spinnakerに紐付くセキュリティグループにこのIPアドレスを指定してパイプラインを始められるようにするため
    - HelmチャートをアップロードするS3バケットをArtifactsに指定しておく

## Buildspec
- コメントで概要を記載

```yaml:buildspec.yaml
version: 0.2

env: 
  variables: 
     REGION_NAME: "ap-northeast-1" 
     REPOSITORY_NAME: "test" 
     BRANCH_NAME: "develop" 
     ECR_REPOSITORY: "XXXXXXXXXXX.dkr.ecr.$REGION_NAME.amazonaws.com/xxxxxxx" 
     # Helmチャートの名前
     CHART_NAME: "test" 
     # Helmチャートをアップロードするバケット内のパス
     ARTIFACT_PATH: "test-dev"
     # Spinnakerのパイプラインでトリガーに設定しているURL
     URL : "deploy-test-dev" 

phases:
  install:
    # Dockerデーモンの起動
    runtime-versions:
      docker: 18
    commands: 
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --storage-driver=overlay&  
      - timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
  pre_build: 
    commands:
      - $(aws ecr get-login --no-include-email --region $REGION_NAME) 
  build:
    commands:
    # - ソースのビルドがあればここに記載
      # CODEBUILD_SRC_DIRは後述
      - cd $CODEBUILD_SRC_DIR
      - docker build -t tmp:latest . 
  post_build:
    commands:
      # CODEBUILD_RESOLVED_SOURCE_VERSIONは、CodeBuildの環境にデフォルトで設定されている環境変数
      # 対象のソースのコミットIDが設定されている
      # ここではビルドするDockerイメージのタグとして使用する
      - COMMIT_ID_APP=`echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8`
      - docker tag tmp:latest $ECR_REPOSITORY:$COMMIT_ID_APP
      - docker push $ECR_REPOSITORY:$COMMIT_ID_APP
      # CODEBUILD_SRC_DIR_infは後述
      - cd $CODEBUILD_SRC_DIR_inf
      # Helmチャートのvalues.yamlにデプロイ対象のDockerイメージのタグが記載されている
      # これを先ほど取得したコミットIDに書き換え
      # 更新ができなかったので、一時的にtmp.yamlという名前で保存→既存のvalues.yamlと置き換えを行なっている
      - yq -y '.image.tag |=  "'$COMMIT_ID_APP'"' $CHART_NAME/values.yaml  > $CHART_NAME/tmp.yaml 
      - mv $CHART_NAME/tmp.yaml  $CHART_NAME/values.yaml
      # packageコマンドでtgz形式に変換
      # この形式でSpinnakerに渡すことができる
      - helm package $CHART_NAME
      # S3にアップロード
      - aws s3 cp $CHART_NAME*.tgz s3://[backet name]/$ARTIFACT_PATH/$CHART_NAME.tgz
      # Spinnakerのパイプラインを開始
      - curl https://XXXXXXXXXX.com/webhooks/webhook/$URL -X POST -d "{ }" -H "content-type:application/json" 
```

### CODEBUILD_SRC_DIRについて
- CodeBuildでは、Sourceに指定したソースコードやファイルがCodeBuildの環境にマウントされている
- CODEBUILD_SRC_DIRにはSource1に指定したSourceのパスが設定されており、例えばリポジトリを設定しておけば、`cd $CODEBUILD_SRC_DIR`コマンドでそのレポジトリのソースが格納されたパスに移動できる
- 2つ以上のSourceを指定することもでき、2つ目以降はSource identiferを指定する。ここでは2つ目のSourceにinfというSource identiferを指定したため、CODEBUILD_SRC_DIR_infにそのSourceのパスが設定されている。つまり、CODEBUILD_SRC_DIR_[自分で設定したidentifer]という環境変数で呼び出すことができる

## CodePipeline
- 対象のCodeCommitのリポジトリのdevelopブランチと対象のCodeBuildを指定して保存
- これにより、developブランチへのpushをトリガーにこのパイプラインか開始される

# CD(開発環境)
## Spinnakerでのパイプランの作成
- Application→Actions→「Create Application」からApplicationを作成

<img width="591" alt="スクリーンショット 2019-09-10 8.52.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/f1f03f48-86ac-f38d-c532-3145bf9d21a0.png">

- 作成したApplicationの画面に遷移し、PIPELINES→「Configure a new pipeline」をクリック

<img width="1409" alt="スクリーンショット 2019-09-10 8.59.14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/301ea3c8-2f04-60c6-d911-51322501aeb6.png">

- パイプラインの名前を入力してCreate

<img width="627" alt="スクリーンショット 2019-09-12 8.09.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/8efe4e79-77c9-5b70-6a48-0a07505487cf.png">

### Configuration
- 最初にConfigurationの画面が表示されるので必要な設定を行う


#### Automated Triggers

- パイプラインを開始するトリガーを設定する
- 今回はCodeBuildからURLを叩いて開始したいので、「Webhook」を指定する
- 「Source」にCodeBuildで指定した文字列を入力する

<img width="862" alt="スクリーンショット 2019-09-12 8.12.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/cc4f215a-f7d3-6306-2acc-631a617348c2.png">

#### Expected Artifacts
- Spinnakerに渡したいArtifactを指定する
- 今回はCodeBuildでS3にアップロードしたHelmチャートを使用したいのでそのパスを入力する
- 「Use Sefault Artifact」にチェックを入れ、同じパスを入力する

<img width="862" alt="スクリーンショット 2019-09-12 8.18.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/53081620-d09c-a51b-4f32-6ed4bba33bbb.png">

## ステージ追加
- 「Add stage」を押してステージを追加する
<img width="1176" alt="スクリーンショット 2019-09-12 8.22.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/5f9fcb4a-6946-236b-9919-03ffb13e89d3.png">

- 今回必要なステージは2つ
    - Bake (Manifest)
        - Helmチャートを読み込ませてマニフェストファイルを生成する
    - Deploy(Manifest)
        - Bakeステージで作成したマニフェストファイルをKubernetesクラスタに適用する

### Bake (Manifest)
#### Bake (Manifest) Configuration
- Template Render
    - リソースの名前やNamespaceを指定する
- Template Artifact
    - パイプラインのConfigurationで指定したArtifactがプルダウン形式で選択できる
- Overrides
    - Helmチャートのvalues.yamlで上書きしたい値がある場合はここで指定できる
 <img width="869" alt="スクリーンショット 2019-09-12 8.27.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/4c2e440c-87ad-8436-8932-18d3ebb0f3d0.png">

#### Produces Artifacts

- Base64を指定する
- 名前を適当に入力


<img width="866" alt="スクリーンショット 2019-09-12 8.33.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/d6aa5d4f-dcfa-083e-0a13-ec4b7362d62e.png">

### Deploy (Manifest)

- Bakeステージが終わってから始めるので「Depends On」に該当のステージ名を入力する
- パラレルで複数のステージを実行することもできる

<img width="1194" alt="スクリーンショット 2019-09-12 8.34.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/18daf859-a0a7-f087-02f2-ef5d70fd321c.png">

#### Deploy (Manifest) Configuration
- Manifest Configuration
    - Artifactを選択して、Bakeステージで生成したArtifactをプルダウンから選択する


<img width="876" alt="スクリーンショット 2019-09-12 8.37.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/6886554c-9651-5230-79f5-f3f596b7742d.png">

- 「Save Changes」を押下して完了！
- 作成したパイプラインが表示されている

<img width="1386" alt="スクリーンショット 2019-09-12 8.48.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269592/a8113f62-e6a0-cf67-b950-6827504ef99a.png">


# CI(本番環境)
## CodeBuildの作成
- CodeBuildでやりたいことは以下の通り
    - 開発環境のパイプラインで作成したHelmチャートをS3から取得し、対象のDockerイメージのタグを取得する
    - 本番環境用のHelmチャートに取得したDockerイメージのタグを上書きする
    - 編集したHelmチャートをS3へアップロード
    - Spinnakerのパイプライン(本番環境用)開始
- CodeBuildの設定
    - 基本的には[開発環境用に作成したCodeBuild](https://qiita.com/str416yb/items/01522a57c0b3c79d9bbc#codebuild%E3%81%AE%E4%BD%9C%E6%88%90)と同じ
    - 対象のソースコードと、Helmチャートが格納されているリポジトリのmasterブランチを「Source」に指定する
   
## Buildspec
- コメントで概要を記載

```yaml:buildspec.yaml
version: 0.2 
 
env: 
  variables: 
     REPOSITORY_NAME_INF: "test_inf" 
     BRANCH_NAME: "master" 
     CHART_NAME: "test" 
     ARTIFACT_PATH: "test" 
     URL : "deploy-test-prod" 
     
phases: 
  post_build: 
    commands: 
      # developのパイプラインで作成したHelmチャートをS3から取得し、対象のDockerイメージのタグを取得する
      - cd $CODEBUILD_SRC_DIR_dev 
      - tar -xvf $CHART_NAME.tgz 
      - CURRENT_COMMIT_ID=`yq -r '.image.tag' $CHART_NAME/values.yaml` 
      # 本番環境用のHelmチャートに取得したDockerイメージのタグを上書きする
      - cd $CODEBUILD_SRC_DIR_inf 
      - yq -y '.image.tag |=  "'$CURRENT_COMMIT_ID'"' $CHART_NAME/values.yaml  > $CHART_NAME/tmp.yaml  
      - mv $CHART_NAME/tmp.yaml  $CHART_NAME/values.yaml 
      # packageコマンドでtgz形式に変換
      - helm package $CHART_NAME 
      - mv $CHART_NAME*.tgz $CHART_NAME.tgz 
      # S3にアップロード
      - aws　s3 cp $CHART_NAME*.tgz s3://[backet name]/$ARTIFACT_PATH/$CHART_NAME.tgz
      # Spinnakerのパイプラインを開始
      - curl https://XXXXXXXXXX.com/webhooks/webhook/$URL -X POST -d "{ }" -H "content-type:application/json" 
```
### Sourceの設定
- CodeBuildのSourceについては[CODEBUILD_SRC_DIRについて](https://qiita.com/str416yb/items/01522a57c0b3c79d9bbc#codebuild_src_dirについて)を参照
- CodeBuildのSourceには以下を指定している
    - Source identifer：無
        - 該当リポジトリのmasterブランチ
    - Source identifer：dev
        - 開発環境用のパイプラインでアップロードしたHelmチャートが格納されているバケット
    - Source identifer：inf
        - Helmチャートが格納されているリポジトリのmasterブランチ

## CodePipeline
- 対象のCodeCommitのリポジトリのmasterブランチと対象のCodeBuildを指定して保存
- masterブランチへのpush(merge)をトリガーにこのパイプラインか開始される

# CD(本番環境)
- 本番環境用のパイプラインをSpinnaker上で作成
- 作成方法は開発環境用に作成したものと同様
- 変更する点は以下
    - Triggerに設定するURL(CodeBuildeの最後に叩いているURLとあわせる)
    - Expected Artifactsに指定するパス
    - デプロイターゲットとなるKubernetesクラスタ

# まとめ
- AWS上のKubernetsクラスタにデプロイするためのパイプラインを作成
- リポジトリのpushとCodeBuildeでのビルドはCodePipelineで管理し、KubernetesクラスタへのデプロイはSpinnakerで管理する方法を紹介
- 改善点があれば随時更新予定！