数年前、あるアプリのキーストアを危うく失いかけたことがあります。古い Mac を初期化する前夜、バックアップの一覧を眺めていて、.jks ファイルがどのバックアップにも入っていないことに気づきました。あの夜に気づいていなければ、そのアプリは更新する手立てを永久に失っていました。コードは生成し直せます。けれど署名鍵は、生成し直せません。
AI Studio や Antigravity が、生成から内部テスト配信までを一画面でつなぐようになったいま、配布のほとんどの工程は機械に預けられます。預けてよいのです。やり直しがきくからです。けれど署名鍵だけは性質が違います。鍵は「アプリの同一性そのもの」であり、失えばそのアプリは別物になってしまう。自動化が進んだパイプラインの中で、署名鍵をどう扱い、どこで人が握り続けるか。その線引きと実装を、長く複数アプリを運用してきた立場から設計として書きます。
まず、鍵を二種類に分ける
Google Play の署名には、性質の違う二つの鍵が関わります。ここを混同したまま自動化に載せると、渡してはいけない鍵まで機械の手の届く場所へ置いてしまいます。
ひとつはアプリ署名鍵です。ユーザーの端末に届く APK に最終的に署名される鍵で、Play アプリ署名を使っていれば Google が管理します。これはアプリの同一性の根幹で、原則として人も機械も日常的には触れません。触れないことが安全です。
もうひとつはアップロード鍵です。あなたが Play Console へ AAB を送るときに署名する鍵で、Google 側でアップロード鍵として登録されています。CI が自動でビルドを送るなら、署名に使うのはこのアップロード鍵です。重要なのは、アップロード鍵は失っても再登録できるという点です。紛失したらサポート経由で新しいアップロード鍵に差し替えられます。一方、Play アプリ署名を使わず自分でアプリ署名鍵を持っている古い構成では、その鍵を失った時点で更新は終わります。
この違いが、自動化の線引きをそのまま決めます。
| 鍵の種類 | 失ったときの結果 | 自動化に触らせてよいか |
|---|---|---|
| アプリ署名鍵(Play 管理) | Google が保持。原則触れない | 触らせない(そもそも手元にない) |
| アップロード鍵 | 再登録で復旧可能 | CI に注入してよい(保管は厳重に) |
| 自己管理のアプリ署名鍵(旧構成) | 更新が永久に不可能 | 絶対に渡さない・Play 署名へ移行を検討 |
私自身、いま運用している複数アプリは、可能なものはすべて Play アプリ署名へ移しました。自己管理の鍵を抱えたまま自動配信を回すのは、不可逆のリスクを毎晩通すようなものだと考えたからです。まだ移していない古いアプリがあるなら、自動化を強める前にここを片付けるのを勧めます。
鍵を平文で置かずに、自動署名へ供給する
アップロード鍵は CI に注入してよい、と書きました。とはいえ .jks をリポジトリに置いたり、パスワードをスクリプトに直書きしたりするのは論外です。生成 AI にコードを触らせる前提なら、なおさらです。鍵そのものと、鍵を開けるパスワードは、コードの世界の外に出します。
ローカルでは、鍵のパスワードを Keychain や環境変数のシークレットストアに置き、Gradle からは値を直接見せない形で渡します。
// app/build.gradle.kts — 鍵情報をコードに焼き込まず、環境から受け取る
import java.io.FileInputStream
import java.util.Properties
android {
signingConfigs {
create("release") {
// パスは環境変数で受け取り、鍵ファイル自体はリポジトリに入れない
val keystorePath = System.getenv("UPLOAD_KEYSTORE_PATH")
if (keystorePath != null) {
storeFile = file(keystorePath)
storePassword = System.getenv("UPLOAD_KEYSTORE_PASSWORD")
keyAlias = System.getenv("UPLOAD_KEY_ALIAS")
keyPassword = System.getenv("UPLOAD_KEY_PASSWORD")
}
}
}
buildTypes {
getByName("release") {
signingConfig = signingConfigs.getByName("release")
}
}
}CI では、鍵ファイルを Base64 で暗号化シークレットに格納し、ジョブの一時領域にだけ展開して、終了時に確実に消します。鍵が平文でディスクに残る時間を、ビルドの数分間だけに限定するわけです。
# GitHub Actions の例 — 鍵は一時展開し、後始末まで含めて門にする
- name: Restore upload keystore
env:
KEYSTORE_B64: ${{ secrets.UPLOAD_KEYSTORE_B64 }}
run: |
echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/upload.jks"
echo "UPLOAD_KEYSTORE_PATH=$RUNNER_TEMP/upload.jks" >> "$GITHUB_ENV"
- name: Build & sign AAB
env:
UPLOAD_KEYSTORE_PASSWORD: ${{ secrets.UPLOAD_KEYSTORE_PASSWORD }}
UPLOAD_KEY_ALIAS: ${{ secrets.UPLOAD_KEY_ALIAS }}
UPLOAD_KEY_PASSWORD: ${{ secrets.UPLOAD_KEY_PASSWORD }}
run: ./gradlew bundleRelease
- name: Shred keystore
if: always()
run: shred -u "$RUNNER_TEMP/upload.jks" 2>/dev/null || rm -f "$RUNNER_TEMP/upload.jks"if: always() を付けた後始末を必ず置くのが肝心です。ビルドが失敗しても鍵ファイルが残らないようにします。AI エージェントに自動署名まで回させるとき、私が最初に固定したのはこの「展開と消去を対にする」型でした。鍵が出ている時間を最小化すれば、生成コードや一時ログに鍵が紛れ込む経路を狭められます。
注意したいのは、shred や rm で消えるのはファイルだけだということです。ビルドログに鍵やパスワードを誤って出力していないかは、別途確認します。署名ステップの標準出力をそのままログに流す設定だと、稀に内部情報が漏れます。ログのマスキング設定を一度確認しておくと安心です。