はじめに
最近、改めて色々と考えながら開発環境構築をする機会があったので、開発プロセスの整備も含めて全体を進めようとしたときに、どのようなことを考えるべきなのかをまとめておくことにします。
例としてPythonをベースに記載していますが、考え方としては他でも共通するので、別環境で考えたいときは読み替えてください。
開発環境構築/開発プロセス整備の目的
目的は以下の通りです。
- (ローカル)開発環境、CI/CD環境、運用環境で適切に環境を再現できるようにすることで、環境差分に由来する不具合が発生しないようにする
- 開発時の標準的なツールをプラットフォームとして整備することで、個々人が環境構築をする際に必要となるコストを最小化する
- 開発プロセスに内包される処理を自動化することで、標準的な開発プロセスに則って進めれば一定の品質が担保される仕組みを実現する
開発に着手するまでに時間がかかる、成果物の品質が安定しないことでレビューがボトルネックになるなどする際に、それらをチームの開発を底上げをする形で解決していくことにつながるはずです。
言い換えると、これまで人が都度気をつけることで成立させていた部分を、環境の再現と機械的な検証に置き換えていく作業になります。置き換えが進んだ範囲の外側にだけ、人の判断が残ります。後半で触れる生成AIの話も、この線引きの延長線上にあります。
開発環境構築のレイヤー構造
開発環境を整備する際には、いくつか管理のレイヤーがあります。原則として開発環境、CI/CD環境、運用環境を近い形に揃えるのが望ましいですが、目的の違いや運用コストなども鑑みた上で、どこまでの再現性を求めるかによってツールを使い分けるとよいでしょう。
どこに境界を引くか
「環境を分ける」と言っても、どこに境界を引くかで分けられるものが変わります。境界は入れ子になっています。
外側から順に、分けられるものは以下の通りです。
- マシン:端末そのもの
- 仮想マシン:カーネルを含むOS全体
- コンテナ:OS上のファイル・プロセス・ネットワーク(カーネルはホストと共有)
- プロセス:そこから読み書きできるファイルと通信先
- パス:ライブラリの解決先
外側の境界を引くほど、人によって違いが出る余地は小さくなります。ただし、起動に時間がかかる、リソースを消費する、運用の手間が増えるといったコストも上がります。
ちなみに、境界は再現性のためだけのものではありません。境界の内側でしか動けないようにしておけば、依存パッケージに仕込まれた悪意あるコードや、誤って実行した破壊的なコマンドの影響範囲もそこで止まります。
承認を挟まずに自動実行させるものほど、境界の強さがそのまま防御の強さになります。ただし、境界の内側から到達できるもの(持ち込んだ資格情報や、許可した通信先)は境界では守れないので、内側に何を置くかまで含めて考えます。
そして、この境界の話は次節と表裏です。次節は「何を固定するか」の話ですが、再現したいものより外側に境界を置かないと固定できません。ライブラリだけを揃えたいならパスの境界で足りますが、OSに入れるツールまで揃えたいならコンテナより外側の境界が要ります。
レイヤーごとに何を固定するか
レイヤーごとの要素と、例をまとめると以下の通りです。
| Layer | Category | ①pipグローバル | ②venv | ③uv | ④mise | ⑤DevContainer | ⑥Nix |
|---|---|---|---|---|---|---|---|
| OS | OS | Host | Host | Host | Host | Container | Host / Container |
| OS | OS-Level Package | 個別 | 個別 | winget / Homebrew / apt・dnf | winget / Homebrew / apt・dnf | イメージに同梱(apt 等) | Nix |
| OS | CLI / GUI Tools | 個別 | 個別 | winget / Homebrew / aqua | winget / Homebrew / aqua / mise | イメージに同梱(Features 等) | Nix |
| Language | Runtime Version | 個別 | 個別 | uv | mise | イメージに同梱 / uv・mise | Nix |
| Language | Package | pip | pip + venv | uv | uv / bun | uv / bun | uv / bun |
- ①:初心者がカオスになる(私も例外なくかつてはまったわけですが)ものとしてあるあるの構成で、Pythonのパッケージをpipでグローバルに入れてしまい収拾がつかなくなる
(そもそもnpmと違って、デフォルトでユーザーグローバルに入れようとするのはお行儀が悪すぎるのではと思ったりします)
- ②:venvを使うことでプロジェクトレベルでパッケージ管理を独立させる、これで何となくパッケージ管理ができるがホストの差異でちょっと何ともになる
- ③:OS-Level PackageやCLI/GUI ToolsをCLIで管理し、パッケージもuvで管理することで、そこそこの再現性を実現できるようになる
- ④:Multi-Runtime環境ではmiseなどを使うといい感じに管理できるようになる
- ⑤:DevContainerを使うことで、OSレベルからまとめて環境を配布できるようになる
- ⑥:Language / Packageのレイヤー以外をNixに集約する形(PackageのレイヤーはPackage Managerに委ねる)、私はまだ使ったことがない(必要性が生じていない)
③以降のどれを選ぶかは、何を揃えたいかで決まります。
- 言語がPythonだけで、OSの違いは各自で吸収できる:③。ホストに直接入れる形なので起動が速く、手数も少ない
- 言語やランタイムが複数ある:④。バージョンの指定を1箇所にまとめられる
- OSレベルの差まで吸収したい、参加した初日に環境を渡したい:⑤。手元にコンテナランタイムが要る代わりに、OS以下をまとめて配れる
- OSそのものを宣言的に再現したい:⑥
③〜⑥は排他ではありません。④はランタイムのバージョン管理をmiseに任せつつパッケージはuvで扱いますし、⑤のコンテナの中でもuvやmiseはそのまま使います。表のLanguageの行が③以降でほとんど変わらないのはそのためで、違うのはOS以下をどう配るかです。
手元の端末で動かすとは限らない
ちなみに、作業マシンをどこに置くかは上の表とは独立に選べます。①〜⑥のどれであっても、手元の端末ではなくVMに置いて、手元からは接続するだけという形にできます。
- VMに出す:ローカルまたはクラウドにVMを立てる。ターミナルも言語サーバもビルドツールも向こう側で動く。OS以下のレイヤーは表のどの構成でもそのまま組める
- マネージドのサービスに出す:⑤のようにDevContainerで環境を定義しているなら、GitHub Codespacesなどが同じ定義をそのまま動かせる。手元の端末にはDockerも言語ランタイムも要らない
後者が効くのは、同じ devcontainer.json が手元のDockerでもクラウドホストでも動く点です。環境定義を書き直さずに、手元で動かすかクラウドで動かすかを切り替えられます。
境界をマシンごと外に出す利点は、再現性だけではありません。手元の端末に業務のコードや資格情報を置かずに済みます。端末を紛失したときの影響範囲が変わります。一方で、運用上の制約は増えます。ネットワークが切れると何もできませんし、常時起動していればコストもかかります。
開発環境がどこにあっても操作は共通化できる
VS Code の場合、開発環境が手元のホストにあっても、コンテナの中にあっても、別のマシンにあっても、インターフェースを VS Code に統一することができます。置き場所によって変わるのは、接続の仕方だけです。
このときの別のマシンは、別端末でも、ローカルやクラウドのVMでも構いません。どちらもSSHで接続するので、扱いは変わりません。
どれを選んでも、エディタもターミナルも操作は同じです。手元のホストでそのまま始めて、後からコンテナに移す、さらに別のマシンへ出す、という順に進められます。
そして、この環境の上に開発プロセスが乗ってくることになります。
開発プロセスと運用の整備
Gitホスティングサービスを基盤に置く
開発プロセスを考える前提として、開発に関する情報がどこに集まるかを決めておく必要があります。結論としては、GitHubやGitLabのようなGitホスティングサービスに寄せるのが望ましいです。
この形にすると、開発に付随する議論(Issue)、コードの変更経緯(Pull Request)、最新のコードが1箇所に集まります。あるコードがなぜそうなっているのかを知りたいとき、まずコードを確認して、コードから該当のPull Requestへ、そこからIssueへと辿れば、実装内容も、実装の理由も、その前段でどんな課題をどう分析したのかも遡れます。後から参加した人が経緯を掴むときも、聞いて回る先がここ1つで済みます。
裏を返すと、ここから外に出た情報は追えなくなります。設計の判断をチャットで済ませてしまうと、決まった結論だけが残って、なぜそう決めたのかは流れていきます。
図でMessage Toolを通知の宛先としてのみ置き、そこから開発フローへ戻る線を引いていないのはこのためです。チャットで議論すること自体は構いませんが、結論はIssueかPull Requestに書き戻します。
Monitorが検知した異常をIssueとして自動起票するのも同じ理屈です。運用で起きたことをチャットの中だけで片付けると、次に同じことが起きたときに参照するものが残りません。Issueにしておけば、対応がそのまま開発のバックログに載り、修正のPull Requestまで同じ場所で繋がります。
これは人間にとって分かりやすいのはもちろんですが、AIが使える情報基盤になりつつ、作業基盤にもできるという点が大きいです。後段のAgentic Codingの話は、この集約が前提になります。
各工程に何を仕込むか
上の図が情報の集まる先を示すのに対して、整備の対象としては以下のような全体像になります。
全体のプラットフォームがあり、開発環境での開発、そこからCI/CDを経て各種環境にデプロイされていきます。なおこれはシンプルなフローの場合なので、場合によってはより複雑なフローになることもあるかもしれません。
さて、このフローに対して、それぞれで対応することを整理します。原則として、自動化可能な品質チェックを早期フェーズに移し、人間のレビューでは自動化しにくい設計/仕様/ドメイン上の判断に集中できるようにするのが望ましいです。
後者のフローほど処理に時間がかかるので、即座に検知できるものはその場で検知し、早期に修正するように徹底すると最低限の品質を担保しやすくなります。
| Category | Detail | Tool / Platform Example | Coding | Commit | Push | Pull Request | Merge | Monitor |
|---|---|---|---|---|---|---|---|---|
| Format | - | prettier / ruff | ○ | ○ | ○ | ○ | - | - |
| Lint | - | ESLint / ruff | ○ | ○ | ○ | ○ | - | - |
| TypeCheck | - | tsc / pyright | ○ | ○ | ○ | ○ | - | - |
| Dependency Lint | - | dependency-cruiser / import-linter | △ | ○ | ○ | ○ | - | - |
| Secret Detection | - | Betterleaks | - | ○ | ○ | △ | - | - |
| Docs Generation | - | pdoc | - | - | △ | △ | - | - |
| Other | - | Secret Detection / Vulnerability Scan etc. | - | △ | △ | △ | - | - |
| Test | Unit | vitest / pytest | - | - | △ | ○ | - | - |
| Test | Integration | vitest / pytest | - | - | △ | ○ | - | - |
| Test | E2E | Playwright | - | - | △ | ○ | - | - |
| Build | stg | GitHub Actions | - | - | - | ○ | - | - |
| Deploy | stg | GitHub Actions | - | - | - | ○ | - | - |
| Verification | stg | - | - | - | - | ○ | - | - |
| Build | prd | GitHub Actions | - | - | - | - | ○ | - |
| Deploy | prd | GitHub Actions | - | - | - | - | ○ | - |
| Verification | prd | - | - | - | - | - | ○ | - |
| Observe | - | Datadog / New Relic | - | - | - | - | - | ○ |
上記のOtherには、例えば以下のようなものがあります。
- Security Lint: ex. actionlint / zizmor / Ruff S Rules
- Static Check: ex. typos / shellcheck
- Vulnerability Scan: ex. Trivy
- SBOM Generation: ex. cyclonedx-py
なお、Dependency Lintは、通常のLint / TypeCheckが見ない「モジュール間の依存の向き」を検査するものです。設計上のレイヤー(例:ドメイン層は外部サービスの実装をimportしない)を設定ファイルの契約として書いておくと、レビューで都度指摘していた構造の崩れを自動検知に落とせます。設計の意図をコードの外に明文化できるのが利点で、Coding時点では拡張機能で即座に得られないことが多いので△にしています。
プラットフォームそのものの構築については、本記事から割愛します。ただし、整備されたプラットフォームを開発フローからどう使うか(特に権限をどう渡すか)は開発プロセスの設計に直結するので、後段で扱います。
手元の開発フローを自動化する
比較的技術的知見が高くはないチームで、シンプルな開発フローを前提とすると、手元の作業がVS Codeで完結する形にするのがとっかかり易いと考えています。背景としては、git / GitHub連携や拡張機能、DevContainerなどを利用することで、上記の要素を簡易に実現しやすいからです。
Format / Lint / TypeCheck
VS Codeでは、拡張機能を利用することでフィードバックを即座に得られます。例えば以下のようにすればよいです。
{
// Format
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.formatOnType": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[python]": {
// Format
"editor.defaultFormatter": "charliermarsh.ruff",
// Lint
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "always",
},
},
// TypeCheck
"python.analysis.typeCheckingMode": "standard",
}
pre-commit / pre-push
gitでは、hookという仕組みを利用することで、
- pre-commit:commit前に特定の処理を実行する機能
- pre-push:push前に特定の処理を実行する機能
があります。Format、Lint、TypeCheckといった比較的軽い処理は前者、Testのような重めの処理は後者にすることで、Remote Repositoryへの同期の前に特定の処理を強制させることができます。
このとき、hookが呼ぶツールは、各自のマシンに入っているものではなく、プロジェクトで版を固定したものにします。版がずれると、手元では通るのにCIでだけLintが落ちます。人によって整形結果が変わり、diffがノイズだらけにもなります。開発環境構築のレイヤーで版を固定したのに、hookがその外のツールを叩いていては意味がありません。
CI/CDで担保する
ここから先は、手元ではなく共有された基盤の上で動く部分です。どう組むかはプラットフォーム次第なので個別の実装は割愛しますが、CI/CDに置いて初めて成立する考え方があります。
CI/CDに置くから意味を持つこと
1つめは、環境定義を守らせられるのがCI/CDだという点です。ツールを決めて版を固定しても、それは「そう書いてある」だけです。手元では各自が逸脱できます。同じ検証を手元で回すことはできますが、通さずに進むのは止められません。共有された基盤の上で毎回動かし、定義から外れたら失敗するようにします。そうして初めて、環境差分の排除が実効性を持ちます。逆に、CI/CDが差分を黙って吸収する作りだと意味がありません。
2つめは、処理の実体を設定ファイルに閉じ込めないことです。設定ファイルの中にしかない処理は、パイプラインを回すまで正しさが分かりません。「その場で検知する」という方針に反します。設定ファイルには順序と条件だけを書きます。処理そのものは、手元でも同じように実行できる形に置きます。そうすれば静的検査にかけられますし、手元で再現して直せますし、手順が二重管理になりません。
3つめは、前倒しの対象が日常的な検査だけではないという点です。FormatやLintは毎日回るので、壊れてもすぐ気づきます。危ないのはBuildやDeployのように実行頻度の低い工程です。壊れていても次に実行するまで露見しません。そしてそれは大抵、いちばん急いでいるときです。こうした工程も、副作用のない部分だけ切り出せば早い段階に置けます。作れるかを確かめるだけで外部には公開しない、といった形です。頻度が低い工程ほど、前倒しの効果は大きくなります。
権限と成果物の受け渡し
CI/CDは人が見ていない状態で毎回動きます。手作業なら不審な挙動に気づく余地がありますが、自動実行では漏れた資格情報が誰にも気づかれないまま使われ続けます。権限の設計がここで効いてくる理由です。
まず、権限は人ではなくジョブの単位で切ります。それぞれに必要な分だけを渡します。読み取りしかしないジョブに書き込み権限を渡さない、検証環境のトークンで運用環境に触れないよう環境ごとに分ける、といった当たり前の切り分けです。長命の個人トークンを設定に置くのは避けます。プラットフォームがジョブ実行中だけ発行する短命のトークンで済むなら、そちらを使います。
次に、権限の制約を実装方式の選択材料として扱います。「この方式にすると強い権限が必要になる」と分かったら、権限を広げる前に方式を変えられないか検討します。権限は一度広げると戻しにくいものです。動かなくなるのが怖くて、誰も絞らないまま残ります。それでも広げると判断するなら、恒久的な選択をしている自覚を持って決めます。
そして、ビルドとデプロイは分けます。受け渡しは成果物だけに限ります。ビルドやテストは、変更されたコードを実行する工程です。Pull Requestの中身は任意のコードです。この工程にデプロイ用の資格情報を置くと、Pull Requestを出すだけで運用環境の資格情報を取れてしまいます。
この分離は再現性の面でも効きます。環境ごとにビルドし直すと、検証したものと運用環境に出るものが別物になります。ビルドは1回だけ行い、同じ成果物を各環境へ配ります。ジョブの間で環境や状態を暗黙に共有しません。渡すものを成果物とそのメタデータに限れば、各工程の入力も明確になります。
失敗したときに何が残るか
CI/CDは複数の工程を順に実行し、外部のシステムに副作用を起こします。途中で失敗すると、そこまでに起こした副作用だけが残ります。中途半端な状態が残ることを前提に設計します。
自動で戻せるものは自動で戻します。権限などの制約で戻せないものは、人がやるべき操作への導線を通知に含めて渡します。「失敗しました」とだけ伝える通知は、何が残っていて次に何をすべきかの調査を丸ごと人に押し付けます。自動化の範囲を広げることと同じくらい、その境界をどう人へ引き渡すかが重要です。
AI活用との接続
AIを使う場合でも、基本となる考え方は大きく変わりません。ここまで人向けに整えてきたものが、そのまま効いてきます。
- 境界を引いておく:コンテナや仮想マシンの境界を引いておけば、承認を挟まずに動かしたときの影響範囲がそこで止まります
- 情報を集約しておく:Gitホスティングサービスに議論と変更経緯とコードが揃っていれば、エージェントはそれを経緯ごと読めます
- 決定論的に検証できるようにしておく:Format / Lint / Testを手元でもCI/CDでも同じ形で実行できるようにしておけば、エージェントの出力を機械的に確かめられます
使い方には手元で動かす形とCI/CDに載せる形があり、記事前半の「手元の開発フローを自動化する」と「CI/CDで担保する」にそれぞれ対応します。
Agentic Coding
手元でエージェントを動かす形です。人が対話しながら進めるので、Gitホスティングサービスは主に情報源として効いてきます。開発フローの順に並べると、以下のような使い方になります。
- Issueの分析:課題を掘り下げ、解決策の選択肢と得失を整理してIssueに書き戻す
- 現状調査:関係する実装と過去の経緯を辿り、どこをどう変えるかの当たりを付ける
- 実装:調査の結果をもとにコードを書く
- Pull Requestの作成:差分から変更の意図と影響範囲を書いて出す
- レビュー指摘への対応:指摘を読んで直し、Pull Requestを更新する
- CIの失敗調査:落ちたジョブのログから原因を切り分ける
コーディングそのものは分かりやすい適用先ですが、その前後の調査や整理まで含めて任せられるのは、経緯が同じ場所に揃っているからです。散らばっていれば、人が集めて渡すところからやり直しになります。
また、このレイヤーで効く仕掛けとして、例えばClaude CodeではHooksという特定の処理を実行しようとする/したときに任意のコマンドを実行できる仕組みがあり、それを上手く利用していくことがポイントになっています。過去に書いた以下の記事も参考までに。
開発フローへの組込み
CI/CDの上でエージェントを動かす形です。Issueの起票やPull Requestの作成といったイベント、あるいは定期実行で起動し、結果をGitホスティングサービスに書き戻すので、人が起動しなくても回ります。こちらも開発フローの順に並べます。
- Issueが起票されたら現状調査する:関係する実装と過去の経緯を辿り、分かったことをIssueにコメントする
- ラベルが付いたら実装する:実装からPull Requestの作成まで進める
- Pull Requestが出たらレビューする:差分を読み、規約からの逸脱や考慮漏れを指摘する
- CIが落ちたら原因を切り分ける:ログを読み、当たりをPull Requestにコメントする
- 定期的に棚卸しする:依存の更新や、コードとドキュメントのずれを拾い、Pull RequestやIssueにして出す
前節と同じ作業が並んでいますが、違いは起動のさせ方です。手元は人が指示して回し、こちらはイベントで勝手に回ります。監視が検知した異常からIssueを起票するような、そもそもAIを使うまでもない部分は仕組みとして自動化しておき、AIはその先の調査から先に使います。
いずれの場合も、入力(Issue、差分、ログ)も出力(コメント、Pull Request)もGitホスティングサービスの上にあるのが共通しています。人が使っている導線をそのまま使えるので、エージェント専用の置き場所を別に用意する必要がありません。
一方で、AIに任せる範囲を広げるということは、人が見ていない状態で動く時間が増えるということでもあります。CI/CDに載せる場合は最初からそうですし、手元でも承認を省いて動かせば同じです。
実体としては、エージェントはRunnerの中で動きます。ジョブごとに作られて捨てられるので、境界としてはコンテナを都度立てるのと同じで、渡すものもチェックアウトしたコードとジョブの権限に限られます。
このとき、境界が止めてくれるのはファイルの読み書きと通信先までです。内側に置いたものは止まりません。Gitの資格情報も、クラウドのトークンも、ジョブに渡した権限も、エージェントから見れば単に使えるものです。届く範囲のものは全部使われる前提で、内側に何を置くかを決めます。ジョブごとに権限を分けます。長命のトークンは置かず、実行中だけ有効なものを使います。ホストの資格情報はコンテナにマウントしません。いずれも前章で書いたことがそのまま効いてきます。
CI/CD環境でのセキュリティの担保については、以下の記事でまとめているのでこちらもご参考ください。
決定論的に担保できる範囲を広げる
どちらの形でも、出力は非決定論的です。そのまま通すのではなく、機械的に検査できる範囲を広げた上で人が見る、という形は変わりません。
冒頭の3点目は、その範囲を意識して広げていく対象でもあります。実行の場所は、手元ならHook、共有基盤ならCI/CDになります。pre-commit / pre-pushやCI/CDで回す処理をスクリプトとして書かせて、それを自動実行の仕組み上に載せていくのも同じ方向です。
こうして決定論的に担保される部分の幅を広げつつ、非決定論的に対応せざるを得ない部分を生成AIでの処理に寄せていくのが適切な役割分担です。
おわりに
さくっとしたまとめになりましたが、何をどこまでやればいいんだっけ?と改めて考えたときに、これくらいの地図があると進めやすいということで、今後も自分で参考にすることになりそうな予感がしています。