上原正吉(EarthLink Network Co., Ltd.)。AI会社・AI社員による業務自動化と、人の逐次介入なしに24時間自動開発を回す仕組みを自社で実践しています。開発ノウハウと具体的な設定方法、運用して分かった改善点を公開しています。

この記事は、連載「dotvault の使い方 ── .env を手で配るのをやめ、CI・チーム・AI エージェントに 1 コマンドで配るまで」の第 7 回です。この連載が終わる時に、dotvault を誰でも無料で使える形で公開する予定です。
GitHub Actions で build する時に秘密の値が要ると、.env の中身を 1 行ずつ GitHub の Secrets に貼ることになります。DATABASE_URL、STRIPE_SECRET_KEY、Sentry の DSN、と貼っていき、値を取り替えた日には dotvault と GitHub の両方を直します。もう 1 つのやり方は、自分のアカウントで login した状態を CI に持たせることですが、それでは CI が自分の権限で動き、自分のパスワードか login の token が CI の側に置かれます。
「CI に秘密を渡すには、値そのものか、自分の login を CI に置くしかない」と考えがちですが、CI に置くのは dotvault の service token 1 つで足ります。値は dotvault に置いたままで、CI は実行のたびに dotvault pull で受け取ります。token が漏れたら console で Revoke すれば、次の実行から受け取れなくなります(この回の Actions では、Revoke の後に同じ workflow を再実行すると、pull の段で AUTH_REQUIRED と終了コード 3 で止まり、build まで進みませんでした)。
この回では、console で service token を発行し、GitHub の Secrets に 1 つだけ置き、GitHub Actions から production の値を pull して、Revoke で止めるところまでを、Actions のログで順に見ていきます。
dotvault token create は console に案内するだけで、発行はできません。CI には Role を Read only、Project をその project 1 つにし、Expiry を選びます(既定は Never=期限なしです)。DOTVAULT_TOKEN 1 つです。 発行した直後に 1 回だけ出る est_… を GitHub の Secrets に入れ、workflow では dotvault pull demo-app --env production を実行します。project-config.json が無いリポジトリでも、project の名前を付ければ受け取れます。demo-app 以外の project の名前を指定しても拒否されます(存在しない名前で確かめました)。Global Keys の pool を見ることもできません。token を絞る単位は project で、同じ project の development と staging も pull できます。pull と run は ✖ AUTH_REQUIRED: invalid service token(終了コード 3)、API を直接呼ぶと HTTP 401 です。dotvault whoami は手元の token の形を見るだけなので、token が生きているかは pull で確かめます。demo-app を、GitHub Actions から pull します。6 回目の「同じ SENTRY_DSN を project ごとに貼っている ── 共通の値は Global Keys に置いて bind する」で、demo-app に Global Keys の値を 2 つ bind した状態(CF_API_TOKEN と SENTRY_DSN_WEB)から続けます。この回の撮影の前に、production に DATABASE_URL と STRIPE_SECRET_KEY(どちらもダミー)を set しました。DOTVAULT_TOKEN だけを渡しました(CI と同じ条件です)。GitHub Actions は、この回のために作った private のリポジトリで実行しました。gh run view --log で取ったログ)を文字どおりに写し、見やすい形に組み直しています。Actions のログの時刻と、npm install の警告の行は省き、省いた所は「…」の 1 行にしました。生の出力に [exit=N] として残した終了コードは、echo $? の行で示しています。est_ で始まる token は、画面でも端末でも est_•••• に置き換えました。この回で発行した token は、撮影の後に 2 本とも Revoke しています。dummy-… や cf_demo_… は、どこにも使えないダミーの値です。est_ で始まります)。「Revoke」は、その token を無効にすることです。「Secrets」は、GitHub のリポジトリに秘密の値を置く場所で、workflow からは ${{ secrets.名前 }} で読みます。「step」は workflow の中の 1 段の処理です。「終了コード」は、コマンドが終わる時に返す数で、0 が成功、それ以外が失敗です。サイドバーの Service Tokens を開くと、まだ 1 本も無い状態ではこう出ます。

Create token を押すと、項目は Name・Role・Project・Expiry の 4 つです。既定は Role が Read only、Project が All projects(org 全体)、Expiry が Never です。

Role は 3 つから選びます。画面の表記と、括弧の中の名前(dotvault の中での役の名前)は次のとおりです。
| 画面の表記 | 画面の説明 | CI に使うか |
|---|---|---|
| Read only (viewer) | Can pull secrets only. Minimal privilege for reading in CI. | 使う(この回) |
| Read/write (member) | In addition to pull, can add, update, and delete keys. | CI から値を書く時だけ |
| Admin / automation (provisioner) | Can also perform admin operations such as creating projects and changing settings. Intended for Claude Code headless onboarding. | 使わない(8 回目で扱う) |
Admin / automation を選ぶと、漏れた時の影響が大きいという警告が出て、Project を選んでも org 全体が対象になる、という注記に変わります。

Expiry の選択肢は Never・30 days・90 days・1 year です。既定の Never のまま作ると、期限の無い token になります。同じ console の Help は「期限は短く」と書いているので、CI に渡す token は期限を選んで作ります。
CI 用に、Name を ci-deploy、Role を Read only、Project を demo-app、Expiry を 90 days にしました。

Create を押すと、est_ で始まる token が 1 回だけ表示されます。Copy で写して、すぐに GitHub の Secrets に入れます(5 節)。この画面を閉じると、同じ token は二度と表示されません。下の画像は、同じ手順で作ったもう 1 つの token(ci-nightly)の画面で、token の文字を伏せてから撮りました。

Done を押すと、一覧に 1 行増えます。ci-deploy の Expires は、Created の 90 日後になっています。

service token の考え方(発行時に 1 回だけ表示されること、漏れたら Revoke すること)は、以前の記事「secretを安全に配る ── .env を1コマンドで配る dotvault を作りました」でも紹介しました。この回は、その手順を実機で通します。
CLI から token を作ろうとすると、console に案内されます。

token list も 1 行目のコマンド名が token list になるだけの同じ案内で、終了コード 1 です。発行・一覧・Revoke は、どれも console の Service Tokens 画面で行います。
GitHub に置く前に、token だけで受け取れることを手元で確かめます。login していない端末で、環境変数の DOTVAULT_TOKEN に token を入れて実行しました。ディレクトリには demo-app の project-config.json だけを置いています。

whoami は、login した時の「メールアドレス(token expires in …s)」の代わりに、service token で動いていることを示します。pull -E production は 5 key を書きました。production に project の key として置いたのは DATABASE_URL と STRIPE_SECRET_KEY の 2 つで、API_BASE_URL は project の Common (_shared) の値、先頭の 2 つ(CF_API_TOKEN と SENTRY_DSN_WEB)は 6 回目に bind した Global Keys の値です。「1 excluded」は、project-config.json に書いてある .env.local が production には無いため、書かなかったという意味です。
dotvault run -E production -- <コマンド> も、同じ token で通りました(値をファイルに書かずにコマンドに渡す形です)。
リポジトリの Settings → Secrets and variables → Actions の New repository secret で、名前を DOTVAULT_TOKEN、値を 2 節で写した token にします。この回は、GitHub の CLI で同じことをしました。
gh secret set DOTVAULT_TOKEN -R <owner>/<repo>
# 入力を求められたら token を貼り付ける
GitHub の Secrets に置くのは、この 1 つだけです。DATABASE_URL などの値は GitHub には置きません。
.github/workflows/deploy.yml を次のようにしました。2 つ目の step で dotvault を入れて、production を pull します。
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Pull dotvault secrets
env:
DOTVAULT_TOKEN: ${{ secrets.DOTVAULT_TOKEN }}
run: |
npm install -g @earthlink/dotvault
dotvault whoami
dotvault pull demo-app --env production
cut -d= -f1 .env
- name: Your build step
run: npm run build
dotvault pull demo-app --env production は、project の名前を付けているので、リポジトリに project-config.json が無くても受け取れます。cut -d= -f1 .env は、key の名前と、dotvault が書く目印の行(# BEGIN と # END)だけを出して、値をログに出さないための行です。npm run build は、この回のために作った小さな build で、.env を読んで DATABASE_URL・STRIPE_SECRET_KEY・API_BASE_URL が入っているかを set / MISSING で出します(値は出しません)。pull は値を .env に書くだけで、次の step の環境変数にはしません。この build は .env を自分で読むので値が入りましたが、.env を読まない build では値は入りません。そういう build では、build の中で必要な key が入っているかを確かめる行を置いてください。
main に push すると、Actions が実行されました。

DOTVAULT_TOKEN: *** は、GitHub が Secrets の値を伏せて表示している所です。whoami の行の est_ に続く 8 文字は、GitHub は伏せず、ログに残ります(画像では伏せています)。ここでは 0 excluded でした。リポジトリに project-config.json を置いていないので、.env 1 つだけを書いたためです。
ci-deploy(Read only・demo-app・90 days)で、届くものと届かないものを確かめました。

| 操作 | 結果 | 終了コード |
|---|---|---|
set FOO=1 -E production(書き込み) | PERMISSION_DENIED(service token is read-only (viewer); write requires a member token) | 4 |
delete STRIPE_SECRET_KEY -E production | 同じ PERMISSION_DENIED | 4 |
pull nonexistent-proj -E production(demo-app 以外の名前) | PERMISSION_DENIED(service token is not scoped to this project) | 4 |
global status(Global Keys の pool を見る) | PERMISSION_DENIED(no access to the org global key pool) | 4 |
pull -E production | 5 keys | 0 |
pull -E development | 8 keys(.env 7 + .env.local 1) | 0 |
pull -E staging | 5 keys | 0 |
書き込みと、demo-app 以外の project は止まりました。この org には demo-app 以外の project が無いので、他の project の確認は、存在しない名前で行っています。サーバーの答えは「この project に絞られていない」という理由でした。Global Keys の pool も、この token からは見えません。
token を絞る単位は project です。同じ token で、production のほかに development(8 key)と staging(5 key)も受け取れました。CI に渡す token は、その CI が受け取ってよい値を持つ project に絞って発行します。
CLI を入れられない環境では、API を直接呼べます。token は HTTP の Authorization ヘッダーに Bearer <token> の形で付けて渡します。

応答の source に、どの key がどこから来たかが出ます。global に CF_API_TOKEN と SENTRY_DSN_WEB があり、4 節の pull に入った先頭の 2 つが、6 回目に bind した Global Keys の値だと API の側でも分かります。file を付けないと、files の配列の中に同じ形で入って返りました。成功は HTTP 200 です。
一覧の行の Revoke を押すと、ブラウザの確認が出ます(ブラウザが出す確認の窓なので、画像はありません)。
Revoke token “ci-deploy”? CI jobs and services using it will lose access immediately.
OK を押すと、Status が revoked になります。一覧の絞り込みは Active only(既定)・All (incl. revoked)・Revoked only の 3 つで、Revoked only にすると Revoke した token だけが出ます。

Revoke の直後に、5 節の実行を再実行しました(gh run rerun を使いました。GitHub の画面の Re-run jobs と同じ操作です)。

pull の段で終了コード 3 で止まり、build の step は実行されませんでした。手元の端末でも同じです。

whoami は、Revoke の後も「CI / Lambda mode」と出て、0 を返しました。whoami は手元にある token の形を見ているだけで、サーバーに問い合わせていないためです。token が生きているかどうかは、pull が通るかで確かめます。
Revoke の後のエラーは PERMISSION_DENIED ではなく AUTH_REQUIRED です。でたらめな est_ の値で pull しても、同じ AUTH_REQUIRED: invalid service token(終了コード 3)でした。CI の失敗の通知でこの文を見たら、token が Revoke されたか、Secrets の値が違うかを疑います。Secrets が空の時(名前の間違いなど)は、「AUTH_REQUIRED: Not logged in. Run dotvault login first.」(終了コード 3)になりました。
| 操作 | 出力 | 終了コード |
|---|---|---|
dotvault token create / token list | dotvault token create / dotvault token list is no longer available in the CLI. … console へ案内 | 1 |
Read only の token で set / delete | PERMISSION_DENIED(read-only (viewer); write requires a member token) | 4 |
demo-app 以外の名前を pull(存在しない名前で確認) | PERMISSION_DENIED(not scoped to this project) | 4 |
Revoke 後・でたらめな est_ の値で pull / run | AUTH_REQUIRED: invalid service token | 3 |
DOTVAULT_TOKEN が空のまま pull(Secrets の名前の間違いなど) | AUTH_REQUIRED: Not logged in. Run dotvault login first. | 3 |
Revoke 後の whoami | Service token (…) — CI / Lambda mode(手元の token の形だけを見る) | 0 |
| token 無しで API を呼ぶ | {"error":"UNAUTHORIZED","message":"Missing token"} | HTTP 401 |
pull は値を .env に書くだけなので、.env を読まない build や、key の名前を打ち間違えた build は、値が無いまま進むことがあります。build の中に、必要な key が入っているかを確かめる行を 1 つ置いておくと、すぐに気づけます(5 節の cut -d= -f1 .env や、この回の build の set / MISSING の行です)。
8 回目は、AI エージェントに token を渡す回です。値を書ける Read/write の token と、1 節で警告を見た Admin / automation の token の 2 本を新しく発行して AI エージェントに渡し、project の作成から pull までを人の login なしで進めます。
dotvault は、.env や設定ファイルの secret をサーバー側に置き、権限のある人・CI・AI エージェントが 1 コマンドで受け取るサービスです(https://dotvault.io)。この連載が終わる時に、dotvault を誰でも無料で使える形で公開する予定です。公開したら、この連載の手順どおりに使い始められます。
興味のある方は、ぜひ「いいね」と記事の購読をお願いいたします。
自社プロダクトは https://www.eln.ne.jp/products にまとめています。
上原正吉。EarthLink Network Co., Ltd. 代表。AI社員に日々の業務を任せ、開発では人が逐一指示しなくても作業が進むHOOTL(Human Out Of The Loop)の仕組みを自社で実践しています。このブログでは、業務自動化と24時間自動開発のノウハウ、具体的な設定方法、日々の運用で見つかった問題と改善を共有します。
また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。
EarthLink Network は、会社の全業務を AI で回すために、必要になったものを自社で作っています。いま作っているプロダクトの一覧と概要は、こちらにまとめています。
→ EarthLink Network が自社でつくっている18のプロダクト
会社と各プロダクトの詳細は、公式サイト www.eln.ne.jp をご覧ください。