AnsibleをAWS Systems Managerから実行するCI/CDを構築する。（その４：CodeBuildからのAnsible実行）

:::note info
2022/12/4更新
**CodeBuild**の「**buildspec.yml**」で**CodeCommit**から取得したソースを予めZIP圧縮してS3に格納しておく必要があったため、記事を更新しました。
:::

# はじめに
前回は`Chatbot`を使用して`Slack`へ通知する仕組みを構築しました。

今回は、[その１](https://qiita.com/sakai00kou/items/5604b94dd2c87922538f)でマネジメントコンソール上から直接`Systems Manager`の`RunCommand`を実行して試したことを`CodeBuild`から実行できるかを試してみようと思います。

- 【前】[AnsibleをAWS Systems Managerから実行するCI/CDを構築する。（その３：SNS、Chatbotの準備）](https://qiita.com/sakai00kou/items/b75e763aef0af475dfb2)
- 【次】[AnsibleをAWS Systems Managerから実行するCI/CDを構築する。（その５：CodePipelineを使用したCI/CD）](https://qiita.com/sakai00kou/items/d6ade5065a5169e470bc)

# CodeBuildとは
「[M2 MacでCodeBuildをローカルで実行する。](https://qiita.com/sakai00kou/items/76150753d3dd1a38d578)」でも書きましたが、`CodeBuild`の名の通り、コードをビルドすることを目的としたサービスです。

`CodeBuild`でソースコードを実際に実行できるような形式にビルドする方法は、`buildspec.yml`というビルドの設計図となるファイルにビルドコマンドや実行コマンドを記載して、実行アプリを生成するやり方となります。

通常は`CodeBuild`でビルド後、ビルドしたアプリを実行するノードに対して`CodeDeploy`等でアプリを配置して`CodePipeline`で一連のサービスを繋げて`CodeCommit`へのコミット契機で`CodeBuild`、`CodeDeploy`と行うのが一般的な`CI/CD`の流れとなります。

今回の最終構成の場合は`CodeDeploy`に当たる操作は`Systems Manager`の`RunCommand`で行うため、`CodeDeploy`は使用しませんが、構成としては同等の構成を構築していきます。

# CodeBuildの仕組み
`CodeBuild`の仕組みを少し深堀りすると、ビルド実行時、まずビルド実行用の`Docker`コンテナを起動して、事前に準備してある`buildspec.yml`ファイルのビルド設計に従って処理を実行、生成物を出力する設定にしていれば生成物（アーティファクト）をS3バケットに格納する仕組みとなります。

したがって、`buildspec.yml`を作成するということは、ビルド用コンテナで実行する内容を設定することとなります。

```plantuml
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v14.0/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/Groups/AWSCloud.puml
!include AWSPuml/General/Client.puml
!include AWSPuml/General/Document.puml
!include AWSPuml/DeveloperTools/CodeBuild.puml
!include AWSPuml/Containers/ElasticContainerServiceContainer1.puml
!include AWSPuml/Storage/SimpleStorageServiceBucket.puml
!include AWSPuml/AWSSimplified.puml

AWSGroupColoring(CodeBuildGroup, #FFFFFF, AWS_COLOR_BLUE, dashed)
!define CodeBuildGroup(g_alias, g_label="CodeBuild") AWSGroupEntity(g_alias, g_label, AWS_COLOR_BLACK, CodeBuild, CodeBuildGroup)

skinparam shadowing false
hide stereotype
'skinparam linetype ortho
skinparam rectangle {
    BackgroundColor AWS_BG_COLOR
    BorderColor transparent
}

left to right direction
'top to bottom direction
title CodeBuildの仕組み
rectangle "$ClientIMG()\nClient" as cl
AWSCloudGroup(cloud){
  CodeBuildGroup(buildgroup) {
    rectangle "$CodeBuildIMG()\nCodeBuild" as build
    rectangle "$ElasticContainerServiceContainer1IMG()\nコンテナ" as container
    rectangle "$DocumentIMG()\nbuildspec.yml" as yml
  }
  rectangle "$SimpleStorageServiceBucketIMG()\nS3" as s3
}

cl-->build :実行要求
build-->container :Build用コンテナ起動\nbuildspec.ymlに従ってビルド実行
build-[hidden]d-yml
container-[hidden]u-yml
container-->s3 :アーティファクトの出力

@enduml
```

# 今回の構成
今回は`CodeBuild`の**ビルドプロジェクト**を作成して、前回作成した`CodeCommit`に格納しているソースファイルを使い、`CodeBuild`経由で`Systems Manager`の`RunCommand`を実行する以下のような構成を作ってみます。

前回`RunCommand`で`Ansible`を実行した際にはマネジメントコンソール画面上から実行しましたが、今回はコマンド操作で`CodeBuild`から実行するようにします。

```plantuml
@startuml
!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/v13.1/dist
!include AWSPuml/AWSCommon.puml
!include AWSPuml/Groups/AWSCloud.puml
!include AWSPuml/General/Client.puml
!include AWSPuml/DeveloperTools/CodeCommit.puml
!include AWSPuml/DeveloperTools/CodeBuild.puml
!include AWSPuml/ManagementGovernance/SystemsManagerRunCommand.puml
!include AWSPuml/AWSSimplified.puml

!procedure $stepnum($number) 
<back:black><color:white><b> $number </b></color></back>
!endprocedure

AWSGroupColoring(CodePipelineGroup, #FFFFFF, AWS_COLOR_BLUE, dashed)
!define CodePipelineGroup(g_alias, g_label="CodePipeline") AWSGroupEntity(g_alias, g_label, AWS_COLOR_BLACK, CodePipeline, CodePipelineGroup)

hide stereotype
'skinparam linetype ortho
skinparam shadowing false
skinparam rectangle {
    BackgroundColor AWS_BG_COLOR
    BorderColor transparent
}

left to right direction
'top to bottom direction
title CodeBuild実行構成
rectangle "$ClientIMG()\nClient" as cl
AWSCloudGroup(cloud){
  rectangle "$CodeCommitIMG()\nCodeCommit" as commit
  rectangle "$CodeBuildIMG()\nCodeBuild" as build
  rectangle "$SystemsManagerRunCommandIMG()\nRunCommand" as ansible
}

cl-->build :$stepnum("1")ビルドを開始
commit<--build :$stepnum("2")ソースファイルを取得
commit-->build
build-->ansible :$stepnum("3")CodeBuildで実行
@enduml
```

# buildspec.ymlの作成
最初にビルドの設計図となる`buildspec.yml`ファイルを作成していきます。

大きく分けると以下９つのシーケンスと呼ばれる構文があり、少なくとも以下必須と書かれている`version`と`phases`は`buildspec.yml`で指定する必要があります。

|シーケンス|説明|
|:--|:--|
|version|buildspecのバージョンを指定（必須）|
|run-as|（Linuxのみ）コマンド実行するLinuxユーザを指定|
|env|環境変数を指定|
|proxy|プロキシサーバでビルドを実行する場合に指定|
|batch|ビルド失敗時の動作などを指定|
|phases|実行コマンドを指定（必須）|
|reports|テストレポートの形式やレポート出力などを指定|
|artifacts|生成物の出力先を指定<br>CodeBuild画面で設定も可能|
|cache|キャッシュの場所の指定|

詳しい記載方法などは以下リファレンスを参照。

- [CodeBuild のビルド仕様に関するリファレンス](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html)

また、`CodeBuild`をローカルでテストしたい場合は以下参照してください。

- [M2 MacでCodeBuildをローカルで実行する。](https://qiita.com/sakai00kou/items/76150753d3dd1a38d578)

## 作成するbuildspec.ymlについて
今回`CodeBuild`から`RunCommand`を実行したいので、ビルド用のコンテナから`Systems Manager`の`RunCommand`を実行するようにコマンドを記載します。

コマンドはAWSのリファレンスから調べて記載しても良いですが、`Systems Manager`の`RunCommand`画面に以下のようにコマンド実行用のCLIコマンドが表示されるため、そちらで表示されるコマンドをベースに作ります。

`RunCommand`作成時でしか以下画面のようなCLIコマンドは確認できないため、新規に作成するか、`RunCommand`の「**コマンド履歴**」から「**Copy to new**」ボタンを選択して確認しましょう。

![Monosnap_20220923_230736.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/f41512e8-c69a-93f5-8779-e6837126226a.png)

CLIコマンドを参考に作成した`buildspec.yml`のファイル構成は以下となります。

本来は`buildspec.yml`ファイルだけで良いですが、汎用性を持たせるため、以下のような構成としました。

|ファイル名|説明|
|:--|:--|
|buildspec.yml|ベースとなるbuildspec.ymlファイル<br>以下ファイルを参照＆生成して実行する処理を行う|
|exec_host.json|ターゲットの指定を行うファイル|
|sourceinfo.json|ソースファイルの場所を指定するファイル|
|ansible.sh|RunCommand実行コマンドを記載した生成ファイル<br>実行後は自動削除|

以降でファイルの内容について記載＆解説します。

### buildspec.yml
作成した`buildspec.yml`は以下となります。

```yml:buildspec.yml
version: 0.2

env:
  shell: bash
  variables:
    EXEC_HOST_FILE: exec_host.json
    SOURCEINFO_FILE: sourceinfo.json
    EXEC_ANSIBLE_SHELL: ansible.sh
    DOCUMENT_NAME: AWS-ApplyAnsiblePlaybooks
    DOCUMENT_VERSION: 1
    TIMEOUT_SECONDS: 600
    MAX_CONCURRENCY: 50
    MAX_ERRORS: 0
    REGION: ap-northeast-1
    SOURCETYPE: S3
    INSTALLDEPENDENCIES: True
    PLAYBOOKFILE: site.yml
    EXTRAVARIABLES: SSM=True
    CHECK: True
    VERBOSE: "-v"
    TIMEOUTSECONDS: 3600
    SOURCEFILE: ssm_ansible.zip

phases:
  install:
    on-failure: ABORT
    commands:
      - zip -r ${SOURCEFILE} *
      - aws s3 mv ${SOURCEFILE} $(jq -c . ${SOURCEINFO_FILE} | awk -F'[.:]' '{print "s3:"$3}')
  pre_build:
    on-failure: ABORT
    commands:
      - EXEC_HOST=$(cat ${EXEC_HOST_FILE} | jq -c .)
      - SOURCEINFO=$(cat ${SOURCEINFO_FILE} | jq -c . | sed -e "s/\"/\\\\\\\"/g")
      - |
        CMD="aws ssm send-command
        --document-name "${DOCUMENT_NAME}"
        --document-version "${DOCUMENT_VERSION}"
        --timeout-seconds "${TIMEOUTSECONDS}"
        --max-concurrency "${MAX_CONCURRENCY}"
        --max-errors "${MAX_ERRORS}"
        --region "${REGION}"
        --targets '${EXEC_HOST}'
        --parameters '{
          \"SourceType\":[\"${SOURCETYPE}\"],
          \"SourceInfo\":[\"${SOURCEINFO}\"],
          \"InstallDependencies\":[\"${INSTALLDEPENDENCIES}\"],
          \"PlaybookFile\":[\"${PLAYBOOKFILE}\"],
          \"ExtraVariables\":[\"${EXTRAVARIABLES}\"],
          \"Check\":[\"${CHECK}\"],
          \"Verbose\":[\"${VERBOSE}\"],
          \"TimeoutSeconds\":[\"${TIMEOUTSECONDS}\"]
        }'"
      - echo $CMD > ${EXEC_ANSIBLE_SHELL}
  build:
    on-failure: ABORT
    commands:
      - bash ${EXEC_ANSIBLE_SHELL}
  post_build:
    commands:
      - rm -f ${EXEC_ANSIBLE_SHELL}
```

`env`の部分が`RunCommand`の各種設定を変数化した内容となるため、実行環境に合わせて値を修正します。

`phases`は`install`、`pre_build`、`build`、`post_build`と分かれており、以下のような処理を実行するようにしています。

|ビルド種別|内容|
|:--|:--|
|install|**RunCommand**で使用する各種ファイルの圧縮＆S3アップロード|
|pre_build|**RunCommand**実行用コマンドシェルの生成|
|build|**RunCommand**実行用コマンドシェルの実行|
|post_build|**RunCommand**実行用コマンドシェルの削除|

`install`では`pre_build`で使用する各種ファイルをZIP圧縮してS3バケットにアップロードを行っています。

`aws s3 mv`で記載している`jq`コマンドは`SOURCEINFO_FILE`から`S3URI`を取得しているだけなので、`jq`コマンドで整形しなくても、変数化して読み込ませてもOKです。

[その１](https://qiita.com/sakai00kou/items/5604b94dd2c87922538f)の「**コマンドの実行**」の設定項目で言うと、圧縮された`Ansible`のソースファイル格納先の設定となる「**Source Info**」と「**ターゲット**」の指定は、コマンド実行する際に`JSON`形式で記載する必要があるため、それぞれ別ファイルとして読み込み、コマンド生成する際に整形してワンライナーのコマンドになるようにしました。

また、今回の場合、わざわざ`pre_build`シーケンスで`RunCommand`実行用のシェルファイルを生成してから`build`シーケンスで生成したシェルファイルを実行、`post_build`で生成したシェルファイルを削除するような回りくどい方法を行っておりますが、`YML`形式となる`buildspec.yml`で`JSON`形式となるターゲット指定とソース指定のパラメータを記載すると、どうやってもフォーマットエラーとなってしまったため、一度シェルを生成してから実行するようにしました。

### exec_host.json
`RunCommand`をコマンド実行する場合、当然ながら画面上でターゲットを選択して実行するようなことができず、`RunCommand`のCLI仕様上、`JSON`形式で指定する必要があるため、別ファイルとしてターゲットを指定するようにしました。

```json:exec_host.json
[
  {
    "Key":"InstanceIds",
    "Values":[
      "i-0a93d08d7d8ed1597"
    ]
  }
]
```

`JSON`形式なので、以下のようにカンマで区切れば複数ターゲットを指定することも可能です。

```json:複数ターゲットを指定する場合のexec_host.json
[
  {
    "Key":"InstanceIds",
    "Values":[
      "i-0a93d08d7d8ed1597",
      "i-0e0fc2d1b6cc86efd"
    ]
  }
]
```

ターゲットをタグで指定したい場合は以下のように設定します。

以下はタグキーを「**Name**」、タグの値を「**webserver**」とした場合の例。

```json:タグ指定する場合のexec_host.json
[
  {
    "Key":"tag:Name",
    "Values":[
      "webserver"
    ]
  }
]
```

タグ指定で複数ターゲットを指定する場合は、タグキーも変わってくるため、以下のように指定。

```json:タグ指定で複数ターゲット指定する場合のexec_host.json
[
  {
    "Key":"tag:Name",
    "Values":[
      "webserver"
    ]
  },
  {
    "Key":"tag:Environment",
    "Values":[
      "dev"
    ]
  }
]
```

ターゲットをリソースグループで指定した場合は今回試していませんが、`RunCommand`のCLIコマンド欄に表示された内容を元に記載しているだけなので、リソースグループで指定したい場合もCLIコマンド欄に表示された内容を元に`exec_host.json`に記載すれば使用できるかと思います。

### sourceinfo.json
`RunCommand`の「**Source Info**」欄に指定する`JSON`形式の内容をそのままファイル化した内容となります。

「**Source Info**」で指定する内容をそのまま記載してください。

```json:sourceinfo.json
{
  "path":"https://[バケット名].s3.[リージョン名].amazonaws.com"
}
```

### ansible.sh
`buildspec.yml`の`pre_build`シーケンスで組み立てたコマンドが書かれる一時ファイル。

自動で生成され、実行後に自動削除されるため、あらかじめファイルを作成しておくなどの処理は不要です。

# CodeBuildの設定
`buildspec.yml`の準備ができたので、いよいよ`CodeBuild`の設定を以下の順番で行っていきます。

- [ビルドプロジェクトの作成](#ビルドプロジェクトの作成)
- [ビルドの実行](#ビルドの実行)
- [ビルド結果の確認](#ビルド結果の確認)

## ビルドプロジェクトの作成
先程準備した`buildspec.yml`はビルド用コンテナ内で実行される処理の内容を記載したファイルとなりますが、ビルド用コンテナ自体をどのコンテナイメージで作成するか、ログ出力はどうするか等、コンテナ外での処理をビルドプロジェクトで設定していきます。

以下、`CodeBuild`のダッシュボードからビルドプロジェクトを作成した場合の例です。
尚、設定を行った項目について以下表に載せておりますが、特に設定変更を行っていない項目については除外しております。

|項目|設定|備考|
|:--|:--|:--|
|プロジェクト名|ssm_ansible|任意のプロジェクト名を入力|
|ソースプロバイダ|AWS CodeCommit||
|リポジトリ|ssm_ansible|自分のリポジトリ名に合わせる|
|環境イメージ|マネージド型イメージ||
|オペレーティングシステム|Amazon Linux 2||
|ランタイム|Standard||
|イメージ|aws/codebuild/amazonlinux2-x86_64-standard:4.0||
|イメージのバージョン|このランタイムバージョンには常に最新のイメージを使用してください||
|環境タイプ|Linux||
|特権付与|チェック||
|サービスロール|新しいサービスロール||
|ロール名|新規作成||
|ビルド仕様|buildspecファイルを使用する||
|アーティファクトタイプ|Amazon S3||
|バケット名|任意|作成済みのS3バケット名を指定|
|名前|ssm_ansible.zip|任意の名前を入力|
|アーティファクトのパッケージ化|Zip||

## ビルドの実行
ビルドプロジェクトの作成ができたため、作成したビルドプロジェクトを使用して実際にビルドプロジェクトを動かしてみます。

いくつか実行する方法はありますが、`CodeBuild`のダッシュボードより「**ビルド**」→「**ビルドプロジェクト**」から作成したビルドプロジェクト（今回は`ssm_ansible`）を選択して「**ビルドを開始**」を実行すれば動きます。

## ビルド結果の確認
実行したビルドプロジェクトの「**フェーズ詳細**」に各ビルドフェーズのステータスが表示されるため、全て「**成功**」と表示されていれば成功です。

![Monosnap_20221130_214432.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569912/a7672e8c-453d-ab52-710e-65599d16f32f.png)

また、今回の場合、指定したバケットにアーティファクト（ビルド実行時のファイル等）がアップロードされているため、S3バケットにアップロードされているかも確認します。

# おわりに
「[その１](https://qiita.com/sakai00kou/items/5604b94dd2c87922538f)」で`Systems Manager`単体で行った方法と比べると、いくつかのファイルを扱っているため複雑に見えますが、汎用性を持たせるために入力ファイル等作り込んでいるだけで、やっていることは`Systems Manager`の画面に表示されたCLIコマンドを`CodeBuild`で実行していることと変わりません。

今回の`buildspec.yml`の中身を見ても分かる通り、ビルド以外でも何でも実行できるため、組み方次第で色々な用途で利用できるサービスになります。

次回は今まで作成してきたサービスを`CodePipeline`で繋いで、コミット契機で実行できるようにしていきたいと思います。