• トップ
  • ブログ一覧
  • Firebase Remote Config の更新を自動化する Python スクリプトを書いてみた
  • Firebase Remote Config の更新を自動化する Python スクリプトを書いてみた

    こー(エンジニア)こー(エンジニア)
    2022.10.24

    IT技術

    はじめに

    こんにちは!福岡でモバイルアプリエンジニアをやっている こー です!

    featureImg2021.11.05YOUは何しにライトコードへ?〜こーくん編〜プロジェクト内で安心感を与えられる存在になりたい!今回は、弊社のエンジニアである高さんにフィーチャー!技術力に定評のあ...

    今回は、モバイルアプリエンジニアであれば馴染みの深い Firebase Remote Config に関する記事でございます。

    スクリプトのお話になるので、モバイルアプリ開発の直接的な技術とは少し距離がありますが、ご覧いただけると幸いです!

    Firebase Remote Config って何?

    Firebase Remote Config (以下 Remote Config)とは、Firebase が提供しているモバイルアプリ向けクラウドサービスの1つで、コンソール上で Key-Value 型の値を設定することで、それをリアルタイムにアプリに渡すことができるサービスです。

    アプリの外観や動作の変更を素直に行う場合、通常はアプリのコードを修正しリリースやアップデートを行いますが、Remote Config を使うと、コンソール上の値を更新するだけでアプリに渡せるので、リリースやアップデートなしにそれらを実現することができます。

    「そんなのあるんだ!?」って興味を持った方は、ぜひ 公式ドキュメント を読んでまず使ってみてください!

    Remote Config の導入や基礎的な内容は、今回の主題から外れますので割愛します。

    ご容赦くださいませ m(_ _)m

    何がやりたかったの?

    僕が担当していたプロジェクトでは、アプリの強制アップデート機能を Remote Config で実現していました。

    1. Remote Config に強制アップデート対象バージョンを登録
    2. アプリ起動時にバージョンが強制アップデートバージョン以下だった場合、インストールページへ誘導

    という仕組みですね。

    ですので、アプリをリリースしたら強制アップデート対象バージョンを更新する必要があるのですが、

    1. Remote Config のコンソールページを開く
    2. 強制アップデート対象バージョンを設定する
    3. 公開ボタンをポチる

      という、アナログな方法を取っていました。

      ですが、この方法には以下の問題点が浮かびます。

      • 更新を失念するかもしれない
      • バージョン情報の打ち間違いが起こるかもしれない
      • 面倒
      • とても面倒
      • めっちゃ面倒

      このプロジェクトでは CI(継続的インテグレーション)に Bitrise を利用していたので、

      「スクリプト書いてリリースのワークフローで実行できたら自動化できるやんけ!」

      と思い、 Remote Config を更新を行うスクリプトを書くことにしました。

      Python を選んだのは、個人的に一番楽に書けるからです(笑)

      どんなコード書いた?

      1# -*- coding: utf-8 -*-
      2#!/usr/bin/env python
      3
      4import json
      5import os
      6import sys
      7from oauth2client.service_account import ServiceAccountCredentials
      8import requests
      9
      10# 引数からバージョン番号を取得
      11version = sys.argv[1]
      12
      13print(f'\n-------- update invalid version of remote config: to {version} -------\n')
      14
      15# ① credentials.json の生成
      16credentials_file_name = "credentials.json"
      17credentials = {
      18  "type": "service_account",
      19  "project_id": os.getenv('FIREBASE_PROJECT_NAME'),
      20  "private_key_id": os.getenv('FIREBASE_PRIVATE_KEY_ID'),
      21  "private_key": os.getenv('FIREBASE_PRIVATE_KEY').replace('\\n', '\n'),
      22  "client_email": os.getenv('FIREBASE_CLIENT_EMAIL'),
      23  "client_id": os.getenv('FIREBASE_CLIENT_ID'),
      24  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      25  "token_uri": "https://oauth2.googleapis.com/token",
      26  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
      27  "client_x509_cert_url": os.getenv('FIREBASE_CLIENT_X509_CERT_URL')
      28}
      29with open(credentials_file_name, mode='wt', encoding='utf-8') as file:
      30  json.dump(credentials, file, ensure_ascii=False, indent=2)
      31
      32
      33# エンドポイントの設定
      34project_name = os.getenv('FIREBASE_PROJECT_NAME')
      35endpoint = f'https://firebaseremoteconfig.googleapis.com/v1/projects/{project_name}/remoteConfig'
      36
      37# アクセストークンの取得
      38scope = ['https://www.googleapis.com/auth/firebase.remoteconfig']
      39credentials = ServiceAccountCredentials.from_json_keyfile_name(
      40  credentials_file_name,
      41  scope
      42)
      43access_token = credentials.get_access_token().access_token
      44if access_token is None:
      45  raise Exception("could not get access token")
      46
      47# 現在のConfigデータ取得
      48headers = {
      49  'Authorization': f"Bearer {access_token}",
      50}
      51response_get = requests.get(
      52  endpoint,
      53  headers=headers,
      54)
      55config_json = response_get.json()
      56
      57# RemoteConfig更新にはetagが必須となるのでレスポンスヘッダから取得しておく
      58etag = response_get.headers['etag']
      59if etag is None:
      60  raise Exception("could not get etag")
      61
      62# ② Remote Configの強制アップデート対象バージョン情報を更新する
      63config_json['parameters']['invalid_version']['defaultValue']['value'] = version
      64headers = {
      65  'Authorization': f"Bearer {access_token}",
      66  'Content-Type': 'application/json; UTF8',
      67  'If-Match': etag,
      68}
      69response_put = requests.put(
      70  endpoint,
      71  headers=headers,
      72  json=config_json,
      73)
      74if response_put.status_code == 200:
      75  print("update succeeded: Remote Config")
      76else:
      77  raise Exception("update failed: Remote Config")
      78
      79# ③ 処理が終了したら、credentials.json を削除
      80os.remove(credentials_file_name)
      81
      82print(f'\n-------- finished updating invalid version of remote config: to {version} -------\n')

      (Python は 3.7.2 を利用)

      まず、このスクリプトで行っていることを端的にまとめると、

      1. プロジェクトの ID やプライベートアクセスキーなどの情報をまとめた credentials.json を作成
      2. credentials.json を利用して、 Firebase APIへのアクセストークンを取得。
      3. Remote Config へのエンドポイントを指定。
      4. GET リクエストで現在の Remote Config の設定情報を取得し、そこから更新の際に必要な etag を保持する。
      5. 更新データの json と etag を付与して、PUT リクエストを送信して更新する。

      となります。

      コード上で番号が振ってあるところをポイントとして、ピックアップして説明していきます。

      ① credentials.json の生成

      1# (15行目〜)
      2# ① credentials.json の生成
      3credentials_file_name = "credentials.json"
      4credentials = {
      5  "type": "service_account",
      6  "project_id": os.getenv('FIREBASE_PROJECT_NAME'),
      7  "private_key_id": os.getenv('FIREBASE_PRIVATE_KEY_ID'),
      8  "private_key": os.getenv('FIREBASE_PRIVATE_KEY').replace('\\n', '\n'),
      9  "client_email": os.getenv('FIREBASE_CLIENT_EMAIL'),
      10  "client_id": os.getenv('FIREBASE_CLIENT_ID'),
      11  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      12  "token_uri": "https://oauth2.googleapis.com/token",
      13  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
      14  "client_x509_cert_url": os.getenv('FIREBASE_CLIENT_X509_CERT_URL')
      15}
      16with open(credentials_file_name, mode='wt', encoding='utf-8') as file:
      17  json.dump(credentials, file, ensure_ascii=False, indent=2)

      この project_id や private_key_id などの項目は、Firebase コンソールの [設定] メニュー -> [サービスアカウント] の [Firebase Admin SDK] 内にある [新しい秘密鍵の生成] でダウンロードできるキーファイルに記述されていますので、生成しダウンロードします。

      ダウンロードの際に出てくるポップアップにもありますが、このキーファイルに記載されている情報は決して公開してはいけません。

      このキーファイルに記載されている情報は決して公開してはいけません。

      推敲のし忘れではありません、大事なことなので2回言いました。

      この情報を使えば Firebase API を通じて、プロジェクト上のデータを読み書きできます。

      それは言い換えると、 このキーファイルの情報さえあれば誰でもプロジェクト内のデータを触れてしまう、ということです。

      プロジェクトの秘匿情報を外部に公開するのと同義なので、重大なインシデントに繋がります。

      これを回避するために、以下の内容を必ず意識しましょう。

      1. コードに秘匿情報をベタ書きしないこと
      2. 秘匿情報は CI 上の Secrets などに配置し、環境変数を通じて取得するように書くこと

      自信がないのであれば、知見のある方にレビューを通してもらったり、一緒に作業するようにしましょう!

      ② Remote Config の強制アップデート対象バージョンの更新

      1# (62行目〜)
      2# ② Remote Configの強制アップデート対象バージョン情報を更新する
      3config_json['parameters']['invalid_version']['defaultValue']['value'] = version
      4headers = {
      5  'Authorization': f"Bearer {access_token}",
      6  'Content-Type': 'application/json; UTF8',
      7  'If-Match': etag,
      8}
      9response_put = requests.put(
      10  endpoint,
      11  headers=headers,
      12  json=config_json,
      13)
      14if response_put.status_code == 200:
      15  print("update succeeded: Remote Config")
      16else:
      17  raise Exception("update failed: Remote Config")

      取得した現在の Remote Config の設定情報の json 内容を書き換えて PUT リクエストを送信します。

      Remote Config の設定情報の json 構成は以下のようになっています。

      1{
      2  "parameters": {
      3    "invalid_version": {
      4      "defaultValue": {
      5        "value": "x.x.x"
      6      },
      7      "description": "強制アップデート対象バージョン"
      8    }
      9  }
      10}

      今回は環境による値の条件分岐を行なっていないので、 defaultValue のみ更新する比較的シンプルな構成になっています。

      分岐条件の conditions や、分岐した値の conditionalValue なども jsonから設定できるので、詳しくは 公式ドキュメント を参照してください。

      PUT リクエストを送信する際は、GET リクエストの際にレスポンスヘッダに付与されている etag を付与する必要があるので気をつけましょう!

      ③ 処理が終了したら、credentials.json を削除

      1# (79行目〜)
      2# ③ 処理が終了したら、credentials.jsonを削除
      3os.remove(credentials_file_name)

      このままスクリプトを終了すると、作成した credentials.json が作業マシン上に残存してしまうので、漏洩を防ぐためにも更新が終了したら credentials.json を削除する処理を忘れずに書くようにします。

      これで、 python update_invalid_version_remote_config.py 2.0.0 のようにコマンド実行することで Remote Config を更新できるスクリプトができました!

      この引数に渡すバージョンの取得や、コマンド実行を CI のワークフローに組み込むことで自動化が実現できるようになります!

      さいごに

      色々意気揚々と作り Bitrise に組み込もうとしたこのスクリプトですが、

      結局プロジェクトでの運用が大きく変わったせいで、このスクリプトが日の目を見ることはありませんでした。

      供養のため、ここに記事を残した次第です(号泣)

      「使われることじゃなく、作ったことに意味がある!」

      めげることなく思いついたら色々作っていこうと思います!!

      参考になったり、使えそうだなと思った方はどんどん使ってあげてください!

      きっとこのスクリプトも喜びます(笑)

      最後までご覧いただきありがとうございました!

      ライトコードでは、エンジニアを積極採用中!

      ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

      採用情報へ

      こー(エンジニア)

      こー(エンジニア)

      おすすめ記事

      エンジニア大募集中!

      ライトコードでは、エンジニアを積極採用中です。

      特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

      また、フリーランスエンジニア様も大募集中です。

      background