GitLab, .NET Core, Kubernetes, and Pulumi - Part 4

GitLab, .NET Core, Kubernetes, and Pulumi - Part 4

This article is a part of the GitLab for .NET developer series.

In the previous article I described how to use my AutoDevOps library to replace the GitLab AutoDevOps deployment entirely with a Pulumi stack, so you can deploy your workloads and other resources without using Helm, or even deploy other type of resources outside of Kubernetes in the same stack.

Automation API

A couple of years ago, Pulumi announced their Automation API. Basically, it’s a way to do the same things as you’d normally do using Pulumi CLI, but from code. In particular, you can:

  • Create, refresh, update, and destroy stacks
  • Add and remove config values, as well as refresh the stack config
  • Get the stack output and use them as an input or configuration for another stack

One of the things that bothered me when I finished the first iteration, is the need to configure the stack from bash. As I know all the CI variables used by GitLab, I decided to try the Automation API to configure the stack from code, and use good defaults when the configuration is incomplete or unavailable. I also wanted to be able to deploy the stack from my machine by using command-line arguments for my new tool, which would override the necessary stack configuration as CI variables aren’t available on my local machine.

After some time, all those ideas got implemented in two new components:

  • Ubiquitous.AutoDevOps.Automation library and NuGet package
  • Ubiquitous.AutoDevOps command-line tool and container image

Automation library

The library has a set of abstractions that allow you to override the default deployment.

First is the deployment options:

public interface IDeploymentOptions {
    string Name    { get; }
    string Stack   { get; }
    string Tier    { get; }
    string Track   { get; }
    string Version { get; }
    bool   Preview { get; }

These are options set by the CLI tool from its arguments. If you don’t use the CLI tool, you can provide the options directly using the DefaultOptions class instance.

The stack configuration interface allows you to specify how the stack is configured, including the necessary plugins installation:

public interface IStackConfiguration<T> where T : IDeploymentOptions {
    Task InstallPlugins(Workspace workspace);
    Task ConfigureStack(WorkspaceStack appStack, AppSettings appSettings, T options);
    LocalWorkspaceOptions GetStackArgs(string name, string stack, string currentDir);

The DefaultConfiguration class implements this interface with DefaultOptions as a generic parameter. It sets the stack configuration using the values provided in the options, as well as GitLab CI environment variables.

The GetStackArgs function allows you to create or load the stack. You can use an inline (same app) stack, or an external stack in any language. For example, the DefaultConfiguration is able to create the inline stack using the DefaultStack class (see the previous article), or a local (external) stack if the tool can find it in the ./deploy path. It means that the CLI tool that uses the DefaultConfiguration would either choose the DefaultStack and configure it from GitLab CI variables, or use your own deployment project from the deploy directory, configure it accordingly, and execute an update.

Finally, the stack deployment interface:

public interface IStackDeployment<T> where T : IDeploymentOptions {
    Task<CommandResult> DeployStack(IStackConfiguration<T> configuration, T options);

The DefaultDeployment class does the following operations:

  • Gets the stack, either inline (default) or local (from the deploy) directory
  • Creates or select the stack by its name
  • Configures the stack using the options provided, as well as GitLab CI variables
  • Installs the necessary plugins (the default configuration only installs the Kubernetes plugin)
  • Refreshes the stack
  • Executes either a stack preview or update
  • One more thing, which I will talk about later

The library references the System.CommandLine packages and implements two commands: deploy and destroy. The deploy command is a generic class that can use any stack deployment, options and configuration that implement the aforementioned interfaces. You can create your own commands based on the Deploy<T, TConfig, TOptions> generic class to deeply customize its behaviour.

Using the library, you can create your own custom CLI tool to deploy the necessary resources using a custom stack (or stacks). However, you can also use the Ubiquitous.AutoDevOps command-line tool using the distributed container image.

CLI tool

The tool is the simplest part of the whole thing. It uses the Deploy generic class with all the default implementations (stack deployment, configuration, and options). So, the source code of the tool is literally this:

var command = new Root(
    new Deploy<DefaultDeployment<DefaultOptions>, DefaultConfiguration, DefaultOptions>(
        new DefaultDeployment<DefaultOptions>(),
        new DefaultConfiguration(),
    new Destroy()

Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
return await command.InvokeAsync(args);

You can get an idea about making your own tool that would use something else that those default classes, if necessary.

So, let’s see how the tool can be used in the GitLab CI.

The image is available on Docker Hub, so it can be used directly in a CI job:

.production: &production_template
  image: "ubiquitousas/autodevops:latest"
  stage: production
    - Ubiquitous.AutoDevOps
    - echo $CI_ENVIRONMENT_URL > environment_url.txt
    name: production
    paths: [environment_url.txt]
    when: always

At this moment, you get about 300 lines of bash and even more lines of Helm charts replaced by C# code that you can read and understand, you can freely customise and adapt to your use case.

Merge request annotation

The last feature I made for the automation-based deployment is the merge request annotation.

Pulumi integrates with GitLab, so you can see the branch, commit, and the contributor information in any stack operation, if it was performed from GitLab CI. Another great feature there is MR annotation. When Pulumi CLI runs a preview, it posts a comment to the merge request:

MR annotation

However, it only works with, which is a bummer. As far as I know, most of GitLab Enterprise customers use self-hosted deployments because the cloud version is based on group-per org model, and this model is not for everyone. The company I work for at the moment, as well as my own company, both have self-hosted GitLab instances.

As this nice feature is something I would like to have, I decided to implement it. It’s now available as part of the Automation library, and it’s the last thing that happens when the stack is previewed (I wrote “one more thing” there if you remember).

It won’t magically work because the deployment tool needs access to GitLab API. You’d need to go to the project settings, then select Access tokens and create a token that can write using the API. Then, copy the token and create a project CI variable called GITLAB_API_TOKEN, paste the token to the variable value. It can also be done on a group level. When that’s configured, the AutoDevOps tool will post a comment to the MR with the stack preview, similar to how Pulumi integration with GitLab works. As the tool itself uses the AutoDevOps.Automation library, the same would work for custom deployments using the automation library.

That’s it folks! Feel free to pop in the repository, try it out, and provide feedback.

See also