Azure

IaC with Containers

In a previous article about IaC co-located with your application I discussed having application specific infrastructure with your code and then in following articles (IaC ARM templates, IaC Ansible and IaC Terraform) I discussed deploying IaC using various methods with Azure Pipelines. What I haven’t discussed is the development environment used in order to test out my infrastructure code, and that is what this article is about.

Lately I have been building various infrastructure using ARM templates, Ansible and Terraform separately and sometimes together to deploy to Azure. And I have lost track of what was installed in order to create and run my infrastructure code. For creating templates, playbooks, etc. I use Visual Studio Code. There are extensions for ARM templates, Ansible and Terraform which provide great help in creating infrastructure code.

For Ansible and Terraform I was using the WSL (Windows Subsystem for Linux) and running a script to install them into that environment and then using PowerShell Core and the Azure CLI for ARM templates.

I thought this was a good setup and provided me with everything I needed but what if I want to share this environment with my team. I could simply put together a list of commands to install all of the tools that I had and maybe create a page in our wiki, that would be a start, but then what about versions of tools and different operating systems and the fact it can be time consuming getting it all set up.

To help solve this I decided that creating a container using Docker would provide a way of consistently building an IaC environment. An environment I can share with others and I could use in a CI/CD pipeline. I decided that the editor is up to the developer so I am not including Visual Studio Code but definitely recommend it. (if you are interested Microsoft provide a guide to installing VS Code into containers https://code.visualstudio.com/docs/remote/containers).

I can imagine that most use only one of the tools for their infrastructure code, PowerShell or Azure CLI or either Ansible or Terraform even though they can work well together. RedHat and HashiCorp presented the concept of using Ansible and Terraform together in this video, it is very interesting to see how the strengths of each can be used together rather than a pro or con for choosing one or the other.

So on to creating some docker files, Microsoft have official images on Docker Hub for Powershell and Azure CLI, Hashicorp have an official image for Terraform. I still prefer to create my own image for Terraform though so I can add other tools to the image.

Note: All dockerfiles shown here are available from my GitHub repository.

Create Image

Terraform dockerfile

ARG IMAGE_VERSION=latest
ARG IMAGE_REPO=alpine

FROM ${IMAGE_REPO}:${IMAGE_VERSION} AS installer-env
ARG TERRAFORM_VERSION=0.12.26
ARG TERRAFORM_PACKAGE=terraform_${TERRAFORM_VERSION}_linux_amd64.zip
ARG TERRAFORM_URL=https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/${TERRAFORM_PACKAGE}

# Install packages to get terraform
RUN apk upgrade --update && \
    apk add --no-cache wget
RUN wget --quiet ${TERRAFORM_URL} && \
    unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    mv terraform /usr/bin

# New stage to remove tar.gz layers from the final image
FROM ${IMAGE_REPO}:${IMAGE_VERSION}

# Copy only the files we need from the previous stage
COPY --from=installer-env ["/usr/bin/terraform", "/usr/bin/terraform"]

# Install additional packages
RUN apk upgrade --update && \
    apk add --no-cache bash 

CMD tail -f /dev/null

Ansible dockerfile

ARG IMAGE_VERSION=latest
ARG IMAGE_REPO=alpine

FROM ${IMAGE_REPO}:${IMAGE_VERSION}

ENV ANSIBLE_VERSION=2.9.9
ENV ALPINE_ANSIBLE_VERSION=2.9.9-r0
ENV WHEEL_VERSION=0.30.0
ENV APK_ADD="bash py3-pip ansible=${ALPINE_ANSIBLE_VERSION}"

# Install core libs
RUN apk upgrade --update && \
    apk add --no-cache ${APK_ADD}

# Install Ansible 
RUN python3 -m pip install --upgrade pip && \
    pip install wheel==${WHEEL_VERSION} && \
    pip install ansible[azure]==${ANSIBLE_VERSION} mitogen && \
    pip install netaddr xmltodict openshift

CMD tail -f /dev/null

As I mentioned at the beginning, I’ve be using a mix of the tools and could do with an image with multiple tools. I ended up with the following dockerfile.

ARG IMAGE_VERSION=latest
ARG IMAGE_REPO=alpine

FROM ${IMAGE_REPO}:${IMAGE_VERSION} AS installer-env

ARG TERRAFORM_VERSION=0.12.26
ARG TERRAFORM_PACKAGE=terraform_${TERRAFORM_VERSION}_linux_amd64.zip
ARG TERRAFORM_URL=https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/${TERRAFORM_PACKAGE}

ARG POWERSHEL_VERSION=7.0.1
ARG POWERSHELL_PACKAGE=powershell-${POWERSHEL_VERSION}-linux-alpine-x64.tar.gz
ARG POWERSHLL_DOWNLOAD_PACKAGE=powershell.tar.gz
ARG POWERSHELL_URL=https://github.com/PowerShell/PowerShell/releases/download/v${POWERSHEL_VERSION}/${POWERSHELL_PACKAGE}

# Install packages
RUN apk upgrade --update && \
    apk add --no-cache bash wget curl python3 libffi openssl

# Get Terraform
RUN wget --quiet ${TERRAFORM_URL} && \
    unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    mv terraform /usr/bin

# Get PowerShell Core
RUN curl -L ${POWERSHELL_URL} -o /tmp/${POWERSHLL_DOWNLOAD_PACKAGE}&& \
    mkdir -p /opt/microsoft/powershell/7 && \
    tar zxf /tmp/${POWERSHLL_DOWNLOAD_PACKAGE} -C /opt/microsoft/powershell/7 && \
    chmod +x /opt/microsoft/powershell/7/pwsh

# New stage to remove tar.gz layers from the final image
FROM ${IMAGE_REPO}:${IMAGE_VERSION}

# Copy only the files we need from the previous stage
COPY --from=installer-env ["/usr/bin/terraform", "/usr/bin/terraform"]
COPY --from=installer-env ["/opt/microsoft/powershell/7", "/opt/microsoft/powershell/7"]
RUN ln -s /opt/microsoft/powershell/7/pwsh /usr/bin/pwsh

ENV ANSIBLE_VERSION=2.9.9
ENV ALPINE_ANSIBLE_VERSION=2.9.9-r0
ENV WHEEL_VERSION=0.30.0
ENV APK_ADD="bash py3-pip ansible=${ALPINE_ANSIBLE_VERSION}"
ENV APK_POWERSHELL="ca-certificates less ncurses-terminfo-base krb5-libs libgcc libintl libssl1.1 libstdc++ tzdata userspace-rcu zlib icu-libs"

# Install core packages
RUN apk upgrade --update && \
    apk add --no-cache ${APK_ADD} ${APK_POWERSHELL} && \
    apk -X https://dl-cdn.alpinelinux.org/alpine/edge/main add --no-cache lttng-ust

# Install Ansible and other packages
RUN python3 -m pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir wheel==${WHEEL_VERSION} && \
    pip install --no-cache-dir ansible[azure]==${ANSIBLE_VERSION} mitogen && \
    pip install --no-cache-dir netaddr xmltodict openshift

CMD tail -f /dev/null

Now I have dockerfiles, I need to create the image. Docker build is the command that I need.

docker build -f <dockerfile to use> -t <image name>
e.g.
docker build -f terraform.dockerfile -t iac_terraform_image

Run Image

Once I have the image, I need to test it out. As I installed bash as part of my image I can run an interactive command that will provide me with the bash shell.

e.g.
docker run -it --entrypoint=/bin/bash iac_terraform_image

from the shell I can then run commands like terraform –version

So now I have an image running in a container, what about my infrastructure I want to run? I could update the image to include git and pull my source code into the container or I could mount a volume to the container pointing to my source code on the host machine. I will use a volume for this example.

docker run -it --entrypoint=/bin/bash --volume <my source code>:/<name of the mount> <image name>
e.g.
docker run -it --entrypoint=/bin/bash --volumeĀ c:\users\<your profile>\Source:/mycode iac_terraform_image

As I want to deploy infrastructure to Azure I need to define some credentials, for this example I will add them as environment variables in a separate local file. Note: this file should not be added to source control.

The file contents looks like this for Terraform:

ARM_SUBSCRIPTION_ID=<subscription id>
ARM_TENANT_ID=<tenant id>
ARM_CLIENT_ID=<app id>
ARM_CLIENT_SECRET=<client secret>

The file contents looks like this for Ansible:

AZURE_SUBSCRIPTION_ID=<subscription id>
AZURE_TENANT=<tenant id>
AZURE_CLIENT_ID=<app id>
AZURE_SECRET=<client secret>

Combining the Terraform and Ansible environment values in the file would be used for the image that uses both of them.

I’ve called the file docker_env.txt and can now add that to the docker run command to include those environment variables.

e.g.
docker run -it --entrypoint=/bin/bash --volume c:\users\<your profile>\Source:/mycode --env-file docker_env.txt iac_terraform_image

Share Image

I now have an image, attached my source code, added my environment and I can run commands. Next step is to be able to share this image with others, I can upload it to Docker Hub (public) or I can use some other container registry like Azure Container Registry (private).  I will use the Azure Container Registry as it’s private.

I need to login to the registry, so I’ll use the Azure CLI to do this.

az acr login --name <your container registry>

Next I need to tag the image I want to push using the Azure Container Registry name with docker tag.

e.g.
docker tag terraform yourcontainerregistry.azurecr.io/iac_terraform_image

And now I can push the image to the Azure Container Registry using docker push.

e.g.
docker push yourcontainerregistry.azurecr.io/iac_terraform_image

For others to now use the image (assuming they have access to the Azure Container Registry) they can use docker pull.

e.g.
docker pull yourcontainerregistry.azurecr.io/iac_terraform_image

Running the newly pulled image.

docker run -it --entrypoint=/bin/bash --volumeĀ  c:\users\<your profile>\Source:/mycode --env-file docker_env.txt yourcontainerregistry.azurecr.io/iac_terraform_image

Summary

Using containers really helps get an environment setup quickly and easily and allows you to concentrate on the task at hand rather than setting up the tools. If you have many developers this can save a lot of time and the image can be easily updated and versioned.

I hope this was useful and helps create your own IaC development environments.

Azure, Azure Pipelines

IaC Terraform with Azure Pipelines

Finishing off this series of articles about IaC with Azure Pipelines, this article is going to follow on from IaC ARM Templates with Azure Pipelines and IaC Ansible with Azure Pipelines and focus on deploying the same simple infrastructure but this time using Terraform.

Azure Pipelines – Releases

For this pipeline I am going to change the Agent Specification to use Ubuntu.

As this is running on an hosted agent, the first thing to add is a task to install Terraform. Microsoft have a task for doing just that.

The Terraform task has a default version but it’s not the latest so I’ll update that.

Note: At the time of writing this the latest version was 0.12.26.

Next I need to add a Terraform task (another Microsoft addition), the task provides the basic Terraform commands, init, validate, plan, validate and apply and destroy.

The Terraform init task requires some configuration, the provider, as I am deploying to Azure this is ‘azurerm’, the configuration directory which is the azure/storage folder in the artifacts and contains the Terraform configuration file. For this example it is named storage.tf.

The task also requires the AzureRM backend configuration to be set.

I need to add the configuration folder as previously with the init command. Also I need to add command arguments to pass variables to my Terraform configuration. Variables are passed using multiple -var command line options.

-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"

Note: Terraform plan creates an execution plan to determine what changes are needed between the configuration and the actual state.

Now I need to apply the Terraform configuration and any changes detected by the plan step. As with the previous steps I need to add the configuration folder and add command arguments to pass variables to my Terraform configuration.

-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"

The configuration above uses a number of variables, so they need to be configured as well.

I initially thought that the AzureRM backend configuration might configure the resource group and storage account when it ran but sadly this is not the case and so further setup is required.

To setup the Azure storage for the AzureRM backend I will add an Azure CLI task.

And configure the Azure CLI to create the resource group, storage account and container that is required.

az group create --location $(storageLocation) --name $(terraformGroup)
az storage account create --name $(terraformStorageName) --resource-group $(terraformGroup) --location $(storageLocation) --sku $(terrformStorageSku)
az storage container create --name $(terraformContainerName) --account-name $(terraformStorageName)

Now it’s all configured let’s run the release and see if it succeeds

Hopefully everything created successfully in Azure, let’s have a look.

Looks like everything was created as expected including the state file in blob storage.


Azure Pipelines YAML

The previous articles cover creating the pipeline in the Azure DevOps portal and so here I will just include the YAML for the Terraform pipeline configuration. As with the other examples I will use a multi-stage, multi-job pipeline and the code is on GitHub.

The Terraform configuration storage.tf can also be found on GitHub.

# A pipeline with no CI trigger
trigger: none

# No PR triggers
pr: none

stages:
- stage: build_test
  displayName: Build and Test
  jobs:
  - job: build_test
    pool:
      vmImage: 'windows-latest' # Currently Windows 2019 and Visual Studio 2019
    steps:
    - script: echo Code Built and Tested!

- stage: release
  displayName: Release
  dependsOn: build_test
  jobs:
   - job: deploy_terraform
     pool:
       vmImage: 'ubuntu-latest' # Currently Ubuntu 18.04
     steps:
      - task: AzureCLI@2
        displayName: 'Azure CLI '
        inputs:
          azureSubscription: 'Twisters Portal'
          scriptType: bash
          scriptLocation: inlineScript
          inlineScript: |
           az group create --location $(storageLocation) --name $(terraformGroup)
           az storage account create --name $(terraformStorageName) --resource-group $(terraformGroup) --location $(storageLocation) --sku $(terrformStorageSku)
           az storage container create --name $(terraformContainerName) --account-name $(terraformStorageName)
          addSpnToEnvironment: false     

      - task: TerraformInstaller@0
        displayName: 'Install Terraform 0.12.26'
        inputs:
          terraformVersion: 0.12.26

      - task: TerraformTaskV1@0
        displayName: 'Terraform : Init Azure'
        inputs:
          workingDirectory: './azure/storage'
          backendServiceArm: 'Twisters Portal'
          backendAzureRmResourceGroupName: $(terraformGroup)
          backendAzureRmStorageAccountName: $(terraformStorageName)
          backendAzureRmContainerName: $(terraformContainerName)
          backendAzureRmKey: $(terraformStateFilename)     

      - task: TerraformTaskV1@0
        displayName: 'Terraform : Plan'
        inputs:
          command: plan
          workingDirectory: './azure/storage'
          commandOptions: '-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"'
          environmentServiceNameAzureRM: 'Twisters Portal'

      - task: TerraformTaskV1@0
        displayName: 'Terraform : Apply'
        inputs:
          command: apply
          workingDirectory: './azure/storage'
          commandOptions: '-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"'
          environmentServiceNameAzureRM: 'Twisters Portal'

   - job: deploy_build
     dependsOn: deploy_terraform
     pool:
       vmImage: 'windows-latest' # Currently Windows 2019 and Visual Studio 2019
     steps:
      - script: echo Deploying Code

Time to run the pipeline and see if it works.

All looks good, let’s see if everything is correct in Azure.

I hope that this article has been useful and is a good introduction to deploying Terrform configurations with Azure Pipelines.