この記事はSTORES Advent Calendar 2022の9日目の記事です。
こんにちは、@tomorrowkey です。
STORES CRMモバイルチームでSTORES ブランドアプリの開発しています。
STORES ブランドアプリとは、お商売をしているオーナーさんごとにオリジナルアプリを作り、お客様へのクーポンの配布やお店からのご案内をとおして、双方にとってよい関係性を築いていくためのサービスです。
開発している側の視点からみると、日々の開発であたらしい機能を作るたびに、アプリをビルド・リリースしているのですが、すべてを手作業でやっていてはスピード感を持ってサービス開発をすすめることはできません。
そこでCRMモバイルチームでは、BitriseをモバイルアプリのCIとして活用し、複数のアプリを自動的にビルド、リリースできる仕組みを構築しています。
CRMチームの面白いところは、サービス開発だけではなくモバイル開発基盤の改善も事業上同じくらい重要な点です。いかに手を動かさずに各オーナーさまへ向けたアプリを提供できるか、日々考えて改善を繰り返しています。
今日はそんなモバイル開発基盤の改善のなかでも一部をご紹介いたします。
なお、BitriseをCIサービスとして利用している事情からサンプルはすべてBitriseを前提としたものになっていますが、このテクニックは他のCIサービスでも応用が効くので、その他サービスを利用されている方もぜひ参考にしてみてください。
どうやってCIのジョブをスケールさせるのか
さきほどお話したとおり、CRMチームではCIを使い複数のアプリを並列ビルドしています。
近年のCIサービスでは並列ビルドをするための仕組みが用意されていますが、いずれもその定義を静的に用意しておくことでしか実現できません。
つまり愚直に運用するならば、あたらしいアプリの開発が増えたときに、CIの定義を書き換える必要があるのです。
いちいちCIの定義を更新していては面倒です。エンジニアなので技術で解決しましょう。
それってどんな定義なの
具体的な定義としては次のとおりです。
--- format_version: '11' default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git project_type: android trigger_map: - push_branch: "*" workflow: build-alpha - push_branch: "*" workflow: build-bravo - push_branch: "*" workflow: build-charlie workflows: build-alpha: envs: - FASTLANE_LANE: build-alpha steps: - activate-ssh-key@4: {} - git-clone@6: {} - bundler@0: {} - install-missing-android-tools@3: {} - fastlane@3: inputs: - lane: "$FASTLANE_LANE" build-bravo: envs: - FASTLANE_LANE: build-bravo steps: - activate-ssh-key@4: {} - git-clone@6: {} - bundler@0: {} - install-missing-android-tools@3: {} - fastlane@3: inputs: - lane: "$FASTLANE_LANE" build-charlie: envs: - FASTLANE_LANE: build-charlie steps: - activate-ssh-key@4: {} - git-clone@6: {} - bundler@0: {} - install-missing-android-tools@3: {} - fastlane@3: inputs: - lane: "$FASTLANE_LANE"
例では任意のブランチにコミットが追加されたときに alpha
, bravo
, charlie
という3つのアプリをfastlaneを使ってビルドするといった定義になっています。 *1
workflow のなかでタスクを定義していくのですが、それぞれのタスクのほとんどは繰り返しで、環境変数を変えてfastlaneをタスクを呼び出しているだけです。*2.
繰り返しがあって冗長ですね。
Bitriseでは、プロジェクトルートに bitrise.yml
ファイルを置くことで、CIの定義と認識されます。
この冗長な定義を改善していきます。
YAMLの記法を使って定義を減らす
まずはYAMLそのものの記法で繰り返しを減らすことができないか検討してみます。
YAMLには anchor と alias という記法があり、定義を参照できます。
この2つの記法を使って設定をリファクタリングします。
--- format_version: '11' default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git project_type: android trigger_map: - push_branch: "*" workflow: build-alpha - push_branch: "*" workflow: build-bravo - push_branch: "*" workflow: build-charlie workflows: build: steps: &build_steps - activate-ssh-key@4: {} - git-clone@6: {} - bundler@0: {} - install-missing-android-tools@3: {} - fastlane@3: inputs: - lane: "$FASTLANE_LANE" build-alpha: envs: - FASTLANE_LANE: build-alpha steps: *build_steps build-bravo: envs: - FASTLANE_LANE: build-bravo steps: *build_steps build-charlie: envs: - FASTLANE_LANE: build-charlie steps: *build_steps
それぞれの workflow
は環境変数が違うだけで steps
の内容は同じなので、定義を1つにまとめてそれぞれから参照する形に変更しました。
build
ワークフローの steps
に build_steps
という名前で anchor をつけ、それぞれのアプリビルドのワークフローで alias により再利用しています。
これでさきほどのYAMLファイルと同義です。だいぶスッキリさせることができました。
定義を動的に生成したい
alpha
, bravo
, charlie
の配列があって、それをもとに定義できればもっとスッキリさせることができそうですが、繰り返しによって定義できないでしょうか。
残念ながら、YAML自身に繰り返しを表現する記法はないですし、CI側にもそう解釈できるような機能もありません。 *3
しかし、発想を変えれば、同等のことを実現できます。
bitrise.yml を常に最新に維持しよう
YAML単体で動的な定義を生成できないのであれば、スクリプト側でどうにかするしかありません。
Fastlaneで動的に bitrise.yml
ファイルを生成します。
すべてをコードで書くのも面倒なので、erb
を使ってできるだけYAMLの書き味をそのままに生かしていきます。*4
require "erb" require "yaml" def app_names [ 'alpha', 'bravo', 'charlie', ] end lane :'update-bitrise-yml' do BITRISE_YAML_TEMPLATE = <<~YAML --- format_version: '11' default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git project_type: android trigger_map: <% app_names.each do |app_name| %> - push_branch: "*" workflow: build-<%= app_name %> <% end %> workflows: build: steps: &build_steps - activate-ssh-key@4: {} - git-clone@6: {} - bundler@0: {} - install-missing-android-tools@3: {} - fastlane@3: inputs: - lane: "$FASTLANE_LANE" <% app_names.each do |app_name| %> build-<%= app_name %>: envs: - FASTLANE_LANE: build-<%= app_name %> steps: *build_steps <% end %> YAML bitrise_yaml = YAML.load(ERB.new(BITRISE_YAML_TEMPLATE, aliases: true).result(binding)) File.write("bitrise.yml", bitrise_yaml.to_yaml) end
app_names
の配列内の要素によって bitrise.yml
の内容を書き換えることができました。
あとは、app_names
にあたらしいビルド対象が追加されるたびに update-bitrise-yml
lane を忘れずに実行し、bitrise.yml
を生成しつづけるだけです。
ちょっとまってください。こういう「忘れずに実行するべき」というものこそ、CIが力を発揮するところではないでしょうか。
要はCIで bitrise.yml
が最新かどうかチェックすればいいのです。
require "erb" require "yaml" def app_names [ 'alpha', 'bravo', 'charlie', ] end lane :'update-bitrise-yml' do BITRISE_YAML_TEMPLATE = <<~YAML --- format_version: '11' default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git project_type: android trigger_map: <% app_names.each do |app_name| %> - push_branch: "*" workflow: build-<%= app_name %> <% end %> workflows: build: steps: &build_steps - activate-ssh-key@4: {} - git-clone@6: {} - bundler@0: {} - fastlane@3: inputs: - lane: update-bitrise-yml - script@1: inputs: - content: |- #!/usr/bin/env bash set -eo pipefail diff_of_yaml="$(git diff bitrise.yml 2>/dev/null)" if [ ! -z "${diff_of_yaml}" ] ; then echo "bitrise.yml is not updated:" 1>&2 echo "${diff_of_yaml}" 1>&2 exit 1 fi echo "bitrise.yml is the latest" - install-missing-android-tools@3: {} - fastlane@3: inputs: - lane: "$FASTLANE_LANE" <% app_names.each do |app_name| %> build-<%= app_name %>: envs: - FASTLANE_LANE: build-<%= app_name %> steps: *build_steps <% end %> YAML bitrise_yaml = YAML.load(ERB.new(BITRISE_YAML_TEMPLATE, aliases: true).result(binding)) File.write("bitrise.yml", bitrise_yaml.to_yaml) end
なんだか間違い探しみたいになってきましたが、アプリをビルドするステップの前に update-bitrise-yml
lane を実行して、Gitにコミットされている内容との差分があれば、それは最新の生成された bitrise.yml
ではないので、ワークフローを中止するようになっています。
このステップのおかげで bitrise.yml
は常に最新の動的に生成されたものが保証されるので、少ない変更でビルドするアプリを追加していくことができました。 *5
さいごに
アプリの数が増えていくこと、それを如何にCIにフィットさせるか、という題材でテクニックを紹介しました。
この内容は、スクリプトを使ってCI定義を動的に生成するといった内容なので、繰り返しに使えるだけではなく、他の動的な要素によってCIの定義を作ることができるということです。
この世のすべての開発に利用できるものだとは思いませんが、頻繁にCIの定義を変更しなくてはならないプロダクトがありましたら、このテクニックが力を発揮するのではないかと思います。ぜひご活用ください。
STORES CRMモバイルチームでは、サービス開発だけでなく開発基盤の改善も大好物だというエンジニアを募集しています。
誌面には書くことができない話もできるかと思いますので、まずはTwitterにてご連絡 お待ちしています 😉
まずは気軽にお話しましょう〜。