STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

fastlane match の内部実装を活用して複数の iOS 開発者証明書の期限を一括チェックする方法

STORES ブランドアプリチームで iOS エンジニアをしている榎本 ( @enomotok_ )です。

STORES ブランドアプリは、オーナーさんが自分のお店専用のアプリを作成できるサービスです。そのため私たちのチームでは、各オーナーさんのアプリを日々定期的にアップデート・リリースしています。現在運用しているアプリは数十個にのぼり、それぞれが異なるオーナーさんの App Store Connect の“組織”に紐づいているため、開発者証明書はアプリごとに存在します。

複数の開発者証明書を管理していると、全ての有効期限を管理するのは困難です。今回は、 fastlane match *1 の実装を活用して、複数の証明書の有効期限を一括でチェックする仕組みを構築したので紹介します。

証明書の期限切れが引き起こす問題

Apple Developer Program の開発者証明書の期限は1年間です。期限切れになると以下のような問題が発生します:

  • ビルドが失敗してリリースできない
  • 緊急修正が必要な時に証明書更新で時間を取られる
  • そもそも複数アカウントを管理していると、証明書の期限がいつ切れるか把握が困難

ブランドアプリの課題

アプリを定期リリースする際には、 CI を使って全てのアプリを一挙にビルドしていますが、開発者証明書の期限が切れているとビルドが失敗するので、そこで初めて期限切れがわかります。つまり、開発者証明書とプロビジョニングプロファイルを作成し直して、CI を実行し直す必要があります。

毎回数十個のアプリをまとめてビルドするため、定期リリース時には大体いくつかのアプリの証明書が期限切れになっています。リリース作業の途中で開発者証明書を作り直すのは、作業負荷が高く心理的にも煩雑さが伴いますし、CI の再実行によるビルド時間やマシンリソースの浪費も無視できないものになっていました。

fastlane match の内部実装に着目する

ブランドアプリでは fastlane match を用いて開発者証明書を一元管理しています。 fastlane match は証明書を Git リポジトリで暗号化して管理するツールですが、「暗号化された証明書を復号化して使っている」ということは、「証明書の中身も読めるはず」と考えました。

実際に fastlane match のソースコードを読んでみると、Match::Encryption::MatchFileEncryption というクラスが証明書の暗号化・復号化を担当していることがわかりました。 *2

fastlane match を利用した有効期限チェッカーの実装

基本的な実装

fastlane match の内部実装を使って、証明書の有効期限を取得するlaneを作りました。

lane :check_cert_expiration do
  require 'openssl'

  certs_path = "./certs/**/*.cer"
  password = ENV["MATCH_PASSWORD"]
  # matchが使っている暗号化クラスを直接利用
  decrypt = Match::Encryption::MatchFileEncryption.new

  Dir.glob(certs_path).each do |cert_file|
    decrypted = decrypt.decrypt(file_path: cert_file, password: password)
    cert = OpenSSL::X509::Certificate.new(decrypted)

    UI.message("#{File.basename(cert_file)} expires on #{cert.not_after.strftime('%Y-%m-%d')}")
    # ... 
  end
end

複数アカウント対応への拡張

私たちの場合、match repository の異なるブランチで異なるアカウントの証明書を管理しています。そこで、全ブランチの証明書を一括でチェックできるように拡張しました。

lane :check_all_cert_expiration do
  branches = %w[brand1 brand2 brand3] # 確認対象のブランチ一覧

  branches.each do |branch|
    UI.message("Checking certificates in branch: #{branch}")

    # ブランチに応じたパスワードを設定
    ENV["MATCH_PASSWORD"] = ENV["MATCH_PASSWORD_#{branch.upcase}"]

    # ブランチをチェックアウトして証明書の有効期限を確認
    checkout_match_repo(branch)
    check_cert_expiration_in_current_branch
  end
end

運用での工夫

実行結果を JSONL 形式で出力できるオプションを用意して、その結果を、別の仕組みと組み合わせることができるようにしました。 JSONL 形式で出力することで、 jq コマンドと組み合わせて柔軟な処理が可能です。

# 30日以内に期限切れになる証明書を抽出
bundle exec fastlane ios check_all_cert_expiration format:jsonl 2>/dev/null | \
  grep '^{' | \
  jq -r 'select(.days_until_expiration <= 30 and .days_until_expiration >= 0)'

これを GitHub Actions で定期実行し、期限が近い証明書があれば issue を自動で作成する仕組みを構築しました。

実装して得られた効果

この仕組みを導入したことによって、全てのアプリの証明書の期限が一目で確認できるようになりました。また証明書の期限を迎える前に更新を実施できるため、 CI の無駄な実行が減り、時間とコストを節約することができました。

まとめ

既存のツールの内部実装を理解し活用することで、効率的な証明書管理の仕組みを構築できました。 fastlane match のような OSS は、単に使うだけでなく、その実装から学び、応用することで新たな価値を生み出せることをあらためて実感しました。

同じような運用で悩んでいる方にとって、少しでも参考になれば嬉しいです。