
Swift製CLIツール「Kura」で .env のシークレットを難読化してiOSアプリに埋め込む
iOSアプリのシークレット管理について、これまで何本か記事を書いてきた。振り返ると、ちょっとした「Ruby依存との戦いの歴史」になっている。
- cocoapods‑keys を使ってシークレット情報をソースコードと分離して安全に扱う
- CocoaPods の終了に備えて、cocoapods‑keys から Arkana + SPMへ移行する
- Xcode 16.4 + macOS 26.0環境のXcode Cloudでbundle installに失敗する
- Xcode 27 beta 1 + macOS 26.4 の Xcode Cloud で bundle install が失敗する問題が再発した
CocoaPods のアーカイブ化(2026年12月に read-only 化予定)に備えて cocoapods‑keys から Arkana + SPM へ移行したものの、Arkana は Ruby 製であるがゆえに、今度は Xcode Cloud のシステムRuby 2.6 に振り回されることになった。XcodeとmacOSのメジャーバージョンがずれるだけで bundle install が失敗する問題を、私は2回踏んでいる。
そもそもとして、システムRubyを使わない案も考えたが、ビルドが約5分増えてしまうため採用には至らなかった。
そこで今回、Ruby 依存そのものを断つという発想で、pure-Swift 製のCLIツール Kura を自作した。.env に書いたシークレットを XOR + salt で難読化し、型安全なSwiftコードとして生成するツールで、Xcode Cloud でもそのまま動く。本記事では、このKuraを作った経緯と使い方を紹介する。
これまでの経緯:なぜ「脱Ruby」なのか
先に、過去記事で扱った課題を簡単に整理しておく。ここが Kura を選ぶ動機に直結する。
cocoapods‑keys と、その終わり
もともとは cocoapods‑keys を使い、キー名だけを Podfile に書いて値はローカルの Keychain に持たせ、ソースにハードコードしない運用をしていた。しかし cocoapods‑keys は CocoaPods プラグインであり、CocoaPods の Specs リポジトリが2026年12月に read-only 化されると、更新が受けられなくなる。
CocoaPodsを使うとXcodeのプロジェクトをフォルダー形式にできないのもマイナス点だ。
Arkana + SPM への移行と、新たな悩み
そこで cocoapods‑keys の README でも案内されている Arkana へ移行した。Arkana は CLI かつ SPM 対応で、.arkana.yml と .env の2ファイルで機密情報を管理できる。CocoaPods 依存からは脱却できたが、Arkana は Ruby 製のため、ビルド時に RubyGems から gem を解決する必要がある。
ここで顕在化したのが Xcode Cloud のシステムRuby 2.6 問題だった。Arkana 最新版(1.5.0 以降)は Ruby 2.7 以上を要求するため、Ruby 2.6 環境では 1.4.0 が上限になる。Xcode Cloud 側でユーザー領域に Bundler を入れて bundle install するなど、CI スクリプトもそれなりに複雑になった。
Xcode と macOS のバージョンが揃わずビルドが失敗した
さらに、システムRuby 2.6 で native extension をビルドする際、XcodeとmacOSのメジャーバージョンが一致していないと SDK パスの不整合でビルドが失敗するという問題を踏んだ。
- 1回目:Xcode 16.4 + macOS 26.0 の組み合わせで、Xcode Cloud がデフォルト環境を macOS 26.0 に切り替えた途端に CocoaPods のインストールが失敗(
MacOSX15.5.sdkを探しに行くが存在しない) - 2回目:Xcode 27 beta 1 + macOS 26.4 で再発。
MacOSX27.0.sdkにはuniversal-darwin26向けの Ruby 2.6 ヘッダしか入っておらず、macOS 26.4(darwin25)が探すuniversal-darwin25が無いためjsongem のビルドに失敗
そして Xcode Cloud と RubyGems サーバーとの接続に問題が発生
2026年6月23日から25日にかけて、Xcode Cloud で gem install bundler と bundle install に失敗する現象が発生した。11回試して9回ビルドが失敗するようになった。
どちらも根っこは同じで、CocoaPods や Arkana が Ruby のシステム依存を持っていることが原因だ。当面は「XcodeとmacOSのメジャーバージョンを揃える」ことで回避できるが、Ruby に依存している限りこの手の問題からは逃げきれない。
すでに CocoaPods からは脱却しており、ライブラリは SPM 経由でインストールするようにしている。Ruby が必要なのはシークレット生成のためだけ、という状況である。
だったら、シークレット生成ツールから Ruby 依存を完全に外してしまえばいい というのが Kura の出発点だ。
Kura とは
Kura(蔵)は、.env ファイルのシークレットを XOR + salt で難読化し、型安全なSwiftコードを生成する pure-Swift の CLIツール(kura-generator)だ。
- Ruby などの外部ランタイム不要。SwiftPM だけで完結する
- 素の SwiftPM 実行ファイルなので、Xcodeプロジェクトやプラグインのサンドボックスに悩まされない
ci_post_clone.sh経由で Xcode Cloud にそのまま対応できる- グローバルなシークレットと環境別(debug/release など)のシークレットを両方扱える
.arkana.ymlの一部オプションを読めるので、Arkana からの移行がしやすい
Ruby を経由しないため、RubyGems サーバーとのネットワーク接続も、XcodeとmacOSのバージョン不一致によるSDKパスの不整合も、そもそも発生しない。前述したCI上のRuby実行環境に起因するトラブルの構造的な原因を丸ごと取り除ける、というのが最大のポイントだ。
既存の選択肢も検討した。たとえば、Xcode Cloud 側でカスタムRubyバージョンを使う方法(関連記事)も試したが、ビルド時間が約5分伸びてしまい採用を見送った。CocoaPods や Arkana のような既存ツールを改修する案も考えたが、どちらもRubyという実行環境そのものに依存した設計になっており、部分的な改修では今回の問題を根本解決できない。そこで、最初からRubyを一切経由しない設計で作り直すことにしたのがKuraだ。
導入手順
ここからは実際にiOSアプリのプロジェクトへ導入していく。大きく分けて4ステップだ。
BuildTools/Package.swiftを作成して Kura を追加する- プロジェクトルートに
.kura.ymlを作成する - プロジェクトルートに
.envを作成する - ジェネレータを実行し、生成された
KuraKeys/をローカルパッケージとしてアプリに追加する
1. BuildTools に Kura を追加する
Kura はアプリ本体のXcodeプロジェクトには追加しないことを推奨している。専用の BuildTools/Package.swift を作り、その中に閉じ込める。こうすることで、Kura が内部で依存している Yams などがアプリ側の依存グラフ(Package.resolved)に混ざり込むのを防げる。SwiftFormat などのビルドツールを BuildTools/ に隔離するのと同じ考え方だ。
この設計にしたのは、Xcode プロジェクトに直接組み込んだときに、内部依存の Yams がアプリ本体の Package.resolved に混ざり込んで依存関係の見通しが悪くなった経験があったためだ。同じ轍を踏まないよう、Kura自体もBuildTools側に隔離できる構造にしている。
プロジェクトルートに BuildTools/ ディレクトリを作り、次の Package.swift を置く。
// swift-tools-version:5.9
import PackageDescription
let package = Package(
name: "BuildTools",
platforms: [.macOS(.v13)],
dependencies: [
.package(url: "https://github.com/CH3COOH/Kura.git", from: "1.0.2"),
],
targets: [
.target(
name: "BuildTools",
dependencies: [
.product(name: "kura-generator", package: "Kura"),
],
path: ""
),
]
)
ターゲット用に、BuildTools/ の中へ空のSwiftファイルを1つ置いておく。
$ touch BuildTools/Empty.swift
2. .kura.yml を作成する
プロジェクトルートに設定ファイル .kura.yml を作成する。以下は主要なオプションをすべて明示した例で、デフォルト値のままでよければこの後の「デフォルト設定のまま使う場合」まで読み飛ばしてかまわない。
import_name: KuraKeys # 生成されるSwiftモジュール名(省略時: KuraKeys)
result_path: . # KuraKeys/ を出力するディレクトリ(省略時: カレント)
swift_declaration: public # public / internal(省略時: public)
# 生成物は独立したパッケージなので、アプリから import するなら public が必要。
# 生成された .swift をアプリターゲットへ直接コピーする場合のみ internal を選ぶ。
preserve_key_case: false # true にするとキー名を camelCase 変換せずそのままプロパティ名にする(省略時: false)
global_secrets:
- API_KEY
- ANALYTICS_TOKEN
environments:
debug:
- DEBUG_ENDPOINT
release:
- RELEASE_ENDPOINT
global_secrets はどの環境でも共通で使う値、environments は環境ごとに切り替えたい値を書く。
設定ファイルは明示指定がなければ .kura.yml → .kura.yaml → .arkana.yml の順で探索される。Arkana から移行する場合、既存の .arkana.yml を読ませることもできるが、対応しているオプションのみが反映される(詳細は後述の移行セクションを参照)。
デフォルト設定のまま使う場合
import_name / result_path / swift_declaration / preserve_key_case はいずれも省略時のデフォルト値で動くため、デフォルトのままでよければ、最低限、以下のように利用したいキーだけを定義しておけばよい。
global_secrets:
- API_KEY
- ANALYTICS_TOKEN
environments:
debug:
- DEBUG_ENDPOINT
release:
- RELEASE_ENDPOINT
3. .env を作成する
ローカル開発用に、プロジェクトルートへ .env を置く。
API_KEY="your_api_key_here"
ANALYTICS_TOKEN="your_token"
DEBUG_ENDPOINT="https://dev.example.com"
RELEASE_ENDPOINT="https://example.com"
ここまでの手順を終えると、プロジェクトルートは以下のようなファイル構成になっているはずだ。

.env をディスクに置かないようにしたい
シークレットが書かれた .env は、必ず .gitignore に追加する必要がある。 Arkana のときと同じく、.env はコミットしないのが原則だ。
ただし、コミットさえしなければ良いというわけでもない。そもそも .env を平文のままディスク上に置いておくこと自体、できれば避けたい。1Password Environments や 1Password CLI(op)を使えば、.env を1Password側の値で仮想的にマウントし、実際のシークレット値をディスクに書き込まずに済む。詳細は以前書いた以下の記事を参考にしてほしい。
本記事では話をシンプルにするため、通常の .env を前提に進める。
4. ジェネレータを実行する
.kura.yml / .env があるディレクトリ(プロジェクトルート)で、次を実行する。
$ swift run --package-path BuildTools kura-generator
--package-path は「どのパッケージでビルドするか」を指定するが、生成された実行ファイルは実行時のカレントディレクトリで動く。そのため .kura.yml / .env の相対パスがそのまま解決され、オプションを明示しなくても動作する。
成功すると KuraKeys/(import_name で指定した名前)というローカルSwiftパッケージが生成される。

これをアプリのローカルパッケージとして追加する。Xcode なら File → Add Packages → Add Local… で内側の KuraKeys フォルダを指定する流れは、Arkana の ArkanaKeys を追加したときと同じだ。
// アプリの Package.swift、または Xcode の Package Dependencies
.package(name: "KuraKeys", path: "KuraKeys")
生成されたコードを使う
生成物は次のようなSwiftコードになる(バイト列と salt は実際にはランダムな値が入る)。
// AUTO-GENERATED by Kura — DO NOT EDIT
private func _kuraDecrypt(_ encoded: [UInt8], salt: [UInt8]) -> String { ... }
public enum KuraKeys {
public enum Global {
public static var apiKey: String { ... }
public static var analyticsToken: String { ... }
}
public enum Debug {
public static var debugEndpoint: String { ... }
}
public enum Release {
public static var releaseEndpoint: String { ... }
}
}
global_secrets は Global の下に、環境別のシークレットは環境名を PascalCase にした enum(Debug / Release など)の下にネストされる。アプリからは次のように型安全に参照できる。
import KuraKeys
let key = KuraKeys.Global.apiKey
let endpoint = KuraKeys.Release.releaseEndpoint
Arkana では ArkanaKeys.Global().apiBaseUrl のようにインスタンス経由で参照していたが、Kura は enum の static プロパティなので、インスタンス生成なしで KuraKeys.Global.apiKey と書ける点が少し異なる。
キー名の変換ルール
デフォルトでは、.kura.yml のキー名が camelCase のプロパティ名に変換される。
.kura.yml のキー |
生成されるプロパティ名 |
|---|---|
API_KEY(アンダースコア区切り) |
apiKey |
APIKEY(区切りなし) |
apikey(小文字化) |
preserve_key_case: true にすると変換をスキップし、キー名をそのままプロパティ名に使う(API_KEY → API_KEY)。既存の生成プロパティ名を変えずに移行したいときに便利だ。
Arkana からの移行
すでに Arkana を使っている場合、.arkana.yml をそのまま読ませることもできるが、Kura が解釈するのは対応オプションのみだ。実際に移行する際は次の点に注意する必要がある。
import_nameとglobal_secretsはそのまま読める。グローバルなキーだけで運用しているなら、ほぼそのまま移行できる。- 環境別シークレットは書き方を変える必要がある。Arkana の
.arkana.ymlはenvironments(環境名のリスト)+environment_secrets(キーのリスト)という構成だったが、Kura はenvironmentsを「環境名→キー配列」の辞書として解釈する。そのため、Kura 側では次のように書き換える。
# Arkana 形式(例)
environments:
- Debug
- Release
environment_secrets:
- apiKey
# ↓ Kura 形式に書き換え
environments:
debug:
- DEBUG_API_KEY
release:
- RELEASE_API_KEY
- 環境ごとに値を変えたいときはキー名を分ける。Arkana は
apiKeyという1つのキーから.envのapiKeyDebug/apiKeyReleaseを読み分けていた。Kura は.env/環境変数のキー名をそのまま引くため、環境ごとに違う値を持たせたい場合は、上記のようにDEBUG_API_KEY/RELEASE_API_KEYと別々のキー名を用意する設計になる。
グローバルキー中心の構成であれば移行はごく簡単で、環境別に値を出し分けているプロジェクトだけ、この差分を意識すればよい。
Xcode Cloud での設定
ここが、Ruby 製ツールから乗り換える一番のご利益が出るところだ。
過去に Arkana を Xcode Cloud で動かしたときは、システム領域が書き込み不可であることに対応するため GEM_HOME をユーザー領域に向け、Bundler をインストールしてから bundle install → bundle exec arkana を走らせる、という CI スクリプトを書いていた。RubyGems サーバーとの接続不調や、XcodeとmacOSのバージョンを揃える手間にも気を配る必要があった。Kura は pure-Swift なので、そのあたりを気にする必要がない。
また、Kura はシークレットを解決する際に .env ファイルよりも環境変数を優先するため、各キーをワークフローの Environment Variable に登録するだけで値が解決される(この「CI では環境変数を参照する」挙動は、cocoapods‑keys や Arkana と同じ考え方だ)。
ci_scripts/ci_post_clone.sh で kura-generator を実行するだけで、KuraKeys パッケージが作成される。
#!/bin/sh
set -e
cd "$CI_PRIMARY_REPOSITORY_PATH/path/to/project" # .kura.yml のある場所へ移動
swift run --package-path BuildTools kura-generator
これで、Xcode CloudにおけるXcodeとmacOSのバージョン不一致や、RubyGemsサーバーとの接続不調に悩まされることがなくなる。
運用上の小ネタ
生成される KuraKeys.swift は、実行のたびにランダムな salt が使われるため、.env の中身が同じでも毎回内容が変わる。運用としては、生成物 KuraKeys/ は .gitignore に入れて都度再生成する方針にしておくと、無用な差分やコンフリクトを避けられる。Xcode Cloud では ci_post_clone.sh で毎回生成されるので、そのままで問題ない。
まとめ
cocoapods‑keys から Arkana + SPM への移行で CocoaPods 依存は外せたが、Ruby 依存が残っている限り、Xcode Cloud における Xcode と macOS のバージョン不一致や、RubyGems サーバーとの接続不調による CI の不安定さからは逃げられなかった。
Kura を作ったことで、次のように整理できた。
- Ruby / RubyGems に依存しない pure-Swift な仕組みで、CI 上での Ruby 実行環境に起因するトラブルが構造的に発生しなくなる
- CocoaPods 終了の影響を受けない(SPM ベース)
.envに書いたシークレットを型安全なSwiftコードとして参照できる- Arkana の設定・運用からの移行がしやすい
一方で、あくまで難読化であって暗号化ではない点は変わらない。バイナリからの平文露出を減らす目的には有効だが、本当に秘匿すべき値はバックエンドプロキシなどクライアント外での管理を検討してほしい。また、.env をディスク上に置きたくない場合は、1Password Environments のような仕組みと組み合わせる運用も検討してほしい。
cocoapods‑keys からの脱却や、Arkana の Ruby 依存による CI 不安定に悩んでいる方の、次の一手の参考になれば幸いだ。自作したばかりのツールで至らない点も多いと思うので、フィードバックをもらえるとうれしい。
関連記事
Kuraのリポジトリは以下で公開している。
これまで CocoaPods や cocoapods‑keys からの脱却を模索する中で書いた記事は以下の通りだ。
- cocoapods‑keys を使ってシークレット情報をソースコードと分離して安全に扱う
- CocoaPods の終了に備えて、cocoapods‑keys から Arkana + SPMへ移行する
- Xcode 16.4 + macOS 26.0環境のXcode Cloudでbundle installに失敗する
- Xcode 27 beta 1 + macOS 26.4 の Xcode Cloud で bundle install が失敗する問題が再発した
.env の取り扱いについては、以下の記事も参考にしてほしい。









