はじめに
今回はASP.NET Core
のMinimal API
で簡単なWebAPIを作成〜Cloud Runに認証付きで公開するまでの方法について紹介したいと思います。
またちょこちょこコマンドを利用した操作をしてますが、bash/zshではなく私はPowerShell教なのでご注意ください。
Mimimal APIを用いたサーバー実装
最新の.NET9
を用いて実装していきます。
$ dotnet new web -f net9.0 -n SampleWebAPI
またOpenAPIドキュメントを自動生成してSwaggerUIにて確認したいので、NSwag.AspNetCore
をインストールしておきます。
$ dotnet add package NSwag.AspNetCore --version 14.2.0
【C#】ASP.NET CoreのMinimalAPIでOpenAPIドキュメントを自動生成してSwaggerUIやReDocで可視化する(NSwag.AspNetCore) - はなちるのマイノート
Program.csの実装
まずはProgram.cs
に諸々の設定を書いていきます。
// Program.cs using SampleWebAPI.Endpoints; var builder = WebApplication.CreateBuilder(args); // Loggerを追加 builder.Services.AddLogging(logging => { logging.ClearProviders(); logging.AddConsole(); }); // OpenAPIドキュメントを自動生成するためにサービス追加(Minimal APIのみ必要) builder.Services.AddEndpointsApiExplorer(); // 生成するOpenAPIドキュメントの設定 builder.Services.AddOpenApiDocument(config => { config.DocumentName = "SampleWebAPI"; config.Title = "SampleWebAPI v1"; config.Version = "v1"; }); var app = builder.Build(); // Endpointsを登録 app.RegisterSampleEndpoints(); // DevelopmentのときだけOpenAPIドキュメントを吐き出し、SwaggerUIを表示できるようにする if (app.Environment.IsDevelopment()) { app.UseOpenApi(); app.UseSwaggerUi(config => { config.DocumentTitle = "SampleWebAPI"; config.Path = "/swagger"; config.DocumentPath = "/swagger/SampleWebAPI/swagger.json"; config.DocExpansion = "list"; }); } app.UseHttpsRedirection(); if (app.Environment.IsDevelopment()) { // launchSettings.jsonから起動するとこちらが実行される app.Run(); } else { // Docker上ではこちらを実行するようにする app.Run($"http://0.0.0.0:{Environment.GetEnvironmentVariable("PORT") ?? "8080"}"); }
実装のコツとして、Endpoint
の定義をそれぞれ別.cs
に切り出すと見通しがよくなります。また不用意にhttp://0.0.0.0
で公開しないでください。公衆wifiなどで不正にアクセスされる恐れがあるので注意です。
Endpointの実装
$ tree -L 2 . ├── Endpoints │ └── SampleEndpoints.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SampleWebAPI.csproj ├── appsettings.Development.json └── appsettings.json
上記のようにEndpoints
ディレクトリを用意して、そこに記述していきます。
// SampleEndpoints.cs using Microsoft.AspNetCore.Http.HttpResults; namespace SampleWebAPI.Endpoints; public static class SampleEndpoints { public static WebApplication RegisterSampleEndpoints(this WebApplication app) { var sample = app.MapGroup("/sample"); // /sample/hoge にアクセスしたときにGetHogeメソッドを実行 sample.MapGet("/hoge", GetHoge); // /sample/fuga にアクセスしたときにGetFugaメソッドを実行 sample.MapGet("/fuga/{name}", GetFuga); return app; } internal static Results<BadRequest, Ok<string>> GetHoge(HttpContext context) { return TypedResults.Ok("Hoge"); } internal static Results<BadRequest<string>, Ok<string>> GetFuga(HttpContext context, string name) { if(string.IsNullOrEmpty(name)) { return TypedResults.BadRequest("Name is required."); } return TypedResults.Ok("Fuga"); } }
HttpContext
からヘッダーの情報を取得とかができます。またResults
のような返り値の型定義の仕方によって、OpenAPIドキュメントの返り値が自動で生成できるのかどうかが変わるので要注意です。
launchSettings.jsonでlocalhostにサーバーを立てる
実際にlocalhostにサーバーを立てて実験してみましょう。launchSettings.json
を開いていただき、再生ボタンを押します。(実行/デバッグ構成から実行してもOKです)

http://localhost:5206/sample/hoge
(ご自身のポート番号に合わせてください)にアクセスすると、正しく値が返ってくることが確認できます。

またhttp://localhost:5206/swagger
にアクセスすると、SwaggerUIが立ち上がります。

Dockerで動作させる
ここまででサーバー実装は終わったので、次はDockerfileを記述していきます。
$ tree -L 2 . ├── Dockerfile ├── Endpoints │ └── SampleEndpoints.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SampleWebAPI.csproj ├── appsettings.Development.json └── appsettings.json
Macで開発するときは少し面倒くさいのですが、以下のようにTARGETARCH
を用いて実装してあげます。
# Cloud Runにあげるときは--platform=linux/amd64を指定してdocker buildしてください FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env ARG TARGETARCH WORKDIR /App COPY . ./ RUN dotnet restore ./SampleWebAPI.csproj -a $TARGETARCH RUN dotnet publish ./SampleWebAPI.csproj -c Release -o ./out --no-restore -a $TARGETARCH FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /App COPY --from=build-env /App/out . ENTRYPOINT ["dotnet", "SampleWebAPI.dll"]
【Docker, C#】Docker buildで「--platform linux/amd64」を指定した際にdotnet restoreでスタックしてしまう対策 - はなちるのマイノート
実際に動作するのか試してみましょう。
# ビルド $ docker build . -t sampe-webapi-sandbox --no-cache # 127.0.0.1:8080のようにしないとデフォルト設定だと0.0.0.0だと認識されてしまうので要注意!! $ docker run -it --rm -p 127.0.0.1:8080:8080 --name sampe-webapi-sandbox-container sampe-webapi-sandbox # 利用し終わったら後片付け $ docker image rm sampe-webapi-sandbox
GCPにデプロイする準備
実際にGCPにデプロイするためにterraform
を用意してあげましょう。
$ tree . └── terraform ├── cloud_run.tf ├── main.tf ├── outputs.tf ├── terraform.tf └── variables.tf
.tfを記述する
# terraform.tf terraform { required_providers { google = { source = "hashicorp/google" version = ">= 5.24.0" } local = { source = "hashicorp/local" version = "~> 2.0" } } # あらかじめGCSにバケットを作成しておく必要があるので注意 # Terraformの設定ファイルにおいて、GCSをバックエンドとして使用 backend "gcs" { bucket = "<your-bucket-name>" prefix = "terraform/state" } required_version = ">= 1.0.0" }
# variables.tf variable "project_id" { description = "gcp project id" default = "<your-project-id>" type = string } variable "region" { description = "gcp region" default = "asia-northeast1" type = string } variable "sample_webapi_docker_registry_url" { description = "artifact registry url (without '/' suffix)" default = "<your-artifact-registry-url>" type = string } variable "sample_webapi_docker_tag" { description = "artifact registry tag" type = string }
# main.tf provider "google" { project = var.project_id region = var.region } # Cloud Runを有効化 resource "google_project_service" "run_api" { service = "run.googleapis.com" disable_on_destroy = false } resource "google_service_account" "sample-webapi-worker" { account_id = "sample-webapi-worker" display_name = "Sample WebAPI Worker Account" } resource "google_service_account" "sample-webapi-invoker" { account_id = "sample-webapi-invoker" display_name = "Sample WebAPI Invoker Account" } resource "google_service_account_key" "sample-webapi-worker-service-account-key" { service_account_id = google_service_account.sample-webapi-worker.name } resource "google_service_account_key" "sample-webapi-invoker-service-account-key" { service_account_id = google_service_account.sample-webapi-invoker.name } resource "local_file" "sample-webapi-worker-service-account-key" { filename = "./output/secrets/sample-webapi-worker-service-account-key.json" content = base64decode(google_service_account_key.sample-webapi-worker-service-account-key.private_key) file_permission = "0600" directory_permission = "0755" } resource "local_file" "sample-webapi-invoker-service-account-key" { filename = "./output/secrets/sample-webapi-invoker-service-account-key.json" content = base64decode(google_service_account_key.sample-webapi-invoker-service-account-key.private_key) file_permission = "0600" directory_permission = "0755" } resource "google_service_account_iam_member" "sample-webapi-worker-service-account-iam-member" { service_account_id = google_service_account.sample-webapi-worker.name role = "roles/iam.serviceAccountUser" member = "serviceAccount:${google_service_account.sample-webapi-worker.email}" }
# cloud_run.tf resource "google_cloud_run_v2_service" "server" { name = "sample-webapi-server" location = var.region ingress = "INGRESS_TRAFFIC_ALL" deletion_protection = false template { containers { image = "${var.sample_webapi_docker_registry_url}:${var.sample_webapi_docker_tag}" } service_account = google_service_account.sample-webapi-worker.email } depends_on = [google_project_service.run_api] } resource "google_cloud_run_v2_service_iam_member" "sample-webapi-worker-policy" { location = google_cloud_run_v2_service.server.location project = google_cloud_run_v2_service.server.project name = google_cloud_run_v2_service.server.name role = "roles/run.invoker" member = "serviceAccount:${google_service_account.sample-webapi-worker.email}" } resource "google_cloud_run_v2_service_iam_member" "sample-webapi-invoker-policy" { location = google_cloud_run_v2_service.server.location project = google_cloud_run_v2_service.server.project name = google_cloud_run_v2_service.server.name role = "roles/run.invoker" member = "serviceAccount:${google_service_account.sample-webapi-invoker.email}" }
# outputs.tf output "service-url" { value = google_cloud_run_v2_service.server.uri description = "The URL on which the deployed service is available" } output "sample-webapi-worker-service-account" { description = "service account for Sample WebAPI worker" value = google_service_account.sample-webapi-worker.email } output "sample-webapi-invoker-service-account" { description = "service account for Sample WebAPI invoker" value = google_service_account.sample-webapi-invoker.email }
gcloud CLIを用いて下準備する
gcloud CLIを既にインストール済の前提で進めていきます。まずは認証を通しておきましょう。
# ログインする $ gcloud auth login $ gcloud auth application-default login # 現在のログイン情報を調べる $ gcloud auth list
Terraformのtfstate
をGCS上で管理するためにGCSバケットを作成しておきましょう。
# tf-state-sample-webapi-serverで定義する $ gcloud storage buckets create "gs://<your-bucket-name>" ` --project <your-gcp-project-id>` --uniform-bucket-level-access ` --location "asia-northeast1"
Artifact RegistoryにImageをアップロードする
次にArtifact Regisotry
にレポジトリを作成し、docker build
を行ってpush
します。
# レポジトリを作成していない場合のみ実行 $ gcloud artifacts repositories create <your-repository-name> --location=asia-northeast1 --repository-format=docker
# Docker用Artifact Registory認証 $ gcloud auth configure-docker asia-northeast1-docker.pkg.dev # docker buildの際に--platform linux/amd64 を付与してください $ docker build ./ -t asia-northeast1-docker.pkg.dev/<your-project-id>/<your-repository-name>/<your-image-name> --platform=linux/amd64 # push $ docker push asia-northeast1-docker.pkg.dev/<your-project-id>/<your-repository-name>/<your-image-name>
terraform applyを実行する
最後にterraform init
とterraform apply
を実行して、GCPにデプロイをしましょう。
$ terraform init $ terraform plan $ terraform apply
Cloud Runにアクセスする
正しくデプロイできていると、ローカルにサービスアカウントのキーが./output/secrets
に.json
として吐き出されているはずです。
それでログインをして、Cloud Runにアクセスしてみましょう。
$ gcloud auth activate-service-account --key-file="<path-to-your-service-account>" # ログインしているサービスアカウントを確認 $ gcloud auth list # リクエスト送信 $ Invoke-WebRequest ` -Uri <your-cloud-run-rul>/sample/hoge ` -Method Get ` -Headers @{ "Content-Type" = "application/json" "Authorization" = "Bearer $(gcloud auth print-identity-token)" } StatusCode : 200 StatusDescription : OK Content : "Hoge" RawContent : HTTP/1.1 200 OK Date: Thu, 20 Mar 2025 09:37:03 GMT Server: Google Server: Frontend ... Headers : {[Date, System.String[]], [Server, System.String[]], [Alt-Svc, System.String[]], [Transfer-Encoding, System.Stri ng[]]…} Images : {} InputFields : {} Links : {} RawContentLength : 6 RelationLink : {}
ちなみに認証無しで実行してもログインできません。
# Authorizationヘッダーなしの場合 $ Invoke-WebRequest ` -Uri <your-cloud-run-rul>/sample/hoge ` -Method Get ` -Headers @{ "Content-Type" = "application/json" } 403 Forbidden Error: Forbidden Your client does not have permission to get URL /sample/hoge from this server.
ActionsでCI/CDを準備するための小ネタ
terraformのセットアップ
hashicorp/setup-terraform
を用いてセットアップするとよいでしょう。
steps: - uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.1.7"
gcloud authを行う
google-github-actions/auth
を用いると良いでしょう。
jobs: job_id: # Any runner supporting Node 20 or newer runs-on: ubuntu-latest # Add "id-token" with the intended permissions. permissions: contents: 'read' id-token: 'write' steps: - uses: 'actions/checkout@v4' - uses: 'google-github-actions/auth@v2' with: project_id: 'my-project' workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
gcloudのセットアップを行う
google-github-actions/setup-gcloud
を用いるとよいでしょう。
- uses: 'google-github-actions/setup-gcloud@v2' with: version: '>= 416.0.0'