
Shell
イベント
該当するコンテンツが見つかりませんでした
マガジン
技術ブログ
はじめに こんにちは、セーフィーに新卒で入社し、サーバーサイドエンジニアをしている古谷です。 入社後、同期の坂上さんと一緒に VApp(Vulnerable Application) という、意図的に脆弱性を仕込んだ学習用Webアプリケーションを作りました。社内のセキュリティ学習を目的に始めたプロジェクトでしたが、作ってみると 「Webフレームワークを使っていても、脆弱な実装をしないように気をつけなければならない」 という、当たり前のようで意外と実感しにくいことを実装ベースで確かめられました。 この記事では、VAppを紹介しつつ、それを通して 自分の中でセキュリティとの向き合い方がど
この記事で伝えたいこと この記事では、PMやデザイナーがGitの細かい操作を覚えなくても、要件定義書やmockupの変更をGitHub上の変更として扱えるようにした取り組みを紹介します。 ポイントは、Gitをなくしたことではありません。Gitの履歴、Pull Request、CI、レビュー可能性は残しつつ、branch切り替え、commit、push、PR作成、auto-mergeといった操作をAIエージェントとスクリプトの裏側に寄せたことです。 近年、AIエージェントはコードを書くためだけの道具ではなくなってきました。 自然言語で作業を依頼し、ローカルファイルの読み取りやコマンド実行、GitHub連携まで進めることで、これまでエンジニアだけが担っていた「変更を安全に届ける」作業の一部を肩代わりできます。 一方で、何でもAIに任せればよいわけではありません。 今回の取り組みでは、AIエージェントが判断する部分と、shell scriptやCIで機械的に守る部分を分けました。 この分担こそが、非エンジニアにもGit管理の恩恵を広げる上で重要だったと考えています。 想定読者 PM、デザイナー、PdMなど、Gitを日常的には使わないがプロダクト開発の成果物を更新する人 非エンジニアの作業成果を、GitHubの履歴やPRに載せたいエンジニア AIエージェントを、単なるコード生成ではなくチームワークの基盤として使いたい人 目次 はじめに 対象リポジトリで実際に起きていたこと 何ができるようになったのか 利用者から見える体験 裏側で起きていること 同じ仕組みを作るなら何を実装するか なぜこれが嬉しいのか 実際の利用者コメント AIエージェント時代の非エンジニアの作業はどう変わるのか エンジニアとして気をつけたこと 作りながら感じたこと 今後やりたいこと まとめ はじめに こんにちは、Insight Edgeの古野です。 プロダクト開発では、ソースコードだけでなく、要件定義書、画面mockup、仕様メモ、検証結果など、多くの成果物が日々更新されます。 これらはエンジニアだけのものではありません。 PMが要件を更新し、デザイナーが画面案を調整し、それをもとにエンジニアが実装します。 ところが、成果物をGitHubで管理しようとすると、非エンジニアにとって急にハードルが上がります。 cloneとは何か branchはいつ切り替えるのか commit messageには何を書くのか pushしてよいbranchはどれか PRを作ったあと何を見ればよいのか conflictと表示されたら何をすればよいのか これらはエンジニアにとっては日常的な操作です。 しかし、要件やデザインを更新したい人にとっては、本来の仕事とは別の認知負荷です。 そこで、PMとデザイナーが担当領域の変更をGitHubに反映するためのAIエージェント向けコマンドを整備しました。 PMは次のように実行します。 /pm merge デザイナーは次のように実行します。 /dsn merge これだけで、裏側ではcommit、push、Pull Request作成、auto-merge設定まで進みます。ユーザーはbranchを切り替えません。 git add や git push も打ちません。 対象リポジトリで実際に起きていたこと この仕組みは、思いつきで作ったものではありません。 対象リポジトリでは、PMが扱う要件と、デザイナーが扱うmockupの更新が継続的に発生していました。 執筆時点で、2026年6月以降の履歴を確認すると、mockup更新のcommitは34件、要件定義書や要求機能一覧の更新commitは8件ありました。 確認には、例えば次のようなログを見ています。 git log --since=2026-06-01 --oneline --grep= 'chore(mockup)' | wc -l git log --since=2026-06-01 --oneline --grep= 'docs(requirements)' | wc -l つまり、これは単発の手作業を楽にするための仕組みではなく、今後も繰り返し発生する「content更新」を安全に回すための仕組みでした。 実装の変遷も、最初から完成形だったわけではありません。 2026年6月3日: 非エンジニア向けの /d skillとcontent PR gateを追加 同日: /d をcheckとmergeに分け、mergeを引数不要に変更 同日: Figma / AI Studio exportの取り込みをmergeに内包 同日: developer向けのdev roleを追加し、要件とmockupの両レーンを自動判定 2026年6月4日: recoverコマンドやallowlist検証の強化 2026年6月17日: 一時worktreeベースに変更し、利用者の作業branchを切り替えない形へ補正 2026年7月3日: /d を役割別窓口の /pm と /dsn に分離し、共通処理をcontent-coreへ集約 この履歴から分かるのは、単にGitコマンドをラップしただけでは足りなかったということです。 実際に運用しながら、利用者が覚える手順を減らし、エンジニアが安全性を担保しやすい形へ寄せていきました。 何ができるようになったのか 今回の仕組みでは、役割ごとに触れる領域を分けました。 役割 主に扱うもの コマンド 反映先 branch PM 要件定義書、要求機能一覧 /pm merge content/requirements デザイナー mockup /dsn merge content/mockup エンジニア PM / デザイナーの代行、両レーン対応 /d git-merge 変更内容に応じて自動判定 PMは要件の変更を、デザイナーはmockupの変更を、それぞれ自分の窓口から反映します。 デザイナー向けには、FigmaやAI Studioからexportしたmockupの取り込みも /dsn merge の中に含めました。 別途importコマンドを覚える必要はありません。 新しいexportがあればその場所を伝え、なければ「なし」と答えるだけです。 この設計にした理由は、非エンジニア向けの導線では「手順を増やさない」ことが重要だからです。 便利なコマンドが複数あっても、どの順番で打つのかを覚える必要があるなら、結局Git操作を覚えるのと同じ構造になってしまいます。 利用者から見える体験 初回セットアップでは、GitHub CLIへのログインやリポジトリのcloneは必要です。 この部分は残しています。GitHubに安全にpushするための認証は必要だからです。 具体的には、最初に次のような準備をします。 対象リポジトリへのGitHub招待を受け、write権限を持つ Claude Codeを使える状態にする git と gh をインストールする gh auth login でGitHubにログインする 対象リポジトリをcloneする 例えば、macOSであれば次のような流れです。 brew install git gh brew install --cask claude-code gh auth login gh repo clone < repository > cd < repository > 今回の仕組みで重要なのは、反映用の merge だけを用意することではありませんでした。 実際にチームで使うには、最初に必要なものがそろっているかを確認する導線、しばらく触っていない間にエンジニアが更新したスキルや手順を取り込む導線、途中で止まったときに状態を診断する導線も必要になります。 そのため、利用者に見せる入口は次のように整理しました。 タイミング PM デザイナー 何をするか 初回セットアップ /pm check /dsn check 必要ツール、GitHub認証、push権限、役割設定を確認する 作業前の最新化 /pm sync /dsn sync 最新のmainを取り込み、エンジニアが更新したスキルや手順に追従する 反映 /pm merge /dsn merge 担当領域の変更をcommit、push、PR作成、auto-mergeまで進める 困ったとき /pm recover /dsn recover 状態を診断し、エンジニアに渡せる情報を出す init と呼びたくなる初期化の領域は、今回の実装では check に寄せました。 初回だけ使う入口を別に増やすより、「準備OKかどうかを見る」という言葉に寄せた方が、PMやデザイナーにとって意味が伝わりやすいと考えたためです。 sync も地味ですが重要です。エンジニアが /pm や /dsn の中身を直したり、安全性のチェックを強くしたりしても、利用者がGitのpullやrebaseを理解する必要はありません。 作業前に /pm sync や /dsn sync を実行すれば、最新のmainを取り込み、更新されたレールに乗り直せます。 ここでもpushは行わず、あくまで手元を最新化するだけにしています。 つまり、日々の運用ではGitの詳細を意識しません。 初回だけ、PMまたはデザイナーの窓口で準備状態を確認します。 /pm check # または /dsn check しばらく作業していない場合や、エンジニアがスキル側を更新したあとには、作業前に最新化します。 /pm sync # または /dsn sync 担当ファイルを編集したあとの反映は、mergeだけです。 /pm merge # または /dsn merge 実行後は、反映されたファイル一覧とPR URLが表示されます。 CIが通れば自動でmainに取り込まれます。 裏側で起きていること 利用者からは /pm merge や /dsn merge の1コマンドに見えますが、裏側では複数のGit操作が動いています。 処理の大枠は次の通りです。 PMとデザイナーがGit操作を意識せずPRまで進む処理フロー(図:筆者作成) 特に重視したのは、次の3点です。 1つ目は、一時worktreeを使うことです。 ユーザーの作業branchは切り替えません。PMやデザイナーがmain上で作業していても、反映処理は裏側の一時worktreeで進みます。 これにより、「今どのbranchにいるか」を利用者が意識しなくて済みます。 2つ目は、allowlistです。 PMは要件ディレクトリ、デザイナーはmockupディレクトリだけを反映できます。 対象外のファイルが混ざっていても、スクリプトはそれをcommitしません。 簡略化すると、裏側では次のような考え方で対象を絞っています。 # 実際のコードを説明用に簡略化した例 case " $role " in pm) allow= "docs/product/requirements" ;; designer) allow= "mockup" ;; esac git -C " $worktree " add -- " $allow " git -C " $worktree " diff --cached --name-only 3つ目は、CIで同じ制約をもう一度確認することです。 ローカルスクリプトだけでは、将来の変更や想定外の操作に弱くなります。 そのため、GitHub Actions側でも content/requirements branchは要件ディレクトリだけ、 content/mockup branchはmockupディレクトリだけ、というルールを検証しています。 # 実際の workflow を説明用に簡略化した例 case "$HEAD_REF" in content/requirements) ALLOW="docs/product/requirements/" ;; content/mockup) ALLOW="mockup/" ;; *) exit 0 ;; esac git diff --name-only "origin/${BASE_REF}...HEAD" AIエージェントに任せる部分はありますが、最終的な安全性はpromptの約束ではなく、shell scriptとCIで担保します。 同じ仕組みを作るなら何を実装するか ここまでだと考え方の紹介で終わってしまうので、同じような仕組みを作るなら何を実装すればよいかも整理します。 最小構成は、次のように分けるのが扱いやすいです。 .claude/skills/ pm/SKILL.md dsn/SKILL.md content-core/ scripts/ check.sh sync.sh merge.sh .github/workflows/ content-pr-guard.yml ポイントは、AIエージェント側のskillにGit操作を直接書きすぎないことです。 /pm や /dsn は利用者向けの入口にして、実際のGit操作はshell scriptへ寄せます。 例えば、skill側はこのくらい薄くできます。 # /pm - ` /pm check ` は ` bash .claude/skills/content-core/scripts/check.sh pm ` を実行する - ` /pm sync ` は ` bash .claude/skills/content-core/scripts/sync.sh ` を実行する - ` /pm merge ` は ` bash .claude/skills/content-core/scripts/merge.sh ` を実行する - Gitが途中で止まったら、利用者に直接 ` reset ` や ` stash pop ` を案内せず、エンジニアへ共有する デザイナー向けの /dsn も同じで、roleだけを designer に固定します。 /pm init や /dsn init という名前を用意してもよいですが、今回の実装では「初回に準備OKかを見る」という意味を優先して check に寄せました。 大事なのは名前ではなく、初回検証、最新化、反映、復旧の入口が分かれていることです。 check: 必要ツールと権限を確認する check では、利用者がGitの状態を読めなくても、作業できる前提がそろっているかを機械的に確認します。 #!/usr/bin/env bash set -euo pipefail role= " ${1:?role is required: pm or designer} " ok= 1 pass() { printf ' ✅ %s\n' " $1 " ; } fail() { printf ' ❌ %s\n' " $1 " ; ok= 0 ; } command -v git > /dev/null 2>&1 && pass "git found" || fail "git not found" command -v gh > /dev/null 2>&1 && pass "gh found" || fail "gh not found" git rev-parse --is-inside-work-tree > /dev/null 2>&1 \ && pass "inside git repository" \ || fail "run this in cloned repository" gh auth status > /dev/null 2>&1 \ && pass "gh authenticated" \ || fail "run gh auth login" can_push= " $( gh api 'repos/{owner}/{repo}' --jq '.permissions.push' 2> /dev/null || echo false ) " [ " $can_push " = "true" ] && pass "push permission ok" || fail "no push permission" case " $role " in pm|designer) printf '%s' " $role " > " $( git rev-parse --git-dir ) /content-role" ;; *) fail "unknown role: $role " ;; esac [ " $ok " = "1" ] || exit 1 echo "準備OK。以降は sync / merge を使えます。" ここで .git/content-role のようなファイルにroleを保存しておくと、 merge 側で毎回「PMですか、デザイナーですか」と聞かずに済みます。 sync: エンジニアが更新したレールに追従する sync は地味ですが、運用上かなり重要です。 エンジニアがskillやscriptを更新しても、利用者に git pull や rebase を説明したくありません。 そこで、利用者には /pm sync や /dsn sync だけを見せ、裏側で最新の main を取り込みます。 #!/usr/bin/env bash set -euo pipefail repo_root= " $( git rev-parse --show-toplevel ) " cd " $repo_root " git fetch origin --quiet stashed= 0 if [ -n " $( git status --porcelain ) " ]; then git stash push -u --quiet stashed= 1 fi if ! git rebase origin/main --quiet; then git rebase --abort > /dev/null 2>&1 || true [ " $stashed " = "1" ] && git stash pop --quiet > /dev/null 2>&1 || true echo "最新mainの取り込みで衝突しました。エンジニアに連絡してください。" >&2 exit 1 fi if [ " $stashed " = "1" ]; then git stash pop --quiet || { echo "退避した変更の復帰で衝突しました。エンジニアに連絡してください。" >&2 exit 1 } fi echo "最新mainを取り込みました。pushはしていません。" sync はpushしません。あくまで手元を最新化するだけです。 反映は次の merge に寄せます。 merge: allowlistだけを一時worktreeへ転送する merge が一番重要です。利用者の作業branchは切り替えず、一時worktree上でcontent用branchを作り、担当領域だけをstageします。 説明用に簡略化すると、中心は次のような処理です。 #!/usr/bin/env bash set -euo pipefail repo_root= " $( git rev-parse --show-toplevel ) " git_dir= " $( git rev-parse --git-dir ) " role= " $( tr -d '[:space:]' < " $git_dir /content-role" ) " case " $role " in pm) branch= "content/requirements" allow= "docs/product/requirements" type = "docs(requirements)" ;; designer) branch= "content/mockup" allow= "mockup" type = "chore(mockup)" ;; *) echo "unknown role: $role " >&2 exit 1 ;; esac git fetch origin --quiet base= "origin/main" if git show-ref --verify --quiet "refs/remotes/origin/ $branch " ; then base= "origin/ $branch " fi tmp= " $( mktemp -d ) " wt= " $tmp /worktree" git worktree add --detach " $wt " " $base " --quiet cleanup() { git worktree remove " $wt " --force > /dev/null 2>&1 || true rm -rf " $tmp " } trap cleanup EXIT if [ " $base " = "origin/ $branch " ]; then git -C " $wt " merge --no-edit origin/main fi while IFS= read -r -d '' entry; do xy= " ${entry:0:2} " path= " ${entry:3} " case " $xy " in *D*) rm -f " $wt / $path " ;; *) mkdir -p " $wt / $( dirname " $path " ) " cp -p " $repo_root / $path " " $wt / $path " ;; esac done < < (git status --porcelain -z -uall --no-renames -- " $allow " ) git -C " $wt " add -- " $allow " この時点では、まだcommitしません。 先にstageされたファイルがallowlist配下だけかを確認します。 outside= " $( git -C " $wt " diff --cached --name-only | while IFS = read -r file ; do case " $file " in "$allow"|"$allow"/*) ;; * ) printf '%s \n ' " $file " ;; esac done )" if [ -n " $outside " ]; then echo "対象外の変更が含まれています:" >&2 echo " $outside " >&2 exit 1 fi if git -C " $wt " diff --cached --quiet; then echo "反映する変更はありません。" exit 0 fi ここまで通って初めてcommit、push、PR作成へ進みます。 summary= " $( git -C " $wt " diff --cached --name-only | head -5 | awk 'NR == 1 { out = $0; next } { out = out ", " $0 } END { print out }' ) " git -C " $wt " commit -m " $type : 更新: $summary " --quiet git -C " $wt " push --force-with-lease origin "HEAD: $branch " --quiet if [ " $( gh pr list --head " $branch " --base main --state open --json number --jq 'length' ) " = "0" ]; then gh pr create \ --base main \ --head " $branch " \ --title " $type : 更新" \ --body "content mergeによる自動作成。対象は $allow / のみ。" fi gh pr merge " $branch " --auto --squash gh pr view " $branch " --json url --jq .url この実装で、利用者はbranchを切り替えず、 git add や git push を打ちません。 一方で、GitHub上にはPRと履歴が残ります。 CI: ローカルと同じ制約をサーバ側でも見る ローカルのshellだけに寄せると、将来の変更で抜け道ができます。 GitHub Actionsでも同じallowlistを確認します。 name : content-pr-guard on : pull_request : branches : [ main ] jobs : content-scope-check : runs-on : ubuntu-latest steps : - uses : actions/checkout@v4 with : fetch-depth : 0 - name : Check content scope run : | case "${GITHUB_HEAD_REF}" in content/requirements) allow="docs/product/requirements/" ;; content/mockup) allow="mockup/" ;; *) exit 0 ;; esac git diff --name-only "origin/${GITHUB_BASE_REF}...HEAD" | while IFS= read -r file; do case "$file" in "$allow" *) ;; *) echo "out of scope: $file" exit 1 ;; esac done このように、AIエージェントに任せるのは「どの入口を呼ぶか」までに留めます。 安全性は、shell scriptとCIで同じルールを二重に確認します。 なぜこれが嬉しいのか この仕組みで嬉しかったことは、単に「Git操作を自動化できた」ことではありません。 一番大きいのは、PMやデザイナーの成果物を、エンジニアの成果物と同じ流れで扱えるようになったことです。 履歴・レビュー・CIの対象として確認できます。 これまでは、要件やmockupの受け渡しが次のようになりがちでした。 Slackにファイルを貼る zipを共有する 「最新版はこちらです」と口頭で伝える エンジニアが手元で取り込んでcommitする この形だと、どれが最新版なのか、いつ何が変わったのか、なぜその変更が入ったのかが追いにくくなります。 エンジニアが代理でcommitする場合も、変更の主体とGitHub上のauthorがずれやすくなります。 今回の仕組みでは、PMやデザイナーが自分の作業として変更を反映できます。 PRが残るため、後から差分を確認できます。 CIも通るため、対象外ファイルやsecret混入を防げます。 PMやデザイナーにとってのモチベーションは、「GitHubを使えるようになること」そのものではありません。 自分が責任を持つ成果物を、自分の作業として履歴に残せることです。 PMであれば、要件変更の背景や優先順位を、実装と同じ場所で追えるようになります。 あとから「この要件はいつ、どの判断で変わったのか」を確認しやすくなります。 デザイナーであれば、mockupの更新をzipや画像共有で終わらせず、実装側が追える変更として渡せます。 「この画面を更新しました」という連絡だけでなく、GitHub上のPR URLを起点に会話できます。 使ってみて感じたのは、価値の中心が「Git操作が簡単になった」ことよりも、「変更の置き場所がチームでそろう」ことにあるという点です。 Gitの知識が少ない人でも、PRという同じ単位で変更を共有できると、会話が「誰が取り込むか」から「何が変わったか」「どう確認するか」に移ります。 エンジニアにとってもメリットがあります。 非エンジニアの作業を毎回手作業で取り込む必要が減ります。 さらに、取り込まれた変更はPRとして見えるため、必要なときにレビューできます。 GitHub上に履歴が残るので、後から実装との対応関係も追いやすくなります。 実際の利用者コメント この記事では、仕組みを作った側だけでなく、実際に使う側の目線も入れたいと考えました。 そこで、PMやデザイナーに「本プロジェクトでGit操作をしてみた感想」を一言ずつもらいました。 次の画像では、氏名、アイコン、メンション、投稿時刻をマスキングしています。 PMとデザイナーからもらった利用者コメントのスクリーンショット(図:利用者コメントをもとに筆者作成) この画像を入れる目的は、「便利になりました」という感想を載せることだけではありません。GitHub上に変更を届ける体験が、PMやデザイナーにとって本当に本来業務の邪魔にならないかを見るためです。 Git操作を覚えることが目的になってしまうと、非エンジニアにとっては負担が増えます。一方で、「自分の成果物を自分の責任範囲で届けられる」「変更の所在がチームで共有される」という実感があれば、GitHubを使う理由が自然に伝わります。 AI エージェント時代の非エンジニアの作業はどう変わるのか AIエージェントが入ると、非エンジニアができることは増えます。 以前なら、GitHubに変更を載せるにはGitの操作を覚える必要がありました。今は、AIエージェントに「この変更を反映して」と依頼すると、裏側でコマンドを実行できます。 ただし、ここで大切なのは「非エンジニアもエンジニアと同じことを全部やる」ことではないと考えています。 PMは、要件の妥当性、業務上の優先順位、ユーザー価値に責任を持つ。 デザイナーは、画面体験、情報設計、操作性、表現品質に責任を持つ。 エンジニアは、実行境界、権限、CI、rollback、保守性に責任を持つ。 AIエージェントは、その間にある手作業や翻訳作業を支援する。 この分担を崩さないことが重要です。AIエージェントが使えるからといって、PMやデザイナーにconflict解消やbranch戦略の判断まで任せる必要はありません。逆に、エンジニアがすべてを代行し続ける必要もありません。 役割の境界を曖昧にするのではなく、それぞれが責任を持つ領域を明確にした上で、境界をまたぐ作業をAIエージェントに手伝ってもらう。この考え方が、今回の取り組みの中心にあります。 エンジニアとして気をつけたこと 非エンジニア向けの仕組みを作るとき、つい「簡単にする」ことばかり考えがちです。 しかし、簡単に見える導線ほど、裏側の安全設計が必要です。 今回、特に気をつけたことは次の4つです。 1つ目は、対象ディレクトリの外を絶対に反映しないことです。 AIエージェントへの指示だけで「対象外は触らないで」と書いても十分ではありません。誰であっても間違える可能性があります。そこで、スクリプトでは対象だけをstageします。CI側でも対象外変更を落とすようにしました。 2つ目は、利用者にGitの復旧操作をさせないことです。 stash 、 checkout 、 reset 、 rebase --abort などは、慣れていない人にとって危険です。途中で止まったときは、利用者が自分で直す必要はありません。診断結果をエンジニアへ渡せるようにしました。 3つ目は、手順を増やさないことです。 デザイナー向けにはmockup exportの取り込みもmergeに含めました。アーカイブ、import、commit、pushのようにコマンドを分けすぎると、利用者は結局オペレーションを覚える必要があります。 4つ目は、エンジニア向けの代行ルートも用意することです。 PMやデザイナーだけでなく、エンジニアが要件やmockupの反映を代行する場面もあります。そのときに通常の開発branchから直接対象ディレクトリを触ると、CIの条件や必須チェックの設計と衝突することがあります。そのため、エンジニア用の /d git-merge も同じcontent branch経由に寄せました。 作りながら感じたこと この取り組みを進めていて感じたのは、AIエージェントによって「誰がリポジトリに変更を届けられるか」の範囲が広がっているということです。 これまでは、GitHubに変更を載せること自体がエンジニア寄りの作業でした。今後は、PMが要件を更新し、デザイナーがmockupを更新し、それが自然にPRとして現れる状態が増えていくと思います。 一方で、AIエージェントがいるからこそ、エンジニアリングの重要性はむしろ上がります。 自然言語で依頼できる体験を作るには、裏側に明確な実行境界が必要です。どのファイルを触ってよいのか。どのbranchにpushしてよいのか。どのCIを通すのか。失敗したときに誰が対応するのか。 こうした境界を設計するのは、やはりエンジニアの仕事です。 AIエージェント時代のチームワークでは、エンジニアがすべてを直接作業するのではなく、他の職種が安全に作業できるレールを作る場面が増えるのではないかと思います。 今後やりたいこと 今後は、次のような改善が考えられます。 PR上のレビューコメントをもとに、AIエージェントが修正候補を出す mockupと実装の差分を自動で検出する 要件変更と実装タスクの対応関係を追えるようにする テックブログやドキュメント更新にも同じ考え方を広げる 特に、mockupと実装の差分検出は重要です。デザイナーがmockupを更新できるようになると、次に必要になるのは「その変更が実装に反映されたか」を追う仕組みです。GitHubに変更履歴が残っていれば、そこを起点に自動レビューや差分検出を組み合わせやすくなります。 まとめ PMやデザイナーがGitを知らなくてもGitHubに変更を届けられるようにする取り組みを紹介しました。 今回実現したことは、Gitを使わない世界ではありません。 むしろ、Gitの履歴、PR、CI、レビュー可能性を活かすために、Git操作の難しさをAIエージェントとスクリプトの裏側へ移した取り組みです。 AIエージェントは、非エンジニアの作業範囲を広げます。ただし、そのためには安全な実行境界が必要です。 AIに任せる部分と、shell scriptやCIで機械的に守る部分を分けることで、チーム全体がGitHubをより自然に使えるようになります。 Gitを覚えてもらうのではなく、Gitの価値をチーム全員が使えるようにする。 今回の仕組みは、そのための小さな一歩だったと思います。
こんにちは。クロスイノベーション本部 AIデータテクノロジーユニット AIトランスフォーメーションセンターの青木 尚人です。 本記事では、SOPS を利用してチーム開発の環境変数管理を標準化する方法を紹介します。 はじめに チーム開発で .env を使っていると、次のような運用になりがちです。 .env の実際の値を Slack や Teams で共有する 新規メンバーが入るたびに、誰かが .env を手作業で渡す .env.example はあるが、実際の値とはずれている どの値が最新なのかわからない 秘密情報とそうでない値が混ざっている .env を誤って Git にコミットしてしまう そこで今回は、 SOPS + Azure Key Vault + mise を使って、暗号化された .env を Git 管理できるようにします。 この記事で紹介するのは、次のような手順での環境変数の管理方法です。 SOPS で .env 形式のファイルを暗号化する Azure Key Vault の Key を SOPS の暗号化・復号に使う mise で sops と azure-cli を導入する mise task で .env の復号・生成コマンドを標準化する この記事の前半では、まず SOPS を利用して暗号化された環境変数を作成します。 その後、mise task を使って .env の生成手順を標準化します。 本記事で紹介するプロジェクトで想定している最小の構成は以下です。 Existing Git Repository ├── .env.sops.env # SOPSで暗号化された.env。Git管理する ├── .env # 復号して生成する.env。Git管理しない ├── .sops.yaml # SOPSの暗号化設定。Git管理する └── mise.toml # ツールとタスク定義。Git管理する Azure Key Vault └── Key Vault Key # SOPSの暗号化・復号に使う鍵 SOPS とは SOPS は、YAML、JSON、ENV、INI などのファイルを暗号化して管理するためのツールです。 公式ドキュメント: https://getsops.io/docs/ GitHub リポジトリ: https://github.com/getsops/sops 一般的な暗号化ツールと違い、ファイル全体を単純にバイナリ化するのではなく、設定ファイルとして扱いやすい形で暗号化できます。 例えば .env 形式のファイルであれば、暗号化後もキー名は読める状態にしつつ、値だけを暗号化できます。 似た選択肢としては、Azure Key Vault Secret に環境変数を1つずつ保存する方法、 .env.example で項目だけ共有する方法、 .env ファイルの暗号化に特化したツールとして dotenvx もあります。 dotenvx は、既存の .env 運用に近い形で暗号化された .env ファイルを扱えるため、 .env を中心にシンプルに管理したい場合は有力な選択肢です。 一方、今回の構成では Azure Key Vault の Key を使って復号権限を Azure 側で制御したかったため、SOPS を採用しています。 SOPS は .env だけでなく YAML、JSON、INI など複数の設定ファイル形式を扱えるため、環境変数以外の設定ファイル管理にも広げやすい点もメリットです。 暗号化前の .env が次のような内容だったとします。 DATABASE_URL=postgresql://demo_user:demo_password@localhost:5432/demo_db API_TOKEN=dummy-api-token JWT_SECRET=dummy-jwt-secret SOPS で暗号化すると、次のようなファイルになります。 DATABASE_URL=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] API_TOKEN=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] JWT_SECRET=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] この状態なら、暗号化された .env ファイルを Git 管理できます。 値は暗号化されているため、リポジトリに置いても平文の secret は見えません。 Azure Key Vault は何に使うのか 今回、Azure Key Vault は、環境変数そのものの保存先ではなく、SOPS がデータキーを保護するための Key Vault Key の管理基盤として使います。 Azure Key Vault では、主に次の3種類のオブジェクトを管理できます。 Key Secret Certificate このうち、今回使うのは Secret ではなく Key です。 環境変数を Azure Key Vault Secret に1つずつ保存する構成ではありません。 SOPS が暗号化・復号に使う鍵を、Azure Key Vault Key として管理します。 Azure Key Vault └── Key └── SOPSが.env.sops.envを暗号化・復号するために使う SOPS は、ファイル本体の値を暗号化し、その暗号化・復号に必要な情報をファイル内に保持します。 ただし、その復号には Azure Key Vault Key へのアクセス権が必要です。 そのため、次のような管理ができます。 Git には暗号化済みの .env.sops.env を配置する 復号できる人は Azure Key Vault の権限で制御する チャットで .env の平文を共有しない メンバー追加・削除時は Azure 側の権限を見直す ここが、この構成の重要なポイントです。 mise とは mise は、プロジェクトで使う CLI ツールのバージョン管理や、タスク定義をまとめて扱えるツールです。 SOPS と Azure CLI を各メンバーが個別にインストールしても、この構成は実現できます。 ただし、それだと次の問題が残ります。 SOPS がインストールされていない Azure CLI がインストールされていない メンバーごとにバージョンが異なる .env を生成するコマンドを毎回説明する必要がある mise を使うと、プロジェクトに必要な CLI ツールとタスクを mise.toml にまとめられます。 [tools] sops = "3.12.2" azure-cli = "2.84.0" これをプロジェクトに置いておけば、メンバーは次のコマンドで必要なツールをそろえられます。 mise install さらに、 .env を生成する処理も mise task にできます。 mise run env:render つまり mise を使う理由は、単にツールを入れたいからではありません。 チーム全員が同じコマンドで、同じ手順を実行できるようにするため です。 ここからは、mise のインストール手順から、Azure Key Vault と SOPS を使った環境変数の管理手順までを順に紹介します。 mise をインストールする macOS では Homebrew でインストールできます。 brew install mise zsh を使っている場合は、シェルに mise を有効化する設定を追加します。 echo 'eval "$(mise activate zsh)"' >> ~/.zshrc source ~/.zshrc インストールできたか確認します。 mise --version macOS 以外のインストール方法は、公式ドキュメントを参照してください。 https://mise.jdx.dev/getting-started.html mise.toml に sops と azure-cli を追加する 既存プロジェクトのルートに mise.toml を用意します。 すでに mise.toml がある場合は、既存の [tools] に追記してください。 [tools] sops = "3.12.2" azure-cli = "2.84.0" コマンドで追加する場合は、以下のようにします。 mise use sops@3.12.2 mise use azure-cli@2.84.0 その後、ツールをインストールします。 mise install 確認します。 sops --version az version Azure にログインする SOPS が Azure Key Vault を使って復号するには、ローカル端末が Azure に認証済みである必要があります。 まず Azure にログインします。 az login 現在のサブスクリプションを確認します。 az account show -o table 必要であれば、利用するサブスクリプションに切り替えます。 az account set --subscription "<subscription-id-or-name>" Azure Key Vault と Key を作成する すでにチームで利用している Key Vault がある場合は、既存の Key Vault に SOPS 用の Key を追加しても構いません。 Key Vault の作成 Azure Portal から操作する場合は以下のようになります。 トップ画面からキーコンテナーを選択します。 作成ボタンを押下して、キーコンテナーを作成します。 基本タブでリソースグループとリージョンを選択します。 アクセス制御タブでは「Azureロールベースのアクセス制御(RBAC)」を選択してください。 ネットワークタブでは必要に応じてアクセス制御を設定します。 ここではデフォルトの「すべてのネットワークからのアクセスを許可する」を選択します。 ※実運用では要件に合わせて制限してください。特に本番 secret に関わる Key Vault では、ネットワーク制限を含めた設計が必要です。 最後に確認タブで設定を確認し、作成ボタンを押下してキーコンテナーを作成します。 Key Vault のアクセス制御で権限を付与する 次に、Key Vault の Key を使えるように、アクセス制御で権限を付与します。 アクセス制御(IAM)タブを開き、ロールの割り当てを追加します。 Key Vault の Key を作成・編集する管理者には「キー コンテナー暗号化責任者」を割り当てます。 一方で、一般的な開発メンバーが環境変数の暗号化・復号に利用するだけであれば、「キー コンテナー暗号化ユーザー」を割り当てるのが適切です。 ここでは、Key を作成するために「キー コンテナー暗号化責任者」のロールを自身に割り当てます。 SOPS 用の Key の作成 [オブジェクト > キー] のタブから「+ 生成/インポート」を押下します。 名前や RSA キーサイズを選択して、環境変数を暗号化する SOPS 用の Key を作成します。 作成した Key の ID を取得します。 出力例は以下のとおりです。 https://kv-sops-xxxx.vault.azure.net/keys/sops-env-key/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx この Key ID を SOPS の設定で使います。 .sops.yaml を追加する プロジェクトルートに .sops.yaml を追加します。 creation_rules : - path_regex : \.env\.sops\.env$ azure_keyvault : - https://kv-sops-xxxx.vault.azure.net/keys/sops-env-key/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx この設定により、 .env.sops.env という名前のファイルを SOPS で作成するときに、Azure Key Vault の Key が使われます。 暗号化前の .env を用意する ここでは、既存の .env から secret を含む値を暗号化する想定で進めます。 記事用の例ではダミー値を使います。 .env.plain という名前で、次のような内容を用意します。 DATABASE_URL=postgresql://demo_user:demo_password@localhost:5432/demo_db API_TOKEN=dummy-api-token JWT_SECRET=dummy-jwt-secret SOPS で .env を暗号化する .env.plain を SOPS で暗号化し、 .env.sops.env を作成します。 sops encrypt \ --filename-override .env.sops.env \ --input-type dotenv \ --output-type dotenv \ .env.plain > .env.sops.env 暗号化後のファイルを確認します。 cat .env.sops.env 次のように値が ENC[...] 形式になっていれば成功です。 DATABASE_URL=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] API_TOKEN=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] JWT_SECRET=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] 暗号化前の一時ファイルは削除します。 rm .env.plain この時点で、Git 管理する対象は .env.sops.env です。 平文の .env や .env.plain ではありません。 復号して .env を生成する 復号できるか確認します。 sops decrypt .env.sops.env 以下のような出力が得られれば成功です。 DATABASE_URL=postgresql://demo_user:demo_password@localhost:5432/demo_db API_TOKEN=dummy-api-token JWT_SECRET=dummy-jwt-secret 問題なければ、 .env に出力します。 sops decrypt .env.sops.env > .env chmod 600 .env これでアプリケーションが読む .env が生成されます。 cat .env 出力例は以下のとおりです。 DATABASE_URL=postgresql://demo_user:demo_password@localhost:5432/demo_db API_TOKEN=dummy-api-token JWT_SECRET=dummy-jwt-secret mise task で .env 生成を標準化する 毎回 sops decrypt .env.sops.env > .env と入力するのは手間です。 そこで、 mise.toml にタスクを追加します。 [tasks."env:render"] description = "Generate .env from encrypted env file" run = ''' set -euo pipefail sops decrypt .env.sops.env > .env chmod 600 .env echo "generated: .env" ''' [tasks."env:decrypt"] description = "Print decrypted env to stdout" run = "sops decrypt .env.sops.env" これで、開発者は次のコマンドだけで .env を生成できます。 mise run env:render 以下のような出力が得られれば成功です。 % mise run env:render [env:render] $ sops decrypt .env.sops.env > .env generated: .env 復号結果を標準出力で確認したい場合は、次を使います。 mise run env:decrypt 以下のような結果が得られれば成功です。 % mise run env:decrypt [env:decrypt] $ sops decrypt .env.sops.env DATABASE_URL=postgresql://demo_user:demo_password@localhost:5432/demo_db API_TOKEN=dummy-api-token JWT_SECRET=dummy-jwt-secret VSCode で暗号化された .env を編集する 暗号化済みファイルを編集する場合は、 sops edit を使います。 VSCode で編集する場合は、次のようにします。 SOPS_EDITOR='code --wait' sops edit .env.sops.env 実行すると /var/folders/s7/86599dyd1g5clgw4f7l3d2840000gn/T/3982786129/.env.sops.env のような一時ファイルが VSCode で開かれます。 適宜編集して保存した後、ファイルを閉じると .env.sops.env が再暗号化されます。 これも mise task にしておくと便利です。 [tasks."env:edit"] description = "Edit encrypted env file with VSCode" run = "SOPS_EDITOR='code --wait' sops edit .env.sops.env" 実行します。 mise run env:edit EMBEDDING_API_TOKEN などの値を適宜書き換えて保存し、VSCode を閉じます。 .env.sops.env が再び暗号化された状態で保存されれば成功です。 DATABASE_URL=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] API_TOKEN=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] EMBEDDING_API_TOKEN=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] JWT_SECRET=ENC[AES256_GCM,data:...,iv:...,tag:...,type:str] 最終的な mise.toml 例 最小構成の mise.toml は次のようになります。 [tools] sops = "3.12.2" azure-cli = "2.84.0" [tasks."env:render"] description = "Generate .env from encrypted env file" run = ''' set -euo pipefail sops decrypt .env.sops.env > .env chmod 600 .env echo "generated: .env" ''' [tasks."env:decrypt"] description = "Print decrypted env to stdout" run = "sops decrypt .env.sops.env" [tasks."env:edit"] description = "Edit encrypted env file with VSCode" run = "SOPS_EDITOR='code --wait' sops edit .env.sops.env" このファイルをプロジェクトに置いておけば、開発者が覚えるコマンドは少なくなります。 mise install mise run env:render 新規メンバーが入ったときの手順 新規メンバーが入ったときは、次の手順でセットアップできます。 git clone <repository-url> cd <repository-name> mise install az login mise run env:render もちろん、「キー コンテナー暗号化ユーザー」などの Key Vault 権限は事前に必要です。 発展:shared / secret / local に分ける 実際のチーム運用では、すべての値を1つの .env.sops.env に入れるより、値の性質ごとに分けたほうが扱いやすい場合があります。 例えば、次のように分けます。 .env.shared # チームで共有してよい非秘密情報 .env.secret.sops.env # SOPSで暗号化した秘密情報 .env.local # 個人用の上書き設定 .env # 最終的に生成されるファイル この場合の考え方は次の通りです。 ファイル Git管理 用途 .env.shared する チームで共有してよい非秘密情報 .env.secret.sops.env する SOPSで暗号化した秘密情報 .env.local しない 個人のローカル上書き設定 .env しない アプリケーションが読む生成物 この構成にすると、非機密情報の差分が読みやすくなります。 一方で、ファイルが増えるため、最小構成よりは運用ルールが必要になります。 例えば、mise task は次のようになります。 [tasks."env:render"] description = "Generate .env from shared, encrypted secret, and local env files" run = ''' set -euo pipefail tmp_secret="$(mktemp)" cleanup() { rm -f "$tmp_secret" } trap cleanup EXIT sops decrypt .env.secret.sops.env > "$tmp_secret" { cat .env.shared printf "\n" cat "$tmp_secret" if [ -f .env.local ]; then printf "\n" cat .env.local fi } > .env chmod 600 .env echo "generated: .env" ''' まとめ SOPS を使うと、 .env 形式のファイルを暗号化して Git 管理できます。 Azure Key Vault を組み合わせることで、復号できる人を Azure 側の権限で制御できます。 さらに mise を使うことで、SOPS と Azure CLI の導入、そして .env の生成コマンドをチームでそろえられます。 最小構成は次の通りです。 .env.sops.env # 暗号化された.env。Git管理する .env # 復号して生成する.env。Git管理しない .sops.yaml # SOPS設定 mise.toml # ツールとタスク定義 開発者が実行するコマンドは、最終的にはこれだけにできます。 az login mise install mise run env:render 環境変数の管理はチーム開発において重要な課題です。 いざ開発に入ると、 .env の値をチャットで共有してしまったり、誰が最新の値を持っているのかわからなくなったりします。 今回の構成を使うことで、Git 管理と Azure Key Vault の権限管理を組み合わせながら、チームで同じ手順で .env を生成できるようになります。 参考資料 SOPS のドキュメント https://getsops.io/docs/ SOPS の GitHub リポジトリ https://github.com/getsops/sops SOPS Azure KMS https://getsops.io/docs/usage/identities/azure-kms/ dotenvx Encryption https://dotenvx.com/docs/quickstart/encryption/ SOPS Config File https://getsops.io/docs/usage/identities/config-file/ mise のインストール方法 https://mise.jdx.dev/installing-mise.html 執筆: @aoki.naoto レビュー: @yamada.y ( Shodo で執筆されました )














