はじめに
この記事では以下のことについて説明しています。
- Cloud Run functions用の Cloud Endpoints を構成する方法
- Terraformで Cloud Endpoints を構成する方法
Cloud Run Functionsで作成したAPIを例えば以下のようなケースで利用しようと想定しています。
- 公開APIで使ってもらいたい
- 公開にはするものの誰でも無制限にリクエストを送信できる状況は阻止したい
- 認証を通したユーザでも1日に何回も呼べる状況にはしたくない
こういうニーズにとてもぴったりなサービスがあります。Cloud Endpoints です。
Cloud Endpointsとは
APIのAPIキーによる保護、モニタリング、分析、使用量の上限設定を行えるようにするAPIのマネジメントツールです。
これをCloud Run functionsと組みわせて使う方法について書いていきます。以下の記事に大体書いてありますが、本記事ではカスタムドメインを設定したものを構成していこうと思います(ドメインはお名前.comで事前に取得したものを使います)。
Cloud Run funcitions に Cloud Endpoints を設定する流れ
Cloud Run funcitions にCloud Endpointsを設定する方法はざっくり、
- Cloud Run上に ESPv2 コンテナを起動させる
- その起動した Cloud Run のサービスに来るリクエストを傍受する Cloud Endpointsのサービスを立ち上げる
- Cloud Run のリクエストを Cloud Run functions に横流しする
という流れです。
Cloud Run をAPIゲートウェイとして機能するサーバーにして、APIリクエストを受信したら認証、モニタリング、ロギングなどの処理を行い、リクエストをバックエンドサービス(Cloud Run functions)へ渡すという構成です。
Cloud Endpointsはこの Cloud Run に来たリクエストの情報をまとめて、コンソールの [エンドポイント/サービス]で見られるようにしてくれます。ついでにAPIキーも設定できるようにしてくれます。
引用:https://cloud.google.com/endpoints/docs/openapi/set-up-cloud-functions-espv2
前準備
必要なAPIの有効化
gcloud コマンドではなく Terraform によって構築していくので、一部GCPのAPIを手動で有効にする必要があります。gcloud コマンドで有効にしましょう。
- Service Control API
- Service Management API
- Cloud Endpoins API
gcloud services enable servicemanagement.googleapis.com
gcloud services enable servicecontrol.googleapis.com
gcloud services enable endpoints.googleapis.com
APIゲートウェイとして起動させる ESPv2 Cloud Run サービスを事前に作っておく
Cloud Endpointsを構成する上で事前にAPIゲートウェイとして機能させる ESPv2 Cloud Run サービスのホスト名が必要になってきます。なので事前に gcloud コマンドで作成しておきます。
gcloud run deploy gateway \
--image="gcr.io/cloudrun/hello" \
--allow-unauthenticated \
--platform managed \
--project=プロジェクト名 \
--region=asia-northeast1
これを実行すると出力される [Service URL] を控えておきましょう。
Deploying container to Cloud Run service [gateway] in project [advent-calendar-2024-w] region [asia-northeast1]
✓ Deploying new service... Done.
✓ Creating Revision...
✓ Routing traffic...
✓ Setting IAM Policy...
Done.
Service [gateway] revision [gateway-00001-pv2] has been deployed and is serving 100 percent of traffic.
Service URL: https://xxx.a.run.app # <- これを控えておく
また、この出力されたドメインのEndpoints サービスを有効化しておきます。
gcloud services enable xxx.a.run.app
Terraform に Local Values を追加
そして Local Values を tfファイルに定義しておきます。事前に作成した ESPv2 Cloud Run サービスのホスト名を gateway_domain
に定義します。また、後ほど使用する ESPv2イメージのバージョンを固定しておきます。
locals {
project = "プロジェクトID"
region = "asia-northeast1"
zone = "asia-northeast1-a"
gateway_domain = "事前に作成した Cloud Run のホスト名"
ESPv2_image_ver = "2.51.0" # バージョン固定
}
1. Cloud Endpoints を構成する
OpenAPI 仕様 v2.0 に準拠した、関数の surface と認証要件を記述した OpenAPI ドキュメントを用意します。ルートディレクトリに openapi-functions.yml
を作成します。そして以下の必須項目を満たすようにします。
-
host
の項目に、事前準備で作成した ESPv2 サービスのホスト名で書き替えるため${CLOUD_RUN_HOST}
と記載 - APIキーの認証設定
securityDefinitions
ディレクティブを設定 -
security ディレクティブに
api_key: []` を設定 - Cloud Run functions のエンドポイントを
x-google-backend
のbackend
に記述- Terraform のリソース作成時に書き換えるため
${CLOUD_RUN_FUNCTIONS_HOST}
と書いておく -
{リージョン名}-{プロジェクトID}-.cloudfunctions.net/関数名
の形式のものがここに入る
- Terraform のリソース作成時に書き換えるため
swagger: '2.0'
info:
title: Cloud Endpoints + GCF
description: Sample API on Cloud Endpoints with a Google Cloud Functions backend
version: 1.0.0
host: ${CLOUD_RUN_HOST}
schemes:
- https
produces:
- application/json
securityDefinitions: # API キーの定義の制限事項
api_key:
type: "apiKey"
name: "x-api-key"
in: "header"
security: # これによりAPI全体に認証がかかる
- api_key: []
paths:
/hello:
get:
summary: Greet a user
operationId: hello
x-google-backend:
address: ${CLOUD_RUN_FUNCTIONS_HOST}
protocol: h2
responses:
'200':
description: A successful response
schema:
type: string
参考
2. Cloud Endpointsのリソースを定義
事前に作成した ESPv2 Cloud Run サービスのホスト名を service_name
に設定し、templatefile
を利用して openapi_config
にて yml に引数を渡して、リソース作成後に書き換えてもらいます。
resource "google_endpoints_service" "openapi_service" {
service_name = local.gateway_domain
project = local.project
openapi_config = templatefile("${path.module}/openapi-functions.yml", {
CLOUD_RUN_HOST = local.gateway_domain
CLOUD_RUN_FUNCTIONS_HOST = google_cloudfunctions2_function.default.service_config[0].uri
})
}
3. ESPv2 サービスのランタイムサービスアカウントに必要なIAMポリシーを追加する
Service Management と Service Control を呼び出す権限を ESPv2 サービスアカウントに追加します。
ランタイムサービスアカウントは {プロジェクト番号}-compute@developer.gserviceaccount.com
の形式です。
resource "google_project_iam_member" "espv2_service_account_service_controller" {
project = local.project
role = "roles/servicemanagement.serviceController"
member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
}
resource "google_project_iam_member" "espv2_service_account_function_invoker" {
project = local.project
role = "roles/cloudfunctions.invoker"
member = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
}
4. ESPv2 イメージをビルドする
公式は動的に値を設定しようとしてややこしい手順になっています。なのでこちらで ESPv2 Cloud Run サービスに使うイメージのバージョンは固定してあげます。
注意点
-
google_endpoints_service
リソースが作成されないと取得できない情報があるので、depends_on
に指定しておきます。 -
openapi.yml
の情報を編集すると Cloud Endpoints の config_id が変更されるため、ESPv2 Cloud Run サービス用のコンテナイメージの再ビルドを行わないといけません。しかしnull_resource
はtriggers
で指定した内容が変更されたときにのみ再実行される仕組みになっているため、triggers
で config_id の指定をする必要があります。 -
null_resource
が追加されたことでterraform plan
をするとError: Inconsistent dependency lock file
が発生すると思います。
$ terraform plan
╷
│ Error: Inconsistent dependency lock file
│
│ The following dependency selections recorded in the lock file are inconsistent with the current configuration:
│ - provider registry.terraform.io/hashicorp/null: required by this configuration but no version is selected
│
│ To update the locked dependency selections to match a changed configuration, run:
│ terraform init -upgrade
nullプロバイダーを初めて使う際に出てくるエラーです。以下を実行しましょう。
terraform init -upgrade
ESPv2 Cloud Run サービスのイメージビルド null_resource
resource "null_resource" "building_new_image" {
triggers = {
config_id = google_endpoints_service.openapi_service.config_id
cloud_run_hostname = google_endpoints_service.openapi_service.service_name
}
provisioner "local-exec" {
command = "chmod +x gcloud_build_image; ./gcloud_build_image -s $cloud_run_hostname -c $config_id -p ${local.project} -v ${local.ESPv2_image_ver}"
environment = {
config_id = google_endpoints_service.openapi_service.config_id
cloud_run_hostname = google_endpoints_service.openapi_service.service_name
}
}
depends_on = [
google_endpoints_service.openapi_service
]
}
5. ビルドしたイメージで事前に作っておいた ESPv2 Cloud Run サービスを更新する
あらかじめ作成しておいた ESPv2 Cloud Run サービス を、先ほどビルドしたイメージで更新します。先ほど null_resource
で作成したイメージを使用するようにします。
ただし gcloud で作成したので リソースが Terraform管理下になっておりません。このまま terraform apply
をすると同じ名前の Cloud Runサービスを作ろうとして、結果重複作成のエラーが発生します。
なので事前に作成した Cloud Run サービスをTerraform の管理下に移行する必要があります。
terraform import google_cloud_run_v2_service.gateway \
projects/プロジェクト名/locations/リージョン/services/[Cloud Run サービス名]
これを実行することで、Cloud Run サービスが重複作成されず、既存の ESPv2 Cloud Run サービスのイメージが更新されるようになります。
Plan: 13 to add, 1 to change, 0 to destroy.
ここで作成する ESPv2 Cloud Run サービスのリソースは以下を設定しています。
-
name
は事前に作成した ESPv2 Cloud Run サービスの名称と同じにする - リクエスト時のみに起動する
- 認証なし
-
depends_on
で実行順を指定
resource "google_cloud_run_v2_service" "gateway" {
name = "gateway"
location = local.region
deletion_protection = false
template {
containers {
# null_resource で作成したイメージを使用
image = format(
"gcr.io/%s/endpoints-runtime-serverless:%s-%s-%s",
local.project,
local.ESPv2_image_ver,
google_endpoints_service.openapi_service.service_name,
google_endpoints_service.openapi_service.config_id
)
# 起動設定
resources {
limits = {
"cpu" = "1"
"memory" = "1Gi"
}
cpu_idle = true
startup_cpu_boost = false
}
}
}
depends_on = [
google_endpoints_service.openapi_service,
null_resource.building_new_image
]
}
#
resource "google_cloud_run_v2_service_iam_binding" "binding" {
project = local.project
location = google_cloud_run_v2_service.gateway.location
name = google_cloud_run_v2_service.gateway.name
role = "roles/run.invoker"
members = [
"allUsers"
]
}
6. いざ terraform apply
完成した main.tf
は以下のレポジトリに載せています。
ここまで作成した main.tf
で terraform apply
をすると、以下のURLが出力されます。
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
function_uri = "https://xxx.a.run.app"
gateway_uri = "https://yyy.a.run.app"
ここで出力された gateway_uri
に出力されたURLを呼び出してみます。
$ curl https://yyy.a.run.app
{"message":"The current request is not defined by this API.","code":404}
おっアクセスできないようになっていますね。openapi-functions.yml
で /hello
パスに Cloud Run functions のURLのマッピングを行っているので、このURLではアクセスできないようになっています。
と言うことで https://yyy.a.run.app/hello
で呼び出してみましょう。
$ curl https://yyy.a.run.app/hello
{"message":"UNAUTHENTICATED: Method doesn't allow unregistered callers (callers without established identity). Please use API Key or other form of API consumer identity to call this API.","code":401}
APIキーをヘッダーに入れていないので認証エラーがちゃんと出ました。
7. APIキーの発行
今度は APIキーを発行します。APIキーの発行は Terraform で行うと面倒くさそうなので gcloud コマンドで行います。
以下の手順を踏みます。
- キーの発行
- キーに制限をかける
キーの発行
test
という名前でキーを発行します。
gcloud services api-keys create --display-name=test
キーが作成されたか確認します。
gcloud services api-keys list
ここで作成したキーの uid
を控えます。
キーに制限をかける
APIキーはどこからでも利用できたらいけません。ESPv2 Cloud Run サービスのエンドポイント(Terraform の ouput で出力される ESPv2 Cloud Run サービスのURL)でのみ使用できるようにします。
その他に利用されるIPやリファラー、プラットフォームの制限もかけられますが、今回は行いません。
gcloud services api-keys update APIキーID \
--api-target=service=[ESPv2 Cloud Run サービスのエンドポイント]
コンソール画面をみると、ちゃんと反映されています。
8. APIキーを使って呼び出してみる
あとは APIキーをヘッダーに含めて読んでみます。
$ curl -H "x-api-key:APIキー" https://yyy.a.run.app/hello
Hello, World!
無事、呼び出すことができました。
参考
おわりに
本記事ではAPIキー認証をCloud Run functionsのエンドポイントに付与する方法を紹介しました。公式のやり方に則って、一回事前に ESPv2 Cloud Run サービスを作っておく方法で行いましたが、カスタムドメインで行う方法の方がラクです。次回以降の記事で紹介したいと思います。
おまけ
terraform destroy
をして再度 terraform apply
しようとしてもで Cloud Endpointsの再生成に関するエラーが発生すると思います。Cloud Endpoints サービスは削除後30日間、同じ名前のサービスを作成できなくなります。
再生成したい場合は以下を実行してあげましょう。
gcloud endpoints services undelete ドメイン名 --project=プロジェクトID
ここの運用はイケていない気がするのでなんとかしたさはありますね。