Connor O'Kane Blog
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.
Before we dive into the code, make sure you have the following prerequisites:
The folder structure should look like this:
example-project/
: This is the root folder for our project..github/
: This folder will contain the pipeline..terraform/
: This folder will be automatically created by Terraform to store plugin binaries and other cached data.providers/
: This folder will contain provider-related files.infra/
: This folder will contain infrastructure-related files.web/
: This folder will contain web-related files, such as HTML, CSS, and JavaScript files..gitignore
: This file will specify which files and directories should be ignored by Git.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
}
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:
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"
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.
data "azurerm_client_config" "current" {}
data "azuread_client_config" "current" {}
data "azurerm_subscription" "current" {}
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]
}
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]
}
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)
}
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.
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.
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.
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:
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"
}
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:
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.
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:
These secrets are securely stored in the GitHub repository and can be accessed by the GitHub Actions 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:
If you encounter any issues or the website doesn't load as expected, here are some troubleshooting tips: