C#でCloud Run動かしてみた

## この記事の概要
C#でCloud Runを動かしてみました。

基本は[公式ガイド](https://cloud.google.com/run/docs/quickstarts/build-and-deploy/c-sharp?hl=ja)通りでしたが、説明が不十分だったり、自分がポカしたとこもあったので、備忘録もかねて簡単にまとめてみます。
## 環境
- ローカルマシン
    - windows10 pro

## 公式の手順(2021.05.29時点)ざっくり
手順変わるかもしれないので、一応現時点のものをざっくり記しておきますが、公式のほうが詳しいので公式を見た方がよいです。

#### 事前準備
- GCPの[プロジェクトセレクタページ](https://console.cloud.google.com/projectselector2/home/dashboard?hl=ja&_ga=2.197219500.2019670567.1622194066-1849852075.1616574603&_gac=1.258883192.1621841919.Cj0KCQjwna2FBhDPARIsACAEc_VanFTgwMi5dvVpFm1PnqmLyg60u5ixL6LkhDxXs7bSI4KNBHOgvmoaAjANEALw_wcB)で、Google Cloud プロジェクトを作っておく。
- Cloud プロジェクトに対して課金が有効になっていることを確認
- [Cloud SDKのインストール](https://cloud.google.com/sdk/docs/install?hl=ja)
    - 自分はdockerではなく、windowsに直接入れました

#### アプリの作成
- 適当なディレクトリで、サンプルのwebプロジェクトを作成する

```cmd
dotnet new web -o helloworld-csharp
```

- 以下の内容に書き換える

```csharp:Program.cs

﻿using System;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace helloworld_csharp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args)
        {
            string port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
            string url = String.Concat("http://0.0.0.0:", port);

            return Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>().UseUrls(url);
                });
        }
    }
}
```

```csharp:Startup.cs

﻿using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace helloworld_csharp
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    var target = Environment.GetEnvironmentVariable("TARGET") ?? "World";
                    await context.Response.WriteAsync($"Hello {target}!\n");
                });
            });
        }
    }
}
```

#### Container Registryにアップロード
- Dockerfileをルートに作成

```docker:Dockerfile
# Use Microsoft's official build .NET image.
# https://hub.docker.com/_/microsoft-dotnet-core-sdk/
FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build
WORKDIR /app

# Install production dependencies.
# Copy csproj and restore as distinct layers.
COPY *.csproj ./
RUN dotnet restore

# Copy local code to the container image.
COPY . ./
WORKDIR /app

# Build a release artifact.
RUN dotnet publish -c Release -o out

# Use Microsoft's official runtime .NET image.
# https://hub.docker.com/_/microsoft-dotnet-core-aspnet/
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine AS runtime
WORKDIR /app
COPY --from=build /app/out ./

# Run the web service on container startup.
ENTRYPOINT ["dotnet", "helloworld-csharp.dll"]
```

- gcloud用のignoreファイルである`.gcloudignore`ファイルを作成

```
# The .gcloudignore file excludes file from upload to Cloud Build.
# If this file is deleted, gcloud will default to .gitignore.
#
# https://cloud.google.com/cloud-build/docs/speeding-up-builds#gcloudignore
# https://cloud.google.com/sdk/gcloud/reference/topic/gcloudignore

**/obj/
**/bin/

# Exclude git history and configuration.
.git/
.gitignore
```
- `.dockerignore`ファイルを、`.gcloudignore` と同じ内容で作成

#### ビルドとデプロイ
- ビルド
```
gcloud builds submit --tag gcr.io/PROJECT-ID/helloworld
```

- デプロイ
```
gcloud run deploy --image gcr.io/PROJECT-ID/helloworld --platform managed
```

## ハマったところ
###  gcloud build submit が成功しない1

`xxxxxx@cloudbuild.gserviceaccount.com does not have storage.objects.get access` と言われて失敗します。これは[Cloud Buildのほうのドキュメント](https://cloud.google.com/build/docs/deploying-builds/deploy-cloud-run?hl=ja)で触れられていますが、`Cloud Storage` に書きこむための権限が必要です。service account permissonの一覧には出てないので、`IAM section`のところから、CloudBuildのサービスアカウント(xxxx@cloudbuild.gserviceaccount.com)に、`Storage オブジェクト作成者`のロールを付与してやる必要があります。

### gcloud build submit が成功しない2
これはただの僕のポカなのですが、gcloud buildを実行しているディレクトリがアプリルートではありませんでした。こんなミスやるのは僕以外にいないと思いますがご注意ください。この際は、`unable to prepare context: unable to evaluate symlinks in Dockerfile path` のようなエラーログが出ます。当たり前ですね。

### gcloud build submit が成功しない3
`The reference assemblies for .NETFramework,Version=v5.0 were not found.`と言われました。は？となりましたが、どうやらdotnet new コマンドで作成したプロジェクトはデフォルトでは.NET 5になるらしく、DockerfileのFROMで指定しているイメージとは異なってしまい、失敗しているようでした。

.csprojを書き換えて対応しました。

```
<TargetFramework>netcoreapp3.1</TargetFramework>
```


### デプロイ後に、ページにアクセスすると403になる
こうなった人は
```
Allow unauthenticated invocations to [helloworld] (y/N)? 
```
になぜかnを答えた人です。
はい、helloworldに認証もくそもないので、これはyを答えておきましょう。

## 終わりに
最近のクラウドにあまり触れてなかったですが、かなりすごいですね。
楽しい。