Do Cloud Better.

How to make dotnetcore work with GCP CloudRun

Posted by Dan Taylor on Apr 11, 2019 9:10:56 AM
Dan Taylor

This is what the .NET community needed to fully embrace GCP

Yesterday at Google Next, they announced something I've been wanting for a long time. As a google partner, sometimes I get to hear about things when they're in alpha and this was one of them. Serverless containers is the ultimate awesome in my mind, especially for webapps and APIs, because everyone knows the biggest headache for containers is orchestration. Well this announcement kinda abrogates the need for orchestration at all. If these things auto-scale then I don't have to manage a K8s cluster at all. I mean, yeah for internal services there is some significant networking work to do, but DUDE. Auto-scaling CONTAINERS that are SERVERLESS. Can life better? I submit that it CANNOT!

 

Dotnet not supported out of the box, but good news

They demonstrated Cloud Run using Node. During the Q&A, the question was asked, "What about dotnet?"

The answer was something to the effect of, "Gmanager, the environment that Cloud Run uses, is a hardened OS that is linux-like, but not linux-like enough for dotnetcore."

 

I'm confused. If this is really a container runtime platform it shouldn't matter what the internal runtime is.

I decided to try it out anyway. I figured:

  1. It will be good to at least validate what the presenters talked about for dotnet runtimes
  2. Maybe they're wrong? It's a CONTAINER. This SHOULD work.

So, I hopped on GCP cloud shell and created a service in the brand-new Cloud Run beta service. I created a new dotnet webapi application on the commandline using the default command dotnet new webapi --name webapi, put it in an aspnetcore image and pushed it to GCR. I referenced it in the Cloud Run service, allowed unauthorized access and clicked Create! service-setup

Spinning spinning spinning. After about 5 minutes it errors out. Presenters are vindicated! Or are they?

What is the problem? I'm curious what the error actually is. noworky-timeout

If you click on the LOGS tab, you can see that the container is starting up just fine, and it begins listening on port 80, just as you'd expect. However, if you scroll, you can see Container terminated by the container manager on signal 9.. Why?

terminated-by-signal-9 

 

At the top of the logs you can also see the message: Container failed to start. Failed to start and then listen on the port defined by the PORT environment variable. Logs for this revision might contain more information

Well, I know the CONTAINER works because it's a CONTAINER and it works. So it has to be something that the Cloud Run platform is looking for that I'm not providing.

If you go back to the service creation page, you can see a message that says Must be stateless and listen for HTTP requests on $PORT.

 

The actual problem

Ok. So the port has to be dynamically assigned. OHHHHHHH. Now I get it. Aspnetcore default config listens on port 80, and since I didn't provide a path, I'm assuming that the Cloud Run's health check is going to be checking on root.

So, I just have to get Aspnetcore to listen on a dynamic port and get it to respond on the root. Luckily, this is easy with Aspnetcore and Kestrel.

 

The fix

In Program.cs, just employ Kestrel, and configure it to listen on the port that you get from the environment variable PORT

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
    var port = Environment.GetEnvironmentVariable("PORT");
    //debugging statement in case the port didn't get passed correctly
    Console.WriteLine($"env PORT is {port ?? ("not found")}");

    return WebHost.CreateDefaultBuilder(args)
        .UseStartup<Startup>()
        .UseKestrel()
        .ConfigureKestrel((context, options) =>
        {
            options.Listen(IPAddress.IPv6Any, Convert.ToInt32(port));
        });
}

That takes care of the dynamic port, now we just need to respond on root.

That's easy too. In the Values controller, just change the route to be root

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace webapi.Controllers
{
    [Route("/")]//<-- Right here; respond on root
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "food", "mood" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

and add a default route in Startup.cs

...
app.UseMvc(routes => 
{
    routes.MapRoute("default", "{controller=Values}/{action=Get}/{id?}");
});
...

Ok now build and push and . . . the health check works! Yayy! workin

 

Wrapping it up

Obviously, this is a toy example. In a production-quality container I wouldn't ever hard code all my routes, and I wouldn't have the Values controller be the one that responds on root. BUT, the point is that dotnet DOES work on Cloud Run and that is great news for all of us that agree that dotnet is pretty cool.

Tags: GCP, .Net Core, CloudRun