0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloud Run functions × TypeScript 開発をしよう!Advent Calendar 2024

Day 16

Cloud Run functions × TypeScript 開発をしよう!【セキュリティ:Cloud Endpoints を利用してAPIキーでの認証を付与する】

Last updated at Posted at 2024-12-22

はじめに

この記事では以下のことについて説明しています。

  • 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を設定する方法はざっくり、

  1. Cloud Run上に ESPv2 コンテナを起動させる
  2. その起動した Cloud Run のサービスに来るリクエストを傍受する Cloud Endpointsのサービスを立ち上げる
  3. Cloud Run のリクエストを Cloud Run functions に横流しする

という流れです。

Cloud Run をAPIゲートウェイとして機能するサーバーにして、APIリクエストを受信したら認証、モニタリング、ロギングなどの処理を行い、リクエストをバックエンドサービス(Cloud Run functions)へ渡すという構成です。

Cloud Endpointsはこの Cloud Run に来たリクエストの情報をまとめて、コンソールの [エンドポイント/サービス]で見られるようにしてくれます。ついでにAPIキーも設定できるようにしてくれます。

espv2-serverless-cloud-functions.png

引用:https://cloud.google.com/endpoints/docs/openapi/set-up-cloud-functions-espv2

前準備

必要なAPIの有効化

gcloud コマンドではなく Terraform によって構築していくので、一部GCPのAPIを手動で有効にする必要があります。gcloud コマンドで有効にしましょう。

  1. Service Control API
  2. Service Management API
  3. 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-backendbackend に記述
    • Terraform のリソース作成時に書き換えるため ${CLOUD_RUN_FUNCTIONS_HOST} と書いておく
    • {リージョン名}-{プロジェクトID}-.cloudfunctions.net/関数名 の形式のものがここに入る
openapi-functions.yml
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 の形式です。

servicemanagement, servicecontroller と Cloud Run functions 呼び出しのIAMポリシーを追加する
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_resourcetriggers で指定した内容が変更されたときにのみ再実行される仕組みになっているため、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

ESPv2 イメージビルド
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 で実行順を指定
APIゲートウェイ(Cloud Run)の作成
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.tfterraform 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 サービスのエンドポイント]

コンソール画面をみると、ちゃんと反映されています。

スクリーンショット 2024-12-23 0.24.39.png

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

ここの運用はイケていない気がするのでなんとかしたさはありますね。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?