羊をめぐるブログ

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

はじめての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を導入するとはどういうこと?

ネイティブアプリでのOCRの性能を検証してみる(Tesseract, Cloud Vision API)

はじめに

小説などを読んでいると時々見たことのない単語に出会すことがあります.

最近だと

「傴僂」 「聯関」 「偏頗」

などに遭遇し,困りました.

全く知らない単語に出会した時,読み仮名がふってあれば良いのですが,ない場合も多く,そうなると検索が非常に困難です(紙だと単語コピーもできない).傴をにんべん口三つなどとググったのは黒歴史です,

手書きで漢字を判定するアプリはよくありますが,細かい漢字を指で書くのは面倒なので写真でできないかなと思い,ネイティブアプリでのOCRを検証してみました. 検証の対象としては,OCRエンジンとして有名そうなTesseractのswift版であるSwiftyTesseract,Cloud Vision APIの一部として提供されているOCR APIを利用しました.

github.com

cloud.google.com

検証内容

与えるデータ

まず,一般に認知されてる少し複雑な単語として以下を試します.単語の認識ソースとしては,ググって出てきたwebページを撮影し,単語箇所を切り抜いたものを利用します.

「溢れる」 「詳細」 「鬱病

f:id:sheep96:20201004012609j:plain f:id:sheep96:20201004012622j:plain f:id:sheep96:20201004012634j:plain

次に,あまり日常生活で見ない単語である

「傴僂」 「聯関」

f:id:sheep96:20201004012717j:plain f:id:sheep96:20201004012729j:plain

を試してみます.

最後に,OCRはそもそも単語でなく文章を読み取るためのものな気がするので,新潮文庫より出版されている小説「金閣寺」(三島由紀夫著)の裏表紙のあらすじを与えてみます.

f:id:sheep96:20200930185545j:plain:w300

使うモデル

モデルとしては以下を利用します

  • Tesseract (速度重視のtessdata_fast,精度重視のtessdata_best,公式ではfastがおすすめらしい)

  • Cloud Vision API

実装の参考は以下

shinjism.com

結果

単語の読み取り

Tesseract Best Tesseract Fast CVA
処理時間 一瞬 一瞬 1秒
溢れる OK OK OK
詳細 OK OK OK
鬱病 OK OK OK
傴僂 価 佑 偏優 區傻
聯関 職 関 隊関 OK
  • 一般的に認知されている単語についてはどれも正確に読み取ることができた

  • 認知度の低い単語については,Vision APIはTesseractより秀でているようです(傴僂についても近い漢字を出せている).モデルの質よりは学習データ上の語彙数とかの問題な気がする.

  • 処理時間に関しては,ほぼ一瞬.Vision APIはレイテンシも含んでいるので少し遅い.

文章の読み取り

Tesseract Fast

処理時間:1,2秒ほど

一九五〇年七月一日、「国宝・金剛
寺焼失。放火犯人は寺の青年信」 と
いう衝撃のニュースが世人の耳目を
驚かせた。この事件の陰に潜められ
た若い学僧の悩みーーハンディを背
負った宿命の子の、生への消しがた
い呪いと、それゆえに金開の美の魔
力に魂を要われん、ついには幻想と心
中するにいたった悲劇……。31歳の
鬼オ三島が全青春の決算として告白
体の名文に組った不朽の金字塔。

精度に関しては所々間違うところはあるが,長文においても処理が速いのは魅力的.

Tesseract Best

処理時間:10秒ほど

ー 九 五 〇 年 七 月 一 日 、「 国 宝 ・ 金 閣
寺 焼 失 。 放 火 犯 人 は 寺 の 青年 僧 」 と
いう 衝撃 の ニュ ー ス が 世 人 の 耳目 を
藻 か せ た 。 こ の 事件 の 陰 に 潜め られ
た 若い 学 僧 の 悩み ーー ハン ディ を 背
負っ た 宿命 の 子 の 、 生 へ の 消し が た
い 上 呪い と 、 そ れ ゆ え に 金閣 の 美 の 訂
力 に 魂 を 奪わ れ 、 つ い に は 幻想 と 心
中 する に いた っ た 悲劇 ……。31 歳 の
鬼 オ 三島 が 全 青春 の 決算 と し て 告白
体 の 名 文 に 綴っ た 不朽 の 金 字 護 。

単語間に空白が混ざるが,精度に関しては2,3箇所間違える程度.

Cloud Vision API

処理時間:7秒ほど

一九五○年七月一日、「国宝 金閣\n寺焼失。放火犯人は寺の青年僧」 と\nいう衝撃のニュースが世人の耳目を\n驚かせた。この事件の陰に潜められ\nた若い学僧の悩み ハンディを背\n負った宿命の子の、生への消しがた\nい呪いと、それゆえに金閣の美の魔\n力に魂を奪われ、ついには幻想と心\n中するにいたった悲劇 。31歳の\n鬼才三島が全青春の決算として告白\n体の名文に綴った不朽の金字塔。\n

精度に関しては完璧.

所感

  • 一般的な単語の認識ならTesseractでも十分だが,複雑な語彙にも対応するならCloud Vision APIを使う方が良さそう.

    • 当初の目的の単語判別をするならCVA使うしかない
    • Vision APIは1000リクエスト/month 以上は課金なので節約を考えつつ組み込むのが怠そう...
    • 複雑な鬱はどのモデルも認識できているので,モデルの性能よりは学習データの語彙の差な気がする.
  • 文章長が処理速度に結構影響している

  • Tesseractは公式の通り,fastを使うのが良さそう(精度の差は少しだが,長文の処理は10倍ぐらい速い)

flutterで内蔵辞書検索(iOS)

はじめに

flutterで内蔵辞書検索 on iOSを実装してみたので,その備忘録を書きます.

方法

以下の手順で行いました

  1. iOS側で辞書検索のためのmethodを実装

  2. flutter側からmethod channelで呼び出す

単純ッ

実装

iOS側の実装

iOSでは内蔵辞書検索のためのAPIとしてUIReferenceLibraryViewControllerが用意されています

Apple Developer Documentation

こちらのapiは,単語を引数として受け取り,内蔵辞書に対する検索結果のViewControllerを返すという動作をします.辞書内に複数見つかった場合はリスト表示され,見つからなかった場合はNot Foundが表示されます.

あくまで辞書の検索結果のページを返すだけであり,検索結果をデータとして取得して弄るなどのことはできないようです(著作権のせい?).

ただ,MacOS用なのででiOSでは使えないのですが,swift自体にはDCSCopyTextDefinitionという検索結果をデータとして得られそうなAPIが用意されているみたいです.

Apple Developer Documentation

とりあえず,今回はflutterからUIReferenceLibraryViewControllerを呼び出せるようにiOS側に実装します.method channelの実装は以下を参考にしました

FlutterでiOSのネイティブコードを呼び出す - Qiita

以下をmethod channelで呼び出します

    func searchDictionary(result: FlutterResult, controller: FlutterViewController, queryWord: String){
        let ref: UIReferenceLibraryViewController = UIReferenceLibraryViewController(term: queryWord)
        controller.present(ref, animated: true, completion: nil)
    }

}

flutter側

iOS側に書いたmethodを呼び出すだけなので省略.

実際の動作

実際に使ってみると,レスポンスは遅いですが,辞書検索の結果が表示されることがわかります. iOSが検索窓などで提供してる辞書検索は一瞬で結果が出るのに,なぜアプリからだとこんなに遅いんだ

streamable.com

その他

内蔵辞書検索機能は,それだけを提供するような単純なアプリだとrejectになるとドキュメントや以下のブログにて言及されています

Apple Developer Documentation [iOS] 内蔵辞書機能を使うとリジェクトされる? | Cocoamix.jp

どの程度シンプルだとrejectになってしまうのかはわかりませんが,サブ機能程度にしておいた方が落ち込む危険性が低そうです

email & socialログインを実装する際のRDBのスキーマについて

はじめに

email & twitterなどのsocialアカウントでのログインの実装をしようと思ったのですが,各サービス用の認証情報のためのRDBスキーマをどう設計するかで詰まりました. 現状のベストプラクティスなどを探してみたのですが,あまり情報が見つからなかったので,自分がやった方法を備忘録としてまとめます. 一旦,emailとtwitterでの認証を用意するという前提で設計しました.

結果

いきなりなのですが,以下が最終的に決定したスキーマになります

f:id:sheep96:20200715224911p:plain

userテーブルが基本的なユーザ情報を格納するテーブルです.

認証情報は,サービスごとにuser_auth_serviceというuserのidを外部キーとして持つテーブルを作り,そこに入れるという感じです.

userがどの方法で認証を行うかはuserテーブルの中に保存しておきます.

メリットとしては以下があるのではと考えています

  • 新たな認証方法を追加しやすい
    • 新たな認証用のテーブルを追加するだけでよく,既存のテーブルに変更を加える必要がない(と思う)
  • ユーザの退会がしやすい
    • ユーザの退会の際にユーザ情報を消そうとすると外部キー制約に引っかかりますが,この方法ならその影響を受けずに認証に関わる部分を一発で消せます

一方で以下のデメリットがあるような気がします

  • 複数の認証情報を持つようなケースに対応しづらい
    • emailと電話番号両方というようなケースに対応する場合はuser_auth_multiとかのテーブルを作るかauth_methodを増やすとかになるんでしょうか...
  • userと認証情報をjoinして列挙したりしづらい
  • デッドロックしないように配慮が必要そう MySQL 外部キー制約のデッドロック | 優技録

検討したけど無理そうだった方法

サービスごとの認証情報を同じテーブルで持つ方法

php - Database structure for social login implementation? - Stack Overflow

認証情報を同じテーブルで持ち,providerというカラムでどのプロバイダ用のものかを判定する方法です

以下のような方法で微妙そうだなとなりました

  • EAVはアンチパターン
    • providerごとにキーの性質が違うときつい
  • providerごとのキーがバッティングしない可能性がゼロではない
  • ユーザがどの方法で認証するかを判定するためにレコードを走査する必要がある

userテーブルにサービスごとのカラムを付け足す方法

空白箇所が増えるなど無駄が多そうなのでやめました

サービスごとにユーザのテーブルを作る方法

外部キー制約をかけるのが辛かったりユーザの列挙が辛かったりその他様々な理由でダメそうでした

最後に

今は良さそうと思っていても,実装するうちに致命的な欠点などが見つかりそうで怖いです

また,サービスのアーキテクチャはよく公開されているのを見かけるのですが,長く運用されているDBのスキーマの変遷の歴史や,ベストプラクティスはあまり見ないので,とても気になるなぁ〜と思いました