In the world of DevOps, it's critical to ensure consistent and repeatable builds, testing and deployment. Continuous Integration (CI) and Continuous Delivery (CD) (CI/CD) should be an important goal to strive for. In this article, you're going to learn about one of those platforms, GitHub Actions. You will learn from both a theoretical and hands-on approach to working with GitHub Actions.

GitHub Actions: What's the Big Deal?

GitHub Actions is a CI/CD system developed by GitHub to integrate directly with GitHub repositories (repos). It allows developers to customize their CI/CD workflows by defining workflows using YAML files.

A workflow, as defined by GitHub Actions, or a pipeline is a common term to describe all of the tasks to perform to build, package, test and deploy code to an environment. A workflow is used to compile code, deploy software or create cloud infrastructure like virtual machines, virtual networks, storage, etc. GitHub Actions is the workflow.

Continuous Integration is the ability to merge all working developer code to a shared location several times a day. This could be to a master branch for example while using proper Gitflow.

Swift Feedback is Important

As developers (devs) build software, they need constant feedback. They need to know what bugs exist in code, what issues arise when running the code on infrastructure or what potential problems may occur. GitHub Actions allows devs to get feedback in a fast and efficient manner.

Devs can commit code to a GitHub repo, begin a build and test process and immediately notice any issue that crop up. That issue could be syntax, a wrong method, or some other bug that they're unaware of. Automating your build and deployment workflow with GitHub Actions allows you to know how your code interacts with the environment right away.

Benefits of GitHub Actions

GitHub Actions brings up a lot of interesting use cases and benefits. Some of the most common use cases include:

  • Continuously deploying code to dev, QA and production environments - Development and QA environments will always have the latest code if you're constantly deploying to environments with GitHub Actions. Once that code has passed QA, you can automagically have it deploy to User Acceptance Testing( UAT) and even prod as well.
  • Seeing code in action right away - The moment code is committed to source control, it can be tested, deployed and provide immediate feedback.
  • Deploying code directly from GitHub repo - When using other CI/CD systems using a GitHub repo, GitHub has to send webhooks to other systems. Using Actions, everything remains within GitHub.
  • Automating deployments to environments at any time - Etsy and Netflix deploy their code 50+ times per day, for example.

Tutorial Overview

Enough about understanding what GitHub Actions can do. Let's see how you can use it in a hands-on tutorial! In the upcoming tutorial, you're going to see a real-world example of how to deploy code with GitHub Actions.

Although there are a near-infinite number of use cases for GitHub Actions, we'll focus on a fairly standard use case for DevOps. You'll learn how to use the infrastructure-provisioning tool Terraform to deploy Azure resources.

In the tutorial, you will:

  • Create a resource group in Azure using Terraform
  • Create a YAML file for GitHub Actions and learn about Terraform-GitHub-Actions extension - This extension allows you to interact with Terraform via GitHub Actions.
  • Execute Terraform configuration and build Azure resources
You don't necessarily have to use Terraform as a GitHub project. You can use PowerShell, Python, Ansible, or any other language to provision infrastructure as code. Please note that this tutorial is designed to use Terraform and using another language will result in differences in the deployment.

Prerequisites

If you plan to follow along with the tutorial, please ensure you have a few items set up ahead of time. Below you will find a list of everything you need to successfully complete this tutorial.

You should also be somewhat familiar with integrating GitHub with Visual Studio Code and syncing local Git repo changes to GitHub.

Preparing for Azure Authentication

Before you can start coding and setting up GitHub Actions, you have to perform a few one-time tasks ahead of time.

Creating an Azure Service Principal

The project in this tutorial will interact with Azure. Most importantly, GitHub will need access to an Azure subscription to deploy resources into.

To allow access to Azure from GitHub, create a role-based access control (RBAC) policy using the Azure CLI via the code snippet below. The command below:

  • Calls the Active Directory (AD) API for AZ CLI to create a service principal
  • Uses the specific command switch for creating a Role Based Access Control service principal
  • Uses the --name parameter to specify a name for your service principal
  • Uses the --role parameter to specify what level of access your service principal has. The contributor role we is being used because you will need a role that can read and write. For more information on Azure service principal roles, check out this link from Azure.
  • Uses the --scopes parameter to specify exactly where your service principal has access to within your subscription
  • Uses the --sdk-auth parameter to specify that your service principal has programmatic access
Be sure to change "your-subscription-id" to your specific Azure subscription!
az ad sp create-for-rbac --name "myApp" --role contributor --scopes /subscriptions/your-subscription-id --sdk-auth

When the above command is complete, you will see output similar to what you see below. Take note of the clientId, clientSecret, and tenantId. You'll need those in a minute.

Creating GitHub Secrets

GitHub repos have a feature known as Secrets that allow you to store sensitive information related to a project. For this tutorial, store three secrets - clientId, clientSecret, and tenantId.  You will create these secrets because they will be used by Terraform to authenticate to Azure.

To create the secrets, navigate to the GitHub repository, click on Settings and then on Secrets as shown below. Repeat the process to create a GitHub secret for each of three required secrets using the value returned from creating the Azure service principal above.

Click the blue "Add a new secret" text and you'll be presented with a place to put your secret name and value. You will need to create three secrets as these secrets are what the Azure provider for terraform expects during the authentication process. You will learn more about the Azure Provider in the next section Writing your Terraform code

When you're done, you should see three secrets created like below.

Creating the Terraform Configuration

Now that you've got all of the one-time prerequisite setups out of the way, it's time to get down to creating the Terraform configuration. In this tutorial, you'll set up GitHub Actions to invoke a Terraform configuration to create an Azure resource group.

Terraform Input Variable Configuration

For any Terraform configuration, it's important that the code is reusable and contains no static values (if possible). To do this, build a variable configuration. This variable configuration will store variables that will then be passed in at runtime during the GitHub Actions deployment.

To get started, create a blank text file called variables.tf in the root of your local Git repo. This file will hold all of the Terraform variables the GitHub Actions YAML workflow will read. You're creating this file first to define variable types.

In the variables.tf file, create four string variables as shown in the following code snippet. You'll notice that three of these variables match up to the three GitHub secrets created earlier.

The variables are:

  • sub - This value should be passed in at runtime because you could have different subscriptions or multiple clients using the same reusable code. Something like a subscription should never be static.
  • client_secret - The client secret comes from the service principal created earlier
  • client_id - The client ID, also known as the Application ID, is also generated from the service principal created earlier.
  • tenant_id - The tenant ID is not generated with your service principal, but instead generated with your Azure account.
variable "sub" { type = string }
variable "client_secret" { type = string }
variable "client_id" { type = string }
variable "tenant_id" { type = string }

Building the terraform.tfvars Configuration

Once the variable configuration is done, it's time create a TFVARS configuration. This is a file ending in .tfvars which represent variables that you pass in at runtime. Passing in variables at runtime keeps code reusable over time. The .tfvars configuration is straight forward. It holds the client_id, tenant_id, and sub as shown below..

Create another text file in the root of the Git repo on your local machine called terraform.tfvars and copy/paste the following code in replacing each of the variables matching your environment.

sub = id_of_your_subscription
client_id = id_of_your_service_principal
tenant_id = tenant_id_for_your_account
You may be asking yourself, "what about the client_secret"? The client secret is just like a password and should be treated as such. You should store the client secret in a secure location that only individuals with access can retrieve at any given time. You added the client_secret inside of GitHub secrets for this exact purpose earlier.

Building the main.tf Configuration

You're now ready to move on to the bread and butter of the code - creating an Azure resource group.

To create an Azure resource with Terraform requires using a Terraform provider. The Terraform provider is how Terraform knows what platform it's interacting with. The provider is also Terraform authenticates to Azure, which you can see in the code below.

Create another text file in the root of the Git repo on your local machine called main.tf and copy/paste the following code in.

The subscription_id has a value of var.sub which comes from our variables.tf configuration. Any time you're calling a variable in Terraform, you will need to use the var keyword followed by the variables name. For more information on variables, please see: terraform variables

Once authenticated with the provider, the configuration below creates a resource. Think of a resource like a coding method or a function. The resource tells Terraform what you want to do in Azure. In this case, the configuration uses the azurerm_resource_group resource. This resource will create an Azure resource group passing in various parameters.

  • name - Name of your resource group
  • location - The location (region) that you want your resource group to be created in
  • tags - The key/value pair of where you want your resource group to be. In our case, it's dev
Notice version 1.38.0 in the AzureRM provider. At the time of writing this, 1.38.0 is the most current API version. Check this link to confirm the version has not been deprecated or changed before proceeding.
provider "azurerm" {
  version         = "1.38.0"
  subscription_id = var.sub
  client_id       = "${var.client_id}"
  client_secret   = "${var.client_secret}"
  tenant_id       = "${var.tenant_id}"
}

resource "azurerm_resource_group" "DevRG" {
  name     = "Dev10"
  location = "eastus"

  tags = {
    environment = "Dev"
  }
}

By the time you're done with building the Terraform configuration, you should have three files in the root of the Git repo.

Once all of the files are in your local Git repo, commit the code and sync changes to the GitHub repo.

Creating the GitHub Actions Workflow

At this point, you've got all of the authentication set up and have created a Terraform configuration to deploy the example Azure resource group. It's finally time to get into GitHub Actions itself! To continuously deliver code with GitHub Actions, you create a workflow. All workflows are defined in a syntax called YAML.

To create the workflow, navigate to your GitHub repo and click on the Actions button. You will then see some pre-made options as you can see below. Rather than use an existing template, create your own YAML file. To do so, click on the Set up a workflow yourself button in the upper right corner of the screen.

Click on the Edit new file tab, you will see an example workflow already created called main.yml. By default this is what the YAML file is called. Since you're building your own, remove all it. It's then time to begin filling in the YAML workflow.

Creating the YAML File

First, define a descriptive name. This tutorial will use the name Terraform deploy to Azure.

name: Terraform deploy to Azure

Next, define a trigger to when the workflow will execute using the on keyword. This section is where the workflow executes the pipeline on a push to the repository, a pull request, or even just by running the workflow manually.

In this tutorial, use push to ensure whenever a code commit is detected, the workflow will run.

on: [push]

Under steps, specify each action to perform when the workflow is run.

The first step checks out the code in the master branch. Checking out code means bringing the code to wherever the place is that the code is running. In this instance, it's the GitHub Actions runner.

The uses keyword means what API call you will be using to interact with the specific resource. In our case, we're going to interact with the actions/checkout API using the master branch.

- name: "Checkout"
  uses: actions/[email protected]

Initialization

The next step is where you should initialize or stage anything to prepare for Terraform code execution.  In the below YAML file snippet, the workflow is executing code created by Hashicorp that to run Terraform in GitHub workflows.  The @master text tells the terraform-github-actions extension to use the code currently stored in the master branch.

The with keyword describes what components you're going to use in the API call within the uses keyword. Since we're calling the terraform-GitHub-actions API, the with components are our options that we have. Think about these kind of like parameters.

- name: "Terraform Init"
  uses: hashicorp/[email protected]
  with:
    tf_actions_version: 0.12.13
    tf_actions_subcommand: "init"
Pay attention to the version. Ensure you're always using the same version of terraform for each step. If you don't, you may run into conflicts between versions such as components missing, backward compatibility issues, etc. The version we're using can be found in the tf_actions_version component.

Planning

The next step in the YAML file is the terraform plan stage. The plan stage goes through each resource and confirms what is being created and if it's correct. For example, you could get an error stating that you're missing a required parameter in the resource that you're using.

You will see an args keyword below. The args keyword represents arguments that you are passing in at runtime. The var Terraform input is a way that Terraform allows us to pass parameters in while Terraform is running on the command line.

- name: "Terraform Plan"
  uses: hashicorp/[email protected]
  with:
    tf_actions_version: 0.12.13
    tf_actions_subcommand: "plan"
    args: '-var="client_secret=${{ secrets.clientSecret }}"'

Deploying

Finally, once the workflow has initialized Terraform, planned for the deployment, it's time to finally deploy the Azure resource group apply command.

- name: "Terraform Apply"
  uses: hashicorp/[email protected]
  with:
	  tf_actions_version: 0.12.13
    tf_actions_subcommand: "apply"
		args: '-var="client_secret=${{ secrets.clientSecret }}"'

The Final Workflow YAML File

Once you've built the entire workflow YAML file, it should look like the following:

name: Terraform deploy to Azure

on: [push]

jobs:
	build:
		runs-on: ubuntu-latest
		
		steps:
		- name: "Checkout"
		  uses: actions/[email protected]

		- name: "Terraform Init"
		  uses: hashicorp/[email protected]
		  with:
		    tf_actions_version: 0.12.13
		    tf_actions_subcommand: "init"

		- name: "Terraform Plan"
		  uses: hashicorp/[email protected]
		  with:
		    tf_actions_version: 0.12.13
		    tf_actions_subcommand: "plan"
		    args: '-var="client_secret=${{ secrets.clientSecret }}"'
		
		- name: "Terraform Apply"
		  uses: hashicorp/[email protected]
		  with:
			  tf_actions_version: 0.12.13
		    tf_actions_subcommand: "apply"
				args: '-var="client_secret=${{ secrets.clientSecret }}"'

Committing the Workflow YAML File

The YAML file has been built, it's time to save it but committing it to the GitHub repo. To do that, click the green Start commit button on the right of the page. Add a commit message and ensure you commit directly to master because that's where your Terraform code is.

Once the commit message has been added and you've ensured you're committing directly to the master branch, click the green Commit new file button.

Once you've committed the workflow YAML file to the GitHub repo, the workflow will be run. It will be executed now because the workflow is set to run on every code commit.

Monitoring the Workflow

Once the workflow is running, you should now monitor its progress. To do so, navigate to the Actions tab again in the GitHub repo. You should now see the workflow running as shown below.

When complete, you should output like below. If so, the workflow was successful!

Now if you navigate to your Azure resource groups, you should see the resource group created as intended.

Summary

In this blog post, you went from start to finish on deploying an infrastructure as code project using CI/CD with a Github Actions workflow. Using an automated workflow, you set up Terraform, performed a basic test with a plan and finally deployed a resource group to your Azure environment.

For your next challenge with this fun and emerging technology, try doing the same thing (create a resource group with GitHub Actions) but instead use PowerShell. The following link should help you get there!