本文へ移動

ホーム / Web / 第5部 品質と運用

第5部 品質と運用・第15章

開発の道具(Git・コンテナ・ビルド)

★★★ 必須12トピック・面接 5問

毎日使う道具の使い方が、変更の安全さと開発の速さを決めます。Git での変更の出し方、Docker で手元に本番に近い環境を作る方法、ビルドと CI を速くする工夫を押さえます。

ひとことで

1つのコミットに1つの理由。1行目で何をしたか、本文でなぜそうしたかを書き、機械的な変更は分ける。

1つのコミットに、1つの理由大きすぎる・機能追加と整形が混ざる・戻すと関係ない所も戻る・レビューで読めないちょうどよい・1つの理由の変更・それだけで動く・テストが通る・メッセージで理由が分かる
コミットの単位の考え方

コミットは「あとで読む人(未来の自分)」への記録です。1つのコミットに1つの理由を目安にし、整形・名前の変更のような機械的な変更は、中身の変更と分けます。

書き方例
1行目は短く、何をしたか注文一覧のN+1を解消する
本文に、なぜ・何を考えたか一覧で SQL が 120 回出ていたため preload に変更。…
決まった接頭辞を使うチームも(Conventional Commits)fix: …・feat: …・refactor: …
課題の番号を付けるRefs #123

「なぜ」はコードからは読み取れないので、メッセージや PR の説明に残します。git log -p・git blame で過去の理由をたどれると、調査がずっと速くなります。

面接ではこう答える

コミットは、あとで読む人への記録なので、1つのコミットに1つの理由を目安にしています。機能の追加とコードの整形が混ざると、レビューで読みにくく、戻すときに関係のない変更まで戻ってしまいます。メッセージは1行目で何をしたかを短く書き、本文でなぜそうしたかを書きます。なぜはコードから読み取れないので、ここに残すことで、障害の調査で git blame から理由をたどれます。

ひとことで

main から短いブランチを切って PR でマージする GitHub Flow が Web の基本。ブランチの寿命を短くし、リリースした版にはタグを付ける。

main はいつでもデプロイできる状態に保つmainfeature/a(1日)fix/b(数時間)feature/c(2日)マージのたびにデプロイ。長いブランチほどコンフリクトが増える
GitHub Flow:短いブランチを切って戻す
戦略しくみ向いている場面
GitHub Flowmain から短いブランチを切り、PR でレビューして main にマージ。main はいつでもデプロイできる多くの Web サービス。毎日デプロイする
トランクベース開発ブランチは数時間〜1日で main に戻す。未完成の機能は Feature Flag で隠すデプロイの頻度が高い・CI がしっかりしている
Git Flowdevelop・release・hotfix などの長いブランチを使い分ける版を決めて配布するもの(アプリ・ライブラリ)。Web では重いことが多い

どの戦略でも、ブランチの寿命が長いほどコンフリクトと統合の手間が増えます。大きな機能は Feature Flag で隠して小さくマージします。

タグとリリース

リリースした版には git tag -a v1.4.0 -m "…" で印を付け、どのコミットが本番かを分かるようにします。版の番号はセマンティックバージョニング(壊す変更・機能追加・修正 = メジャー・マイナー・パッチ)がよく使われます。GitHub の Releases でタグに変更点を添えたり、タグの push を合図に CI でデプロイしたりします。

面接ではこう答える

Web サービスでは、main から短いブランチを切り、PR でレビューして main に戻す GitHub Flow が基本です。さらに頻繁にデプロイするチームは、ブランチを1日以内に戻すトランクベース開発にし、未完成の機能は Feature Flag で隠します。Git Flow は develop や release の長いブランチを使うので、版を決めて配布するアプリやライブラリに向きます。どれでも、ブランチの寿命が長いほど統合が大変になります。

ひとことで

PR は小さく出して目的と確かめ方を書き、形式は機械に任せ、人は正しさ・安全・設計を見る。

小さく出し、機械に任せ、人は中身を見る小さな PR目安 400 行以下説明を書く目的・確かめ方CI が見るlint・型・テスト人が見る正しさ・設計マージすぐデプロイ
PR が出てからマージまで
出す側レビューする側
目的・変更点・確かめ方・画面のスクリーンショットを書くまず目的を読み、変更がそれに合っているかを見る
大きな変更は、準備のリファクタリングと本体を分ける正しさ・セキュリティ → 設計 → 読みやすさの順で指摘
自分で一度差分を読み、迷った所にコメントを残す好みの指摘は「nit:」など軽い印を付ける
指摘には理由で答える。直したら知らせる人ではなくコードの話をする。良い所も伝える

レビューの待ち時間は開発の速さを大きく左右します。チームで「1日以内に一度見る」などの目安を決め、形式の指摘は lint と formatter に任せて、人は中身に集中します。

面接ではこう答える

PR は、レビューする人が短い時間で理解できるように小さく出し、目的と変更点、どう確かめたかを説明に書きます。形式の確認は lint や formatter、テストは CI に任せ、人は正しさやセキュリティ、設計を見ます。指摘はその順番で優先し、好みの指摘は軽い印を付けて区別します。レビューの待ち時間が長いと開発全体が遅くなるので、チームで目安を決めて早く返すようにしています。

ひとことで

merge は履歴をそのまま合わせ、rebase は自分のコミットを相手の先端の上に作り直し、squash は1つにまとめる。共有したブランチは rebase しない。

同じ変更でも、履歴の残り方が違うmergeマージコミットrebase作り直したコミットsquash1つにまとめるmain のコミット自分のブランチのコミット共有しているブランチを rebase すると、相手の手元の履歴と食い違う
merge・rebase・squash の履歴の違い
方法履歴向いている場面
merge分かれた履歴をそのまま残し、マージコミットで合わせる履歴をそのまま残したい。共有しているブランチ
rebase自分のコミットを、相手の先端の上に作り直す(ハッシュが変わる)手元のブランチを最新の main に追いつかせる
squashブランチのコミットを1つにまとめて main に入れるPR を1つの変更として残したい(多くの Web チームの既定)

他の人と共有しているブランチを rebase しないのが基本です。履歴が書き換わり、相手の手元と食い違います。自分のブランチを rebase して push し直すときは、相手の変更を消さないよう git push --force-with-lease を使います。

コンフリクトの解き方

<<<<<<< HEAD(自分)
const limit = 50;
=======
const limit = MAX_LIMIT;
>>>>>>> main(相手)
  • どちらかを選ぶのではなく、両方の変更の意図を確かめて合わせる(相手の PR を読む)。
  • 解いたらビルドとテストを通す。印(<<<)の消し忘れは CI で見つける。
  • コンフリクトが多いのは、ブランチの寿命が長いか、同じファイルを多くの人が触っている合図。

面接ではこう答える

merge は分かれた履歴をそのまま残してマージコミットで合わせる方法、rebase は自分のコミットを相手の最新の上に作り直す方法、squash はブランチのコミットを1つにまとめて取り込む方法です。rebase はコミットのハッシュが変わるので、他の人と共有しているブランチでは使いません。コンフリクトは、どちらかを選ぶのではなく、両方の変更の意図を確かめて合わせ、テストを通してから進めます。

履歴から消す前に、まず鍵を無効にする鍵を無効化最優先利用ログの確認使われたか履歴から消すgit filter-repo再発防止push の保護
秘密情報をコミットしてしまったときの順番
.gitignore に入れるもの例
秘密情報.env・*.pem(見本は .env.example としてコミット)
生成物node_modules/・dist/・.next/・カバレッジ
手元の設定エディタの設定・OS のファイル(.DS_Store)

秘密情報を push してしまったら、公開された時点で取られた前提で動きます。最初に鍵を無効化・入れ替えし、そのあと必要なら git filter-repo などで履歴から消します(全員の手元の複製にも残るので、消すだけでは解決しません)。GitHub の secret scanning の push protection を有効にすると、既知の形式の鍵を含む push を止められます。

鍵のローテーションと漏えい時の対応

ひとことで

イメージは読み取り専用のレイヤーの重なりで、そこから起動した隔離されたプロセスがコンテナ。ホストのカーネルを共有するので軽い。

イメージは読み取り専用の層の重なり。コンテナは1枚書き込みの層を足すコンテナの層(書き込み)消すとなくなる。データはボリュームへCOPY . .(アプリのコード)変わりやすい層は上にRUN pnpm install(依存)ロックファイルが同じなら使い回すベースイメージnode:24-slim など
イメージのレイヤーと、コンテナの書き込みの層
用語意味
イメージアプリと動かすのに要るもの(OS の一部・ランタイム・依存)をまとめた、読み取り専用のひな形
コンテナイメージから起動した、隔離されたプロセス。仮想マシンと違い、ホストの OS のカーネルを共有するので軽い
レイヤーDockerfile の命令ごとの差分の層。変わっていない層はキャッシュから使い回す
ボリュームコンテナを消しても残すデータ(DB のデータなど)の置き場
レジストリイメージを置く場所(Docker Hub・ECR・GHCR など)

コンテナの中で書いたファイルは、コンテナを作り直すと消えます。DB のデータやアップロードされたファイルは、ボリュームや外部のストレージに置きます。

SRE の「コンテナの仕組み」

面接ではこう答える

Docker のイメージは、アプリとそれを動かすのに必要なランタイムや依存をまとめた読み取り専用のひな形で、Dockerfile の命令ごとのレイヤーが重なってできています。コンテナはイメージから起動した隔離されたプロセスで、仮想マシンと違ってホストのカーネルを共有するので、起動が速く軽いです。コンテナの中で書いたものは作り直すと消えるので、DB のデータなどはボリュームに置きます。

ひとことで

ビルド用のステージで依存を入れてビルドし、実行用のステージには成果物と本番の依存だけを COPY する。イメージが小さく安全になる。

ビルドの道具は最終のイメージに入れないbuild ステージ依存を入れてビルド成果物だけCOPY --from=build実行ステージ小さなベース・本番の依存だけ
マルチステージビルドの流れ
# ---- ビルド用
FROM node:24-slim AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./      # 先に依存だけ(キャッシュが効く)
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod    # 開発用の依存を外す

# ---- 実行用
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node                               # root で動かさない
EXPOSE 3000
CMD ["node", "dist/server.js"]
書き方の要点理由
変わりにくいものを先に COPYロックファイルが同じなら依存のインストールの層を使い回せる
.dockerignore に node_modules・.git・.envビルドが速くなり、秘密情報がイメージに入らない
マルチステージにするコンパイラや開発用の依存を最終のイメージに入れない
USER で一般ユーザーに乗っ取られたときの被害を小さくする
CMD は配列の形シグナル(停止の合図)がアプリに届き、きれいに終われる

面接ではこう答える

マルチステージビルドは、1つの Dockerfile の中で、ビルド用と実行用のステージを分ける書き方です。ビルド用のステージで依存を入れてビルドし、実行用のステージには COPY --from で成果物と本番の依存だけを持ってきます。コンパイラや開発用の依存が最終のイメージに入らないので、小さく安全になります。あわせて、変わりにくいロックファイルを先に COPY してキャッシュを効かせ、一般ユーザーで動かします。

ひとことで

Web・DB・Redis などをまとめて1つのファイルに書き、1つのコマンドで同じ開発環境を立ち上げる仕組み。

1つのコマンドで、Web・DB・Redis をそろえるdocker compose up(同じネットワーク)weblocalhost:3000dbpostgres:17・ボリュームredisredis:8db:5432redis:6379
docker compose の手元の開発環境
# compose.yaml
services:
  db:
    image: postgres:17
    environment: { POSTGRES_PASSWORD: dev }
    ports: ["5432:5432"]
    volumes: [dbdata:/var/lib/postgresql/data]
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 2s
  redis:
    image: redis:8
    ports: ["6379:6379"]
  web:
    build: .
    ports: ["3000:3000"]
    environment:
      DATABASE_URL: postgres://postgres:dev@db:5432/postgres
      REDIS_URL: redis://redis:6379
    depends_on:
      db: { condition: service_healthy }
volumes:
  dbdata:
  • コンテナどうしはサービス名(db・redis)で互いに届く。手元から使うときは ports で出した localhost:5432。
  • depends_on は起動の順番だけ。DB が受け付けられるまで待つには healthcheck と service_healthy。
  • DB だけ compose で動かし、アプリは手元で pnpm dev にすると、ファイルの変更の反映が速い(特に macOS)。
  • DB の版は本番にそろえる。ここに書くパスワードは手元専用の値にする。

面接ではこう答える

docker compose は、アプリと DB、Redis などの複数のコンテナを1つの YAML に書き、docker compose up の1つのコマンドで立ち上げる仕組みです。新しく入った人でもすぐ同じ環境を作れ、DB の版を本番にそろえられます。コンテナどうしはサービス名で通信でき、depends_on と healthcheck で DB が準備できてからアプリを起動します。ファイルの変更をすぐ反映したいときは、DB だけを compose で動かし、アプリは手元で動かすこともあります。

ベースと中身で、イメージの大きさは大きく変わるnode(フル)+開発用の依存大きいnode:slim +本番の依存distroless など+成果物だけ小さい
イメージの大きさのイメージ(相対値)
工夫効果
小さなベース(-slim・distroless)取得が速い。入っているものが少ないほど脆弱性も少ない
マルチステージ・本番の依存だけ開発用の道具を入れない
root で動かさない・読み取り専用で動かす乗っ取られたときの被害を小さく
ベースの版を固定し、定期的に更新同じものが作れる。脆弱性の修正を取り込む
イメージの脆弱性スキャン(Trivy・レジストリの機能)既知の CVE を CI で見つける
秘密情報を入れないビルド時の秘密は --mount=type=secret、実行時は環境変数やシークレット管理から

手元の compose と本番(Kubernetes・ECS など)では、ネットワーク・ログの出し方・設定の渡し方・台数が違います。本番では、ログは標準出力に出し、設定は環境変数で受け、どのコンテナがいつ止められてもよいように作ります。

SRE の「コンテナと仮想マシンの比較」

ひとことで

開発サーバーは ES Modules のまま配って変更だけ差し替えて速く、本番のビルドはまとめて小さくする。Vite・webpack・esbuild・Turbopack はその実装の違い。

開発中は「まとめない」で速く、本番は「まとめて」小さく開発サーバー(Vite など)・ES Modules のまま配る・変更したファイルだけ差し替え(HMR)・起動がすぐ本番のビルド・まとめて圧縮・ツリーシェイキング・分割・ハッシュ付きのファイル名
開発サーバーと本番のビルドの役割
道具特徴
Vite開発中は ES Modules のまま配って速く起動する。依存は esbuild で事前にまとめる。本番は Rollup 系でまとめる(新しい版では Rust 製の Rolldown への移行が進んでいる)
webpack歴史が長く、設定とプラグインが豊富。大きな既存のプロジェクトに多い
esbuildGo 製でとても速い変換・まとめ。単体でも、他の道具の部品としても使われる
TurbopackNext.js 向けの Rust 製のバンドラー。Next.js の新しい版では既定になっている
Rspackwebpack と互換を目指した Rust 製のバンドラー。webpack からの移行で速くしたいとき

開発サーバーが遅いときは、まず起動時と変更時のどちらが遅いかを分けます。巨大な依存を毎回変換していないか、アイコンやライブラリを丸ごと読み込んでいないかを確かめます。

SRE の「JSバンドルとバンドラー」

面接ではこう答える

フロントのビルドの道具は、開発中と本番で役割が違います。開発中は Vite のように ES Modules のままブラウザに配り、変更したファイルだけを差し替える HMR で、起動も反映も速くします。本番では、まとめて圧縮し、ツリーシェイキングやコード分割、ハッシュ付きのファイル名で配信用に最適化します。webpack は設定が豊富、esbuild はとても速い変換、Turbopack や Rspack は Rust 製で速さを狙った道具です。

ひとことで

入力が同じなら前回の結果を使い回す。依存はロックファイル、成果物は入力のハッシュを鍵にし、Docker は変わりにくい層を先に置く。

変わっていないものは、作り直さない依存のキャッシュロックファイルが鍵ビルドの成果物入力のハッシュが鍵Docker のレイヤー変わりにくい順にテストの分割並列に流す
CI を速くするキャッシュの種類
キャッシュやり方の例注意
依存actions/setup-node の cache: pnpm。鍵はロックファイルのハッシュロックファイルが変わったら作り直す
ビルドの成果物Next.js の .next/cache、タスクランナーのキャッシュ(Turborepo・Nx)入力(ソース・設定・環境変数)が鍵に入っているか
Docker のレイヤーBuildKit の cache-from・cache-to(GitHub Actions のキャッシュやレジストリ)COPY . . を依存のインストールより後に
依存のダウンロード(Docker 内)RUN --mount=type=cache,target=… でストアを使い回すイメージには入らない

キャッシュは「鍵が正しいこと」がすべてです。鍵に入れ忘れた入力があると、古い成果物が使われて、手元と CI で結果が変わる事故になります。

SRE の「ビルド・CIの高速化」

面接ではこう答える

CI を速くする基本は、入力が同じなら前回の結果を使い回すことです。依存はロックファイルのハッシュを鍵にしてキャッシュし、ビルドの成果物は Turborepo や Nx のようなタスクランナーが入力のハッシュで管理します。Docker では、変わりにくいロックファイルの COPY と依存のインストールを先に置き、BuildKit のキャッシュを CI に保存します。鍵に入力が漏れていると古い結果が使われるので、鍵の設計が一番大事です。

ひとことで

関係するコードを1つのリポジトリに置き、依存の関係から変更の影響を受ける所だけをビルド・テストする。

依存の関係をたどって、影響を受ける所だけshared変更したweb対象api対象adminshared を使わない矢印=依存(web と api は shared を使う)
変更の影響を受けるパッケージだけを対象にする
モノレポポリレポ
形Web・API・共通部品を1つのリポジトリにサービスやパッケージごとに別のリポジトリ
良い点型や部品の共有が簡単。1つの PR で全体を直せる権限・リリースを独立できる。CI が小さい
難しい点全部をビルド・テストすると遅い → 変更のあった所だけにまとめて直すのが大変。版の食い違い

モノレポでは、パッケージどうしの依存の関係から、変更の影響を受ける所だけをビルド・テストします。

# pnpm:main から変わったパッケージと、それに依存するものだけテスト
pnpm --filter "...[origin/main]" test
# Turborepo:入力が同じタスクはキャッシュから(リモートキャッシュも使える)
turbo run build test --affected

共通の部品を変えると多くのパッケージが対象になるので、共通部品は小さく安定させておきます。

面接ではこう答える

モノレポは、Web のフロントと API、共通の部品などを1つのリポジトリに置く形で、型や部品の共有が簡単になり、1つの PR で全体を直せます。その代わり、毎回全部をビルドしてテストすると遅くなるので、パッケージの依存の関係から、変更の影響を受ける所だけを対象にします。pnpm の filter や Turborepo、Nx がその仕組みを持ち、入力が同じタスクはキャッシュから結果を返します。

この章のまとめ

🖨 まとめシート(印刷・PDF)
  • コミット・ブランチ・PR・merge と rebase を使い分け、レビューしやすい変更を出せる
  • Docker の仕組みを理解し、Dockerfile と docker compose で手元の環境を作れる
  • バンドラー・CI のキャッシュ・変更のあった所だけのビルドで、待ち時間を減らせる

この章のクイズ

面接ではこう聞かれる

まず答えを見ずに声に出して答え、そのあと開いて比べる。

Q1merge と rebase の違いと、使い分けを教えてください。

模範回答

merge は、分かれた2つの履歴をそのまま残して、マージコミットで合わせます。rebase は、自分のコミットを相手の最新の先端の上に作り直すので、履歴が一直線になりますが、コミットのハッシュが変わります。

使い分けとしては、手元の自分のブランチを最新の main に追いつかせるときは rebase、他の人と共有しているブランチは履歴を書き換えないよう merge にします。main に入れるときは、チームの方針で squash merge にして、PR を1つの変更として残すことが多いです。rebase したブランチを push し直すときは --force-with-lease を使います。

深掘りされたら

  • squash merge の欠点は何ですか?
  • --force と --force-with-lease の違いは?

見られているポイント履歴の違いと、共有ブランチを書き換えない判断

Q2コードレビューで気をつけていることは何ですか?

模範回答

出す側としては、PR を小さくし、目的と変更点、どう確かめたかを説明に書きます。レビューする側としては、まず目的を読み、正しさとセキュリティ、次に設計、最後に読みやすさの順で見ます。形式の指摘は lint や formatter に任せて、人は中身に集中します。

指摘には理由を添え、好みの話は「nit」のように軽い印を付けて区別します。人ではなくコードについて話し、良い所も伝えるようにしています。待ち時間が開発の速さを左右するので、1日以内に一度は返すようにしています。

深掘りされたら

  • 大きすぎる PR が来たらどうしますか?

見られているポイント優先順位のある見方と、チームの速さへの配慮

Q3Dockerfile を書くときに気をつけることは?

模範回答

まず、キャッシュが効く順番にします。ロックファイルだけを先に COPY して依存をインストールし、そのあとでアプリのコードを COPY すると、コードを変えても依存の層を使い回せます。.dockerignore で node_modules や .env を除き、秘密情報がイメージに入らないようにします。

次に、マルチステージビルドで、ビルド用の道具や開発用の依存を最終のイメージに入れず、小さなベースイメージを使います。実行は一般ユーザーにし、CMD は配列の形で書いて停止のシグナルがアプリに届くようにします。

深掘りされたら

  • イメージが大きいと何が困りますか?

見られているポイントキャッシュの順番・マルチステージ・安全の3つを話せるか

Q4手元の開発環境はどう作っていますか?

模範回答

例えば、DB や Redis は docker compose で本番と同じ版を立ち上げ、アプリは手元で pnpm dev で動かしています。compose では healthcheck を付けて、DB が準備できてからマイグレーションや起動をするようにしています。

新しく入った人が README の数行で同じ環境を作れることを目標にし、テスト用のデータを入れるスクリプトも用意しています。CI の結合テストも同じ compose や同じ版の DB を使うので、手元と CI で結果がずれにくくなります。

深掘りされたら

  • アプリもコンテナで動かさないのはなぜですか?

見られているポイント再現できる環境と、本番・CI との版のそろえ方

Q5CI が遅くなってきました。どう速くしますか?

模範回答

まずどのジョブのどの手順に時間がかかっているかを測ります。よくあるのは、依存のインストール、ビルド、テストの3つです。依存はロックファイルを鍵にキャッシュし、ビルドは Next.js のキャッシュやタスクランナーのキャッシュで入力が同じなら使い回します。

モノレポなら、変更の影響を受けるパッケージだけをビルド・テストするようにし、テストは分割して並列に流します。Docker のビルドは、依存の層を先に置き、BuildKit のキャッシュを保存します。キャッシュの鍵に入力が漏れていないかも確かめて、古い結果を使う事故を防ぎます。

深掘りされたら

  • キャッシュのせいで手元と結果が違うときはどう調べますか?

見られているポイント測ってから直す、キャッシュの鍵の正しさまで話せるか