fastlane match を使用して iOS の証明書管理を行う

## 前書き

iOS 開発において「証明書」という単語を聞くだけでアレルギー反応を起こす人も少なくないかもしれません。
iOS 開発チームにメンバーが追加された際は、少なくとも以下のような作業を行ってもらう（もしくは、行う）必要があります。

* Apple Developer アカウントの作成
* Apple Develper Portal（以下、Dev Portal）にメンバーを招待
* コード署名証明書（以下、証明書）の作成
* プロビジョニングプロファイル（以下、プロファイル）の更新

これらの作業を手動で行うのには非常に手間がかかります。
[Xcode 8 から Automatic Signing が導入](https://developer.apple.com/videos/play/wwdc2016/401/ "What's New in Xcode App Signing - WWDC 2016 - Videos - Apple Developer")されましたが、証明書管理が iOS 開発チームの各メンバーに依存することにより発生する証明書周りのトラブルは後を絶ちません。個人開発では発生しなかったトラブルがチーム開発では付いて回ります。
また、CI 上で証明書とプロファイルをどのように扱うかといった課題も残ります。

そこで [fastlane match](https://docs.fastlane.tools/actions/match/ "match - fastlane docs")（以下、match） です。
match のアプローチについては[こちら](https://codesigning.guide/ "Code Signing Guide for Teams")に記載されています。

## match とは

match は、iOS 開発チームで共通の証明書とプロファイルの作成や維持を行い、プライベートリポジトリに格納、そして全ての開発者間で共有できるようにします。
つまり、メンバーが増える度に Apple Developer アカウントや証明書を作成する必要がなくなります。

構成は以下をご覧下さい。

> ![fastlane match](https://tech.bizreach.co.jp/posts/161/ios-manage-certificates/fastlane_match.png)
> ざっくり説明すると、以下の2点が運用される状態を作ろうと思いました。
> - adminに任命された人は、matchを使って証明書＆ Provisioning Profile の作成＆commitを行う
> - memberは、matchを使って証明書＆ Provisioning Profile をダウンロードして使う
>
> 引用元：[fastlane match でiOSアプリ開発者を「証明書管理の苦しみ」から解放せよ！ - BizReach Tech Blog](https://tech.bizreach.co.jp/posts/161/ios-manage-certificates/)

## match の導入

### インストール

match は fastlane の action のため、fastlane をインストールすることで match を使用することができます。
fastlane は [RubyGems や Homebrew を利用してインストール](https://docs.fastlane.tools/getting-started/ios/setup/#installing-Fastlane "Setup - fastlane docs")することができます。
Gemfile を使用するには[こちら](https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile "Setup - fastlane docs")を参照して下さい。

```
# Using RubyGems
sudo gem install fastlane -NV

# Alternatively using Homebrew
brew cask install fastlane
```

### 事前準備

#### プライベートリポジトリの作成

証明書やプロファイルを保存するためのプライベートな Git リポジトリを作成します。
リポジトリ名は `certificates` などにします。

#### Apple Developer アカウントの用意

チーム全体で共有する Apple Developer アカウントを用意します。（例：office@company.com）
権限を App Manager にして下さい。

App Manager 以上の権限でないと、match を実行した際に以下のようなエラーが発生する場合があります。

```
[App Store Connect unification] Spaceship::UnexpectedResponse: [!] Apple provided the following error info: Access Unavailable - You currently don't have access to this membership resource
```

詳しくは、[こちら](https://github.com/fastlane/fastlane/issues/14228 "[App Store Connect unification] Spaceship::UnexpectedResponse: [!] Apple provided the following error info: Access Unavailable - You currently don't have access to this membership resource · Issue #14228 · fastlane/fastlane")をご覧下さい。


#### Xcode プロジェクト設定

Xcodeプロジェクトのプロファイルを自動に設定しないでください。常に正しいプロファイルが選択されるわけではありません。
Xcode プロジェクト設定「 _General > Automatically manage signing_ 」を無効にしておきましょう。

詳しくは、[こちら](https://docs.fastlane.tools/codesigning/xcode-project/ "Xcode Project - fastlane docs")をご覧下さい。

#### App ID の用意

iOS アプリのための App ID を用意します。

[produce](https://docs.fastlane.tools/actions/produce/ "produce - fastlane docs") を使用することで、Dev Portal と App Store Connect の両方に新しい iOS アプリを作成することが可能です。また、アプリケーションサービスの変更やアプリケーショングループの変更も可能です。

`-a` もしくは `--app_identifier` オプションを使用して、アプリの Bundle Identifier を指定できます。

```sh
fastlane produce -a <app_identifier>
```

`-i` もしくは `--skip_itc` オプションを使用して、App Store Connect でのアプリ作成をスキップできます。

```
fastlane produce -a <app_identifier> -i
```

#### メンバーの権限設定

App Store Connect の「ユーザーとアクセス」でメンバーの権限を設定できます。
「デベロッパーリソース」の項目から証明書やプロファイルのアクセス権限を設定することが可能です。

![スクリーンショット 2020-04-10 15.58.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83038/be892605-913e-f30c-9cd9-a655801077e1.png)

こちらにチェックが付いているメンバーは Xcode 上から証明書の更新が可能となってしまいます。
誤って更新されることを防ぐために、管理者ではないメンバーにはアクセス権限を与えないことをお勧めします。

> 「Certificates, Identifiers & Profile (証明書、ID & プロファイル)」へのアクセスは、組織のチームのメンバーである、App Manager または Developer の役割を持つユーザに追加で割り当てることができる特権です。この権限が追加されたユーザは、すべての App に関連付けられた証明書、ID、およびプロファイルを表示できます。
>
> 引用元：[役割の権限 - App Store Connect ヘルプ](https://help.apple.com/app-store-connect/?lang=ja-jp#/deve5f9a89d7)



### 初期設定

match を使用するための初期設定を行います。

```
fastlane match init
```

Git リポジトリの URL や、パスフレーズが要求されるため入力して下さい。

証明書やプロビジョニングプロファイルは `openssl` を使用してパスフレーズによって暗号化され、Git リポジトリに保存されます。
そのため、パスフレーズはチームに共有する必要があります。
環境変数を使用して復号化するようにパスフレーズを設定するには、`MATCH_PASSWORD` を使用します。

また、上記コマンドを実行すると、カレントディレクトリに `Matchfile` が作成されます。
`fastlane init` を実行したプロジェクト（以下、fastlane プロジェクト）のプロジェクトルートで上記コマンドを実行した場合は `./fastlane/` に作成されます。

## match の実行

fastlane プロジェクトのプロジェクトルート外で match を実行した場合、チーム全体で共有する Apple Developer アカウントの Apple ID や、アプリの Bundle Identifier が要求されるため入力して下さい。
入力を省略したい場合は、カレントディレクトリの `Matchfile` を修正して下さい。

```ruby:Matchfile
app_identifier("<app_identifier>")
username("<username>")
```

### 作成

証明書とプロビジョニングプロファイルを作成します。
基本的に**管理者が行う作業**です。 

```
$ fastlane match development
$ fastlane match adhoc
$ fastlane match appstore
```

```ruby:Fastfile
match(type: "development")
match(type: "adhoc")
match(type: "appstore")
```

既存の証明書とプロファイルを残したまま、match で作成した証明書とプロファイルを混在させ、徐々に移行することも可能です。

match を実行した例は以下をご覧下さい。

> ![match appstore small](https://docs.fastlane.tools/img/actions/match_appstore_small.gif)
>
> 引用元：[match - fastlane docs](https://docs.fastlane.tools/actions/match/#run)

#### 具体的な処理の流れ

上記コマンドの具体的な処理の流れは以下をご覧下さい。

1. Dev Portal に証明書とプロファイルを作成
2. Git リポジトリに証明書（とその秘密鍵）とプロファイルを格納
3. プロビジョニングプロファイルが `~/Library/MobileDevice/Provisioning Profiles` にインストール
4. 証明書と秘密鍵が、キーチェーンにインストール

※ 既に証明書とプロビジョニングプロファイルが作成されている場合は更新されます。

#### リポジトリの構造

上記コマンド実行後、Git リポジトリには2つのディレクトリが含まれます。

* 証明書とその秘密鍵を含む `certs` ディレクトリ
* プロビジョニングプロファイルを含む `profiles` ディレクトリ

具体的にどのような構造になるか確認したい場合は、[こちらのサンプル](https://github.com/KrauseFx/certs)を参照して下さい。

### 取得

Git リポジトリから証明書とプロビジョニングプロファイルのインストールを行います。
基本的に**メンバーが行う作業**です。
新規メンバーのマシンに証明書とプロビジョニングプロファイルをインストールしたい場合など、Dev Portal にアクセスする必要がない場合は `--readonly` オプションを使用します。


```shell
$ fastlane match development --readonly
```

```ruby:Fastfile
match(type: "development", readonly: true)
```

### 削除

無効や期限切れ、Xcode によって管理された証明書やプロビジョニングプロファイルを Dev Portal から削除します。

```shell
$ fastlane match nuke
$ fastlane match nuke development
$ fastlane match nuke distribution
```

削除される予定の証明書とプロファイルのリストが表示され、削除するかどうかの確認が要求されるため、`y` か `n` を入力して下さい。
`y` を入力し削除しても、App Store と TestFlight で既に利用可能なアプリは引き続き機能します。

※ 初めて match を実行する前に、上記コマンドを使用して既存の証明書とプロファイルの消去を考慮することが推奨されています。match の検証や、既存の証明書とプロファイルを残したい場合は上記コマンドの使用はお勧めしません。

nuke を実行した例は以下をご覧下さい。

> ![match nuke](https://docs.fastlane.tools/img/actions/match_nuke.gif)
>
> 引用元：[match - fastlane docs](https://docs.fastlane.tools/actions/match/#nuke)

### パスフレーズの変更

パスフレーズを変更します。

```
$ fastlane match change_password
```

実行後、全てのマシンで新しいパスフレーズが要求されます。

### 新しいデバイスの登録

[register_device](https://docs.fastlane.tools/actions/register_device/ "register_device - fastlane docs") を使用することで、Dev Portal に新しいデバイスを登録することが可能です。

```
$ fastlane run register_device name:"<name>" udid:"<udid>"
```

```ruby:Fastfile
register_device(
  name: "<name>",
  udid: "<udid>"
)
```

`--force_for_new_devices` オプションを使用して、Dev Portal のデバイス数が変更されたかどうかを確認し、必要に応じてプロビジョニングプロファイルを自動的に更新します。

```shell
$ fastlane match development --force_for_new_devices
$ fastlane match adhoc --force_for_new_devices
```

```ruby:Fastfile
match(type: "development", force_for_new_devices: true)
match(type: "adhoc", force_for_new_devices: true)
```

### 証明書の更新

証明書の有効期限は1年です。
証明書の期限が切れた状態で match を実行すると、以下のようなエラーが発生します。

> [!] Your certificate 'XXXXXXXXXX.cer' is not valid, please check end date and renew it if necessary

以下の二つのソリューションどちらかの実施後に match を再度実行すると、新しい証明書とプロファイルが再作成されます。
基本的に**管理者が行う作業**です。 
証明書の更新が完了したらメンバーに `fastlane match development --readonly` を実行して最新の証明書を取得するよう促してください。

#### 1. 証明書とプロビジョニングプロファイルを `fastlane match nuke` で削除

前述した [nuke](https://qiita.com/kotarella1110/items/840af2cf80aaea1fb035#%E5%89%8A%E9%99%A4) を使用して証明書とプロビジョニングプロファイルを全て削除します。
※ nuke は特定の証明書または App ID を選択して削除することはできません。

```
$ fastlane match nuke development
$ fastlane match nuke distribution
```

※ 破壊的なため、実施する際は自己責任でお願いします。

#### 2. 証明書とプロビジョニングプロファイルをプライベートリポジトリから手動で削除

特定の証明書やプロビジョニングプロファイル、アプリケーションのみを削除したい場合は、リポジトリ内のファイルを削除するだけです。

* `/certs/development/XXXXXXXXXX.cer`
* `/certs/development/XXXXXXXXXX.p12`
* `/profiles/development/Development_xxxxxxxxxx.mobileprovision`

削除が完了したらリポジトリへプッシュします。

また、リポジトリ内の削除したファイルに対応する Dev Portal 上からプロビジョニングプロファイルを削除する必要があります。
※ Dev Portal 上からプロビジョニングプロファイルを削除しない場合、 ランダムな数字がサフィックスとして追加されたプロファイル名になります（fastlane が名前衝突を避けるため）。

### CI で使用

#### 取得

CI 上で match を実行するときは、常に `--readonly` オプションを使用することが推奨されています。

```ruby:Fastfile
lane :beta do
  match(type: "appstore", readonly: is_ci)

  gym(scheme: "Release")
end
```

#### キーチェーンの作成

CI 上で match を実行するときは、キーチェーンを名前とパスワード付きで作成する必要があります。

詳しくは以下をご覧下さい。

> 具体的には、キーチェーンのパスワードを入力して許可する必要がありました。自分のマシンの場合はログインパスワードを入力すれば良いのですが、CIマシン上での場合は新しくキーチェーンを名前・パスワード付きで作成して、match実行時にそれを用いる、という手順が必要でした。
>
> コードとしては、MATCH_KEYCHAIN_NAME・MATCH_KEYCHAIN_PASSWORDに適当な値を環境変数としてセットした上で、以下をmatchコマンド前に実行すれば解決しました。
>
> 引用元：[Bitrise Xcode 8.1などのSierra製CIマシンとfastlaneの組み合わせでビルドが固まる問題が解決🎉](https://medium.com/swift-column/fastlane-bitrise-ef1125a1d89a)

```ruby:Fastfile
before_all do
  if is_ci?
    create_keychain(
      name: ENV['MATCH_KEYCHAIN_NAME'],
      password: ENV['MATCH_KEYCHAIN_PASSWORD'],
      timeout: 1800
    )
  end
end
```

※ AppStore のプロビジョニングプロファイルにはデバイス情報が含まれていないため、プロファイルタイプが `appstore` の場合無視されます。

## 参考文献

* [fastlane docs](https://docs.fastlane.tools/)
* [fastlane match でiOSアプリ開発者を「証明書管理の苦しみ」から解放せよ！ - BizReach Tech Blog](https://tech.bizreach.co.jp/posts/161/ios-manage-certificates/)
* [役割の権限 - App Store Connect ヘルプ](https://help.apple.com/app-store-connect/?lang=ja-jp#/deve5f9a89d7)
* [How to Renew Cert for Fastlane Match | @samwize](https://samwize.com/2019/05/02/how-to-renew-cert-for-fastlane-match/)
* [What's New in Xcode App Signing - WWDC 2016 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2016/401/ "What's New in Xcode App Signing - WWDC 2016 - Videos - Apple Developer")
* [Bitrise Xcode 8.1などのSierra製CIマシンとfastlaneの組み合わせでビルドが固まる問題が解決🎉](https://medium.com/swift-column/fastlane-bitrise-ef1125a1d89a)