Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

Google Cloud Functionsを使ってSlackで簡単にCDN上のキャッシュを消せるようにする話

この記事は、 Mercari Bold Challenge Monthの最終日の記事です。

SREチームの@catatsuyです。

メルカリでは様々な用途でCDNを使っています。基本的にCDN経由で静的ファイルを配信する場合、CDNからはオリジンからのキャッシュを表示するように設定しています。 しかしキャッシュからデータを削除したいこともあります。例えば古いファイルが配信されているので更新したいなどの理由です。

こうしたCDNのキャッシュからの削除依頼は日々様々なチームで発生します。しかしCDNのキャッシュを削除するにはAPIトークンが必要だったり、管理画面にログインする必要があったりするので知識と権限が必要になります。CDNの知識がなくても社内の人なら簡単に削除できる仕組みが欲しいところです。

そこで弊社では以下のようにSlackで簡単にCDN上のキャッシュを削除できるようにしています。今回はその仕組みの紹介です。

f:id:catatsuy:20190918201158p:plain
SlackでCDN上のキャッシュを削除している様子

SlackのSlash Commands

https://api.slack.com/slash-commands

SlackのSlash Commandsには以下のような特徴があります。

  • Slack上で簡単に登録できて使えるようになる
  • /todo のようなコマンドを使えるようになる
  • 登録したURLに決められた形式のPOSTのリクエストをSlack側が送ってくれる
  • 適切なレスポンスを返すとSlack上にメッセージを表示できる
  • コマンドが実行されたときだけリクエストを投げてくれるのでチャンネル上の会話が外部に漏れることがない

非常に嬉しい特徴ばかりではないでしょうか? 私たちがやらなければならないことは、Slack上にコマンドを登録することと、Slackから送られてきたリクエストを適切に処理するアプリケーションを実装して運用するだけです。

もちろんSlackからいつリクエストが送られてくるか分からないので、リクエストを処理するアプリケーションは常に動かしている必要があります。そうなるとそのアプリケーションが動いているかどうかの監視もしたくなりますし、考えることはどうしても増えてしまいます。

ということでGoogle Cloud Functionsを次に紹介します。

Google Cloud Functions

Google Cloud Functionsを使えば、サーバーやランタイム環境を管理すること無く、HTTPサーバーを運用することができます。対応している言語はいくつかありますが、今回はGoを使う前提で紹介します。

Goを使ってGoogle Cloud FunctionsでHTTPサーバーを提供する場合、用意しなければならないのは http.HandlerFunc インターフェースを満たす関数のみです。つまりGoogle Cloud Functions専用の仕組みなどは一切不要で、一般的なGoのコードがそのまま動かせるということです。

そんなうまい話があるわけないと思ったかもしれません。けど本当です。専用のライブラリを読み込む必要もなければ、専用の仕組みを作る必要もありません。

使い始めるのも簡単です。以下のドキュメントを参考にCloud Functions APIを有効にしてから、gcloudコマンドを実行するだけです。

https://cloud.google.com/functions/docs/quickstart https://cloud.google.com/functions/docs/concepts/go-runtime

今回はそんなGoogle Cloud Functionsを使って、実際にSlackのSlash Commandsで使うアプリケーションを作るときのTipsなどを紹介できればと思います。

ディレクトリ構成

いくつかやり方が考えられますが、今回紹介するアプリケーションは以下のような構成になっています。

functions/ # functionsパッケージとして、この中に実装していく
gcloudf.go
Makefile

リポジトリの直下に置いているgcloudf.goの実装は以下のようになっています。

package gcloudf

import (
    "net/http"

    "github.com/catatsuy/example/functions"
)

func PurgeCache(w http.ResponseWriter, r *http.Request) {
    functions.PurgeCache(w, r)
}

このPurgeCache関数はgcloud経由で指定されるエントリーポイントとして使用されます。以下のように--entry-pointで指定します。

gcloud functions deploy <URLに含まれる名前> --project <プロジェクト名> --runtime go111 --entry-point PurgeCache --trigger-http --region <リージョン名>

Makefileに関してはこれから説明していきます。

デプロイ前にミスに気付きたい

完璧そうに見えるGoogle Cloud Functionsですが、唯一欠点を上げるとすると、構成にもよりますがデプロイに数分かかるところが挙げられます。 それに加えて、そもそもコンパイルできないコードをデプロイした場合でもエラーが出るまでにしばらく時間がかかります。コンパイルできないコードならすぐにエラーにしたいところです。今回Makefileを使ってコマンドを実行しているので、以下のようにしています。

export GO111MODULE=on

.PHONY: check_for_deploy
check_for_deploy:
    go build gcloudf.go

.PHONY: deploy_slack
deploy_slack: check_for_deploy
    gcloud functions deploy <URLに含まれる名前> --project <プロジェクト名> --runtime go111 --entry-point PurgeCache --trigger-http --region <リージョン名>

Google Cloud FunctionsではGo Modulesを使うことが前提になっているので、GO111MODULE=onを最初に設定しておきます。gcloudf.goにはmainパッケージのmain関数は含まれていないのでgo buildしても何も生まれません。 しかしコンパイルできないコードならばgo buildできません。そのgo buildをcheck_for_deployという名前で作っておき、デプロイ自体はその依存という扱いにしています。こうすることでコンパイルできないコードをデプロイしてしまうことを防ぐことができます。

環境変数でトークンを扱う

一般に、CDNのキャッシュを削除する際はAPIトークンが必要になります。 Google Cloud Functionsではそのような情報を環境変数で扱います。詳しくは以下のドキュメントを参考にしてください。

https://cloud.google.com/functions/docs/env-var

リポジトリ内で扱う場合はyaml形式で設定ファイルを置き、そのファイルをアップロードすることができます。そのリポジトリを暗号化するのに今回はansible-vaultを使って以下のようにしました。

.PHONY: upload_env_slack
upload_env_slack: env.yml
    gcloud functions deploy <URLに含まれる名前> --project <プロジェクト名> --env-vars-file env.yml --runtime go111 --entry-point PurgeCache --trigger-http --region <リージョン名>

env.yml: env.yml.vault
    ansible-vault view --vault-password-file ~/.vault_password env.yml.vault > env.yml

暗号化したファイルをenv.yml.vaultというファイルでリポジトリに含んでおき、env.ymlは.gitignoreに入れておきます。Makefileの依存として扱うことでmake upload_env_slackを実行するだけで手元にファイルがなければ自動で作った上でアップロードをしてくれます。

init関数と環境変数の読み込みについて

Google Cloud Functionsではinit関数を使うことで初期化を行うことができます。ドキュメントではAPIクライアントの作成やデータベースアクセスの構成に使うことが例示されています。

https://cloud.google.com/functions/docs/concepts/go-runtime

先程紹介した環境変数でトークンを渡す方法ですが、環境変数の更新とデプロイは別のタイミングで行いたいこともあります。なのでコード上でinit関数を使って起動時に環境変数を読み込むコードにしていると、環境変数の更新をしても反映されないことがあります。 その場合は空デプロイを行えば更新することができますが、環境変数のような変わりうる値を取得する処理はリクエストの度に行う方がおすすめです。

工夫と乱用を防ぐための取り組み

SlackのSlash Commandsは使用するチャンネルを制限する方法はありません。しかしSlackからのリクエストにchannel_idchannel_nameが入っているので、そちらをチェックすることで使用するチャンネルを制限しています。

また弊社では複数のCDNを利用しています。そこで弊社が利用しているCDNとドメインのリストをアプリケーション側に持っておき、その中にないドメインが渡された場合はエラー扱いにしています。これにより利用者側はどのCDNを利用しているのか意識すること無くキャッシュから削除できますし、乱用も防げます。

後は各CDNのAPIを適切に使用すればCDNのキャッシュを削除することができます。

最後に

Google Cloud Functionsを使ってSlackで簡単にCDN上のキャッシュを消せるようにする方法を紹介しました。本当に関数をそのままデプロイできますし、メンテナンスも基本的には必要ないのでとても便利です。この記事を参考にして使ってみてもらえると嬉しいです。