Bitbucket PipelinesでGCPに対してTerraformでインフラCI/CDする

[Bitbucket](https://bitbucket.org/)には[Bitbucket Pipelines](https://www.atlassian.com/ja/software/bitbucket/features/pipelines)というパイプラインツールが付属しており、CI/CDなど処理の自動化に利用できます。
無料プランでも、ビルド実行時間にして月間500分までは使うことができます。[^1]

[^1]: https://bitbucket.org/product/ja/pricing

本稿では、Bitbucket Pipelinesで[Terraform](https://www.terraform.io/)を実行し、[Google Cloud Platform](https://cloud.google.com/)の構成管理を行う方法を紹介します。
また、GitOpsによるインフラCI/CDの作業フローも紹介します。

# 更新履歴

- 2020-05-03 Bitbucketのリポジトリ変数設定を変更し、bitbucket-pipelines.ymlを簡素化
- 2020-05-02 初稿投稿

# 動作確認環境

- terraform v0.12.24
- [terraform-provider-google](https://github.com/terraform-providers/terraform-provider-google/) v3.19.0

# 準備するもの

- Bitbucketアカウント
- Bitbucket上のリポジトリ ... Terraformのコードとbitbucket-pipelines.ymlを入れる
- GCPプロジェクト
  - Service Account
     - Bitbucke Pipelines内で実行するTerraformで利用する
     - Terraformで行う操作に必要なCloud IAMの権限をつける
     - 今回の構成では、少なくともGCSへの読み書き権限が必要
  - GCSバケット ... TerraformのBackendとして利用し、tfstateを保存する
  - 必要なAPIを有効化する
     - 例）GCEインスタンスの操作には、Compute Engine APIの有効化が必要

## 推奨開発環境

ローカルで `terraform` コマンドを実行して動作確認を行うため、以下のツールを使えるようにしておくことをお薦めします:

- [terraform](https://www.terraform.io/downloads.html)
- [Google Cloud SDK](https://cloud.google.com/sdk/install)

# セットアップ手順

1. Terraform用のService AccoutのService Account KeyをJSON形式で生成し、保存する
2. 1. をBitbucketのリポジトリ変数に設定する
  - 「Repository Settings > PIPELINES > Repository variables」
  - キー名は `GOOGLE_CREDENTIALS` とする（※5/3 `GOOGLE_CREDENTIALS_DATA` から変更）
  - 値のBase64化は不要
  - ※「Secured」にチェックを入れる
3. リポジトリのルート直下に、下のようなbitbucket-pipelines.ymlを用意する

## bitbucket-pipelines.yml の設定
bitbucket-pipelines.ymlはBitbucket Pipelinesのビルド構成ファイルです。

下は、TerraformでインフラCI/CDを行うための、サンプル設定です。

```YAML:bitbucket-pipelines.yml
image: hashicorp/terraform:0.12.24

# パイプライン設定で重複するステップの記述をまとめるため、YAMLアンカーを定義
definitions:
  steps:
    - step: &terraform-plan
        name: terraform plan
        script:
          - terraform init -input=false
          - terraform plan -input=false
    - step: &terraform-apply
        name: terraform apply
        script:
          - terraform init -input=false
          - terraform apply -input=false --auto-approve

# パイプライン設定
pipelines:
  # プルリクエストによって実行されるパイプライン
  pull-requests:
    '**': # 任意のブランチからのPRが対象
      - step: *terraform-plan

  # ブランチの更新によって実行されるパイプライン
  branches:
    master:
      - step: *terraform-plan
      - step:
          <<: *terraform-apply
          # terraform applyは手動で実行する
          trigger: manual
```

※2020-05-03 追記: 環境変数をファイルに書き出す処理を削除しました。（後述）

あまり解説の必要もないかもしれませんが、上のパイプライン設定により、以下が実現できます:

- プルリクエストによってトリガーされるパイプラインによって、 `terraform plan` を実行する
  - マージ元ブランチの更新によってもトリガーされる
- masterブランチの更新によってトリガーされるパイプラインによって、 `terraform plan` 及び `terraform apply` を順番に実行する
  - `terraform apply` のステップには `trigger: manual` を指定しているので、パイプラインの画面から手動で「Run」ボタンを押して実行します

これにより、インフラコードをGitで管理し、プルリクエストで変更をレビューしてCI/CDで適用する、といった流れのGitOpsが実現できます。
実際にどのような挙動になるかは、作業フローと合わせて後述の「デモ」の節で見ていきます。

### GCPへの認証設定について（5/3編集）

作成したService Account Keyを次の2つの認証で用います:

- BackendのGCSに対する認証
- terraform-provider-googleによる認証

それぞれの仕様では、Service Account Keyファイルのパスを指定する環境変数が微妙に違いますが、共通して `GOOGLE_CREDENTIALS` を使うことができます。

（5/3追記）
Backendの方のリファレンスを見て、パス名しか指定できないと思い込んでいましたが、試したところ、Providerでの設定と同様に、JSONファイルの内容そのものを値に設定しても問題ありませんでした。
従って、上のリポジトリ変数設定を変更し、bitbucket-pipelines.ymlのパイプライン設定を簡素化することができました。

参考:

- https://www.terraform.io/docs/backends/types/gcs.html#configuration-variables
- https://www.terraform.io/docs/providers/google/guides/provider_reference.html#full-reference

### 任意のブランチの更新でterraform planを実行する場合

例えば、上記のYAMLで `pipelines:` 以下の行を次のように書き換えると、master以外のブランチで `terraform plan` を実行することができます。

```YAML:bitbucket-pipelines.yml
# :
#（ここまでは上のYAMLと同じ）

# パイプライン設定
pipelines:
  branches:
    '**':
      - step: *terraform-plan
    master:
      - step: *terraform-plan
      - step:
          <<: *terraform-apply
          trigger: manual
```

masterブランチ更新時の挙動は、先ほどと同様です。

ブランチ運用のスタイルによっては、この方式も便利だと思います。

### 参考ドキュメント

- [Pipelines を使用したビルド、テスト、およびデプロイ](https://ja.confluence.atlassian.com/bitbucket/build-test-and-deploy-with-pipelines-792496469.html)
- [bitbucket-pipelines.yml の設定 - アトラシアン製品ドキュメント](https://ja.confluence.atlassian.com/bitbucket/configure-bitbucket-pipelines-yml-792298910.html)
- [YAML アンカー - アトラシアン製品ドキュメント](https://ja.confluence.atlassian.com/bitbucket/yaml-anchors-960154027.html)

# デモ: GitOpsによるインフラCI/CD

上で示したbitbucket-pipelines.ymlの最初の例（プルリクエスト駆動）を設定した状態で、インフラCI/CDのワークフローがどのようになるかを見ていきます。

## 0. 下準備

先に示した事前準備とセットアップ手順に加えて、以下を行っています:

- Terraformでテスト用のService Accountを作成するため、GCPプロジェクトでCloud Resource Manager APIを有効化しています
- リポジトリの「CHAT NOTIFICATIONS」設定により、Slack通知を有効にしています

### 5/3追記: デモでのパイプライン設定の差異について

流れは変わりませんが、念のために注記しておきます。

5/3にリポジトリ変数を変更してbitbucket-pipelines.ymlを簡素化しました。
以下のデモでは、次の2点が異なっています:

- リポジトリ変数に設定したService Account Keyのキー名が `GOOGLE_CREDENTIALS` ではなく、 `GOOGLE_CREDENTIALS_DATA` となっている
-bitbucket-pipelines.ymlのアンカー `&terraform-plan`, `&terraform-apply` それぞれで定義したscriptの先頭に以下の2行がある

```diff
         script:
+          - echo "${GOOGLE_CREDENTIALS_DATA}" > service-account-key.json
+          - export GOOGLE_APPLICATION_CREDENTIALS=service-account-key.json
           - terraform init -input=false
           :
```

## 1. topicブランチを作成し、コード変更をプッシュ

1. `topic/add-test-sa` というブランチを作成します
1. .tfファイルに `google_service_account` リソースを定義します
  - 実はこのコードには誤りがあるので、この後のパイプラインは失敗します 
1. Bitbucketにブランチをプッシュします。

```sh
% git checkout -b topic/add-test-sa
% $EDITOR main.tf
% git diff
diff --git a/main.tf b/main.tf
index b6bb61a..d5c0613 100644
--- a/main.tf
+++ b/main.tf
@@ -31,3 +31,7 @@ resource "google_project_iam_member" "sa_terraform" {
   role    = "roles/owner"
   member  = "serviceAccount:${google_service_account.terraform.email}"
 }
+
+resource "google_service_account" "test" {
+  account_id = "test"
+}
% git add .
% git commit -m "Add test service account"
% git push origin topic/add-test-sa
```

master以外のブランチにはトリガーを設定していないので、ここではパイプラインは実行されません。

## 2. プルリクエストの作成

Bitbucket画面上でプルリクエストを作成します。

![Screenshot from 2020-05-02 14-30-52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/326d39dc-0fd6-cf5e-bd4c-64eddea18d8d.png)

bitbucket-pipelines.ymlの `pull-requests` で設定したトリガーにより、 `terraform plan` のパイプラインが実行されます。

## 3. ビルド結果（失敗）の確認

Slackにビルド結果が通知されました。

![Screenshot from 2020-05-02 14-31-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/319f6eed-9c4b-3213-2e11-3e7827e0ee51.png)

「Pipeline #37」のリンクをクリックして、ビルドログを確認します。

![Screenshot from 2020-05-02 14-32-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/d9a807a9-fe0e-b469-3deb-33f2f1fdf29e.png)

`google_service_account.test.account_id` のバリデーションでエラーになっていることがわかりました。

## 4. コードを修正して再プッシュ

`google_service_account.test.account_id` を修正して、ブランチを再プッシュします。

```sh
% $EDITOR main.tf
% git diff
diff --git a/main.tf b/main.tf
index d5c0613..fd9a3ec 100644
--- a/main.tf
+++ b/main.tf
@@ -33,5 +33,5 @@ resource "google_project_iam_member" "sa_terraform" {
 }
 
 resource "google_service_account" "test" {
-  account_id = "test"
+  account_id = "test01"
 }
% git add .
% git commit -m "Fix test service account id"
% git push origin topic/add-test-sa
```

先ほどと同様に、bitbucket-pipelines.ymlの `pull-requests` で設定したトリガーにより、 `terraform plan` のパイプラインが実行されます。

## 5. ビルド結果（成功）の確認

Slackにビルド結果が通知されました。

![Screenshot from 2020-05-02 14-34-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/3cac716e-bc26-efba-6887-719a41e73353.png)

「fixed」となっており、成功したようです。
パイプラインのログも確認しておきましょう。

![Screenshot from 2020-05-02 14-36-46_blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/9b1611f8-d91a-9340-7b06-ab97d9404352.png)

`terraform plan` の出力結果が意図通りになっていることが確認できました。

## 6. PR画面の確認とマージ

プルリクエストの「活動履歴」タブで、ビルドを含む履歴を確認することができます。

![Screenshot from 2020-05-02 14-35-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/7bca5f22-322f-bbca-63f5-07c7d651e920.png)

問題ないので、マージします。

![Screenshot from 2020-05-02 14-38-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/9194daa5-b9e2-3bb3-44c2-adb6fec679b9.png)

## 7. masterブランチ上でのパイプライン実行

bitbucket-pipelines.ymlの `branches.master` で設定したトリガーにより、 `terraform plan` + `terraform apply` のパイプラインが実行されます。

Slack通知によってmasterブランチでパイプラインが実行されたことがわかります。

![Screenshot from 2020-05-02 14-39-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/2c7f873c-600c-fc17-b5bf-93eed703237a.png)

「Pipeline #41」のリンクをクリックして、パイプライン画面に遷移します。[^2]

[^2]: ※別のパイプライン実行が間に挟まったので、直前から番号が飛んでいます。

![Screenshot from 2020-05-02 14-39-52_blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/1e509612-b39f-4788-170a-43c243553c9c.png)

今回のパイプライン設定では、ここでもう一度 `terraform plan` の結果を確認することができます。

`terraform apply` のステップには `trigger: manual` を設定しているので、人間が手動で確認して実行します。
問題がなければ、「Run」ボタンを押して、 `terraform apply` を実行します。

![Screenshot from 2020-05-02 14-40-40_blur.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/55070514-2252-04a4-9bcb-c01c412ba9b9.png)

`terraform apply` も問題なく成功しました :tada: 

# まとめと所感

Bitbucket PipelinesでTerraformを実行し、GCP環境のインフラCI/CDを行う方法と、GitOpsによる作業フローのイメージを紹介しました。

今回、Bitbucket Pipelinesについて全く知らない状態から色々と調べて構築してみたのですが、普通に使えるCIサービスだなと思いました。
最初、なかなか求める情報が見つけられず、「機能が乏しいのではないか」などと思ったりしてしまったのですが、振り返ってみれば、必要十分な機能は揃っているように思います。また、必要な情報はだいたい公式ドキュメントにまとまっていました。

ここ1〜2年で、GitHubでもプライベートリポジトリの作成が無料化されたり、GitHub Actionsというパイプラインツールが登場したので、敢えてBitbucketを使う理由は減っていると思いますが、Bitbucketでも特に困ることはないなと思いました。

# 脚注