Skip to content

test(ci): コンテナイメージのビルドと /health 起動確認を CI に追加する - #155

Merged
izumacha merged 3 commits into
mainfrom
claude/dreamy-brown-d592z8
Aug 8, 2026
Merged

test(ci): コンテナイメージのビルドと /health 起動確認を CI に追加する#155
izumacha merged 3 commits into
mainfrom
claude/dreamy-brown-d592z8

Conversation

@izumacha

@izumacha izumacha commented Aug 8, 2026

Copy link
Copy Markdown
Owner

背景

コードレビューの過程で、CI に Dockerfile を評価する経路が一切ないことが判明した。build-and-test ジョブはホストの SDK 上で dotnet build / dotnet test を実行するだけで、Dockerfile はビルドすらされない。

このため、ベースイメージを壊す変更が CI 緑のまま素通りする

実際に素通りするケース(現在 open な Dependabot PR #142

  • アプリは net8.0 を対象にビルドされ、生成される runtimeconfig.jsonMicrosoft.AspNetCore.App 8.0.0 を要求する(rollForward は未設定=既定の Minor)。
  • Minor はメジャー跨ぎのロールフォワードをしないため、ランタイム段を aspnet:8.0aspnet:9.0 に上げると 8.0 ランタイムが存在せず、コンテナは起動時に落ちる
  • それでも dotnet build / dotnet test はホストの SDK 8 で走るので緑のまま。
// 現在の runtimeconfig.json(rollForward 指定なし)
"frameworks": [
  { "name": "Microsoft.NETCore.App",    "version": "8.0.0" },
  { "name": "Microsoft.AspNetCore.App", "version": "8.0.0" }
]

変更内容

.github/workflows/ci.ymldocker-image ジョブを 1 本追加する。既存の build-and-test は変更しない。

  1. docker build でイメージを実際にビルドする
  2. コンテナを起動する(DB は既定の SQLite なので追加サービス不要)
  3. /health が 200 を返すまで最大 60 秒ポーリングする
  4. 失敗時のみ docker logs を出力する(起動失敗の原因は stdout に出るため)
  5. 成否にかかわらずコンテナを後片付けする

これによりベースイメージとアプリの TFM のズレを機械的に検出できる。

検証(ローカルで実機確認済み)

Docker デーモンが使えない環境のため、コンテナ内と同じ構成をホスト上で再現してスモークテストの前提を 2 点とも実測で確認した。

確認項目 結果
dotnet build 成功 / 0 warning・0 error
dotnet test 374 passed / 0 failed
npm run typecheck 成功
ci.yml の YAML パース 成功(jobs: build-and-test, docker-image

前提1: Production かつプレーン HTTP で /health が 200 を返すか

UseHttpsRedirectionMapHealthChecks より前に実行されるため 307 リダイレクトを疑ったが、HTTPS ポート未構成時はリダイレクトせず素通しすることを実測で確認した。

HTTP/1.1 200 OK
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.

コンテナは ASPNETCORE_URLS=http://+:8080 のみで HTTPS エンドポイントを持たないため、同じ経路になる。

前提2: Audit__HashSalt が本当に必要か

未設定で起動すると fail-closed で失敗することを確認した。そのためスモークテスト専用の使い捨て値を渡している(本番の秘密情報ではない)。

Unhandled exception. System.InvalidOperationException: Audit:HashSalt is required in Production.

期待される効果

補足: 関連する Dependabot PR の調査結果

本 PR の範囲外だが、同じレビューで以下を実測した(いずれも本 PR では変更しない)。

PR 内容 実測結果
#146 Npgsql.EFCore.PostgreSQL 8.0.11→9.0.4 6 tests failed — EF Core core だけ 9.0.1 に引き上がり、SQLite/SqlServer プロバイダは 8.0.29 のまま混在。TypeLoadException: Method 'get_LockReleaseBehavior' ... does not have an implementation
#144 EFCore.SqlServer 8.0.29→9.0.18 6 tests failed — 同一の混在メカニズム
#142 aspnet 8.0→9.0(ランタイム段) 本 PR の docker-image ジョブが検出する対象

#144 / #146 は既存の dotnet test が検知できるため CI は赤くなる。#142 だけが検知できていなかった。

🤖 Generated with Claude Code

https://claude.ai/code/session_018nkWELTPsYuGxyayheyhPG


Generated by Claude Code

claude added 3 commits August 8, 2026 08:18
build-and-test は SDK 上で直接ビルド/テストするだけで Dockerfile を一切
評価しないため、ベースイメージの破損が CI 緑のまま素通りしていた。

具体的な検知漏れ:
アプリは net8.0 を対象にビルドされ、runtimeconfig.json は
Microsoft.AspNetCore.App 8.0.0 を要求する。rollForward は既定の "Minor" で
メジャー跨ぎのロールフォワードをしないため、ランタイム段を aspnet:9.0 へ
上げると 8.0 ランタイムが存在せずコンテナは起動時に落ちる。しかし
dotnet build / dotnet test はホストの SDK 8 で走るので緑のままになる。

対策として docker-image ジョブを追加し、イメージを実際にビルドして起動し
/health が 200 を返すところまでを検証する。これによりベースイメージと
アプリの TFM のズレを機械的に検出できる。

検証内容 (ローカルで実機確認済み):
- Production + プレーン HTTP で /health が 200 を返すことを確認。
  UseHttpsRedirection は HTTPS ポート未構成時にリダイレクトせず素通しする
  ("Failed to determine the https port for redirect" を確認済み)。
- Audit__HashSalt 未設定だと Production 起動が fail-closed で失敗することを
  確認したため、スモークテスト専用の使い捨て値を渡している。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nkWELTPsYuGxyayheyhPG
新設した docker-image ジョブが、main の時点で既にコンテナイメージを
ビルドできない状態だったことを検出した。

原因:
.csproj の CompileTypeScript ターゲットが publish 中に `npm ci` と
`npx --no-install tsc` を実行するが、mcr.microsoft.com/dotnet/sdk:8.0 には
Node/npm が含まれない。このため publish が次のエラーで失敗する。

    /usr/bin/sh: npm: not found
    error MSB3073: The command "npm ci" exited with code 127.

CI が Dockerfile を一度もビルドしていなかったため、この破損は今まで
検知されずに残っていた。

対応:
- build ステージへ公式 node イメージから Node/npm/npx をコピーする。
  apt ではなくイメージからコピーするのは、バージョンをタグで固定でき
  再現性が高いため。sdk:8.0 は Debian 12 (bookworm) ベースなので
  glibc を揃えるため node も bookworm 系を使う。
- package.json / package-lock.json だけ先にコピーして `npm ci` を実行し、
  レイヤキャッシュを効かせる。これにより publish 内の CompileTypeScript は
  Condition="!Exists(node_modules)" により npm ci をスキップする。
- .dockerignore に node_modules を追加する。除外しないと後続の `COPY . .` が
  ホスト側 (別 OS/アーキテクチャの可能性がある) のバイナリで上書きしてしまう。

SkipTsBuild=true での回避は採らない。wwwroot/js/*.js は tsc の生成物で
Git 管理外のため、スキップすると JS を一切含まないイメージが出来上がり、
ダッシュボード/分析のグラフが動かなくなる。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nkWELTPsYuGxyayheyhPG
コードレビューの指摘に対応する。

1. packages.lock.json をイメージのビルド前にコピーし --locked-mode で restore する
   これまで sln/csproj しかコピーしていなかったため、RestorePackagesWithLockFile=true
   の下で NuGet はロックファイルを「検証」せず「新規生成」していた。結果として
   CI の `dotnet restore --locked-mode` が守っている依存の固定が、実際に配布される
   イメージでは効かず、ロック更新漏れの PR でも docker build だけ通る状態だった。

2. .dockerignore に var/backups/ を追加する
   scripts/backup-db.sh の出力先で PHI を含む pg_dump が置かれる (.gitignore でも
   除外済み)。除外しないと `COPY . .` が患者データを build ステージのレイヤに
   焼き込む。最終イメージには残らないが、レイヤキャッシュや --cache-to /
   --target build のエクスポート経由で外部に出得るため、コンテキストに載せない。

3. TypeScript 生成物がイメージに含まれることを検証する手順を追加する
   /health は DB 接続しか見ないため、JS が 1 つも無いイメージでも 200 を返す。
   つまり本 PR の Dockerfile 修正が守ろうとしている退行を、スモークテスト自身が
   検出できなかった。publish 出力の wwwroot/js/*.js を直接確認するようにした。

4. コンテナが起動待ちの途中で終了したら即座に打ち切る
   ランタイム不一致で起動直後に落ちるケース(このジョブが主に狙う退行)で、
   60 秒待って「応答なし」と報告するのは原因から遠い。異常終了を検出して
   exit code を報告する。

5. ログ出力ステップをコンテナ生成後に限定する
   `if: failure()` だけだとイメージビルド失敗時にも走り、"No such container" で
   二重に赤くなって本当の失敗理由が埋もれる (実際に初回実行で発生した)。

6. CI の Node を 22 に上げて Dockerfile と揃える
   同じ CompileTypeScript ターゲットを CI は Node 20、イメージは Node 22 で
   動かしており、「CI は緑だがイメージでは失敗する」食い違いが起き得た。

7. README / docs/deployment.md の docker run 例に Audit__HashSalt を追記する
   Dockerfile は ASPNETCORE_ENVIRONMENT=Production を固定しており、この変数が
   無いと fail-closed で起動に失敗する。ドキュメント通りに実行すると必ず落ちる
   状態だった。

8. Dockerfile のコメントから過剰な再現性の主張を削る
   `node:22-bookworm-slim` は 22.x のパッチ更新で中身が変わる可動タグであり、
   「バージョンがタグで固定される」という記述は誤り。

検証: dotnet build (0 warning) / dotnet test 374 passed / npm run typecheck /
`dotnet restore --locked-mode` 成功 / publish 出力に wwwroot/js の 3 ファイルを確認。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nkWELTPsYuGxyayheyhPG
@izumacha
izumacha merged commit 0deacf7 into main Aug 8, 2026
2 checks passed
@izumacha
izumacha deleted the claude/dreamy-brown-d592z8 branch August 8, 2026 16:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants