複数iOSアプリの証明書運用を一元化するための継続的改善 - エムスリーテックブログ

エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

複数iOSアプリの証明書運用を一元化するための継続的改善

【マルチデバイスチーム ブログリレー6日目】

エンジニアリンググループ マルチデバイスチームの藤原です。

私たちのチームでは10近いiOSアプリを開発しています。各アプリには専任の開発者がおり、プロビジョニングプロファイルは fastlane match を使ってGitリポジトリで管理しています。

しかし、アプリごとに fastlane match の運用環境が独立していたため、テストデバイスの追加や証明書更新のたびに各担当者へ作業を依頼する必要がありました。さらに、スクリプトの配置や実行手順がアプリ間で微妙に異なることから、担当者以外が対応するには認知負荷が高いという課題を抱えていました。

本記事では、既存の fastlane match の仕組みを活かしつつ、トイル(手作業の繰り返し)となっていたデバイス追加運用を「誰でも簡単に対応できる」体制へと改善したプロセスをご紹介します。

Before:アプリ数の増加に伴い顕在化した課題

当初は各アプリのリポジトリ内で fastlane match を半自動化して運用していました。しかし、アプリ数が増加するにつれて、この運用フローが徐々にトイル化していきました。

特に、デバイス追加のたびに各担当者が個別対応する体制には無駄が多く、ボトルネックになっていました。

取り組み前の状態

  • サイロ化した運用: 10近いアプリごとに独立したリポジトリで管理
  • 手順の属人化: アプリごとにスクリプトの場所や実行方法が微妙に異なる
  • コミュニケーションコスト: 窓口担当 → Slackで各アプリ担当者へ依頼 → 個別作業という伝言ゲーム
  • 横断対応の煩雑さ: 1台のテスト端末を複数アプリに登録する際、別々の担当者へ依頼が必要
  • 担当者不在時の引き継ぎ: 休暇・離席時は、他メンバーが不慣れな手順で対応せざるを得ない

典型的な対応フロー

  1. 新しいテストデバイスの追加依頼を受領(多くの場合、複数アプリへの追加が必要)
  2. 代表者が Apple Developer Portal にデバイスを登録
  3. Slack上で各アプリの担当者にプロビジョニングプロファイルの更新を依頼
  4. 各担当者が、自身のリポジトリで fastlane match を実行
  5. 各開発者に更新完了を連絡

アプリ数が少ないうちは許容できていたこのフローも、10近いアプリを抱える規模になると無視できない負担となります。特に、「1台のデバイスを複数アプリに登録するケース」では、アプリごとに異なる担当者へ連絡し、個別の手順で対応してもらう必要があり、非常に煩雑でした。

アプローチ:小さく始めて、段階的に育てる

この課題に対して「最初から完璧な自動化」を目指すのではなく、段階的なアプローチで継続的に改善を重ねる方針をとりました。

Phase 1: 専用リポジトリの構築

まず、fastlane match が証明書やプロファイルを保管するリポジトリとは別に、更新処理を一元管理するための専用リポジトリを新設し、1つのアプリから試験運用を開始しました。

既存のアプリ側リポジトリから実行していた処理を、この専用リポジトリへ移譲する形です。将来的な複数アプリへの横展開を見据え、初期段階から「設定とロジックを分離したYAMLベースの拡張可能な構成」を採用したのがポイントです。

Phase 2: 全アプリへの横展開

最初のアプリで動作が安定したことを確認後、他のアプリへの適用を進めました。

ここでの目標は、「自分の担当外のアプリであっても、一元管理の専用リポジトリから同じ手順で更新できる状態」を作ることです。各アプリに散らばっていた更新作業が集約され、全アプリのプロファイル管理が1箇所で完結するようになりました。

Phase 1で導入したYAMLベースの構成が、この横展開で大きな効果を発揮しています。

YAMLベースの設定管理

Fastfileには共通ロジックのみを記述し、アプリ固有の情報をYAMLに切り出しています。これにより、新しいアプリを追加する際はYAMLファイルをコピーして編集するだけで済みます。また、アプリ情報とアカウント情報を分離することで、複数の Apple Developer Account にも柔軟に対応できるよう設計しました。

apps/sample-app.yml (サンプル):

name: "Sample App"
account: "company"
git_url: "ssh://git@example.com/ios-fastlane-match.git"
git_url_https: "https://example.com/ios-fastlane-match.git"
git_branch: "master"
app_identifiers:
  - "com.example.app"
  - "com.example.app.dev"

accounts/company.yml (サンプル):

name: "Company Account"
team_id: "XXXXXXXXXX"
apple_id: "developer@example.com"

ディレクトリ構造

├── apps/                 # アプリ定義(YAML)
│   ├── m3com.yml
│   ├── lounge.yml
│   ├── webinar.yml
│   └── ...
├── accounts/             # Apple ID設定(YAML)
│   └── m3.yml
└── fastlane/
    └── Fastfile          # 共通ロジック

Phase 3: GitHub Actionsを用いた自動化(CI/CD)

ローカル環境での運用が定着したタイミングで、CI/CD環境への移行に着手しました。

導入した主な機能:

  • App Store Connect APIによる認証(2要素認証の回避)
  • HTTPS + Basic認証を利用したGit証明書リポジトリへのアクセス
  • 手動トリガー(workflow_dispatch)による安全な実行制御

.github/workflows/update-m3com-devices.yml:

name: Update m3.com Devices

on:
  workflow_dispatch:

jobs:
  update-devices:
    runs-on: ci-mac  # 自前のMacをSelf-hosted Runnerとして登録
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup iOS Keychain
        # 一時キーチェーンの作成と破棄を行うカスタムActionを使用
        uses: ./.github/actions/setup-ios-keychain
        with:
          keychain-password: ${{ secrets.CI_KEYCHAIN_PASSWORD }}

      - name: Install dependencies
        run: make bundle

      - name: Update m3.com devices
        run: rbenv exec bundle exec fastlane update_devices_ci app:m3com
        env:
          MATCH_PASSWORD: ${{ secrets.MD_MATCH_PASSWORD }}
          APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
          APPLE_API_KEY_ISSUER_ID: ${{ secrets.APPLE_API_KEY_ISSUER_ID }}
          APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY_CONTENT }}
          MATCH_REPOSITORY_ACCESS_TOKEN: ${{ secrets.MATCH_REPOSITORY_ACCESS_TOKEN }}

完全自動化(イベント駆動)ではなく手動トリガーを採用した理由は、意図しないタイミングでのプロファイル更新を防ぐためです。また、過渡期の対応としてローカルでの実行フローも残し、update_devices(ローカル用)と update_devices_ci(CI用)でレーンを分割しています。

実装の詳細

一元管理を実現するにあたり、工夫した実装のポイントを解説します。

1. YAMLからの設定読み込み

Rubyの標準ライブラリを用いてYAMLから設定を動的に読み込み、fastlane match の引数に渡す仕組みです。

# YAMLファイルを読み込むヘルパー
def load_app(app_name)
  file_path = "../apps/#{app_name}.yml"
  YAML.load_file(file_path)
end

def load_account(account_name)
  file_path = "../accounts/#{account_name}.yml"
  YAML.load_file(file_path)
end

# 実行ロジック
lane :update_devices do |options|
  app_config = load_app(options[:app])
  account_config = load_account(app_config['account'])

  ['development', 'adhoc'].each do |type|
    match(
      type: type,
      app_identifier: app_config['app_identifiers'],
      team_id: account_config['team_id'],
      username: account_config['apple_id'],
      git_url: app_config['git_url'],
      force_for_new_devices: true
    )
  end
end

2. ローカルとCIで認証方式を分離

ローカルとCI環境では最適な認証アプローチが異なるため、実行レーンを分けています。YAML内に git_url(SSH用)と git_url_https(HTTPS用)の両方を定義し、環境に応じて適切な手段を選択できるようにしました。

ローカル環境:

bundle exec fastlane update_devices app:m3com
# → SSH経由でGit証明書リポジトリにアクセス
# → Apple IDとパスワードで認証

CI環境:

bundle exec fastlane update_devices_ci app:m3com
# → HTTPS + Basic認証でGit証明書リポジトリにアクセス
# → App Store Connect API Keyで認証

3. 統一されたインタフェース

どのアプリであっても、全く同じコマンド構造で実行できるようになりました!

# ローカル
bundle exec fastlane update_devices app:m3com
bundle exec fastlane update_devices app:lounge
bundle exec fastlane update_devices app:webinar

# CI(GitHub Actions)
bundle exec fastlane update_devices_ci app:m3com

効果:定性的な変化

この継続的改善により、運用フローには以下のような変化が生まれました。

Before → After 比較

項目 Before After
対応可能者 各アプリの専任担当者のみ チーム全員(誰でも)
実行場所 各アプリのリポジトリ内で個別に実行 専用リポジトリから一元実行
対応手順 アプリごとに微妙に異なる 全アプリで統一
複数アプリへの追加 アプリごとに担当者へ個別連絡 1つのリポジトリで一括対応可能
実行方法 ローカルでコマンド実行 GitHub Actionsで実行

副次的効果

「プロビジョニング更新=面倒で属人的な作業」という認識が払拭され、心理的ハードルが大きく下がったと思います! また、新規アプリ追加時もYAMLファイルの追加のみ(PRベース)で進められるようになりました。

まとめ

各アプリのリポジトリに散らばっていた fastlane match の運用を専用リポジトリへ集約し、さらにCI/CD化することで、デバイス追加運用におけるトイルと属人化を解消しました。

今後は「デバイス追加自体の自動化」や「証明書ローテーションの自動化」など、さらなる開発体験の向上を目指していきます。

地味な領域ではありますが、確実な開発生産性の向上につながる改善をこれからも続けていきたいと思います!

参考リンク


We are hiring!

エムスリーでは、10近いiOSアプリを開発・運用しながら、日々の開発体験を改善していくエンジニアを募集しています。小さな改善を積み重ねて、チーム全体の生産性を高めることに興味がある方、ぜひお気軽にお問い合わせください!

jobs.m3.com