Guide to Publishing ASP.Net Core 6 WebAPI Apps on Google CloudRun (GCP)

What is Google Cloud Run?

The best way to describe Cloud Run is to quote Google (almost verbatim)

About Google Cloud Run
  1. Cloud Run is a managed compute platform that enables you to run containers that are invocable via requests or events. Cloud Run is serverless: it abstracts away all infrastructure management, so you can focus on what matters most — building great applications.
  2. It allows you to develop and deploy highly scalable containerized applications on a fully managed serverless platform.
  3. It allows you to write code your way using your favorite language
  4. It is built upon the container and Knative open standards, enabling portability of your applications
  5. Cloud Run automatically and horizontally scales out your container image to handle the received requests, then scales in when demand decreases. You only pay for the CPU, memory, and networking consumed during request handling.
Sources: (i) About Cloud Run (ii) Automatic Scaling and Costs

In this guide, we will need to perform tasks in the Visual Studio environment as well as in the Google Cloud platform.

Setting Up Your Environment

  1. Install Visual Studio 2022 If you do not already have Visual Studio 2022 installed, you can heard over to the link below, select your preferred version and install it:
  2. Setup Google Cloud Environment
    Follow the steps below to create and setup you Google Cloud Environment.
  • Obtain a Google Cloud account. If you do not already have a Google Cloud Account, you can heard over to the link below to obtain an account, Google may still have a $300 credit to get started:
  • Install Google Cloud SDK You will need the cloud SDK to work with Cloud Run. Follow the installation instructions at the link below:
  • In this example we will be using the gcloud-cli (installed by the SDK); there's no need to install the optional App Engine components (see step 5 of the instructions for installing the SDK). Since we will be using docker to push out images to the Google Cloud platform, we will need the gcloud-cli to authorize access and perform other cloud-based functions.
    Of most interest to us are the following gcloud commands used to initialize the cli and to set it to handle authorization requests:
    • gcloud init
    • gcloud auth login
    Follow the directions at the link below to initialize gcloud and have it handle cloud authorization for Docker.
    • Run gcloud auth configure-docker. Since we'll be using docker, configure docker to use gcloud to provide credentials for Google Cloud. Instructions are provided here:
    • Make a decison on which geographical location in which to host your application and containers. A full list of regions can be found at the link below. In this example, I am using us-east1.
NOTE: The Google Cloud SDK gets a lot of updates, so it is a good idea to check the referenced url's since the instructions may have been updated.
Suggested Naming Convention
Before going any further, I suggest you come up with a naming convention for the various components we'll be using in the Cloud Run platform.
  1. Project Name
  2. Docker Image Name
  3. Artifactory Repository Name
  4. Service Name
If you (or your team) is working on several projects you'll find this to be very helpful. I am naming the VS WebAPI project WeatherForecastAPI. The names that I chose for this guide are:
  1. Project Name: WeatherForecastAPI-proj  -- Google will generate a Project ID from this.
  2. Docker Image Name: weatherforecastapi-image
  3. Artifactory Repository Name: weatherforecastapi-repo
  4. Service Name: weatherforecastapi-service

3. Create a Google Cloud Project

If you are not already there, navigate to the Google Cloud Console and create a new project.

The project name is up to you, but if you want to follow along with this blog, we are naming the project WeatherForecastAPI-proj

Once the project is created, note the Project ID, you will need it to interact with the platform.  Note that the ID is generated automatically, and may be different from what you expect.

4. Create an Artifact Repository

The next step is to create a repository to hold your containers (images).  On the console, search for and select Artifact Registry.

If prompted, enable the API.

The next step is to add a repository to hold your container images.  In this sample we will be hosting a docker image.

Provide the repo details as shown below and click CREATE.

If you run into an error like shown below, ignore it; it takes a few minutes for the repository to be propagated.

4 Create the Visual Studio 2022 Project

Start Visual Studio and create an ASP.Net Core WebAPI app. In this example, I am naming my project WeatherForecastAPI

When you click "Next" be sure to provide the additional information as shown in the image below.

I am using controllers, by personal preference. If you have any major arguments about my choice, see Ben Foster's insightful article ASP.NET 6.0 Minimal APIs - Why should you care?

After Visual Studio completes the setup, run the app (Ctrl-F5) to make sure it works.

For our purposes we are going to make some changes.

1) Replace the code in Program.cs with the following:


var builder = WebApplication.CreateBuilder(args);

int hostPort = Int16.Parse(builder.Configuration["Host:Port"]);

builder.WebHost.UseKestrel(options =>
{
    options.Listen(System.Net.IPAddress.Any, hostPort);

});

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseRouting(); 

app.UseSwagger();
app.UseSwaggerUI();

app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", async context =>
    {
         context.Response.Redirect("swagger");
    });
});


//app.UseHttpsRedirection();

//app.UseAuthorization();

app.MapControllers();

app.Run();

In the above code, note the following:

  1. I have commented out app.UseHttpsRedirection() - an explanation will be provided later in this blog.
  2. I have also commented out app.UseAuthorization — it is not the focus of this sample
  3. I am defaulting to show the swagger UI in all the environments (Dev, Staging, Production), by design, not just for the Dev environment.
  4. By default, Kestrel binds to ports 5000 and 5001. In the above code, I have generalized the code (lines 3-9), to use the port specified in appsettings.json. This allows you to easily change the port to any acceptable value. We will need this value when we configure the service in Google CloudRun.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "Host": {
        "Port": 5175
    },
    "AllowedHosts": "*"
}
appsettings.json

I have also made some changes to the WeatherForecastController to make it a little more interesting.


using Microsoft.AspNetCore.Mvc;

namespace WebApiGCloud.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger _logger;

        public WeatherForecastController(ILogger logger)
        {
            _logger = logger;
        }
        private IEnumerable GetWeatherData()
        {
            return Enumerable.Range(1, 30).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = Random.Shared.Next(-20, 55),
                Summary = Summaries[Random.Shared.Next(Summaries.Length)]
            }).ToArray();
        }
        [HttpGet("/weatherforecast")]
        public IEnumerable Get()
        {
            return GetWeatherData();
        }
        [HttpGet("/weatherforecast/{code:alpha:length(3,10)}")]
        public IEnumerable Get(string code)
        {
            return GetWeatherData().ToArray().Where(p => p.Summary.ToLower() == code.ToLower());
        }
    }
}

Before testing locally, make sure your profiles section in launchSettings.json is similar to this:

    "profiles": {
        "WeatherForecastAPI": {
            "commandName": "Project",
            "launchBrowser": true,
            "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
            },
            "applicationUrl": "http://localhost:5175",
            "dotnetRunMessages": true
        },
profile - launchSettings.json

In the above launchSettings.json, note the following:

  1. "launchUrl": "swagger", is removed; the functionailty is being handled in the code Program.cs.
  2. "applicationUrl" is changed to use only http, with the Kestrel port identical to the Host:Port value in appSettings.json

You can run the project  (Ctrl+F5) to make sure it is working.  You should see the Swagger UI as shown below.

You may run into an error like this when you try to run the application.

If you  do run into this error, check to make sure your application profile is set to WeatherForecastAPI as shown below

You can test the API by navigating to either:
  • http://localhost:5175/weatherforecast
    OR
  • http://localhost:5175/weatherforecast/hot

The first link will return a json that looks like:


[
    {
        "date": "2022-01-20T11:42:26.4073824-05:00",
        "temperatureC": -18,
        "temperatureF": 0,
        "summary": "Hot"
    },
    {
        "date": "2022-01-21T11:42:26.4086175-05:00",
        "temperatureC": 53,
        "temperatureF": 127,
        "summary": "Cool"
    },
    {
        "date": "2022-01-22T11:42:26.4086275-05:00",
        "temperatureC": 35,
        "temperatureF": 94,
        "summary": "Warm"
    },
    {
        "date": "2022-01-23T11:42:26.408628-05:00",
        "temperatureC": -3,
        "temperatureF": 27,
        "summary": "Scorching"
    },
    {
        "date": "2022-01-24T11:42:26.4086283-05:00",
        "temperatureC": 33,
        "temperatureF": 91,
        "summary": "Sweltering"
    },
    {
        "date": "2022-01-25T11:42:26.4086287-05:00",
        "temperatureC": -15,
        "temperatureF": 6,
        "summary": "Mild"
    },
    {
        "date": "2022-01-26T11:42:26.40863-05:00",
        "temperatureC": 30,
        "temperatureF": 85,
        "summary": "Balmy"
    },
    {
        "date": "2022-01-27T11:42:26.4086304-05:00",
        "temperatureC": -14,
        "temperatureF": 7,
        "summary": "Chilly"
    },
    {
        "date": "2022-01-28T11:42:26.4086314-05:00",
        "temperatureC": -4,
        "temperatureF": 25,
        "summary": "Freezing"
    },
    {
        "date": "2022-01-29T11:42:26.4086318-05:00",
        "temperatureC": -17,
        "temperatureF": 2,
        "summary": "Cool"
    },
    {
        "date": "2022-01-30T11:42:26.4086329-05:00",
        "temperatureC": -11,
        "temperatureF": 13,
        "summary": "Scorching"
    },
    {
        "date": "2022-01-31T11:42:26.4086332-05:00",
        "temperatureC": -15,
        "temperatureF": 6,
        "summary": "Chilly"
    },
    {
        "date": "2022-02-01T11:42:26.4086335-05:00",
        "temperatureC": -17,
        "temperatureF": 2,
        "summary": "Scorching"
    },
    {
        "date": "2022-02-02T11:42:26.4086339-05:00",
        "temperatureC": 9,
        "temperatureF": 48,
        "summary": "Warm"
    },
    {
        "date": "2022-02-03T11:42:26.4086342-05:00",
        "temperatureC": -11,
        "temperatureF": 13,
        "summary": "Freezing"
    }
]

The second will return results filtered for dates where the summary is Hot:


[
    {
        "date": "2022-01-20T12:02:53.7801378-05:00",
        "temperatureC": -4,
        "temperatureF": 25,
        "summary": "Hot"
    },
    {
        "date": "2022-01-23T12:02:53.7801469-05:00",
        "temperatureC": 37,
        "temperatureF": 98,
        "summary": "Hot"
    },
    {
        "date": "2022-02-14T12:02:53.7801519-05:00",
        "temperatureC": 25,
        "temperatureF": 76,
        "summary": "Hot"
    },
    {
        "date": "2022-02-17T12:02:53.7801526-05:00",
        "temperatureC": 7,
        "temperatureF": 44,
        "summary": "Hot"
    }
]


Why not Use Https Redirection in the Net Core App?
As was stated earlier, we are not using app.UseHttpsRedirection in the pipeline. This is because
Cloud Run redirects all HTTP requests to HTTPS but terminates TLS before they reach your web service
source: https://cloud.google.com/run/docs/triggering/https-request
If you use it (UseHttpsRedirection), Kestrel will re-redirect to https. This will create a circularity resulting in a "Too many Redirects" error.

Preparing the App for Google CloudRun

The steps are:

  • Publish the app
  • Create a Docker image of the app
  • Tag the image
  • Push the image to Cloud Run

Publish The App


For this step, we'll use the usual Visual Studio publish steps.  I am publishing to a output folder in the application directory (C:\Sample\WeatherForecastAPI\output) and setting the target framework to linux-x64

Publish Settings
Why Target Linux-x64 Runtime?
Well.... you can target one of the windows variants instead. Just be aware that
When you create an on-demand Windows Server instance on Compute Engine, Google includes the cost of the license with the cost of the instance.
Source: Microsoft Licensing FAQ

Update your Dockerfile


FROM mcr.microsoft.com/dotnet/sdk:6.0
COPY ./output /publish
WORKDIR /publish

EXPOSE 5175
ENTRYPOINT ["dotnet", "WeatherForecastAPI.dll"]
Note on the Dockerfile
I have simplified the auto-generated Dockerfile by letting Visual Studio do the build. Docker then just picks up the VS build output and creates the image from there.

Create the docker image

Using Powershell,  switch to the application directory, and create the docker image for the project,


cd C:\Sample\WeatherForecastAPI

docker build -t weatherforecastapi-image  -f Dockerfile .
Use Powershell

Tag the image


docker tag weatherforecastapi-image us-east1-docker.pkg.dev/weatherforecastapi-proj/weatherforecastapi-repo/weatherforecastapi-image:v1.0

You can tag the image version according to your needs or just use latest.

Push the Image To Artifact Registry


docker push us-east1-docker.pkg.dev/weatherforecastapi-proj/weatherforecastapi-repo/weatherforecastapi-image:v1.0
Docker Push Syntax
The syntax of the push command is:
docker push REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY_NAME/TAGGED_IMAGE_NAME
For our purposes these are
  • REGION: us-east1
  • PROJECT_ID: weatherforecastapi-proj
    Unless they are the same, make sure it's the project_ID and not the project_NAME.
  • REPOSITORY_NAME: weatherforecastapi-repo
  • TAGGED_IMAGE_NAME: weatherforecastapi-image:v1.0
Your Artifact Registry Docker images are stored in specific regions
In this sample, we are using us-east1
A full list of regionscan be found at:
Geography and Regions

Create a Service

Once the image has been pushed to the Artifact Registry, go to the cloud console and select your Project - -> Cloud Run - -> Create Service

On the Create Service screen enter the values as indicated in the sample below.  

To specify the container image for the service, click the SELECT - -> ARTIFACT REGISTRY, and then select the image version you pushed to the repo.

Enter the rest of the service parameters as shown in the sample below.

Note:  In the example, I am allowing unauthenticated traffic.  If authentication is required by your app, you'll set that up in the Kestrel pipeline in Program.cs.

Once you click the CREATE button, the process for creating and configuring the service will start.

If you are following along with this example, you will receive an error similar to this:

As of this writing, you'll always get this error unless you set your Kestrel port to 8080. To fix this error edit and re-deploy the service; changing the container port to 5175, the Kestrel listening port we used in Program.cs.

Hey! Google
....... if you are listening, please have the Cloud Run team include the Container Port in the Create Service UI. This will obviate this error and the manual correction step. As of the time of this writing the Container Port entry shows up only in the edit step; not the create step.
You can also edit the YAML file to specify the correct port; if you do this, you'll also have to update the revision property. Unfortunately, as of the date of this writing, the UI for edititing the YAML file is a little kludgy; you can't scroll through it, you can't "Select All", you can't copy/paste -- even in Chrome; you can't download/edit and upload a replacement. Hopefully, the Cloud Run team will fix this.

In any case, this is not a show-stopper; you can always use the EDIT AND DEPLOY NEW REVISION button at the top of the screen.

Service Deployed


Once you update the container port and re-deploy, the service deployment should be successful.  You'll see a screen similar to this:

Click on the service to take you to the details page

About the URL generated by Cloud Run


Note that Cloud Run generates a URL for the service automatically.
Cloud Run also generates a Certificate for the service and ensures only https access.  The certificate expires in a little less than 90 days. Since Cloud Run is fully managed, the Certificate will be automatically renewed prior to expiration.

Clicking on the generated URL takes you to the Swagger UI, shown earlier when the app was tested in the development environment.

You can test the service through the Swagger UI, or by appending the URL fragments (/weatherforecast, /weatherforecast/hot), as shown earlier.

Note that the redirection to https is handled by the platform, and occurs before the Kestrel pipeline.

Using a Custom Domain

In case you prefer to use your own domain instead of the generated one, click on Manage Custom Domains, and then Add Mappings as shown below.

Follow the prompts to select the service to map and the custom domain to use.  

In this example, I have elected to map the service to a new subdomain of this blog site (alumdb.com).  After clicking the continue button Cloud Run will provide you information needed to update your DNS records as shown below.

Once the service has been updated to use the custom domain the service screen will look like this:

You can now access the service using the custom domain as shown in the examples below:

  • weather.alumdb.com
  • weather.alumdb.com/weatherforecast
  • weather.alumdb.com/weatherforecast/hot

Note: These links are publicly available, feel free to try them out.

Note:  Even after you setup your custom domain, the platform-generated service url is still available and functional.  If you click the information icon (Show Info on Service URL's) you'll see both urls for the service:

Summary
This post has provided a step-by-step guide to implementing and deploying an ASP.NET Core 6 WebAPI to Google Cloud Run. If you are new to Cloud Run, I hope this helps you over the rough edges.