GCP・Terraform・CircleCIでゼロからテックブログ更新通知botを作ったので解説(GitHubリポジトリあり)

6 min read読了の目安(約5900字

テックブログの更新通知をRSSから取得してDiscordに投稿するbotをGCPで作ったので解説します!
GCP・Terraformをしっかり触ったのはこれが初めてなので、ご指摘などあればぜひコメントいただきたいです。

botの動作イメージ

image.png

ソースコード(★ください!)

https://github.com/hiroga-cc/gcp-rss-webhook

全体像

アーキテクチャ

image.png

デプロイフロー

image.png

一応上記のアーキテクチャの元ファイル

https://docs.google.com/presentation/d/1bPJmY_O2kh6mIyC_UyGhKS9TbcKhMVfcbCO-gQau200/edit?usp=sharing

手順

1. リポジトリの作成

手元にリポジトリを作成します。↓をフォークしても良いと思います(★ください!)

https://github.com/hiroga-cc/gcp-rss-webhook

また、GitHubリポジトリをCircleCIに接続しておいてください。この時点のビルドは失敗しても大丈夫です。

2. プロジェクトの作成

メニューからプロジェクトを作成します。
image.png

3. サービスアカウントの作成

IAMからサービスアカウントを作成します。
権限はとりあえずプロジェクト編集者、としています。本当は利用するリソースにしぼった方が良いのでしょうけど、デプロイ時に変なところでハマりたくなかったので。
キーをJSONで作成し、ダウンロードします。

4. 鍵のローカル・CircleCIへの配置

ダウンロードしたJSONファイルを、ローカルとリモートの両方に配置します。
ローカルへの配置は、Terraform・CircleCIの挙動確認で必要になります。

ローカルに配置

image.png

CircleCIに配置

CircleCIのcontextを適当に作成します。作成したcontextの中に、 GCLOUD_SERVICE_KEY という名前でJSONファイルの中身を保存します。もっと良いやり方をご存知の方は教えてください...。
image.png

5. CloudStorageを作成

Terraformのstate管理のファイルやソースコードをアップロードするバケットを作成します。リポジトリ内のmb.shを使ってください。

gsutil mb -l asia gs://${BUCKET_NAME}/

なお、リポジトリ内の main.tf にCloudStorageの名前を直接書いているところがあります。これを直さないと動かないのでご注意ください。

6. まずはローカルからTerraformを起動!

この段階で、必要なAPIをGCPのコンソール上から有効にします。Cloud Pub/SubCloud SchedulerのAPIを有効にする必要があります。それぞれメニューから開く、ジョブを途中まで作る、などしてみて下さい。
続いて、環境変数をセットした上で、ターミナルからTerraformを実行します。tfファイルはリポジトリ内を参照してください。

export GOOGLE_APPLICATION_CREDENTIALS=./key.json
export TF_VAR_GOOGLE_APPLICATION_CREDENTIALS=./key.json
export TF_VAR_project_id="${PROJECT_ID}"
export TF_VAR_bucket_name="${BUCKET_NAME}"
# export TF_LOG=DEBUG

ポイント

クレデンシャルの渡し方

GOOGLE_APPLICATION_CREDENTIALS を2通りの方法で渡す必要があることです(これもなんとかなりませんか?)
1行目のexportではTerraformがCloudStorageにアクセスするためのクレデンシャルを設定し、2行目のexportではTerraformがデプロイする際に必要なクレデンシャルを設定しています。

ソースコードの指定

ソースコードのファイル名を毎回変えているのにお気付きですか?
2019-05-19時点でTerraformは、CloudFunctionsのソースコードのパスがそのままだと、変更を検知できないようです。そのため、毎回ファイルのパスを変更しています。
参考: https://github.com/terraform-providers/terraform-provider-google/issues/1938

resource "google_cloudfunctions_function" "function" {
  name                = "rss-webhook-function"
  entry_point         = "handler"
  available_memory_mb = 256
  project             = "${var.project_id}"
  runtime             = "nodejs8"

  environment_variables = {
    BUCKET_NAME = "${var.bucket_name}"
  }

  event_trigger {
    event_type = "google.pubsub.topic.publish"
    resource   = "${google_pubsub_topic.topic.name}"
  }

  source_archive_bucket = "${var.bucket_name}"
  source_archive_object = "src/${data.archive_file.function_src.output_base64sha256}.zip"
}


~~

resource "google_storage_bucket_object" "archive" {
  name       = "src/${data.archive_file.function_src.output_base64sha256}.zip"
  bucket     = "${var.bucket_name}"
  source     = "function_src.zip"
  depends_on = ["data.archive_file.function_src"]
}

node_modulesを同梱しない

CloudFunctionsでjavascriptの関数を作る場合、node_modulesを同梱しないでもpackage.jsonがあれば向こうでなんとかしてくれます。
この辺Lambdaとの違いですね。

7. ローカルからCircleCIを起動!

適切に環境変数をセットしたのち、CircleCIをローカルから起動して設定をチェックします。プロジェクト名、バケット名は自分で指定して下さい。

circleci config validate

export GCLOUD_SERVICE_KEY=$(cat ./key.json)

circleci local execute --job test \
    -e GCLOUD_SERVICE_KEY="$GCLOUD_SERVICE_KEY" \
    -e TF_VAR_project_id="${PROJECT_ID}" \
    -e TF_VAR_bucket_name="${BUCKET_NAME}"

circleci local execute --job build \
    -e GCLOUD_SERVICE_KEY="$GCLOUD_SERVICE_KEY" \
    -e TF_VAR_project_id="${PROJECT_ID}" \
    -e TF_VAR_bucket_name="${BUCKET_NAME}"

8. リモートのCircleCIのcontextを指定

必要なcontextを設定します。

image.png

9. コンソールから実行

取得するrssとwebhookの設定をまとめたjsonファイルをCloudStorageにアップロードし、CloudFunctionsを起動します。
webhookアドレスを公開すると私のbotが暴れまわってしまうのでこのような構成にしました。職場のリポジトリとかでやる場合は普通にコミットしていいと思います。

設定ファイルのアップロード

以下のようなファイルを作成します。

[
    {
        "label": "クックパッド開発者ブログ",
        "rss": "http://techlife.cookpad.com/rss",
        "webhook": "https://discordapp.com/api/webhooks/***/***"
    },
    {
        "label": "hnapp",
        "rss": "http://hnapp.com/rss?q=score%3E100",
        "webhook": "https://discordapp.com/api/webhooks/***/***"
    }
]

その後、CloudStorageにアップロードします。

gsutil cp -r rss_webhooks.json gs://${BUCKET_NAME}/

実行

ここまでの準備ができたら、Cloud Schedulerから今すぐ実行を押してサービスを起動します。

image.png

呼び出しが発生していればOKです。お疲れ様でした!
image.png

まとめ

TerraformでGCPのリソースを作成し、それをCircleCIでデプロイできる環境を整えました。
やって思ったのは、素直にDeployment Manager使えばよかった...普段AWSを利用しているので、GCPを単体で使うことはないと判断してTerraformに挑戦してみましたが、なにぶん外部サービスなのでクレデンシャルの扱いに苦労しました。
ただ、CloudFormationのYAMLよりは読みやすいですね!

今後の課題

  • CircleCIへのサービスアカウントのクレデンシャルの渡し方は、本当にcontext経由でいいのでしょうか?
    https://circleci.com/docs/2.0/google-auth/
  • terraformの実行時、-lock=falseにしないと動かなかったのが悔しいです。
  • tfファイル内でバケット名を変数にする方法が分かりませんでした。
  • TerraformをCIで回す場合(このシチュエーションがそもそもまれ?)、planをtestとして実行するのは一般的なのか。私は理にかなっていると思いますが。