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)
- 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.
- It allows you to develop and deploy highly scalable containerized applications on a fully managed serverless platform.
- It allows you to write code your way using your favorite language
- It is built upon the container and Knative open standards, enabling portability of your applications
- 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.
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
- 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:
-
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:
- gcloud init
- gcloud auth login
- 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.
- Project Name
- Docker Image Name
- Artifactory Repository Name
- Service Name
- Project Name: WeatherForecastAPI-proj -- Google will generate a Project ID from this.
- Docker Image Name: weatherforecastapi-image
- Artifactory Repository Name: weatherforecastapi-repo
- Service Name: weatherforecastapi-service
3. Create a Google Cloud 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:
- I have commented out app.UseHttpsRedirection() - an explanation will be provided later in this blog.
- I have also commented out app.UseAuthorization — it is not the focus of this sample
- I am defaulting to show the swagger UI in all the environments (Dev, Staging, Production), by design, not just for the Dev environment.
- 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.
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:
In the above launchSettings.json, note the following:
- "launchUrl": "swagger", is removed; the functionailty is being handled in the code Program.cs.
- "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
- 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"
}
]
Cloud Run redirects all HTTP requests to HTTPS but terminates TLS before they reach your web service
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
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"]
Create the docker image
Using Powershell, switch to the application directory, and create the docker image for the project,
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 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
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.
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: