API's From Dev to Production - Part 11 - Pulumi - IAC
Tags: dotnet, docker, csharp, iac
Published: 2021-06-07 16:30:20

## Series Introduction
Welcome to **Part 11** of this blog series that will go from the most basic example of a .net 5 webapi in C#, and the journey from development to production with a [**shift-left**](https://dev.to/peteking/shift-left-engineering-4fpp) mindset. We will use **Azure**, **Docker**, **GitHub**, **GitHub Actions** for CI/C-Deployment and Infrastructure as Code using **Pulumi**.

In this post we will be looking at:

* Infrastructure as Code
  * Pulumi

---

## TL;DR
We got to grips with IaC and made use of our C# skills to build our own infrastructure - Go shift-left! Pulumi has been a joy to use, just so easy, the integration of GitHub Actions and Pulumi has equally been awesome.

We were able to preview our infrastructure changes from a Pull Request using our CI Workflow and achieved C-Deployment by deploying our infrastructure upon merging into `main` using our CD Workflow.

The Pulumi GitHub Bot works great and the Pulumi Console is simply invaluable. :nerd_face:

---

## GitHub Repository

{% github https://github.com/peteking/Samples.WeatherForecast-Part-11 no-readme %}

---

## Requirements
We will be picking-up where we left off in [Part 10](https://dev.to/peteking/api-s-from-dev-to-production-part-10-9j1), which means you’ll need the end-result from [GitHub Repo - Part 10](https://github.com/peteking/Samples.WeatherForecast-Part-10) to start with.

If you have followed this series all the way through, and I would encourage you to do so, but it isn't necessary if previous posts are knowledge to you already.

> Don't forget to ensure you have setup Code Climate Quality with your repository.

If you want to follow along from the code from Part 10, that is really ideal, I have everything in order and it should all work fine. If however, you want to skip to the end and run it all, you can too, but there there are some things you'll need to do. 

1. Clone the GitHub Repo (Part-11) into your own GitHub
1. Install/Obtain Access to Azure CLI
1. Install Pulumi CLI
1. ***Navigate*** → `./src/Samples.Weatherforecast.Infra`
1. Follow the instructions in this post about: Create a Service Principal
1. Follow the instructions in this post about: Securely store Azure Service Principal credentials
1. ***Execute*** → `pulumi preview`

This should create the Pulumi project and its stack.

---

## Introduction
This post is all about Infrastructure as Code, we want to be more autonomous to build and manage our own infrastructure instead of being *fully* dependant on others. Infrastructure as Code has been around a while and there are many solutions, each cloud provider such as Microsoft Azure, AWS, GCP and more all have their own native solutions, and of course the number one name for some time has been Hashicorp's Terraform (OSS, Cloud, Enterprise). However, they all have something in common... they are all in their own languages, from declarative to DSL's and JSON. Using DSL's can be cumbersome as is learning a new language; however, a purpose built language *could* have some advantages. Pulumi is one of those solutions that takes a different angle, the ability to get to your end goal of having your infrastructure written as code but in a language you use all the time.

In this post we will dive head-first into, Pulumi!

*Let's get started.* :metal:

---

## Microsoft Azure
Get access to Azure, this is kind of mandatory, we will be using Microsoft Azure - If however, you don't have an account, you can get one for FREE! :grin:

> [Sign-up](https://azure.microsoft.com/en-gb/free/) to Microsoft Azure.

---

## Install/Obtain access to Azure CLI

[Azure CLI on your OS](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-windows?tabs=azure-cli)

OR

[Azure CLI in Docker](https://docs.microsoft.com/en-us/cli/azure/run-azure-cli-docker)

OR

[Azure Cloud Shell in BASH](https://docs.microsoft.com/en-us/azure/cloud-shell/quickstart)

OR

If you already have [chocolately](https://chocolatey.org/install) installed, you can simply execute:
```console
choco install azure-cli
```

> As long as you can get access, that's all we need, so use whichever method you prefer.

---

## Pulumi
### What is Pulumi?
[Pulumi](https://www.pulumi.com) is a modern infrastructure as code platform built for engineers; developers and infrastructure.

The beauty of the product is you can build, deploy and manage your applications and infrastructure using languages and tools you are familiar with. Follow your existing engineering practices and guidelines, and take advantage of the same toolchain you use today.

This means if you *major* in C# for instance, you have all the power of the C# language and all your existing tools. It means you don't need to learn a bespoke language just to manage and provision your infrastructure = Learning curve is minimised.

When you deploy applications, everyone can work together, or you can choose to be isolated; maybe I'm trying something out or I have something very unique. You can work together and share infrastructure packages no matter what language it is written in, deploy with confidence by validating beforehand and automate it all!

If that wasn't enough, with Pulumi you can use policies to provide guardrails for teams, allowing them to feel empowered with autonomy (shift-left), and with the Pulumi Console you can see a full audit history. This is known as Policy as Code.

Pulumi is a breath of fresh air compared to other solutions, now I know what you're thinking, Terraform... Yes, I love it too, and I've worn that T-shirt, it's a good one, everyone has their go-to favourite T. However, I have a preference for Pulumi, its simplicity by using the same language I'm building API's in, whether that is C#, Go, JS/TS, Python etc. The Terraform T-shirt is now washed, ironed, and in the cupboard for a while. Luckily Pulumi can connect to Terraform also, so even if you have a centralised Platform team building foundational infrastructure, initial virtual networking topology, firewalls, SIEM etc., engineering teams can shift-left to build and manage their own components whilst using the outputs of Terraform. If for example I needed the output of a VNET from Terraform, Pulumi can get that and then I can provision my product using Pulumi as a pretty-well autonomous engineering team.

---

### Sign-up
Pulumi has a free tier and they say it's [free forever](https://www.pulumi.com/pricing?_ga=2.230476764.1724642515.1621867711-1194162379.1621867711#community-edition) (for individual use) :grin:

> [Sign-up](https://app.pulumi.com/signup) to Pulumi.

### Pulumi CLI
Pulumi is controlled primarily using its command line interface (CLI). It works with the Pulumi service to deploy changes to cloud apps and infrastructure. It also keeps a history of who updated what in your team and when; which is awesome! The CLI has been designed for productivity and ultrafast feedback, in addition to the ability to use it in CI/CD scenarios.

> Install [Pulumi CLI](https://www.pulumi.com/docs/get-started/install/).

(Again) If you already have [chocolately](https://chocolatey.org/install) installed, you can simply execute:
```console
choco install pulumi
```

---

## Requirements check
:white_check_mark: Everything we've done previously, i.e. [Part 10](https://dev.to/peteking/api-s-from-dev-to-production-part-10-9j1) etc.
:white_check_mark: Azure account
:white_check_mark: Azure CLI - Version used `v2.24.0`
:white_check_mark: Pulumi account (Console)
:white_check_mark: Pulumi CLI - Version used `v2.14.0`

---

## Let's get started with Pulumi!

Fire-up your console of choice and ***navigate*** to the `src` directory of the WeatherAPI.

***Create*** a folder called, `Samples.WeatherForecast.Infra` and ***navigate to it***
```console
mkdir Samples.WeatherForecast.Infra
cd Samples.WeatherForecast.Infra
```
**Inside** the `infra` directory - ***Create*** a new Pulumi Project.
```console
pulumi new azure-csharp
```

From here, you'll be prompted by the Pulumi CLI for some values.

* **project name:** `Samples.WeatherForecast.Infra`
* **project description:** `Infra for WeatherForecast`
* **stack name:** `dev`
* **azure-native:location: The Azure location to use:** `uksouth`

I've chosen `UK South` given my location, for you, please choose whatever suits your needs best.

> :information_source: TIP
> If you don't know what locations are available, below is a very quick & easy way to find out.

```console
az login

# Once login successful, execute
az account list-locations --output table
```

***Open VS Code*** by executing `code .`

You should see a nice, brand new Pulumi C# program! :smile:

If you also login to the Pulumi Console, you should see your project there too!

![blog-11-01](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1zz66ccji81739lq7fyp.png)

---

![blog-11-02](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/659qyc9ltmix7nhp55uy.png)

---

## Our API Infrastructure
![blog-post-11-arch](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x66q7hztfwukd7xq2pth.png)

---

### Steps
#### Step 1
***Open*** VS Code for the `WeatherForecast-API` if you haven't already, and ***Navigate*** → `/src/WeatherForecastApi-Infra/`

---

#### Step 2
***Rename*** `MyStack.cs` to `AzureStack.cs`

---

#### Step 3
Inside `AzureStack.cs`, ***replace*** all code with the following

```csharp
using Pulumi;
using Pulumi.AzureNative.Resources;
using Pulumi.AzureNative.Web;
using Pulumi.AzureNative.Web.Inputs;

class AzureStack : Stack
{
    public AzureStack()
    {
        var config = new Pulumi.Config();
        
        // Obtain our docker image from config
        var dockerImage = config.Require("docker-image");

        // Resource Group
        var rg = new ResourceGroup("rg-weatherforecastapi-uks-");

        // AppService Plan
        var appServicePlan = new AppServicePlan("appplan-weatherforecastapi-uks-", new AppServicePlanArgs() {
            ResourceGroupName = rg.Name,
            Location = rg.Location,
            Kind = "Linux",
            Reserved = true,
            Sku = new SkuDescriptionArgs
            {
                Name = "B1",
                Tier = "BASIC"
            },
        });

        // WebApp for Containers
        var app = new WebApp("app-weatherforecastapi-uks-", new WebAppArgs() 
        { 
            ResourceGroupName = rg.Name,
            ServerFarmId = appServicePlan.Id,
            SiteConfig = new SiteConfigArgs
            {
                AppSettings = new[] 
                { 
                    new NameValuePairArgs
                    {
                        Name = "WEBSITES_ENABLE_APP_SERVICE_STORAGE",
                        Value = "false"
                    },
                    new NameValuePairArgs
                    {
                        Name = "DOCKER_REGISTRY_SERVER_URL",
                        Value = "https://ghcr.io"
                    },
                    new NameValuePairArgs
                    {
                        Name = "WEBSITES_PORT",
                        Value = "8080" // Our custom image exposes port 8080. Adjust for your app as needed.
                    }
                },
                AlwaysOn = true,
                LinuxFxVersion = $"DOCKER|{dockerImage}"
            },
            HttpsOnly = true
        });

        this.Endpoint = Output.Format($"https://{app.DefaultHostName}/weatherforecast");
        this.HealthEndpoint = Output.Format($"https://{app.DefaultHostName}/health"); 
    }

    [Output] public Output<string> Endpoint { get; set; }

    [Output] public Output<string> HealthEndpoint { get; set; }
}
```

I appreciate there is a chunk of code there, so let's break it down just a little.

The first couple of lines is simply getting the config from Pulumi. I have chosen the `Require` method because I would rather it fails miserably if the config setting is not present; without it, we shouldn't continue because we'll have no container image. :laughing:

Up next is the resource group creation, note that we do not actually specify its *location* - The location comes from the config file - `Pulumi.dev.yaml`, it's value is from, `azure-native:location`. :wink:

> There are a bunch of other Azure native settings, for more information on these, please see, [Azure Native Configuration](https://www.pulumi.com/docs/intro/cloud-providers/azure/#configuration)

Following-on from there is the creation of the Azure AppService Plan, this is essentially our underlying Virtual Machine (VM), I have picked the smallest available WebApp for Containers (Linux) I can; so if you are paying for this I recommend when you test, to destroy afterwards - Still, if you didn't, it would be a under GBP£10 per month anyway.

Then we have the WebApp itself, this is the *AppService*, there are a few things we need to set here, mainly stating where the Docker registry is, in our case, GitHub Container Registry - Please note that the default is always DockerHub, you can easily point it to ACR (Azure Container Registry) also. We set `AlwaysOn` and `HttpsOnly` to `true` and we of course need to set the Docker Image, this is the, `LinuxFxVersion` property.

We need a way to output values, we want to output the address of the host as the end of the resource names will be dynamically generated. We set one for the `weatherforecast` and just to be helpful we set one for the `health` endpoint too.

Finally, the last 2 lines of the file are the declarations of the output properties, what denotes these as outputs is simply the 'Output' attribute.

---

#### Step 4
Modify the `Pulumi.dev.yaml` file.
***Replace*** its contents with the below:

```json
config:
  azure-native:location: uksouth
  Samples.WeatherForecast.Infra:docker-image: ghcr.io/peteking/samples-weatherforecast-part-11:3dae90cc47eff6861687444a6e91be89a65bed9f
```

We've added our *own config* here which is our docker image, therefore, it's configuration instead of C# code.

**This should be the location of your Docker image.**

> If you're using the GitHub Container Registry, it should be a simple swap.

---

#### Step 5
We have enough to give it a good manual "interactive" test with the Pulumi CLI, just to make sure everything is working as we expect it to.

**Open** your terminal and ***Navigate*** → `/src/WeatherForecastApi-Infra/`

**Execute**:
```shell
pulumi up
```

---

Here you can see it has executed `preview` as part of the `up` command, it highlights what it plans to do, in this case, it will **create 4 resources**.

![blog-11-04](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ey9f0l7t4eldc0nnuglw.png)
 
---

**Select** → *yes*

Once you've selected *yes*, Pulumi will go-ahead and *create* those resources, below is the response.

![blog-11-05](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1xrngxwxgi4z7u8ule5x.png)

> You can see the Pulumi `outputs` we created, one that links to the `weatherforecast` endpoint and one to the `health` endpoint; we've done this because the resources are dynamic, i.e. at the end of our Azure Resources is a random string to ensure uniqueness. If this doesn't suit your use case, you can ensure consistency instead - For more information, see [Pulumi - Autonaming](https://www.pulumi.com/docs/intro/concepts/resources/#autonaming)

---

We now have the opportunity to test our cloud infrastructure as code deployment and see if our API works!

You can use the links from the `output` to save typing.

The weather forecast gets returned! :tada:

![blog-11-08](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d60lmy5s49dfk4y4hzrd.png)

---

Even the `/health` endpoint is reporting good news! :fireworks:

![blog-11-09](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6h1xlps8wstfm3jozuax.png)

---

If you want to minimise your costs :heavy_dollar_sign:, you can of course *destroy* :boom: your resources, and with the powers of Pulumi that is also a breeze with the, `destory` command.

**Execute**:
```shell
pulumi destory
```

![blog-11-06](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pnkktp4f54fsoeqejaul.png)

---

**Select** → *yes*

Once you've selected *yes*, Pulumi will go-ahead and *destroy* those resources, below is the output. :thumbsup:

![blog-11-07](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qp4mtahj7a3ihpgbjlly.png)

> :information_source: 
> You'll notice the output at the bottom stating the *stack is still available* - All this means is you have all the details, history and everything about your stack in the Pulumi Console; which is all rather helpful, don't you think?

---

## GitHub Actions with Pulumi
All of what we have achieved is great, we have got everything we need infrastructure wise, built it all as code, we've even deployed and tested it for real. However, it's all rather interactive, we need to bake this into both our CI Workflow and our CD Workflow.

Our CI Workflow will execute `pulumi preview` and utilise the Pulumi GitHub Bot for instant feedback in the Pull Request; by doing this, the person reviewing the PR will be able to see what resources will be created/destroyed/updated - If anything is a miss then you have the opportunity to do something about it.

Our CD Workflow will execute `pulumi up` and deploy our resources to Azure; but only once all our tests pass, code coverage pass and our container scan is all good too.

In order to interact with Azure in GitHub Actions we will create a Service Principal.

---

### Azure Service Principal
#### What is it?
A Service Principal is an object in a single tenant or directory in Azure AD. It is the identity of an application instance, it defines access for the application and what resources it can access. Instead of using a *user* identity, we can create a *service* identity and manage its security and access.

---

### Create Azure Service Principal
#### Step 1
***Create*** an, *Azure Subscription* (if you don't already have one).

---

#### Step 2
Obtain the subscription id:
```shell
az account list #It's the 'id' field.
```

---

#### Step 3
Set the subscription:
```shell
az account set --subscription=<id> #<id> is the 'id' field from above
```

---

#### Step 4
```shell
az ad sp create-for-rbac --name sp-weather-api
```

You should see a result from Step 4 with the following values

```json
{
  "appId": "WWWWWWWW-WWWW-WWWW-WWWW-WWWWWWWWWWWW",
  "displayName": "ServicePrincipalName",
  "name": "http://ServicePrincipalName",
  "password": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
  "tenant": "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
}
```

> :information_source: TIP
> If you'd rather it in another format to save time later, you can use the `--sdk-auth` parameter.

---

### Securely store Azure Service Principal credentials
Now we have the Service Principal, we need to be able to store the details securely.

We can store these in GitHub Secrets which is totally fine right, but that means they are not linked to our Pulumi Stack, so another way is to store them inside Pulumi; which can also store *secrets*

From the output of our Service Principal creation command, the following values can be mapped like so:

|Service Principal Value|SDK Name|
|-----------------------|------------------|
|appId|clientId|
|password|clientSecret|
|tenant|tenantId|

> This is where the `--sdk-auth` parameter can help, it will automatically map these for you.

---

#### Pulumi Secrets :secret:
By storing these in Pulumi we get multi-user access for deployment which includes using it interactively; if we stored them in GitHub Secrets, we can only use them directly from the pipeline itself; there are pros and cons this of course - Choose what suites you best.

```shell
pulumi config set azure-native:clientId "WWWWWWWW-WWWW-WWWW-WWWW-WWWWWWWWWWWW"
```

---

```shell
pulumi config set azure-native:clientSecret "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" --secret
```
> :exclamation: Important
> Note the `--secret` at the end of the command, this is very important; otherwise, the value will be stored in plane text, whereas we want it to be encrypted and stored as a secret.

---

```shell
pulumi config set azure-native:tenantId "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY"
```

---

```shell
pulumi config set azure-native:subscriptionId "ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ"
```

---

Once you have executed these commands, you'll notice your `Pulumi.dev.yml` file has been updated accordingly.

---

### CI Workflow
Let's modify our **CI** Workflow `./github/workflows/ci-pull-request.yml`

***Add*** a new *job* at the bottom of the Workflow.

```yaml
infra:
  needs: ci
  runs-on: ubuntu-latest

  steps:
    - name: Checkout repo
      uses: actions/checkout@v2
      with:
        # Disabling shallow clone is recommended for improving relevancy of reporting
        fetch-depth: 0

    - name: Install Pulumi CLI
      uses: pulumi/action-install-pulumi-cli@v1.1.0

    - name: Set Pulumi config values
      working-directory: ./src/Samples.WeatherForecast.Infra
      env:
        PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
      run: |
        pulumi stack select dev
        pulumi config set Samples.WeatherForecast.Infra:docker-image ${{ env.image-name }}

    - name: Pulumi Preview
      uses: pulumi/actions@v3.1.0
      with:
        command: preview
        stack-name: dev
        work-dir: ./src/Samples.WeatherForecast.Infra
        github-token: ${{ secrets.GITHUB_TOKEN }}
      env:
        PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
```

---

#### Where do I get the Pulumi Personal Access Token from?
1. ***Navigate*** → Pulumi Console - https://app.pulumi.com
1. ***Navigate*** → Your profile (top-right)
1. ***Click*** → *Access Tokens* - Quick link here: https://app.pulumi.com/YOUR_PULUMI_USER_NAME/settings/tokens
1. ***Click*** → *Create token*
1. ***Copy*** the secret generated
1. Safely store the secret in GitHub Actions Secrets with the name of `PULUMI_ACCESS_TOKEN`

---

### Install the Pulumi GitHub Bot
Unless you want to dive into the Actions log to find out how the `pulumi preview` went, you better [install the Pulumi GitHub Bot](https://github.com/apps/pulumi)

> [Step by step guide on Pulumi Docs](https://www.pulumi.com/docs/guides/continuous-delivery/github-app/)

Once you have the bot installed, when you initiate a PR, you'll see something like the below:

![blog-11-10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hxxxdvt06kglr37bqoo.png)

---

Even better though, is you can view more details in the Pulumi Console like so:

![blog-11-11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m9t46n9s070zb8j5zxmh.png)

---

![blog-11-12](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tnlaymqflvzb7mm12w6k.png)

---

### CD Workflow
Let's modify our **CD** Workflow `./github/workflows/build-and-publish.yml`

***Add*** a new *job* at the bottom of the Workflow.

```yaml
infra:
  needs: build-and-push
  runs-on: ubuntu-latest

  steps:
    - name: Checkout repo
      uses: actions/checkout@v2
      with:
        # Disabling shallow clone is recommended for improving relevancy of reporting
        fetch-depth: 0

    - name: Install Pulumi CLI
      uses: pulumi/action-install-pulumi-cli@v1.1.0

    - name: Set Pulumi config values
      working-directory: ./src/Samples.WeatherForecast.Infra
      env:
        PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
      run: |
        pulumi stack select dev
        pulumi config set Samples.WeatherForecast.Infra:docker-image ${{ env.image-name }}

    - name: Pulumi Up
      uses: pulumi/actions@v3.1.0
      with:
        command: up
        stack-name: dev
        work-dir: ./src/Samples.WeatherForecast.Infra
        github-token: ${{ secrets.GITHUB_TOKEN }}
      env:
        PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
```

#### Commit your code
It's time to commit you code, but before you do, let's update our *branch protection rule* - Now we have an `infra` job in our Workflow which does a `pulumi preview`, we most likely want this to be mandatory and pass.

![blog-11-13](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vsavt2nvgbji5jl0z0g5.png)

--- 

Now that we have our branch protection rules updated and CD Workflow file completed, let's ensure it all works.

1. Raise your PR and see the CI Workflow kick-off.
1. Verify everything is fine by looking at the Pulumi GitHub Bot or the Pulumi Console.
1. Wait until all GitHub Status Checks pass and merge your PR into `main`.
1. Verify your Workflow is successful.
1. Either:
  - Go to the Pulumi Console and see your Activity.
  - OR see the output from the `infra` job in the CD Workflow.
1. Try the *output* links to verify your API is running.

---

I prefer the Pulumi Console, see below, you can see the merge of my branch and the creation of the infrastructure once it was merged into `main`.

![blog-11-14](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aijjlw68i93s17z0cw2a.png)

I was then able to validate that both the `weatherforecast` and `health` endpoints was functional. 

---

## What have we learned? :thinking:
We have learned all about IaC - **I**nfrastructure **a**s **C**ode, and utilised [Pulumi](htts://www.pulumi.com) to achieve this - We have leveraged our existing C# skills to shift-left on our infrastructure; therefore, we are now more autonomous.

We've been able to test (preview) and verify our infrastructure changes during Pull Requests as part of CI, and we have achieved C-Deployment by releasing our infrastructure in our CD Workflow.

Relief - That's it! :relieved: 

If you've made it this far, congratulations and thank you :handshake:, I know it's a long post and a lot to take in, but I hope you have found it worthwhile.

There is unfortunately or fortunately, depending on how you look at it, still a lot to do :smirk: So there will be a more coming soon! :zany_face:

---

**UPDATE AUGUST 27TH 2021**
The below GitHub Issue has now been resolved.

> :page_facing_up: Note
> Adding Pulumi to your repo in GitHub will break CodeQL, ~~there is an active GitHub issue open for this.~~
> {% github https://github.com/github/codeql-action/issues/544 %}

## Next up
Part 12 in this series will be about:

* **More** Infrastructure as Code with Pulumi
  * Environmental support (multiple Stacks)
  * Security
      * What *can* we do on the cheap in Azure?
      * What *should* we do in a more real scenario?
* GitHub Actions
  * Clean-up a little and add Environment capabilities

---

## More information
* https://www.azure.com
* https://dotnet.microsoft.com/
* https://www.github.com
* https://www.docker.com
* https://www.pulumi.com
* https://github.com/coverlet-coverage/
* https://www.codeclimate.com/quality