Most of my time at Console is spent testing developer tools, but I can only evaluate them properly if I understand the problems they're solving. I therefore block out every Friday to code.
Sometimes this is improving our internal tools, but most of the time it's building small side projects. Although I've been coding for almost two decades, this helps me stay up to date with the very latest shiny tech.
We don't have any servers at Console, but we do have a few internal tools that help us run the newsletter and website. One of these is a stats bot that posts our daily subscriber numbers into our Basecamp chat room. This is an important KPI but it can become unhealthy if we're constantly checking it. Once per morning is enough.
From Python to Rust to Go
The chatbot is not complicated. It runs once per day, querying the Mailchimp API and then posting the stats into Basecamp. Originally written in Python, earlier this year I rewrote it in Rust as a small project to learn the language.
Rust is great for systems level programming where efficiency and safety are important. However, it's not an easy language to use. I have recently been spending more time with Go because it is better for the web/API type sideprojects I've been building. So I decided to rewrite the chatbot into Go.
Serverless function vs containerized app
The Rust chatbot is deployed as an Azure Function because at the time, the best two serverless functions platforms were Azure Functions and AWS Lambda and wanted to play around with Azure.
Although they just announced the private preview of a new version of serverless functions, Google has also been rapidly developing a new serverless product called Google Cloud Run. This is similar to AWS Fargate (or Copilot, or Lightsail) where you write your code to run inside a container that is then built, run and deployed into a managed environment. The platform deals with the underlying resources, which you pay for on a usage basis.
Serverless functions are good for very small units of code, often an API endpoint or a task that executes on an event. Containers are more useful when you need to run in a specific environment, have a larger codebase and/or have several supporting services running inside the container.
Containerized apps are more maintainable
I have a Python Google Cloud Function that runs on a personal project. I wrote it two years ago and it continues to run every day. I recently wanted to change the configuration so it would use a different instance class with more memory, a change which required it to be redeployed. Nothing else changed, but on changing the config the deployment failed.
Lots can change in two years. It was written against Python 3.7 whereas the latest is now Python 3.10. Imported libraries have been updated and APIs it uses may be deprecated. Debugging the failed deployment would require me to dig into the code and platform to see what has changed since I last touched it.
This is the disadvantage of deploying un-containerized code - you have less control over the execution environment. The advantage is that you can focus more on the code, which is fine if you can keep everything up to date. But if it were deployed inside a container, that would narrow down the failure scenarios. If you can build the container, the application will run.
Picking the most sustainable cloud
For my use case of a chatbot that runs once per day, either a single function or a containerized app would work fine. However, Google also announced a new sustainability calculator for their cloud customers to get a report of carbon emissions associated with their resource consumption.
Microsoft and Google are engaged in a battle to see who is the most sustainable, but Google has been leading on most areas like being carbon neutral and in how they procure renewable energy. Azure has a similar calculator but it's only available to "Enterprise" customers.
As we are aiming for Console to be a net-zero sustainable business, this was enough motivation to migrate the serverless function from Azure to a new Google Cloud Run app. The challenge with cloud services is the lack of transparency and ability to report emissions, so moving the few resources we have to Google Cloud will help us accurately account for the environmental impact of our business operations.
I was able to use Google's region sustainability table to decide to deploy the Cloud Run app in Finland (europe-north1) because it has the highest carbon free energy % and lowest grid emissions.
Testing and monitoring Go in Google Cloud Run
I wrote the previous version of the Python chatbot very quickly and most of my time porting it to Rust was spent learning the language, so I didn't get around to implementing proper tests or logging. As I'm more familiar with Go and Google Cloud Run bundles several platform features like monitoring, logging, builds and deploys, I could spend more time doing things correctly.
This allowed me to implement things like structured logs and mocked API endpoints for Basecamp and Mailchimp using the excellent httpmock library. The code is longer than the Python or Rust versions, but it has a full set of unit tests, logging and tracing.
In the past, building Google Cloud Functions has been a very raw experience because they had no IDE integrations or local emulator. In contrast, Azure and AWS have extensive documentation and IDE plugins that make developing for their platforms much easier.
This has improved for Google Cloud Functions, but it is still not as well integrated into the IDE. Cloud Run benefits from a complete integration into VS Code using the Cloud Code extension. It also includes a local emulator of the entire environment, UI for streaming logs and deploying new versions, and it also connects you to the Secrets Manager product.
The previous versions of the chatbot all used secrets defined in environment variables and injected as part of the deploy process using secrets stored in GitHub Actions. This is better than nothing, but leaves those secrets vulnerable to certain types of exploits.
This new Go app has taken over from the Python and Rust versions in production. It's better tested, secrets are stored more securely, streams logs and traces to Google Cloud Monitoring and is deployed into the most sustainable region from a carbon and renewable energy perspective.
As it's inside a container, it should continue to run without issues for much longer than if it were a raw function. And I'm more up to date on the latest Google Cloud features, and how to write better Golang code (open source and on our GitHub account).
This was a fun project, and a great example of the type of internal tool that these full managed environments are really suited to.