🐁

Go ランタイム Cloud Functions でメタデータサーバーからプロジェクトIDを取得しよう

2023/09/17に公開

はじめに

Cloud Functions は下記に示すドキュメントにて確認できるようにランタイム環境変数が自動的に設定されています。

https://cloud.google.com/functions/docs/configuring/env-var?hl=ja#runtime_environment_variables_set_automatically

しかし、プロジェクトIDは設定されていません。Google Cloud 内のほかサービスを利用する際にはプロジェクトIDは必要となります。

例) Cloud Pub/Sub でメッセージをトピックへパブリッシュする

https://cloud.google.com/pubsub/docs/publisher?hl=ja

https://github.com/GoogleCloudPlatform/golang-samples/blob/993a6162d95844e06564b429034b39f6da7dff72/pubsub/topics/publish_single.go

ランタイム環境変数はデプロイ時などに設定できるのでプロジェクトIDも設定すればいいかと思っていましたが、

https://cloud.google.com/functions/docs/configuring/env-var?hl=ja#setting_runtime_environment_variables

Google Cloud Professional Cloud Developer(PCD) の勉強をしている際に VM メタデータ という存在を知りました。

G.I.G. に参加して Professional Cloud Developer を取得しました!

どうやらこれを利用すればプロジェクトIDを取得できるとのことなので Cloud Functions での利用について確認してみました。

VM メタデータ

VM メタデータ とはすべての仮装マシン(VM)インスタンスに保存されているメタデータのことです。

https://cloud.google.com/compute/docs/metadata/overview?hl=ja

デフォルトのVM メタデータは以下にて参照できます。

https://cloud.google.com/compute/docs/metadata/default-metadata-values?hl=ja

すべての仮想マシンということは Cloud Functions でも使えそうですね。
ということで Go で実装して確認してみます。

実行環境

uname -a
Darwin MacBook-Pro-7.local 22.6.0 Darwin Kernel Version 22.6.0: Wed Jul  5 22:22:52 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8103 arm64
go version
go version go1.21.1 darwin/arm64
docker version
Client:
 Cloud integration: v1.0.35-desktop+001
 Version:           24.0.5
 API version:       1.43
 Go version:        go1.20.6
 Git commit:        ced0996
 Built:             Fri Jul 21 20:32:30 2023
 OS/Arch:           darwin/arm64
 Context:           desktop-linux

Server: Docker Desktop 4.22.1 (118664)
 Engine:
  Version:          24.0.5
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.6
  Git commit:       a61e2b4
  Built:            Fri Jul 21 20:35:38 2023
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
gcloud version
Google Cloud SDK 437.0.1
bq 2.0.93
core 2023.06.30
gcloud-crc32c 1.0.0
gsutil 5.24

Go で実装

せっかくなので最近リリースされた gonew を使ってみます。
以下のコマンドにて Cloud Functions 用のテンプレートをもとにプロジェクトを新規作成します。

gonew github.com/GoogleCloudPlatform/go-templates/functions/httpfn github.com/otakakot/research-cloud-functions-metadata
gonew した直後の実装
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package helloworld

import (
	"fmt"
	"net/http"

	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

func init() {
	functions.HTTP("HelloHTTP", helloHTTP)
}

// helloHTTP is an HTTP Cloud Function.
func helloHTTP(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	if name == "" {
		name = "World"
	}
	fmt.Fprintf(w, "Hello, %s!", name)
}

go.mod のバージョンが go 1.20 だったので go 1.21 に更新しておきます。

go mod edit -go=1.21.1

動作確認のため、この状態でデプロイしてみます。

gcloud functions deploy hello-http \
	--gen2 \
	--runtime=go121 \
	--region=asia-northeast1 \
	--source=. \
	--entry-point=HelloHTTP \
	--project=${project_id} \
	--trigger-http \
	--allow-unauthenticated

※ README.md では us-central1 が指定されていたが asia-northeast1 に変更
※ 動作確認したいだけなので未認証を許可

デプロイに成功したので動作確認のためアクセスしてみます。

curl -i https://asia-northeast1-${project_id}.cloudfunctions.net/hello-http
HTTP/2 200
content-type: text/plain; charset=utf-8
x-cloud-trace-context: 3618b0d91492a6d57f54de85b5eb683d;o=1
date: Xxx, xx Xxx 20xx xx:xx:xx GMT
server: Google Frontend
content-length: 13
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Hello, World!%

無事にレスポンスがありました。
こちらを修正して実装を進めていきます。

本題のメタデータを取得するためにピッタリのライブラリを発見したのでそちらを使います。

https://github.com/googleapis/google-cloud-go/blob/main/compute/metadata/metadata.go

実装を以下のように修正します。

package helloworld

import (
	"fmt"
	"net/http"

	"cloud.google.com/go/compute/metadata"
	"github.com/GoogleCloudPlatform/functions-framework-go/functions"
)

func init() {
	functions.HTTP("HelloHTTP", helloHTTP)
}

// helloHTTP is an HTTP Cloud Function.
func helloHTTP(w http.ResponseWriter, r *http.Request) {
	pid, err := metadata.ProjectID()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)

		return
	}

	fmt.Fprintf(w, "Hello, %s!", pid)
}

再デプロイしアクセスしてみます。

curl -i https://asia-northeast1-${project_id}.cloudfunctions.net/hello-http
HTTP/2 200
content-type: text/plain; charset=utf-8
x-cloud-trace-context: e5bc5194da75e9aa1b513bdd571c4a03;o=1
date: Xxx, xx Xxx 20xx xx:xx:xx GMT
server: Google Frontend
content-length: 32
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Hello, ${project_id}!%

無事にレスポンスにてプロジェクトIDを取得することができ、メタデータからプロジェクトIDを取得できていることを確認できました。
便利ですね。これならデプロイ時に環境変数としてプロジェクトIDを指定しなくてよいです。

ローカル開発

ライブラリを利用して簡単に VM メタデータを使ってプロジェクトIDを取得することができました。
しかし、ローカル環境でこちらを動かしてみたらどうなるでしょうか?
以前以下の記事にてローカル環境における Cloud Functions 開発環境を考えてみましたが VM メタデータ はどうすればよいでしょうか。

Go ランタイム Cloud Functions の開発環境を考える

ローカル環境での動作確認なのでとりあえず実行してみます。

curl -i http://localhost:8080
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Xxx, xx Xxx 20xx xx:xx:xx GMT
Content-Length: 125

Get "http://169.254.169.254/computeMetadata/v1/project/project-id": dial tcp 169.254.169.254:80: connect: connection refused

失敗しました。
利用しているライブラリの実装を確認すると

https://github.com/googleapis/google-cloud-go/blob/main/compute/metadata/metadata.go#L39

だったりでアクセス先が決め打ちで設定されているのでローカルではアクセスできなくて当然ですね。

じゃあ、どうやって解決しよう ... DIですね。

以下のようにインターフェイスを用意して実行環境ごとに切り替える形にしてみました。

package config

import (
	"context"
	"fmt"

	"cloud.google.com/go/compute/metadata"

	"github.com/otakakot/research-cloud-functions-metadata/env"
)

type Project interface {
	GetID(context.Context) (string, error)
}

func NewProject(env env.Env) Project {
	if env.IsLocal() {
		return LocalProject{}
	}

	return CloudProject{}
}

type LocalProject struct{}

func (lp LocalProject) GetID(_ context.Context) (string, error) {
	return "local", nil
}

type CloudProject struct{}

func (cp CloudProject) GetID(_ context.Context) (string, error) {
	pid, err := metadata.ProjectID()
	if err != nil {
		return "", fmt.Errorf("failed to get project id: %w", err)
	}

	return pid, nil
}

ローカルへアクセスします。

curl -i http://localhost:8080
HTTP/1.1 200 OK
Date: Xxx, xx Xxx 20xx xx:xx:xx GMT
Content-Length: 13
Content-Type: text/plain; charset=utf-8

Hello, local!%

クラウド環境へアクセスします。

curl -i https://asia-northeast1-${project_id}.cloudfunctions.net/hello-http
HTTP/2 200
content-type: text/plain; charset=utf-8
x-cloud-trace-context: ed2a2bc10a2eb84408ce85397e743a1f;o=1
date: Xxx, xx xxx 20xx xx:xx:xx GMT
server: Google Frontend
content-length: 32
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

Hello, ${project_id}!%

狙い通り切り替わっていることが確認できました!

知りたい

なぜ環境変数からではなく、VM メタデータサーバーからデータを取得する必要があるのか明確な根拠がわかっていないので知りたいです。
セキュリティ的に懸念があるのだろうとは思っていますが ...

おわりに

お気づきかと思いますが、最初に示した Cloud Pub/Sub を利用するサンプルコードではプロジェクトID以外にトピックIDも必要となっています。せっかくプロジェクトIDを環境変数以外から取得したのでトピックIDも環境変数以外から取得したいですね。
しかし、どうやったら設定・取得できるのでしょうか ...

いろいろやりようはありそうです。どの方法がよいのかそのうち調査してみます。

今回利用したサンプルコードは以下に置いておきます。

https://github.com/otakakot/research-cloud-functions-metadata

Discussion