Deploying Your Website with Terraform and GitHub Actions CI/CD Pipeline

Connor O'Kane Blog

Introduction

In this blog post, we'll explore how to deploy your own website using Terraform and set up a CI/CD pipeline with GitHub Actions. This CI/CD pipeline will deploy your HTML/CSS files. By leveraging the power of infrastructure as code and automated deployment processes, we can streamline the deployment of your website and ensure a smooth and efficient workflow.

The code repository for this tutorial can be found at https://github.com/okaneconnor?tab=repositories.

Prerequisites

Before we dive into the code, make sure you have the following prerequisites:

Step 1: Set up the Folder Structure and Provider File

Folder Structure

The folder structure should look like this:

With the folder structure in place, let's move on to the provider file.

In our Terraform configuration, the provider file is used to define the providers required for the deployment. In this case, we have two providers: azurerm and github.

provider "azurerm" {
  features {}
}

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.54.0"
    }
  }
}

// Configure the GitHub Provider
provider "github" {
  token = var.github_token
}

Step 2: Variables

In addition to the provider file, we also define variables in our Terraform configuration. These variables allow us to parameterize our deployment and make it more flexible. Here are the variables used in this configuration:

variable "rg_name" {
    type        = string
    description = "Name of the resource group"
    default     = "example"
  }
  
  variable "location" {
    type        = string
    description = "Location"
    default     = "example"
  }
  
  variable "sa_name" {
    type        = string
    description = "Storage Account Name"
    default     = "example"
  }
  
  variable "github_token" {
    type        = string
    description = "GitHub personal access token"
    sensitive   = true
  }
  
  variable "asuid_value" {
    description = "ASUID value for TXT record"
    type        = string
  }
  
  

These variables define the resource group name, location, storage account name, GitHub personal access token, and ASUID value for the TXT record. You can customize these values according to your requirements.

The github provider is used to interact with GitHub resources, such as creating secrets or configuring repositories. It is defined using the provider block, and the token argument is set to the value of the github_token variable.

GitHub Token: The GitHub token is a personal access token that grants permissions to interact with your GitHub account programmatically. It is used by the github provider to authenticate and authorize requests to the GitHub API.

To create a GitHub token:

  1. Go to your GitHub account settings.
  2. Navigate to "Developer settings" > "Personal access tokens".
  3. Click on "Generate new token".
  4. If you need to create secrets or configure repositories, you may need the repo and admin:repo_hook scopes.
  5. Click on "Generate token" and copy the generated token for your tfvars file.

It is important to keep the GitHub token/ASUID Value secure and avoid exposing it in your code or version control system. Instead, you can store the token as a Terraform variable or as an environment variable.

In the provided code, the GitHub token and ASUID value from your DNS is expected to be defined in a tfvars file:


github_token = "YOUR_GITHUB_TOKEN"
asuid_value = "YOUR_ASUID_VALUE"
  

Step 3: Retrieving Data with Terraform Data Sources

When working with Terraform, we can use data sources to retrieve information from external sources. They allow you to fetch data from APIs, databases, or other Terraform resources and use that data within your Terraform configuration.

  1. azurerm_client_config data source: The azurerm_client_config data source retrieves information about the current Azure client configuration. It provides access to the client ID, tenant ID, and subscription ID associated with the authenticated Azure account.
  2. azuread_client_config data source: The azuread_client_config data source retrieves information about the current Azure Active Directory (Azure AD) client configuration. It provides access to the client ID and tenant ID associated with the authenticated Azure AD account.
  3. azurerm_subscription data source: The azurerm_subscription data source retrieves information about the current Azure subscription. It provides access to the subscription ID and other details associated with the authenticated Azure account.

data "azurerm_client_config" "current" {}
data "azuread_client_config" "current" {}
data "azurerm_subscription" "current" {}
  

Step 4: Configuring Terraform

To begin, let's take a look at the Terraform configuration files. The main file contains the resource definitions for deploying your website on Azure.

In this code snippet, we define an Azure resource group, an App Service plan, and an App Service. The azurerm_resource_group resource creates a resource group to store all our resources. The azurerm_app_service_plan resource creates an App Service plan with the desired name, location, and SKU. Finally, the azurerm_app_service resource creates an App Service with the specified name and links it to the App Service plan.

// Creates a resource group that we store all of our resources inside
resource "azurerm_resource_group" "rg" {
  name     = var.rg_name
  location = var.location
}

// Creates an App Service Plan
resource "azurerm_app_service_plan" "app_service_plan" {
  name                = "example-app-service-plan"
  location            = var.location
  resource_group_name = var.rg_name
  depends_on          = [azurerm_resource_group.rg]

  sku {
    tier = "Basic"
    size = "B1"
  }
}

// Create an App Service
resource "azurerm_app_service" "app_service" {
  name                = "example-app-service"
  location            = var.location
  resource_group_name = var.rg_name
  app_service_plan_id = azurerm_app_service_plan.app_service_plan.id
  depends_on          = [azurerm_resource_group.rg]
}

Step 5: Setting up Azure AD Application and Service Principal

To grant GitHub Actions access to deploy our website, we need to create an Azure AD application and a service principal.

The azuread_application resource creates an Azure AD application, while the azuread_application_federated_identity_credential resource sets up a federated identity credential for GitHub Actions. The azuread_service_principal resource creates a service principal associated with the Azure AD application.

resource "azuread_application" "main_application" {
  display_name = "example-application"
  owners       = [data.azuread_client_config.current.object_id]
}

resource "azuread_application_federated_identity_credential" "federated_id" {
  application_object_id = azuread_application.main_application.object_id
  display_name          = "GithubActionsFederatedCredential"
  description           = "Federated credential for GitHub Actions"
  audiences             = ["api://AzureADTokenExchange"]
  issuer                = "https://token.actions.githubusercontent.com"
  subject               = "repo:YourOrganization/YourRepository:ref:refs/heads/main"
}

// Create a service principal for the application
resource "azuread_service_principal" "sp" {
  application_id               = azuread_application.main_application.application_id
  app_role_assignment_required = false
  owners                       = [data.azuread_client_config.current.object_id]
}

Step 6: Generating Client Secret and Storing it in GitHub Secrets

To securely authenticate the GitHub Actions workflow with Azure, we generate a client secret and store it as a GitHub secret.

// Generate a client secret for the service principal
resource "azuread_service_principal_password" "client_secret" {
  service_principal_id = azuread_service_principal.sp.id
  end_date_relative    = "8760h"  # Set the expiration date (e.g., 1 year)
}

resource "azuread_application_password" "client_secret" {
  application_object_id = azuread_application.main_application.object_id
  end_date_relative     = "8760h"  # Set the expiration date (e.g., 1 year)
}

 

Step 7: Configuring DNS and SSL Certificate

To use a custom domain for your website and enable HTTPS, you need to configure DNS records and obtain an SSL certificate. I have used the IONOS Domain Provider for buying my domain name, but you can use any domain provider that suits. If you decided to use IONOS, I will walk you through the process of setting up DNS records with IONOS and obtaining an SSL certificate.

7.1 Configuring DNS Records with IONOS

  1. Log in to your IONOS account and navigate to the DNS settings for your domain.
  2. Add an A record that points your domain to the IP address of your website hosting server. The A record should have the following settings:
    • Type: A
    • Host: @
    • Points to: IP address of your website hosting server
    • TTL: 3600
  3. Add a CNAME record for the "www" subdomain that points to your main domain. The CNAME record should have the following settings:
    • Type: CNAME
    • Host: www
    • Points to: yourdomain.com
    • TTL: 3600
  4. Save the changes to your DNS settings.

NOTE: In the code below we create an SSL cert for free through Azure which is perfectly fine to use. If you want to set up a SSL cert through your domain provider you can follow the below steps, if not then you can skip to step 8.

7.2 Obtaining an SSL Certificate

To enable HTTPS for your website, you need to obtain an SSL certificate. Many website hosting providers offer free SSL certificates through services like Let's Encrypt. Here's how you can obtain an SSL certificate.

  1. Check if your website hosting provider offers a free SSL certificate through Let's Encrypt or a similar service.
  2. If available, follow your hosting provider's instructions to enable SSL for your website. This process may involve a few clicks or some configuration steps.
  3. If your hosting provider doesn't offer a free SSL certificate, you can purchase one from a trusted SSL certificate authority such as Comodo, Symantec, or GeoTrust.
  4. Once you have obtained the SSL certificate, install it on your website by following the instructions provided by your hosting provider or SSL certificate issuer.

After configuring DNS records and obtaining an SSL certificate, your website should be accessible via your custom domain and HTTPS.

One additional step you may need to take is to add a TXT record to your DNS settings for domain verification purposes. Some SSL certificate providers or hosting platforms may require you to add a specific TXT record to prove that you own the domain. If required, add a TXT record with the following settings:

  • Type: TXT
  • Host: @
  • Value: The verification code provided by your SSL certificate provider or hosting platform/ASUID Value provided by Azure.
  • TTL: 3600

By following these steps and configuring your DNS records correctly, your website should be accessible via your custom domain and secured with an SSL certificate, enabling HTTPS for your visitors. Who wants to visit unsecure websites?

// Creates a DNS zone to host the DNS Records for our domain
resource "azurerm_dns_zone" "dnszone" {
  name                = "example.com"
  resource_group_name = var.rg_name
  depends_on          = [azurerm_resource_group.rg]
}

// Creates an A record for the domain name
resource "azurerm_dns_a_record" "a_record" {
  name                = "@"
  zone_name           = azurerm_dns_zone.dnszone.name
  resource_group_name = var.rg_name
  ttl                 = 300
  records             = split(",", azurerm_app_service.app_service.outbound_ip_addresses)
  depends_on          = [azurerm_dns_zone.dnszone, azurerm_app_service.app_service]
}

// Creates a TXT record for the domain name
resource "azurerm_dns_txt_record" "asuid_txt_record" {
  name                = "asuid"
  zone_name           = azurerm_dns_zone.dnszone.name
  resource_group_name = azurerm_resource_group.rg.name
  ttl                 = 300
  record {
    value = "YOUR_ASUID_TXT_RECORD_VALUE"
  }
}

// Binds a custom domain to the Azure App Service.
resource "azurerm_app_service_custom_hostname_binding" "custom_domain" {
  hostname            = "example.com"
  app_service_name    = azurerm_app_service.app_service.name
  resource_group_name = azurerm_resource_group.rg.name
  depends_on          = [azurerm_dns_txt_record.asuid_txt_record]
}

// Creates a managed SSL certificate for the custom domain.
resource "azurerm_app_service_managed_certificate" "ssl_cert" {
  custom_hostname_binding_id = azurerm_app_service_custom_hostname_binding.custom_domain.id
}

// Binds the managed SSL certificate to the custom domain on the App Service, enabling HTTPS.
resource "azurerm_app_service_certificate_binding" "sslcert_binding" {
  hostname_binding_id = azurerm_app_service_custom_hostname_binding.custom_domain.id
  certificate_id      = azurerm_app_service_managed_certificate.ssl_cert.id
  ssl_state           = "SniEnabled"
}

Step 8: Assigning the Contributor Role

Before deploying your website to Azure, you need to ensure that the service principal used in the GitHub Actions workflow has the necessary permissions to create and manage resources in your Azure subscription. One way to achieve this is by assigning the "Contributor" role to the service principal at the subscription level.

To assign the "Contributor" role, you can use the Azure CLI command az role assignment create. Here's an example of how you can assign the role:

az role assignment create --assignee <service-principal-id> --role "Contributor" --scope "/subscriptions/<subscription-id>"

Let's break down the command:

  • --assignee <service-principal-id>: Replace <service-principal-id> with the client ID of your service principal, which is stored in the "App Registrations > Owned Applications" in the Azure Portal.
  • --role "Contributor": Specifies the role to assign to the service principal. In this case, we are assigning the "Contributor" role, which grants broad permissions to manage resources in the subscription.
  • --scope "/subscriptions/<subscription-id>": Replace <subscription-id> with the ID of your Azure subscription, which is stored in the "Subscriptions" section in the Azure Portal. This parameter specifies the scope at which the role assignment applies.

You can run this command locally using the Azure CLI or add it as a step in your GitHub Actions workflow before the Terraform deployment steps.

By adding the Assign Contributor Role step before the Terraform deployment steps, you ensure that the service principal has the necessary permissions to create and manage resources in your Azure subscription.

Make sure to replace <service-principal-id> and <subscription-id> with your own values.

With this additional step, your GitHub Actions workflow will assign the "Contributor" role to the service principal before proceeding with the Terraform deployment.

Step 9: GitHub Actions Secrets

To automate the deployment process and securely manage sensitive information, we can utilize GitHub Actions secrets and create a workflow. In this step, we will configure the necessary secrets and define the GitHub Actions workflow to deploy our website.

First, let's take a look at the Terraform code used to create the GitHub Actions secrets:

 resource "github_actions_secret" "azure_client_secret" { repository = "example-repo" secret_name = "AZURE_CLIENT_SECRET" plaintext_value = azuread_application_password.client_secret.value }
resource "github_actions_secret" "azure_client_id" {
  repository      = "example-repo"
  secret_name     = "AZURE_CLIENT_ID"
  plaintext_value = azuread_application.main_application.application_id
}

resource "github_actions_secret" "azure_tenant_id" {
  repository      = "example-repo"
  secret_name     = "AZURE_TENANT_ID"
  plaintext_value = data.azuread_client_config.current.tenant_id
}

resource "github_actions_secret" "azure_subscription_id" {
  repository      = "example-repo"
  secret_name     = "AZURE_SUBSCRIPTION_ID"
  plaintext_value = data.azurerm_subscription.current.subscription_id
}

In this code snippet, we define four GitHub Actions secrets using the github_actions_secret resource:

  1. AZURE_CLIENT_SECRET: Stores the client secret value for authentication.
  2. AZURE_CLIENT_ID: Stores the application ID of the Azure AD application.
  3. AZURE_TENANT_ID: Stores the tenant ID of the Azure AD.
  4. AZURE_SUBSCRIPTION_ID: Stores the subscription ID of the Azure subscription.

These secrets are securely stored in the GitHub repository and can be accessed by the GitHub Actions Pipeline.

Step 10: GitHub Actions CI/CD Pipeline

With the Terraform configuration in place, you can set up a GitHub Actions pipeline to automatically deploy your website whenever changes are pushed to the main branch. Which is what we are going to do below:

Create a new file named .github/workflows/terraform.yml in your repository with the following content:

name: Terraform Deploy
on:
  push:
    branches:
      - main

env:
  AZURE_WEBAPP_NAME: connorokaneblog
  AZURE_WEBAPP_PACKAGE_PATH: './web'

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2

      - name: Azure Login
        run: |
          az login --service-principal -u ${{ secrets.AZURE_CLIENT_ID123 }} -p ${{ secrets.AZURE_CLIENT_SECRET123 }} --tenant ${{ secrets.AZURE_TENANT_ID123 }}
          az account set --subscription ${{ secrets.AZURE_SUBSCRIPTION_ID123 }}

      - name: Terraform Init
        run: terraform init

      - name: Terraform Apply
        run: terraform apply -auto-approve

      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v2
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}

When you now make a push to your main branch with the HTML/CSS/JS files your pipeline will push the files and deploy your website. After deployment it's essential to test the website to ensure it is functioning as expected. Here are some steps you can take to verify your website's functionality and troubleshoot any issues that may arise:

  1. Open a web browser and navigate to your website's URL (e.g., https://connorokane.blog).
  2. Check if the website loads successfully and displays the expected content.
  3. Test the website's responsiveness by resizing the browser window or accessing it from different devices (desktop, tablet, mobile).
  4. Click on various links, buttons, and navigation elements to ensure they work correctly and lead to the appropriate pages.
  5. Verify that any forms, contact pages, or interactive features are functioning as intended.

If you encounter any issues or the website doesn't load as expected, here are some troubleshooting tips:

  • Check the GitHub Actions workflow logs to see if there were any errors during the deployment process.
  • Verify that the Terraform configuration files are correct and free of syntax errors.
  • Ensure that the GitHub secrets (AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID) are set correctly in your repository settings.
  • Double-check that the Azure resources (App Service, App Service Plan, etc.) are provisioned successfully in the Azure portal.
  • Review the Azure Web App logs for any error messages or warnings.
  • Clear your browser cache and cookies, and try accessing the website again.

Web Page Example