Google Cloud Run + Google Cloud FireStore + Firebase AuthenticationとGoで作る認証付きデータ保存、取得API


Firebase + Google Cloud Functions を使った認証 API の作成を以前紹介しました。

Google Cloud Functions + Google Cloud FireStore + Firebase Authentication と Go で作る認証付きデータ保存、取得 API

これを作成中、docker コンテナそのまま deploy できたら楽なのにという気持ちがありました。とはいえ、個人の遊びで kubernetes はお金の面で厳しく、AWS の Fargate という選択肢はあるものの AWS は個人利用の範囲で使った限り、GCP より高めに請求がくることもしばしば。

そんな時、GCP にも Cloud Run という docker コンテンを deploy して動かせるサービスが始まっているのを知りました。現在 beta 版なのでサービス開始後に料金がどうなるかは不明ですが、とてもおもしろそうに見えます。前回 Cloud Functions で動かしたものをそのまま Gloud Run で動かしてみました。

前提

GCP や Firebase の 登録。Firebase の Authentication の設定、それにアクセスするフロント側、あるいは API テスト用のアプリケーションなどは作成済みとします。

Google Cloud Functions + Google Cloud FireStore + Firebase Authentication と Go で作る認証付きデータ保存、取得 API

上記の記事で自分の環境は紹介しています。

実際のコード

Cloud Run のサンプルコードを参考にして、前回の Functions で使用したコードをほぼそのまま使用しています。Firebase の設定情報と GCP のプロジェクト ID をコード中に入れます。前回は環境変数からとっていましたが今回はコード中でそのまま使用しています。

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go"
)

func handler(w http.ResponseWriter, r *http.Request) {
	log.Print("Hello world received a request")
	target := os.Getenv("TARGET")
	if target == "" {
		target = "World"
	}
	fmt.Fprintf(w, "Hello %s!\n", target)
}

func auth(w http.ResponseWriter, r *http.Request) bool {
	w.Header().Set("Access-Control-Allow-Origin", "*")
	w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept,Authorization")

	idToken := r.Header.Get("Authorization")
	splitToken := strings.Split(idToken, "Bearer")
	if len(splitToken) != 2 {
		return false
	}

	idToken = strings.TrimSpace(splitToken[1])
	ctx := context.Background()

  // firebase設定
	conf := &firebase.Config{
		ServiceAccountID: "",
		ProjectID:        "",
	}
	app, err := firebase.NewApp(context.Background(), conf)
	if err != nil {
		return false
	}
	authClient, err := app.Auth(context.Background())
	if err != nil {
		return false
	}

	_, err = authClient.VerifyIDToken(ctx, idToken)
	if err != nil {
		return false
	}
	return true
}

type User struct {
	First string `firestore: "first"`
	Last  string `firestore: "last"`
	Born  int    `firestore:"born"`
}

func AddUser(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	authCheck := auth(w, r)
	if authCheck != true {
		res := map[string]interface{}{
			"status":  "ng",
			"message": "認証エラー",
		}
		json.NewEncoder(w).Encode(res)
		return
	}
	first := r.FormValue("first")
	last := r.FormValue("last")
	born, err := strconv.Atoi(r.FormValue("born"))
	if err != nil || first == "" || last == "" {
		res := map[string]interface{}{
			"status":  "ng",
			"message": err,
		}
		json.NewEncoder(w).Encode(res)
		return
	}

	user := User{
		First: first,
		Last:  last,
		Born:  born,
	}

	// GCPのfirestoreをおいているプロジェクトid
	projectID := ""
	ctx := context.Background()
	client, err := firestore.NewClient(ctx, projectID)
	if err != nil {
		res := map[string]interface{}{
			"status":  "ng",
			"message": "書き込みに失敗しました",
		}
		json.NewEncoder(w).Encode(res)
		return
	}
	defer client.Close()
	_, _, err = client.Collection("users").Add(ctx, user)
	if err != nil {
		fmt.Fprint(w, err)
		return
	}
	res := map[string]interface{}{
		"status": "ok",
	}
	json.NewEncoder(w).Encode(res)
	return
}

func main() {
	log.Print("Hello world sample started")

	http.HandleFunc("/", handler)
	http.HandleFunc("/addUser", AddUser)

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}

dockerfile はサンプルコードに get 1つ足したものです。multistage build は古い docker だと動きません、エラーがでる場合は docker をアップデートしてください。

FROM golang:1.12 as builder

WORKDIR /go/src/github.com/knative/docs/helloworld
COPY . .
RUN go get firebase.google.com/go
RUN CGO_ENABLED=0 GOOS=linux go build -v -o helloworld

FROM alpine
RUN apk add --no-cache ca-certificates
COPY --from=builder /go/src/github.com/knative/docs/helloworld/helloworld /helloworld

CMD [ "/helloworld" ]

GCP 側の設定

課金を有効にし、サンプルコード通りなら cloud Run だけでなく Cloud build も有効にします。ローカルに gcloud をインストールしていなけれがインストールし、ターミナルから gloud に login 及びプロジェクト設定を行います。

Cloud build に docker イメージをアップロードして Cloud Run に deploy

ローカルで作成した go の実行ファイル入りの docker イメージを cloud build に登録します。登録したら、そのイメージを Cloud Run に deploy します。

gcloud builds submit --tag gcr.io/projectID/helloworld
gcloud beta run deploy --image gcr.io/projectID/helloworld --platform managed

dockerhub からでもいけそうですね。location は northasia、名前は指定せずに Enter。最初失敗しましたが、2 回目に同じコマンドを打って成功しました。

deploy に成功すると URL が表示されるので

Google Cloud Functions + Google Cloud FireStore + Firebase Authentication と Go で作る認証付きデータ保存、取得 API

ここで作成したフロントの API URL をそれに変更して動作チェックしました。

便利

Cloud Functions のコードほぼそのままコピーするだけで動作しました。Cloud Functions よりもローカル開発がしやすく、docker コンテナなので何でも動かせるのが素晴らしいですね。次はフロント入の PHP のアプリケーションを動かしてみるつもりです。