みなさん、こんにちは!S.Kです!

3年ぐらい前にChatworkの記事を書かせていただきましたが、そこから歳月が経ち、現在はVueとFirebase、時々Golangで仕事をしています!
今週がんばって習得したものがあり、せっかくなのでみなさまにお披露目したいと思います!

今回は
GolangでRemote Configを操作してみよう!
がテーマです!なんと日本語で書かれた記事が見当たらないので、もしかしたらこれが日本初!?(すでにありましたらすみません。)

 

RemoteConfigとは

アプリのアップデートを公開しなくても、アプリの動作と外観を変更できます。コストはかからず、1 日あたりのアクティブ ユーザー数の制限もありません。

とこちらの公式ドキュメント
https://firebase.google.com/docs/remote-config?hl=ja
にはありますが、
簡単に言えば設定内容などをクラウド上に保存しておく仕組みです。
特に、

コストはかからず

弊社の一部製品でこのRemoteConfigを導入する上で、これが重要なのです!

「単純にクラウドにデータを保管しておきたいならCloud Firestoreでもいいじゃないか」と・・・思うじゃないですか。
しかし、Cloud Firestoreには読み込みや書き込み、削除などアクセスの積み重ねが大きなコストになります。
Firebaseを扱う上では、如何に無駄なCloud Firestoreへのアクセスをなくすかを考えて設計・実装しなければなりません。

そこで、我々が注目したのがこのRemote Configです!

上の画像のように、Firebaseのコンソールからそのままデータを変えることもできますし、場合によっては条件をつけることもできるそうです(がここはまだ使ったことがないのでもし後日機会があれば・・・)

ちなみに、上の説明文には

アプリのアップデートを公開しなくても、

とありますが、アプリに限らずWebサービスでも使えます!

 

GolangでRemote Configを操作したい・・・

これだけでもだいぶ便利にはなりますが、中には管理画面みたいなのを作ってそこからRemote Configを操作したい・・・・といった要望もあると思います。

今回、私が関わっているプロジェクトでは、Golangで実装した管理画面でRemoteConfigを操作したいという要件があったので、調査しました!が・・・・


こちらの公式ドキュメント「プログラムで Remote Config を変更する」
https://firebase.google.com/docs/remote-config/automate-rc?hl=ja
にコードごとのっているのはNode.jsとJavaしかない・・・・

Golangではできないの?と思いましたが、
https://pkg.go.dev/google.golang.org/api@v0.45.0/firebaseremoteconfig/v1
こちらにライブラリがあるようです!


が、しかしこの中には日本語の公式ドキュメントほどわかりやすくは書かれていません・・・・
どんな関数が何を引数にして何を返すのか・・・・どの構造体に何の型のデータがあるのか・・・・といったことが書かれていますが、初見でこれを理解するのはとても時間と経験を要します。

 

まず値を取得!

まず、データを取得する方法を探さなければなりません。
となると、データとはなにか、ということを考えましょう。


こちらのValue stringというのが当たりそうですね。このValueとやらはRemoteConfigParameterValueという構造体に含まれているものなので、このRemoteConfigParameterValueを含んでいる場所を探してみましょう。


このRemoteConfigParameter構造体に、DefaultValueという名前のRemoteConfigParameterValueがありますね!このDefaultValueというのが、先程コンソールで設定した値だ、ということがわかりました。
さぁ、このDefaultValueを含むのは、RemoteConfigParameterという構造体なので、これを返したり構造体に含むものはなにか、探してみましょう。


ありましたね!このParametersというものです。
map[string]RemoteConfigParameterとありますが、これはRemoteConfigParameterの型だけで構成される配列のようなもの、と思ってください!

ここまででわかったのは、
RemoteConfigというものを取得しなければならない、ということです。
そろそろ構造体ではなく、これを返す関数を探しましょう。


最初はGetすることが目的です。
今2つ検索結果がでていますが、このうちのProjectsGetRemoteConfigCallというところにフォーカスを当てて見ましょう。

ProjectsGetRemoteConfigCallの中のDoという関数がRemoteConfigを返すようですね。
これを、同じように追ってみましょう。

ProjectsGetRemoteConfigCallを呼び出すにはProjectsServiceにある関数GetRemoteConfigという関数が必要。
ProjectsServiceを呼び出すにはNewProjectServiceという関数が必要で、その関数の引数にServiceという構造体が必要、
そしてServiceはどのように生成するのか・・・・

context := context.Background()

credential, err := google.CredentialsFromJSON(context, []byte(credentialJSON), "https://www.googleapis.com/auth/firebase.remoteconfig")
if err != nil {
  fmt.Println(err)
}
option := option.WithCredentials(credential)

service, err := firebaseremoteconfig.NewService(context, option)
if err != nil {
  fmt.Println(err)
}

このように実装しましょう!

credentialJSONは、Firebaseコンソールの
・プロジェクトの概要
・プロジェクトの設定
・サービスアカウント
という順番にアクセスし、

「新しい秘密鍵の生成」ボタンを押せばダウンロードされるので、そのJSONごとfuncの上にconstとして定義しましょう。

const (
 credentialJSON = `{
   // ここに情報がたくさん書かれている
  "project_id": "今回使うproject_id"
 }`
 yourProjectId = "projects/{{Your Project}}"
)

credentialJSONは機密情報なので取り扱いには注意しましょう!
yourProjectIdは、projects/{あなたのプロジェクトのID}というように設定します。JSON中のproject_idに書いてあるものでOKです!

ここで、 constの上に大事なライブラリをimportします!

import (
 "fmt" //標準出力などで使うもの
 "context" //contextが必要と書かれているライブラリを使うときにはこれを書きましょう

 "golang.org/x/oauth2/google" //認証系に必要
 "google.golang.org/api/option" //serviceの取得に必要
 "google.golang.org/api/firebaseremoteconfig/v1" //今回の肝心なRemoteConfigですね!
)

これで準備は整いました。

なお、関数が返す値として、(xxxxx, error)と定義されているものについては、エラー処理をしておきましょう!

if err != nil {
//エラーが起きたときの処理を書く
}

これで基本的にはOKです!

コード中のサンプルでは何も返さないのでfmt.Println(改行する標準出力)程度でいいと思いますが、もし自分でエラーを返す関数として定義しているときには、エラーを返すようにします。

例えばfunc twoValues() (int, error)というように、intとerrorを返すようにしているのであれば、

if err != nil {
 return 0, err
}

というような処理です。Goで書かれたプログラムは、こういったエラーの連鎖のように書かれた内容が多いです。

取得に必要なコードの全容はこうです!

package main

import (
 "fmt"
 "context"

 "golang.org/x/oauth2/google"
 "google.golang.org/api/option"
 "google.golang.org/api/firebaseremoteconfig/v1"
)

const (
 credentialJSON = `{
   "type": "service_account",
 }`
 yourProjectId = "projects/{{Your Project}}"
)

func get(){
 context := context.Background()

 credential, err := google.CredentialsFromJSON(context, []byte(credentialJSON), "https://www.googleapis.com/auth/firebase.remoteconfig")
 if err != nil {
   fmt.Println(err)
 }
  option := option.WithCredentials(credential)

 service, err := firebaseremoteconfig.NewService(context, option)
 if err != nil {
   fmt.Println(err)
 }

 remoteConfig, err := service.Projects.GetRemoteConfig(yourProjectId).Do()
 if err != nil {
   fmt.Println(err)
 }

 //ここの段階で取得ができますね!
 fmt.Println(remoteConfig.Parameters["myconfig"].DefaultValue.Value)
}

 

値を変更してみよう!

ここからはすでにあるパラメータの値を変更してみましょう!
取得するときにはGetRemoteConfigと使ったので、変更するのであればおそらくupdateであろう・・・・と思ったら、

ここ
(https://github.com/googleapis/google-api-go-client/blob/v0.45.0/firebaseremoteconfig/v1/firebaseremoteconfig-gen.go#L565)
にありましたね!

ところで、ここでお伝えしておきますが、ドキュメントには以下のようにあります。

Hence there are no Create or Delete operations.

つまり、このライブラリでできるのはGetかUpdateのみとなります。

追加や削除はGolangだけではできないようなので、そこは注意してください。

さて、早速値の変更をコードに書いてみましょう。

remoteConfig.Parameters["myconfig"].DefaultValue.Value = "Hello New World"

“myconfig”にはパラメータ名を記載し、右辺には変更したい内容を書きましょう。

そしていよいよUpdateの実行に入ります。
UpdateRemoteConfigは見るとわかりますが、
呼び出すためにはRemoteConfigが必要になります。さらに、Serviceも必要みたいなので、このように書きましょう。

updateRemoteConfig := service.Projects.UpdateRemoteConfig(yourProjectId, remoteConfig)

どちらにしろ、Updateをするためには既存のRemoteConfigが必要みたいなので、先程のRemoteConfigを取得するコードが利用できますね!

さぁ、あとはupdateRemoteConfigをDoすれば行ける!と思ったのですが・・・・

No If-Match header found on UpdateRemoteConfig call, failedPrecondition

エラーが出てきてしまいました。
私、実はここで一番詰まったと言っても過言ではありません。

Golangを弊社に持ってきた我らのチーフエンジニア力を少しだけ借りて、このように解決しました。

updateRemoteConfig.Header().Add("If-Match", "*")

Headerにこのようなおまじないをかけるとアップデートできるようになりました!あとは、

updatedRemoteConfig, err := updateRemoteConfig.Do()
if err != nil {
  fmt.Println(err)
}

本当にDoするだけですね!

すると・・・・・


書き換わりましたね!

 

コードまとめ

今回の全コードをまとめておきます!

package main

import (
    "context"
    "fmt"

    "golang.org/x/oauth2/google"
    "google.golang.org/api/firebaseremoteconfig/v1"
    "google.golang.org/api/option"
)

const (
    credentialJSON = `{
        "type": "service_account",
        "project_id": "{{Your Project}}",
        //あとは諸々機密情報がここに書いてあります。
    }`
    yourProjectId = "projects/{{Your Project}}"
)

func get() {
    context := context.Background()

    credential, err := google.CredentialsFromJSON(context, []byte(credentialJSON), "https://www.googleapis.com/auth/firebase.remoteconfig")
    if err != nil {
        fmt.Println(err)
    }

    option := option.WithCredentials(credential)

    service, err := firebaseremoteconfig.NewService(context, option)
    if err != nil {
        fmt.Println(err)
    }

    remoteConfig, err := service.Projects.GetRemoteConfig(yourProjectId).Do()
    if err != nil {
        fmt.Println(err)
    }

    //ここの段階で取得ができますね!
    fmt.Println(remoteConfig.Parameters["myconfig"].DefaultValue.Value)
}

func update() {
    context := context.Background()

    credential, err := google.CredentialsFromJSON(context, []byte(credentialJSON), "https://www.googleapis.com/auth/firebase.remoteconfig")
    if err != nil {
        fmt.Println(err)
    }

    option := option.WithCredentials(credential)

    service, err := firebaseremoteconfig.NewService(context, option)
    if err != nil {
        fmt.Println(err)
    }

    remoteConfig, err := service.Projects.GetRemoteConfig(yourProjectId).Do()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println("変更前", remoteConfig.Parameters["myconfig"].DefaultValue.Value)
    remoteConfig.Parameters["myconfig"].DefaultValue.Value = "Hello New World"

    updateRemoteConfig := service.Projects.UpdateRemoteConfig(yourProjectId, remoteConfig)

    //ここ大事!
    updateRemoteConfig.Header().Add("If-Match", "*")

    updatedRemoteConfig, err := updateRemoteConfig.Do()
    if err != nil {
        fmt.Println(err)
    }
    // Update後の値を取得することもできます。
    fmt.Println("変更後", updatedRemoteConfig.Parameters["myconfig"].DefaultValue.Value)
}

func main() {
    //getかUpdate、どちらか好きなほうをコメントアウトして実行してください。
    update()
}

 

今回は私にとって、Golangのドキュメントの読み方など勉強になりました!
これが少しでもお役に立てば幸いです。
間違いなどありましたら、TwitterなどのSNSにお知らせください!