羊をめぐるブログ

趣味の色々について書きます

sqldef + Cloud Buildで自動migration

はじめに

これは2021年振り返りカレンダーの6日目の記事です.

前回は,firebase周りのCI/CD構築と,環境分離を行うという内容でした.

serenard.hatenablog.com

今回は,CDに,sqldefを用いたデータベース(RDB)migrationを組み込む内容について書いていきます.

sqldefはRDBmysql, postgresなど)用のとても便利なmigrationツールで,テーブル定義のsqlファイルと 現状のスキーマを比較し,自動で差分のapplyを行ってくれます. 詳細は以下.

github.com

k0kubun.hatenablog.com

また,使い方は以下などを参照されたし.

qiita.com

sqldefはとても便利ですが,migrationを実行する際に,対象のDBと接続する必要があります. ローカルのDBにかけるだけならば特に気にすることはないのですが,開発環境,本番環境などに 接続してコマンドを実行するのはホストやパスワードの指定が必要で面倒臭いですし, 間違えてapply先を間違えるようなミスすると大変です. よって,sqldefのapply操作をラッピングし,CDフローに組み込むことで,いろんな面倒さを 解消することを目指します.

仕様

今回は,GCP上で,開発環境(service-dev)と本番環境(service-prd)の2つが存在し, それぞれのために以下のsqldefによるmigration操作を行いたいとします.

  • sqldefのdry-run (apply されるスキーマの差分の確認).自動で行われる.
  • sqldefのapply(スキーマ変更の実施).任意のタイミングでコマンドを実行し行う.

また,DBはCloud SQL上に立っており,接続にcloud-sql-proxyが必要とします.

実装

まず,ディレクトリ構成は以下のようになります.

root/
├── api
│  └── schema
│     └── schema.sql
└── release
   ├── dev
   │   ├── migration_dry_run.yaml
   │   └── migration.yaml
   └── prd
       ├── migration_dry_run.yaml
        └── migration.yaml

release下にdev, prd,それぞれのディレクトリを作り,更にその下にdry-run及び 実際のapplyを行うCloud Buildの設定ファイルを作成します.

Migration Dry-run

新機能などに関するPRがマージされた時,それによってDBスキーマの 変更が起きるかを知りたいというケースは往々にしてあると思います.

sqldefではdry-run機能があり,テーブル定義SQLと現状のDBのスキーマの差分を比較し, 実行されるSQLを確認することが可能です.

今回はCloud Build上でDry-runを行い,その結果をslackに通知することとします.

流れは以下のようになります.

  1. cloud-sql-proxyを開始し,Cloud SQLと繋げるようにする.
  2. cloud-sql-proxyの接続が確立されるのを待つ.
  3. dry-runを行う & 結果をslack通知.
  4. cloud-sql-proxyの接続を切る.

最終的な設定ファイルは以下の感じです.これを,任意のトリガーで実行させます.

# migration_dry_run.yaml
steps:
  - id: start_sql_proxy
    name: gcr.io/cloudsql-docker/gce-proxy:1.16
    args:
      - /cloud_sql_proxy
      - -dir=/cloudsql
      - -instances=$_INSTANCE_CONNECTION_NAME
    volumes:
      - name: cloudsql
        path: /cloudsql
  - id: wait_sql_proxy_start
    name: gcr.io/cloud-builders/gcloud
    entrypoint: bash
    args:
      - -c
      - |
        while [ ! -e "/cloudsql/$_INSTANCE_CONNECTION_NAME" ]; do
          sleep 1
        done
    volumes:
      - name: cloudsql
        path: /cloudsql
    waitFor: ["-"]
  - id: migration
    name: gcr.io/cloud-builders/gcloud
    dir: api/schema
    entrypoint: bash
    args:
      - -c
      - |
        apt -y update && apt -y install wget curl \
        && wget https://github.com/k0kubun/sqldef/releases/download/v0.8.7/mysqldef_linux_amd64.tar.gz \
        && tar -zxvf mysqldef_linux_amd64.tar.gz \
        && curl -X POST --data-urlencode "payload={ \"attachments\":[
                {
                   \"fallback\":\"sqldef dry-run notification\",
                   \"color\":\"#1E90FF\",
                   \"fields\":[
                      {
                         \"title\": \"sqldef dry-run $TAG_NAME\",
                          \"value\": \"
                            $(
                              ./mysqldef --dry-run -S /cloudsql/$_INSTANCE_CONNECTION_NAME \
                              -u $(gcloud secrets versions access latest --secret=$_SECRET_DB_USERNAME) \
                              -p$(gcloud secrets versions access latest --secret=$_SECRET_DB_PASSWORD) \
                              service < schema.sql 2>&1
                            )
                          \"
                      }
                   ]
                }
              ]
            }" $(gcloud secrets versions access latest --secret=$$SECRET_SLACK_WEBHOOK_URL)
    volumes:
      - name: cloudsql
        path: /cloudsql
    waitFor: ["wait_sql_proxy_start"]
  - id: kill_sql_proxy
    name: gcr.io/cloud-builders/docker
    entrypoint: bash
    args:
      - -c
      - docker kill -s TERM $$(docker ps -q --filter ancestor=gcr.io/cloudsql-docker/gce-proxy:1.16)
    waitFor: ["migration"]
substitutions:
  _INSTANCE_CONNECTION_NAME: service-dev:asia-northeast1:service-dev
availableSecrets:
  secretManager:
    - versionName: projects/service-dev/secrets/service-dev-mysql-username/versions/latest
      env: "MYSQL_USER"
    - versionName: projects/service-dev/secrets/service-dev-mysql-password/versions/latest
      env: "MYSQL_PASSWORD"
    - versionName: projects/service-dev/secrets/service-dev-slack-webhook-url/versions/latest
      env: "SLACK_WEBHOOK_URL"

面倒な点としてはCloud BuildからCloud SQLに接続するのにsql-proxyが必要という点です. 他ステップでCloud SQLに繋げられるように,最初にバックグラウンドでsql-proxyの接続ステップを走らせて, 全部が終わってからそれを切断します. 詳細は以下などが参考になります.

qiita.com

medium.com

sqldef実行の際は,公式レポジトリよりバイナリを落としてきて,それを実行します. バイナリポン,最強.取ってくるバージョンは適当なものを指定します.

また,結果の通知の際に2>&1を最後に入れていますが,これは標準エラー出力も表示させるためです. これがないと,migration実行が失敗した際に空の通知が送られてきます.

qiita.com

Migration Apply

applyについては,前述のdry-runオプションを消すだけです. 胆汁. うまくやればdry-runとファイルを共通化できると思います.

実際のマイグレーションは,sqldefによるスキーマの変更だけで済まないケースも多いので, 以下のようなコマンドを用いて任意実行できるようにしておくのがいいと思います.

#!/bin/bash

function sqldef_apply() {
  gcloud beta builds triggers run --tag $2 service-dev-migration
}

$1 $@

その他細かい知見など

  • コマンドの実行結果を送ったりする際は,標準エラー出力も出るように2>&1をつける.
  • Cloud BuildからCloud SQLにつなぐ方法は,なるほど〜となった(接続確立=unixソケットが生成される?までwait).
  • 権限周りはよしなに

最後に

migrationを簡単に,安全に行うために,sqldefをCloud Build上から呼び出す方法について書きました. 実際のサービスだとDBのデータをいじるようなmigarationも多く起きると思うので,今回書いたように 単純にはいかないと思いますが,開発中&リリース初期などのスキーマ追加を多く行うケースでは便利 なんではないでしょうか. 複雑なmigrationをどうラップするかについてはまた勉強したいと思います(そもそもmigrationをたくさんしたくはないが...).

Firebase周りのCI/CDと,環境分離について

はじめに

これは2021年振り返りカレンダーの5日目の記事です.

前回はBitrise上でExpo OTAを行うワークフローを構築するという内容でした.

serenard.hatenablog.com

今回は,最近の初期アプリにはほとんど使われてそうなFirebase周りのCI/CDと, 環境分けについて書いていきます.

CI/CDの対象となるFirebaseのサービスはFirebase Functionです. また,storageやfirestoreのセキュリティールールのデプロイは 環境ごとに対象バケット名が少し変わったりするので, そこをどう分けて管理するかについてです.

仕様

今回は,service-dev,service-prd の2つのfirebase projectがあり, それぞれのために以下を作りたいとします.

  • firestore のセキュリティルール及びインデックスのデプロイコマンド
  • firebase storage のセキュリティルールのデプロイコマンド
  • firebase functions のCI/CD

実装

まず,ディレクトリ構造は以下のようになります.

root/
├── firebase
│  ├── functions
│  ├── .firebaserc
│  ├── firebase_dev.json
│  ├── firebase_prd.json
│  ├── firestore.indexes.json
│  ├── firestore.rules
│  └── storage_assets.rules
├── release
   ├── dev
    │   └── deploy_functions.yaml
   └── prd
        └── deploy_functions.yaml

セキュリティルールなどのデプロイコマンド

各環境で,どのセキュリティールールをデプロイするかなどを,firebase_{dev|prd}.jsonで管理します. 内容は以下の感じになります.

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "storage": [
    {
      "rules": "storage_assets.rules",
      "bucket": "service-dev-public-assets"
    }
  ]
}

また,デプロイは以下のようなコマンドを用意しておきます.

# ストレージのセキュリティルールのデプロイ
firebase deploy --only storage:rules --config firebase_{dev|prd}.json
# firestoreのセキュリティルール
firebase deploy --only firestore:rules  --config firebase_{dev|prd}.json

configを用意したことで毎回それをオプションで指定する必要が出てきますが, 常に環境を意識させられるのでまぁいいかぁという印象です. デフォルトはfirebase.jsonを参照するので,devはそれにしてもいいかもしれません.

firebase functions のCI/CD

functions のCI/CDの設定は,release下の各環境ごとのCloud Buildファイルを参照させて行います.

Cloud Buildの設定ファイルの流れは以下のようになっています.

  1. functionsのコードのビルド
  2. デプロイ先の環境を設定
  3. 環境変数,シークレット変数を設定
  4. デプロイ

シークレット変数は,secret managerに入れておき,Cloud Buildからそれを参照します.

cloud.google.com

実際のCloud Buildのファイルは以下のような感じです.トリガーのタイミング自体は適当に設定しましょう.

steps:
  - id: build_functions
    name: node:14.15.4
    dir: firebase/functions
    entrypoint: bash
    args:
      - -c
      - |
        npm i \
        && npm run build
  - id: deploy_functions
    name: gcr.io/$_PROJECT_ID/firebase
    dir: firebase
    entrypoint: bash
    args:
      - -c
      - |
        firebase use dev --config $_FIREBASE_CONFIG_JSON \
        && firebase functions:config:set slack.channel=$_SLACK_NOTIFICATION_CHANNEL --config $_FIREBASE_CONFIG_JSON \
        && firebase functions:config:set slack.oauth_token=$$SLACK_OAUTH_TOKEN --config $_FIREBASE_CONFIG_JSON \
        && firebase deploy --project=$_PROJECT_ID --only functions --config $_FIREBASE_CONFIG_JSON
    secretEnv: ["SLACK_OAUTH_TOKEN"]
    timeout: "600s"
substitutions:
  _PROJECT_ID: service-dev
  _FIREBASE_CONFIG_JSON: firebase_dev.json
  _SLACK_NOTIFICATION_CHANNEL: service_dev_notice_test
availableSecrets:
  secretManager:
    - versionName: projects/service-dev/secrets/service-dev-slack-oauth-token/versions/latest
      env: "SLACK_OAUTH_TOKEN"

その他細かい知見など

  • firebase functions のCloud Build上でのビルドはコードにエラーが無くてもたまに落ちることがある(nodeのエラーが出てたがよくわからなかった).再実行しやすくしておくとよい.
  • GCPのプロジェクト切り替えてもfirebaseは切り替わらないのでデプロイの際は注意.

最後に

内容薄いのですが備忘録として.firebaseも色々terraform化できるようになったら嬉しいな〜と少し思いました.

Bitrise上でExpo OTA

はじめに

これは2021年振り返りカレンダーの4日目の記事です.

前回は,はじめてBitriseを用いてreact-nativeのCI/CD環境を構築する(新春SP)という内容でした.

serenard.hatenablog.com

Expoには Over The Air (OTA) update という,審査を通さなくてもアプリのアップデートを 配信できる機能があります. 細かいが致命的なバグなどを緊急に修正したい場合には,2,3日かかってしまう審査を 通さずにすぐ修正版を配信できるので便利です.

前回の記事ではOTAを実行するワークフローの構築については省略したので, 今回はそれについて書こうと思います.

OTAってなんぞってときには以下が,

docs.expo.dev

Expo OTAの仕様の詳細については以下がとても参考になりました.

zenn.dev

仕様

今回はreact-native (expo bare workflow) で開発しているiOSアプリのOTAを行います.

環境はstgとprdの2つあるとします.

OTAは緊急時に行うデプロイ手段のため,自動トリガーではなく,手動でトリガーすることとします.

実装

Expo projectの作成

Expo OTAを行うには,ExpoアカウントとExpoのプロジェクトが必要です.

https://expo.dev/へ行き,Create new projectからプロジェクトを作りましょう.

今回はserviceという名前で作ったとします.

Bitrise上でOTAワークフローの構築

先ほど作成したアカウントとプロジェクトを用いてOTAワークフローを作ります.

以下が全体像です.

f:id:sheep96:20220118233958p:plain

多くの部分は前回解説した部分と同じなので,異なる部分を解説していきます.

Expo publish

ここではOTAを行っています.内部では以下のスクリプトを実行しています.

  1. expo-cliをインストール
  2. 先ほど作成したexpoアカウントにログイン.パスワード,ユーザ名はsecretに保存したものを参照する.
  3. OTA配信を行う.release-channelを指定することにより,dev環境にだけ配信するようにする.

とてもシンポゥーです.

#!/usr/bin/env bash
# fail if any commands fails
set -e
# debug log
set -x

# expo publish
npm install -g expo-cli
expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
cd ui
cp .env.dev .env
expo publish --release-channel dev

prdについては,release-channelをprdにするだけです.

ワークフローの実行コマンド

こちらについては,前回解説したワークフロートリガー用のweb apiを叩くことで行います.

以下のようなものをdev, prd環境ごとに用意します.

#!/bin/bash

function deploy_ui_ota() {
  TAG_NAME=$2
  command="curl https://app.bitrise.io/app/hogehogehoge/build/start.json --data '{\"hook_info\":{\"type\":\"bitrise\",\"build_trigger_token\":\"hogehogehoge\"},\"build_params\":{\"branch\":\"production\",\"workflow_id\":\"deploy_ota\",\"tag\":\"${TAG_NAME}\"},\"triggered_by\":\"curl\"}'"
  eval $command
}

$1 $@

実行は以下のような感じです.

sh deploy.sh deploy_ui_ota v0.0.0

OTAで間違えてバグを配信した時

こちらが参考になります.

docs.expo.dev

手段としては以下が考えられそうです.

  • バグを修正して新規バージョンをリリース
  • バグが入ってない箇所までバージョンをロールバックし,OTAリリース(Expoは常に最新のOTAがないかを見に行くため.バージョンを落としても,リリースが後ならば取りに行く.)
    • https://qiita.com/kaba/items/b6e777d1c0b7ce1f78c6 の記事によるとExpoは1回の起動で1個しかOTA updateを確認しに行かないため,ユーザは1回はバグバージョンを見ることになる(バグをとってきた次の起動時に修正後を取りに行く).
  • expoのロールバックコマンドを利用する.
    1. expo publish:history --platform ios で過去のバージョンを取得.
    2. expo publish:set --release-channel prd --publish-id id で指定したバージョンに戻す.1個前ならばexpo publish:rollback --release-channel production --sdk-version 36.0.0も使える.

その他細かい知見など

  • OTAしたバージョンが1つしかない場合,ロールバックや他のビルドへの移動ができないため,再度OTAを行うか,Expoプロジェクトを削除するしかなさそう.
  • 自分は間違えて1度v999.0.0をOTA配信し,ユーザにアップデート通知が出ない状態にしてしまいましたが,バージョンを修正したものを再度OTAすることでことなきを得ました.
  • プロジェクトページからだとOTA履歴が見れないので,あったら嬉しい.
  • 一応BitriseにもExpo ejectというpublishを行うステップはあるが,release-channelを設定できない.

最後に

Expo OTAをBitrise上でワークフロー化する内容について書きました.とても便利で心強いOTAですが, 少し間違えると想定してないものを配信してしまったりするので,注意が必要です. ローカルで実行しようとするとExpoアカウントやrelease-channelの切り替えが必要になりますが, 今回のようにワークフロー化することで,手間やミスを減らせるんじゃないかと思います. 間違ってる部分は往々にしてありそうなので,ぜひお伝えいただけると嬉しいです.

はじめてのBitriseを用いたreact-nativeアプリ (iOS) のCI/CDこうちく

はじめに

これは2021年振り返りカレンダーの3日目の記事です.

前回はGCP+ネイティブアプリの構成でGitLab flowを構築するという内容でした.

serenard.hatenablog.com

ネイティブアプリと言ってるくせにそこのCD周りは完全に省略するという半詐欺行為を行って しまいましたが,今回はそれについて書きます.

代表的なネイティブアプリ用のCI/CDツールとしては以下のようなものがあります.

  • Bitrise
  • Fastlane
  • CodeMagic

他にも,汎用CI/CDプラットフォームのMacインスタンスを提供しているものを 利用することも考えられます.

今回は,採用例や情報が多くreact-native (expo) のビルドも簡単にできそうなBitriseを使用することにしました (調べていた時はFastlaneを知らなかったのですが,お金あんまり使いたくなかったので, 知ってたらFastlane使ってたかもです.Bitriseも無料枠があるのですが, 時間制限がキツかったのでOrgにしたら1万ぐらいしてビビりました.).

仕様

今回はreact-native (expo) で開発しているアプリのiOSビルドを行います.

また,環境はstgとprdの2つあり,ビルド対象は以下のパターンがあるとします.

  • devアプリを配布用にビルド
  • devアプリをOTA(今回は省略)
  • prdアプリをAppStore審査に提出
  • prdアプリをOTA(今回は省略)

これらを前回作ったGitLab flowに組み込みます.

devアプリの配布ビルドは新たなバージョンリリースを行なった際に自動で走り, prdアプリの審査ビルドはprd環境にリリースPRがマージされた時に行われるとします.

実装

Bitriseには,yamlとかからワークフローを作るモードとUI上でワークフローを組む方法がありますが, 今回は直感的にできるUIでの構築を行います.

アプリケーションとワークフローの作成

Bitriseでは,まずアプリケーションを作り,その下にビルドのワークフローを作るという流れになっています.

作り方は以下を参照.

devcenter.bitrise.io

devcenter.bitrise.io

ここでは,環境ごとにアプリを作成し(serviceというアプリに対して,service_stgとservice_prd), その下にビルドワークフローを作ることとします. 以下のような感じです.

f:id:sheep96:20220118130655p:plain

次に,各アプリの下に必要なワークフローを定義します. stgにはdeploy_distributeとdeploy_otaを,prdにはdeploy_storeとdeploy_otaを作成します.

f:id:sheep96:20220118131041p:plain

stgアプリの配信ワークフローの構築(service_devのdeploy_distribute)

このワークフローでは,チームの人全員が新機能などの動作確認ができるように, stgアプリのビルドとその成果物の配布を行います. ワークフローの全体像は以下のような感じになっています.

f:id:sheep96:20220118160800p:plain

半年ぐらい前に作ったのでdeprecatedになってる箇所もありますが, 各要素について説明していきます.

Activate SSH key

まず,最初のActivate SSH keyではGitHubからpullするためにssh keyの確認を行なっています. ssh keyの設定についてはアプリケーションのsettingsから変更でき, 以下の3つ設定方法があります.

SSHキーペアの自動追加:Bitriseは公開SSHキーをGitリポジトリに自動的に登録します。リポジトリに対する管理者権限がある場合は、これを選択してください。

SSHキーペアを生成する:BitriseはSSHキーペアを生成するため、Gitリポジトリに公開キーを手動で登録する必要があります。

独自のSSHキーペアを使用する:認証用に独自のSSHキーペアを提供し、公開キーをGitリポジトリに手動で登録する必要があります。

devcenter.bitrise.io

自分はレポジトリ連携時に自動で登録されたものを使っていました(というかあんまり意識してなかった).

Git Clone Repository

次のGit Clone Repositoryは,名前の通りレポジトリのクローンを行います.

Do anything with Script step

次のDo anything with Script stepは,任意のbash scriptを実行できるものです. 今回はビルドをトリガーしたバージョンタグを読み取り,expoの設定ファイルの バージョン部分を上書きするのに使いました(ステップの名前はちゃんと変えたほうがいいと思う).

実行するスクリプトの内容は以下のような感じです.

VERSION=`echo $BITRISE_GIT_TAG | sed -e "s/v//"`
envman add --key BUNDLE_VERSION_STRING --value $VERSION
APP_JSON=`jq ".expo.version=\"$VERSION\" | .expo.ios.bundleIdentifier=\"$BUNDLE_ID\"" ui/app.json`
echo $APP_JSON > ui/app.json

bitriseには,ワークフロー全体で参照できる環境変数がいくつか定義されており,BITRISE_GIT_TAGには ビルドをトリガーしたタグが入っています. 今回はそこからバージョンを読み取り,expoの設定ファイルであるapp.jsonへ書き込みを行なっています. これにより,リリース時に手動で設定ファイル中のバージョンをあげる作業をなくすことができます.

Run yarn command, Run cocoaPods install

ここ2つでは,yarnによるパッケージのインストール,cocoaPodsによるモジュール?のインストールを 行なっています.

時間がかかる場合はこちらの前後でキャッシュ処理を挟むこともできます.

devcenter.bitrise.io qiita.com

iOS Auto Provision with App Store Connect API

(注)現在はdeprecatedになっている模様.Xcodeビルドのステップと合体したらしい.

blog.bitrise.io

こちらでは,アプリのProvision Profileファイルの生成を行います.

Provision Profileの詳細については以下など.

hirokuma.blog

通常はProvisionファイルはAppStore Connectで作る必要がありますが,Bitriseでは その自動生成を行ってくれます.

stgでは配布用アプリのビルドを行うので,Distribution Typeの設定をad-hocにします.

そして,BitriseがAppStore Connectへ接続できるようにするため,App Store Connect API key をBitriseに登録しておきます. 以下が参考になります.Appleアカウントでの連携も可能ですが,定期的に連携が切れるため,API keyを使うのが 推奨されています.

devcenter.bitrise.io

また,事前にAppStore Connect上で配布先デバイスの登録を行っておきます. 以下が参考になります.

https://softmoco.com/devenv/how-to-register-devices-apple-developer.php

Xcode Archive & Export for iOS

ここではビルドを行います.配布用ビルドなので,Distribution methodをad-hocにしておきます. また,環境ごとにschemeを避けている場合は,それも設定します.

Deploy to Bitrise.io

ここでは,前のステップで生成されたipaファイルを,Bitriseの配信サーバへデプロイし, チームの人がWebページからダウンロードできるようにします.

ページの見た目は以下のような感じになっています(めんどかったのでアプリの名前やIDは適当です).

Send a Slack message

ビルドの結果をslackに通知します.

パラメータとしてはslackのoauth tokenと通知先のチャンネルが必要です.

oauth tokenはBitriseのシークレットに保存しておき,それを参照します.

devcenter.bitrise.io

これで一旦,stgアプリの配信用ワークフローが完成です.

prdアプリの審査提出ワークフローの構築(service_prdのdeploy_store)

審査提出ワークフローの全体像は以下のような感じです.

f:id:sheep96:20220118175534p:plain

大体の部分はstgの配信ワークフローと同じですが,違う部分について書いていきます.

Set Xcode Project Build Number

あるバージョンのアプリをストア審査に出した時,それにバグがあり,同じバージョンでもう一度出し直したい というケースや,アプリがどのビルドで提出されたかを特定したいというケースがあると思います.

iOSアプリではバージョンとは別にビルド番号を持っており,毎回のビルドで別の値をセットすることで, 上記の要件を満たすことができます. 以下が詳しいです.

tech.gunosy.io

Bitriseではワークフローの実行ごとにインクリメントされるBuildNumberを持っているため, このステップではその値を設定しています.

iOS Auto Provision with App Store Connect API

stg配信の時はad-hocにしましたが,app-storeにします.

Xcode Archive & Export for iOS

stg配信の時はad-hocにしましたが,app-storeにします.

Deploy to App Store Connect

ipaファイルをAppStore Connectにアップロードしてくれます.

アカウント名とパスワードをシークレットに入れ,設定します. また,二段階認証を有効にしてる場合はApplication-specific-passwordも設定します.

App 用パスワードを使う - Apple サポート (日本)

記事の一番最後に一応ワークフロー全体のyamlもはっておこうと思います.

ワークフローのトリガーと既存のCI/CD flowとの連携

Bitriseでは,連携したGitHubレポジトリへのpush,pull request,tag pushに応じたトリガー を設定することが可能です.

ビルドの開始 - Bitrise Docs

また,各アプリケーションには以下のようにビルドを開始するためのURLが発行されるため, それを用いて既存のCI/CDと連携することも可能です.GitHub Actionsなどの上で用いれば, コードとして管理できるため,便利だと思います.

f:id:sheep96:20220118181131p:plain

自分は以下のようなstore審査開始コマンドを作ってローカルやGitHub Actionsから使っていました.

#!/bin/bash

function deploy_ui_distribute() {
  TAG_NAME=$2
  command="curl https://app.bitrise.io/app/hogehogehoge/build/start.json --data '{\"hook_info\":{\"type\":\"bitrise\",\"build_trigger_token\":\"hogehogehoge\"},\"build_params\":{\"branch\":\"production\",\"workflow_id\":\"deploy_store\",\"tag\":\"${TAG_NAME}\"},\"triggered_by\":\"curl\"}'"
  eval $command
}

$1 $@

productionブランチにリリースPRマージされたときに審査ビルドするGitHub Actions

on:
  pull_request:
    branches:
      - production
    types: [closed]

jobs:
  deploy_prd:
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true && contains(github.head_ref, 'release')
    steps:
      - uses: actions/checkout@v2
      - name: Deploy UI to Store
        id: deploy_store
        working-directory: ./release/prd
        run: |
          bash deploy.sh deploy_ui_store `echo ${{ github.head_ref }} | sed -e "s/release\///"`

その他細かい知見など

  • ビルドに90分ぐらいかかることもあり,プランの制限を超えて落ちることもあった.もう一回ビルドすると成功したりする.Xcode Archiveがやっぱ遅いので,そこの最適化周りは勉強したいかも.
  • ビルドステップは必要に応じて自分で作ることもできる(欲しいパラメータあったのでリクエスト送ったら開発チームに伝えとくけど自分でも作れるよと返信をくれました).
  • ワークフローをコードで管理したい感あるが,GitHubと連携させると,WebUIじゃなくなるぽい?のでどうするのがいいんだろう.

最後に

去年行ったBitriseでreact-nativeアプリのCI/CD構築について書きました.ネイティブアプリ初めてかつ,一人でCI/CD構築をしたので とりあえずビルドできればいいやなテキトーな部分が多く, 使えてない機能とかも多いのですが,UIで直感的にワークフローを組めるのは すごく便利でした(パラメータもいい感じに設定してあるし,ドキュメントも豊富). ビルド時間が結構シビアで,お金払わないときつかったりもするのですが,そこら辺 クリアできるならばまた勉強して使ってみたいと思います. あとFastlaneも勉強してみます.

以下,Bitrise ワークフローのyamlです.貼り付ければそのまま再現できます.

---
format_version: '8'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: react-native
workflows:
  deploy_distribute:
    steps:
    - cache-pull@2: {}
    - activate-ssh-key@4:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4: {}
    - script@1:
        title: Do anything with Script step
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # set env vars
            cp ui/.env.prd ui/.env

            # set release version
            VERSION=`echo $BITRISE_GIT_TAG | sed -e "s/v//"`
            envman add --key BUNDLE_VERSION_STRING --value $VERSION
            APP_JSON=`jq ".expo.version=\"$VERSION\" | .expo.ios.bundleIdentifier=\"$BUNDLE_ID\"" ui/app.json`
            echo $APP_JSON > ui/app.json
    - yarn@0:
        inputs:
        - workdir: ui
        - command: install
    - set-xcode-build-number@1:
        inputs:
        - plist_path: ui/ios/service/info.plist
    - cocoapods-install@2: {}
    - ios-auto-provision-appstoreconnect@2:
        inputs:
        - distribution_type: app-store
    - xcode-archive@3:
        inputs:
        - export_method: app-store
    - deploy-to-bitrise-io@1: {}
    - slack@3:
        inputs:
        - text: "*$BUNDLE_VERSION_STRING* です"
        - api_token: "$SLACK_OAUTH_TOKEN"
        - channel: "$SLACK_NOTIFICATION_CHANNEL"
        - webhook_url: "$SLACK_WEBHOOK_URL"
    - cache-push@2: {}
    description: For stg distribution
  deploy_ota:
    steps:
    - cache-pull@2: {}
    - activate-ssh-key@4:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4: {}
    - yarn@0:
        inputs:
        - workdir: ui
        - command: install
    - cocoapods-install@2: {}
    - script@1:
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # set release version
            VERSION=`echo $BITRISE_GIT_TAG | sed -e "s/v//"`
            envman add --key BUNDLE_VERSION_STRING --value $VERSION
            APP_JSON=`jq ".expo.version=\"$VERSION\"" ui/app.json`
            echo $APP_JSON > ui/app.json
        title: set version numbers
    - set-xcode-build-number@1:
        inputs:
        - build_short_version_string: "$BUNDLE_VERSION_STRING"
        - plist_path: ui/ios/service/Info.plist
    - script@1:
        title: expo publish
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # expo publish
            npm install -g expo-cli
            expo login -u $EXPO_USERNAME -p $EXPO_PASSWORD
            cd ui
            cp .env.prd .env
            expo publish --release-channel prd
    - slack@3:
        inputs:
        - text: "$BUNDLE_VERSION_STRING"
        - api_token: "$SLACK_OAUTH_TOKEN"
        - channel: "$SLACK_NOTIFICATION_CHANNEL"
        - webhook_url: ''
    - cache-push@2: {}
    description: For stg distribution
  deploy_store:
    steps:
    - cache-pull@2: {}
    - activate-ssh-key@4:
        run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
    - git-clone@4: {}
    - script@1:
        title: Do anything with Script step
        inputs:
        - content: |-
            #!/usr/bin/env bash
            # fail if any commands fails
            set -e
            # debug log
            set -x

            # set env vars
            cp ui/.env.prd ui/.env

            # set release version
            VERSION=`echo $BITRISE_GIT_TAG | sed -e "s/v//"`
            envman add --key BUNDLE_VERSION_STRING --value $VERSION
            APP_JSON=`jq ".expo.version=\"$VERSION\" | .expo.ios.bundleIdentifier=\"$BUNDLE_ID\"" ui/app.json`
            echo $APP_JSON > ui/app.json
    - set-xcode-build-number@1:
        inputs:
        - build_short_version_string: "$BUNDLE_VERSION_STRING"
        - plist_path: ui/ios/service/Info.plist
    - yarn@0:
        inputs:
        - workdir: ui
        - command: install
    - cocoapods-install@2: {}
    - ios-auto-provision-appstoreconnect@2:
        inputs:
        - distribution_type: app-store
    - xcode-archive@3:
        inputs:
        - compile_bitcode: 'no'
        - upload_bitcode: 'no'
        - export_method: app-store
    - deploy-to-itunesconnect-application-loader@1:
        inputs:
        - itunescon_user: "$APPLE_ID_EMAIL"
        - password: "$APPLE_ID_PASSWORD"
        - app_password: "$APPLE_APP_PASSWORD"
        - connection: 'off'
    - deploy-to-bitrise-io@1: {}
    - slack@3:
        inputs:
        - text: "$BUNDLE_VERSION_STRING"
        - api_token: "$SLACK_OAUTH_TOKEN"
        - channel: "$SLACK_NOTIFICATION_CHANNEL"
        - webhook_url: ''
    - cache-push@2: {}
    description: For stg distribution
app:
  envs:
  - opts:
      is_expand: false
    PROJECT_LOCATION: ui/android
  - opts:
      is_expand: false
    MODULE: ui
  - opts:
      is_expand: false
    VARIANT: ''
  - opts:
      is_expand: false
    BITRISE_PROJECT_PATH: ui/ios/service.xcworkspace
  - opts:
      is_expand: false
    BITRISE_SCHEME: service_prd
  - opts:
      is_expand: false
    BITRISE_EXPORT_METHOD: develop
  - opts:
      is_expand: false
    EXPO_USERNAME: service-dev
  - opts:
      is_expand: false
    BUNDLE_ID: com.service.service
  - opts:
      is_expand: false
    SLACK_NOTIFICATION_CHANNEL: "#service_notice_bitrise"

GCP + ネイティブアプリでのGitLab flow構築

はじめに

これは2021年振り返りカレンダーの2日目の記事です.

前回はCloud Run上で動くサービス用にGitHub flowでのCD環境を構築する話でした.

serenard.hatenablog.com

前回の最後やいろんな記事で述べられているとおり,GitHub flowは審査が必要なネイティブアプリなど,アプリケーションの リリースタイミングをこちら側でコントロールできないケースで不都合が発生します(モノレポ想定です).

例えばあるアプリについて,後方互換のない新機能が入ったバージョンv2がmainにマージされ,stg環境に 上がっているとします. この時,prdのネイティブ以外の部分でなんらかのバグが見つかり,すぐに修正を行いたいとき,通常のサービスならば, 修正PRをmainにマージし,新規機能とともにリリースすることができます. しかしネイティブアプリの場合,アプリの審査がリリース前に挟まれるため,修正をデプロイするには, 審査を待つか,既にmainに入っている新機能をrevertするしかありません.

対策としては,ネイティブアプリのレポジトリを分けることが考えられますが, 自分のいたチームでは全員が機能開発に対してバックエンド,アプリを両方書いていたため, PRの管理やリリースバージョンの統一が面倒になるなど別の問題が想定されました.

そこで,今回はGitHub flowをやめ,GitLab flowを採用することとしました. GitLab flowの詳細については以下に詳しく書いてあります.

postd.cc

要点をまとめると以下のような感じです.

  • mainブランチに加え,環境(stg, prdなど)ごとにブランチを用意する.
  • 新規の変更はmainからブランチを切って,mainにマージする.
  • 検証環境にデプロイする際はmainをそのブランチへマージし,デプロイする
  • 検証が終わったら,検証ブランチを本番ブランチにマージし,デプロイ
  • 本番に近いブランチは,必ずその前の全ての検証ブランチを通っているようにする

仕様

今回はクライアントはiOSアプリ,バックエンドはCloud Runに乗せている形を想定します. また,環境は一旦stg環境とprd環境の2つとします.

そして,仕様は次のような形です.

  1. 新規の変更はmainブランチから切り,mainブランチにマージ.また,検証用ブランチとしてstaging,本番ブランチとしてprductionを用意
  2. 常にmainからstgへのPRを生成し,リリースのタイミングでそれをstagingへマージし,リリースコマンドを打つ.このタイミングで,stagingからproductionへのPRを生成.
  3. staging上での検証が終わったら,stgからprdへのPRをマージ.アプリをビルドし,審査へ提出.審査にapiの変更が必要な場合はこのタイミングでデプロイ.
  4. 審査が通ったら,必要に応じてmigrationを行い,apiをデプロイ.

また,hotfixの流れは以下のような形です,

  1. stgからhotfixブランチを切り,stgにマージし動作確認.確認が終わったら,ブランチをmainとstgにマージ.
  2. stgをprdにマージし,審査&デプロイ.

実装

ディレクトリ構成は前回と同じです. CDフローに必要なファイルなどは,release下に,環境ごとに用意します.

root/
├── .github/workflows
│  ├── release.yaml
│  ├── hotfix.yaml
│  └── prerelease.yaml
├── api
├── ui
├── release
│  ├── dev
│  └── prd
└── release.sh

新しいバージョンのリリースとstagingへのデプロイ

新規バージョンのリリースの際はまず,mainからstgingブランチへ差分をマージします. この差分PRを常時生成するために, git-pr-releaseを用いました.

GitHub - x-motemen/git-pr-release: Release pull request generator

git-pr-releaseでは,ブランチ同士を比較して, 差分がある場合はPRの自動生成と差分コミットの詳細を作ってくれます.

設定ファイルは以下のような感じです. GIT_PR_RELEASE_BRANCH_STAGINGにはマージ元を,GIT_PR_RELEASE_BRANCH_PRODUCTIONにマージ先を設定します. また,GIT_PR_RELEASE_TEMPLATEには生成するPRのテンプレートを設定することができます.

# prerelease.yaml
on:
  push:
    branches: [main]

jobs:
  create-release-pr:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Setup Ruby
        uses: actions/setup-ruby@v1
        with:
          ruby-version: 2.7.x

      - name: Create Staging release pull Request
        env:
          GIT_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_PR_RELEASE_BRANCH_PRODUCTION: staging
          GIT_PR_RELEASE_BRANCH_STAGING: main
          GIT_PR_RELEASE_LABELS: staging
          GIT_PR_RELEASE_TEMPLATE: .github/workflows/prerelease_template
        run: |
          gem install -N git-pr-release -v "1.9.0"
          git-pr-release --no-fetch --squashed

次に,新規バージョンタグをプッシュした際の動きを実装します. その際には以下を行います.

  • リリースノートの生成
  • stagingからproductionへのPR生成
  • staging環境へのデプロイ

リリースノートの生成は,前回と同様以下を用います.

zenn.dev

stagingから,productionへのPR生成は,上記のrelease.yamlに以下の動作を追加し行います.

リリース用のブランチ(名前はrelease/v0.0.0)を生成したのち,そのブランチからproductionへ 向けたPRを生成しています.

# release.yaml

on:
  push:
    tags:
      - "v*"

name: Create Release

jobs:
  build:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Get version
        id: get_version
        run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}

     ############### 省略 ###############
      
      # リリース用のブランチを生成
      - name: Create Release Branch
        id: create_release_branch
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git checkout pre-production
          git checkout -b release/${{ steps.get_version.outputs.VERSION }}
          git push origin release/${{ steps.get_version.outputs.VERSION }}

     # 作成したリリースブランチからprdブランチへのPRを生成
      - name: Create Prd Release Pull Request
        id: create_release_pr
        uses: repo-sync/pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          pr_title: Production Release ${{ steps.get_version.outputs.VERSION }}
          pr_label: production-deploy
          destination_branch: production
          source_branch: release/${{ steps.get_version.outputs.VERSION }}

staging環境へのデプロイは,前回と同じく,cloud buildのトリガーを用います. また,iosアプリの方のCDについてはまた次回以降で書く予定です.

productionへのデプロイ

productionへのデプロイは,以下の2つのステップに分かれています.

  • ネイティブアプリの審査提出
  • apiのデプロイ

ネイティブアプリの審査への提出は常に行われますが,apiのデプロイは ケースによって審査に必須であったり必要なかったりします(バージョン整合のため).

なので,アプリの審査提出は自動化し,apiのデプロイは手動で行うこととします.

ネイティブアプリの審査提出に関しては,また次回以降で解説します.

apiのデプロイに関しては,前回と同様,release/prd下のスクリプトを叩くことで行います.

#!/bin/bash

function deploy() {
  gcloud beta builds triggers run --tag $2 service-prd-api-trigger
}

$1 $@
cd release/prd
sh deploy.sh deploy v0.0.0

これで通常のCDフローは完了です.

hotfix

次はhotfixへの対応です. hotfixというprefixを持つブランチがpushされた時,自動でmainとstagingブランチへのPRを生成するようにします (ぼーっとしてるとどっちかを入れ忘れるため). コードは以下のような形です.

# hotfix.yaml
on:
  push:
    branches: ["hotfix/*"]

env:
  BRANCH_NAME: $(echo ${GITHUB_REF#refs/heads/})

jobs:
  create_hotfix_pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Main Create Hotfix Pull Request
        id: main_create_hotfix_pr
        uses: repo-sync/pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          pr_title: "Main ${{ env.BRANCH_NAME }}"
          pr_label: "hotfix"
          destination_branch: main

      - name: Pre-Production Create Hotfix Pull Request
        id: preproduction_create_hotfix_pr
        uses: repo-sync/pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          pr_title: "Pre-Production ${{ env.BRANCH_NAME }}"
          pr_label: "hotfix"
          destination_branch: staging

これで全体の構築が完了です.

その他細かい知見など

  • git-pr-releaseはsquashマージと通常マージの両方に対応可能.
  • mainからstagingブランチへのマージをsquashにしてしまうと,リリースノートがsquashコミットだけになって 差分の確認がしづらくなるので,ここは通常マージにしたほうが良い(めんどいので,もっといい方法ないかな...).
  • hotfixとmainでコンフリクトが起きた時の対処がよくわかっていない...

最後に

GitHub flowで起きる問題を解決した,GitLab flowの実装解説をしました. といっても今回解説したのだと,stagingにアプリが上がってるときにhotfixが必要になった場合 またリバートしなくてはならないので,本当にこの問題を対策するときはstagingやmainへのマージを慎重に行ったり,apiデプロイするようにしたりなど追加の検討が必要そうです. ただ柔軟性が高く拡張しやすいので,アプリならば最初から採用しておくのが楽かな〜という所感でした.

省略したアプリのCD周りなどについては,次回以降に書く予定です.

Cloud Run におけるGitHub flowでのCDフロー構築

はじめに

これは2021年振り返りカレンダー及びついに卒業かぁカレンダーの1日目の記事です.

昨年はスタートアップにて新規サービス開発におけるCDフローの構築を複数回行ったので,そこから得られた知見をまず書いていこうと思います.前提として,チームは数人,トラフィックもそんなに大きくないという想定での設計です.

まずはCloud RunでのGitHub flow の構築についてです. 最近見る新規サービスはこれに乗ってるのをよく見る気がします.

engineering.mercari.com

(今回作るのはこんな強そうなのではないですが...)

お金になるかわからない初期サービスにおいてCloud Runを用いるメリットは以下があると思っています.

  • リクエストがない時は一定時間でインスタンスが落ち,課金もされない
  • オートスケール
  • revision機能があり,新規バージョンでバグが出た時はすぐに前のバージョンに戻せる(DBの依存なければ).

また,GitHub flowの解説については,大まかに要点をまとめると,

  • mainブランチは常にデプロイ可能
  • 新規の変更はmainからブランチを切りPRを出しmainにマージ.

みたいな感じです.

仕様

今回は,フロントエンド(SSR),バックエンドの両方をCloud Run上に乗せている形を想定します. また,環境は一旦stg環境とprd環境の2つとします(何個でも良い).

そして,仕様は次のような形です.

  1. mainブランチがプロダクトのデプロイ可能な最新コードになっているよう管理.
  2. 新機能は feature ブランチ,fix ブランチなどとして,mainから切り,レビュー後に main にマージ.
  3. きりの良いタイミングで,stg環境へのデプロイを行う.バージョンタグとともにコマンドを以下の形で打つと,リリースノートの生成と,stg環境へのデプロイがされる. sh release.sh release v0.0.0

  4. 動作確認ができたタイミングで,migarationなどを行ったのち,prd環境をデプロイ.

  5. prdでバグが起きたら,修正 or 前のバージョンにロールバック

実装

ディレクトリ構成は以下のような感じです(モノレポ). CDフローに必要なファイルなどは,release下に,環境ごとに用意します.

root/
├── .github
├── api
├── ui
├── release
│  ├── dev
│  └── prd
└── release.sh

リリース

リリースでは以下を行います.

  • mainの最新にバージョンタグを振る(githubのtag機能を用いる)
  • 前回のリリースからの差分をまとめたリリースノートを生成
  • stg環境へのデプロイ

リリースタグをpush

以下のようなスクリプトを用意しておくと,tag pushがしやすいです.

#!/bin/sh

function release() {
    git checkout main
    git pull origin main
    git tag -a $2 -m "release $2"
    git push origin $2
}

$1 $@

以下のような形で使います.

 sh release.sh release v0.0.0 

リリースノート生成

各バージョンごとに,前回のリリースからどのような変更があったのかを見れるようにしておくのは,

などのために有用です.

今回は,前回のリリースからの差分コミットをリリースノートにのせるように, 以下のようなGitHub Actionsのワークフローを作りました.

この記事の内容をそのまま使わせていただきました. zenn.dev

バージョンタグを表す v から始まるtagがpushされた時に,リリースノート生成が行われます.

リリースノートの生成は,もっと新しい方法が色々ありそうです.

www.publickey1.jp

stg環境へのデプロイ

stg環境へのデプロイは,cloud buildをgithubと連携させることで行います.

デプロイ用のスクリプトなどは,release/stg下で,以下のような構成で管理します.

release/stg
├── .env
├── .env.secret
├── deploy.sh
├── deploy_api.yaml
└── deploy_ui.yaml

deploy.shはapi及びuiをデプロイするためのコマンドを,各yamlファイルはcloud buildの構成を, .envは環境変数を,.env.secretはGCPのsecret managerに保存したシークレットの名前を管理します.

まずdeploy.sh,.env,.env.secretの内容について解説します.

deploy.shのdeploy_apiでは,バージョンタグ,.env,.env.secretを引数として受け取り, apiのコンテナをcloud runにデプロイします.

sh deploy.sh deploy_api v0.0.0 .env .env.secret

deploy_uiもだいたい同じです.

このように.envや.env.secretを別個に用意する構成にすることで, メンバーの全員が環境変数の追加を簡単にできる などのメリットがあります(特に開発初期は追加が多いので).

#!/usr/loca/bin/bash
# deploy.sh

function deploy_api() {
  # 固定変数の定義
  [[ -n "$PROJECT_ID" ]] || PROJECT_ID="service-stg"
  [[ -n "$IMAGE_ID" ]] || IMAGE_ID="service-api"
  [[ -n "$SERVICE_NAME" ]] || SERVICE_NAME="service-stg-api"
  [[ -n "$SERVICE_ACCOUNT" ]] || SERVICE_ACCOUNT="service-stg-api"
  [[ -n "$REGION" ]] || REGION="asia-northeast1"
  [[ -n "$INSTANCE_CONNECTION_NAME" ]] || INSTANCE_CONNECTION_NAME="service-stg:asia-northeast1:service-stg"
  [[ -n "$CLOUD_RUN_MIN_INSTANCES" ]] || CLOUD_RUN_MIN_INSTANCES="1"
  [[ -n "$CLOUD_RUN_MAX_INSTANCES" ]] || CLOUD_RUN_MAX_INSTANCES="5"
  [[ -n "$CLOUD_RUN_MEMORY" ]] || CLOUD_RUN_MEMORY="2Gi"
  [[ -n "$CLOUD_RUN_CPU" ]] || CLOUD_RUN_CPU="2"

  # バージョンタグを引数として受け取る
  TAG_NAME=$4

  # cloud runのサービス名のsuffixにバージョンを付ける
  BASE_COMMAND="
  gcloud beta run deploy $SERVICE_NAME \
  --revision-suffix $(echo $TAG_NAME | sed -e s/\\./-/g) \
  --image gcr.io/$PROJECT_ID/$IMAGE_ID:$TAG_NAME \
  --region $REGION \
  --platform managed \
  --min-instances $CLOUD_RUN_MIN_INSTANCES \
  --max-instances $CLOUD_RUN_MAX_INSTANCES \
  --memory $CLOUD_RUN_MEMORY \
  --cpu $CLOUD_RUN_CPU \
  --service-account $SERVICE_ACCOUNT \
  --add-cloudsql-instances $INSTANCE_CONNECTION_NAME \
  "
 
  # .envから環境変数を読み込み
  command=$BASE_COMMAND
  while read line
  do
    IFS='=' read -r -a array <<< $line
    env_name=${array[0]}
    env_value=${array[1]}
    command="$command --update-env-vars $env_name=$env_value"
  done < $2

  # secretを読み込み
  secret_options="--set-secrets "
  while read line
  do
    IFS='=' read -r -a array <<< $line
    secret_name=${array[0]}
    secret_value=${array[1]}
    secret_options="$secret_options$secret_name=$secret_value:latest,"
  done < $3

  command="$command $secret_options"

  $($command)
}

$1 $@
#.env
APP_PORT=8080
APP_CLIENT_HOST=http://service-stg.com
...
#.env.secret
DB_PASSWORD=service-stg-mysql-password
...

cloud buildの構成ファイルでは,コンテナのビルドを行った後,上記のスクリプトを使ってデプロイを行います. デプロイスクリプトを構成ファイルと分離したことで内容がかなりスッキリし,記法が独特でわかりにくい構成ファイルを 見る人の数も減らせます.

# deploy_api.yaml
steps:
  - id: build_api_image
    name: gcr.io/cloud-builders/docker
    dir: api
    args: ["build", "-t", "gcr.io/$_PROJECT_ID/$_IMAGE_ID:$TAG_NAME", "."]
  - id: push_api_image
    name: gcr.io/cloud-builders/docker
    args: ["push", "gcr.io/$_PROJECT_ID/$_IMAGE_ID:$TAG_NAME"]
  - id: deploy_api
    name: gcr.io/google.com/cloudsdktool/cloud-sdk
    dir: release/dev
    entrypoint: bash
    args: ["deploy.sh", "deploy_api", ".env", ".env.secret", "$TAG_NAME"]
images:
  - gcr.io/$_PROJECT_ID/$_IMAGE_ID:$TAG_NAME
substitutions:
  _PROJECT_ID: service-stg
  _IMAGE_ID: service-api

最後に,バージョンタグをプッシュした際にデプロイが自動で行われるよう, cloud buildとgithubの連携とトリガーの作成を行います. 詳細は以下.

cloud.google.com

これで,リリースからstgへのデプロイまでの自動化が完了です.

prd環境へのデプロイ

prd環境へのデプロイも,stgへのデプロイと内容はほぼ一緒です. 異なる点は,リリースの際に自動デプロイされるのではなく, 開発者の任意のタイミングでデプロイを行うという部分です. そのため,cloud buildのトリガーを,tag pushではなく,manualに設定します. 以下のようなスクリプトをprd下に用意しておくことで,簡単に作業が行えます.

# deploy.sh
function deploy() {
  gcloud beta builds triggers run --tag $2 service-prd-api-trigger
  gcloud beta builds triggers run --tag $2 service-prd-ui-trigger
}

今回はprdのデプロイ時にコンテナのビルドとcloud runへのデプロイを行う形にしましたが, リリースの際にprdコンテナのビルドとcloud runへのトラフィック無しでのデプロイを 行い,prdデプロイの際はトラフィックを切り替えるだけ,という方式にすると,prdデプロイが 速くなります.その場合のデプロイコマンドは以下のようになります.

# deploy.sh
function deploy() {
  gcloud beta run services update-traffic service-prd-api --to-revisions service-prd-api-$(echo $2 | sed -e s/\\./-/g)=100 --region asia-northeast1 --platform managed
  gcloud beta run services update-traffic service-prd-client --to-revisions service-prd-client-$(echo $2 | sed -e s/\\./-/g)=100 --region asia-northeast1 --platform managed
}

以上が,構築したcloud run上でのgithub flowでのCDフローの全体になります.

その他細かい知見など

  • クラウド上のリソースにプロジェクトIDを入れるようにしておくと,プロジェクト切り替えを忘れてた時なども事故らなくて済む(cloud buildのトリガーなど,開発者が叩く部分は特に).
  • ローカルmacでbuildしたコンテナをcloud runにあげるとマシンの違いでアプリケーションがバグることがあるのでビルドはcloud build上で行うようにする
  • cloud runのリビジョンをUIから弄りまくったりすると,たまに切り替えられなくなるバグが起きる
    • 新規サービスをデプロイしたのにトラフィック切り替わらなくて,結局サービスを作り直すことがあった(請求先設定忘れててプロジェクトが落ちた前後や,連続でトラフィック切り替えまくった際などに起きた.条件がよくわかりませんでした...).

最後に

昨年度の最初に作ったCDフローの解説を行いました. 単純な構成ですが,初期においてはメンバー全員が設定(環境変数)の追加をしやすい,高速でデプロイサイクルを回せるなどの利点がありました. リリース生成部分や,環境変数管理の部分などは新しいやり方がもっとありそうですし,チームの規模やサービスの性質によって内容も変わると思います. 実際,ネイティブアプリのようなリリースタイミングをこちらで制御できないケースにおいては,緊急修正(hotfix)がすごく面倒でした. アプリの場合については,次回以降のGitlab flow編で書く予定です.

knowledge graph構築のサーベイ論文:Architecture of Knowledge GraphConstruction Techniquesを読む

論文情報

https://acadpubl.eu/jsi/2018-118-19/articles/19b/24.pdf

概要

knowledge graph(KG)の構築に関するsurvey.

Architecture of Knowledge Graphs

KGの構築方法は大まかにtop-down方式とbottom-up方式に分けられる.

top-down方式はKGのontologyとschemaがあらかじめ定義されており,そこに新たな情報を追加していくというもの.ドメインをよく表現できるontologyの定義ついて議論されることが多い.

一方,bottom-up方式では,Linked Open Data(Wikiとか?)や他の知識ソース(テキストとか?)から情報の抽出及びKGの構築を行う.

この論文では主にbottom-up方式について解説をする.

Screen Shot 2020-10-19 at 20 39 47

Procedures of Bottom-up ApproachofKnowledge Graphs

bottom-upアプローチを構成する手法について解説する

Knowledge Extraction

data sources

主なデータソースとしてはhtml, xml, jsonのような構造データ,フリーテキストや画像のような非構造データがある.これらのデータを,機械が理解しやすいRDFJSON-LDformatに落とす.

初期のKGでは,単一のソースのみから情報抽出を行っていた.例えば,医療の分野では,診療録や医学雑誌などを対象にしていた.

一方で大規模KGの構築では様々なデータソースから情報抽出を行う.例えば,Google Knowledge Vaultはテキスト,HTML tree,HTML tables,人間がアノテーションしたページなどをソースとしていた.最近では特に,Wikipediaなどの情報抽出が非常に行いやすいソースが重要視されている.YAGOやDBpediaはWikipediaをソースとしている代表例である.

その他,医療業界など特定ドメインのKGにおいてもWikipediaは利用されている.

Wikipediaの他にも,WordNetやGeoNames, Concept Net, IMDB, MusicBrainzなども良いソースであると言える.

Types of Knowledge Extraction

extractionの手法は大まかに3つに分けられる.

  • Entity extration
  • Relation extraction
  • Attribute extraction

attribute extractionはrealtion extractionの特別な場合とも言える

Entity Extractionとはknowledge resourceからエンティティを見つけ,事前に定義されているカテゴリに分類する作業である.Entity Extractionの質は後続の処理に大きく関わるため,Knowledge Extractionの中でも最も重要なパートと言える.

Entity Extractionの後,entity間のrelationの解析を行い,relation extractionを行う.

relation extractionがdenotational semanticsを定義するのに対し,attribute extrationはエンティティのintentional semanticsを定義する(よくわからない.エンティティの属性をより明確に表す情報を取り出すということか?).

Approaches of Knowledge Extraction

Knowledge Extractionの手法は以下の分野と関わりが強い.

  • NLP
  • text mining
  • machine learning

初期のKnowledge Extrationは,人手でアノテーションされたコーパスに対してルールベース,辞書ベースの手法を適用することで行っていた.

機械学習的アプローチでは人手で訓練データセットを作って教師あり学習をするケースが多いが,アノテーションに多くの専門家と時間がが必要であることから,結果として得られるコーパスは小さくなりがちなため,KEとはミスマッチ感がある.また,教師あり学習アプローチは対応できる範囲が既存のコーパスの範囲内のみであり,新たなエンティティへの対応が必要なKEには利用しづらい.

近年では,アノテーションのコストを避けられる反教師あり,もしくは教師なし学習が主に研究されている. 主に以下のような分類モデルが利用されている

  • Hidden Markov Model
  • CRF
  • kNN
  • Maximum Entropy Model
  • SVM

評価指標としてはF-measureが主に利用される.

Knowledge Fusion

Knowledge Fusionのゴールはエンティティの順序関係を捉え,オントロジーを構築することである.これはiterativeに行われる.オントロジーの構築プロセスは,結果の質が一定の評価基準を超えるまで繰り返される.

Entity Alighment

Entity AlighmentはEntity Resolution,Entity Matchingなどとも呼ばれたりする. 具体的には異なるEntityが現実世界における同一のオブジェクトを指すかなどの判断を行う作業である. Entity Alighmentでは通常,複数のEntity Matchingの手法をKGの特性に合わせて組み合わせて用いることをする.

以下がEntity Alighmentのフロー.

Screen Shot 2020-10-19 at 16 52 58

Data Processingはデータのstandardizationなどとも呼ばれ,Entity Alighmentの中の重要なステップの一つであり,以下のような問題に対応する必要がある.

  • 複数のソース上で異なる名称として存在する,定義が異なる,データの表現方法が異なるなどの問題に対応し,データの完全性,一貫性を保つ

Entity Alighmentのためには主にEntityの持つテキスト情報などのattributeから求められる類似度を使うfeature matchingが利用される.

Attributeのmatchや比較のためにはテキスト類似度が用いられ,relationのmatchなどには構造的類似度が用いられる

Entity Alighmentは,通常はWikipediaやentity情報などの外部情報に依存している.

近年では他のKGを補助情報として知識推論を行う手法などの研究が行われている.

Ontology Construction and Evaluation

KG構築のためには, entity及びrelationのalignの他にも次のような作業が必要となる.

  • taxonomy(分類体系)や階層構造のの構築
  • metadataの追加
  • 他のデータソースの情報の追加

KGの質の担保のため,オントロジーにはFOAFのような一般的なものを使い,また,metadataはschema.orgに対応することが必須となる.

Q

  • KGの質の評価の具体的方法
  • FOFA, schema.orgを導入するとはどういうこと?