Published
- 8 min read
Streamlit Deployment Guide Part 4: Terraform Apply & Destroy
This post showcases a GitHub Workflow walkthrough for executing the necessary Terraform commands to provision and tear down Azure resources for the Streamlit application. It continues a series detailing the process of deploying a Streamlit app to Azure, broken down into the following parts:
- Part 1: Containerizing a Streamlit app
- Part 2: GitHub Workflow for Building and Publishing to ghcr.io
- Part 3: Azure Infrastructure via Terraform
- Part 4: GitHub Workflow for Terraform Apply & Destroy You are here đ
Do you want to deploy your Streamlit application on Azure right now? Use the template repository đ
TL;DR
See the completed Apply Workflow and Destroy Workflow.
Prerequisites
A basic understanding of GitHub Actions is required. The Workflow assumes you have the files from Part 3:
- /infra/main.tf
- /infra/providers.tf
- /infra/variables.tf
- /infra/locals.tf
- /infra/web-app.tf
If you have your own Terraform configuration, ensure all dependent files are available and stored within the /infra folder for this workflow to function correctly.
Create GitHub Workflows
The workflow files should be stored within the following folder: .github/workflows/
. Suitable names could be terraform-plan-apply.yaml
and terraform-destroy.yaml
.
Setup Azure Application Registration
Refer to Azure Federated Identity Credentials for Terraform: A GitHub Actions Guide to enable authenticating the workflow with Azure.
Setup Environment
- Navigate to your repository on GitHub.
- Go to âSettingsâ > âEnvironmentsâ.
- Create a new environment named âproductionâ
Add Secrets
To add secrets:
- Navigate to your repository on GitHub.
- Go to âSettingsâ > âEnvironmentsâ > Select âproductionâ.
- Go to âEnvironment secretsâ and select âAdd environment secretâ
- Add the following secrets using the output from Setup Azure Application Registration: AZURE_ENTRA_ID_CLIENT_ID, AZURE_ENTRA_ID_TENANT_ID and AZURE_SUBSCRIPTION_ID
These will be used as environment variables provided to the Terraform actions.
Completed Apply Workflow
name: Terraform Plan & Apply Infrastructure
on:
push:
branches: ['main']
paths:
- 'infra/**'
tags:
- '*'
workflow_dispatch:
env:
TF_VAR_resource_group_name: 'rg-streamlit-poc'
WORKING_DIRECTORY: './infra'
jobs:
terraform:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
# image name needs to be lowercase, some accounts have uppercase letters
- name: Get owner/repo name and convert to lowercase
id: get-image-name
run: echo "image-name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
- name: Extract image tag
id: extract-tag
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
TAG=${GITHUB_REF#refs/tags/}
else
TAG=$(git describe --tags --abbrev=0)
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
- name: Azure login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.8.4
- name: Terraform fmt
id: fmt
run: terraform fmt -check
continue-on-error: true
- name: Terraform Init
id: init
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: true
run: terraform init
- name: Terraform Validate
id: validate
working-directory: ${{ env.WORKING_DIRECTORY }}
run: terraform validate -no-color
- name: Terraform Plan
id: plan
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: true
TF_VAR_docker_image_name: '${{ steps.get-image-name.outputs.image-name }}:${{ steps.extract-tag.outputs.tag }}'
working-directory: ${{ env.WORKING_DIRECTORY }}
run: terraform plan -no-color
continue-on-error: false
- name: Terraform Apply (auto-approve)
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: true
TF_VAR_docker_image_name: '${{ steps.get-image-name.outputs.image-name }}:${{ steps.extract-tag.outputs.tag }}'
run: terraform apply -auto-approve
Apply Workflow Walkthrough
Workflow Definition
name: Terraform Plan & Apply Infrastructure
This sets the name of the workflow, which is displayed under the Actions tab. If omitted, the file name will be shown.
Triggers
on:
push:
branches: ['main']
paths:
- 'infra/**'
tags:
- '*'
workflow_dispatch:
The workflow triggers on pushes to the âmainâ branch and only if the changes are made in the âinfraâ directory. The âworkflow_dispatchâ allows the workflow to be triggered manually. Additionally, there is a trigger for any tags created on the repository. Tags are use to manage the version of the Streamlit application.
Environment
env:
TF_VAR_resource_group_name: 'rg-streamlit-poc'
WORKING_DIRECTORY: './infra'
This sets two environment variables available to the entire workflow. The working directory is set to where the Terraform source resides (i.e. /infra) and TF_VAR_resource_group_name allows you to set the resource group used to provision resources within.
Jobs
jobs:
terraform:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
There is a single job defined which covers the full process of running the Terraform commands. âruns-onâ specifies it should run on the latest version of Ubuntu. Environment ensures this job uses the created âproductionâ environment (a prerequisite when using federated identity credentials). Permissions grant the necessary permissions for the job to update the id-token, which is necessary for using federated credentials with Terraform after the Azure login
Steps
1. Checkout the Repository
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
This checks out the repository to the job runner, allowing the runner to access the repository content.
2. Get Owner/Repo Name and Convert to Lowercase
- name: Get owner/repo name and convert to lowercase
id: get-image-name
run: echo "image-name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT
Retrieves the repository name and converts it to lowercase for use as the Docker image name (lowercase name is required). You can easily use an environment variable for your image name. This just allows it to be automated.
3. Extract tag version
- name: Extract image tag
id: extract-tag
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
TAG=${GITHUB_REF#refs/tags/}
else
TAG=$(git describe --tags --abbrev=0)
fi
echo "tag=$TAG" >> $GITHUB_OUTPUT
Since the Azure Web Application is running our published image in ghcr.io, retrieving the tag is neccessary inorder to run the lastest version by setting docker_image_name. The alternative to this is to ignore image tag changes and use a separate deploy task to update the image version in the web app.
4. Azure login
- name: Azure login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
This task logs into Azure using the federated identity credentials. Once authenticated, the following Terraform tasks will be able to perform actions against the subscription.
5. Terraform Setup
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.8.4
This sets up Terraform CLI in a GitHub Actions workflow, allowing you to run Terraform commands within the workflow.
6. Terraform Init
- name: Terraform Init
id: init
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: true
run: terraform init
Terraform init initializes a Terraform working directory by downloading and installing the necessary provider plugins and setting up the backend configuration for the project. The working directory is set to the env value which is set to âinfraâ and this is configured for all Terraform actions. The ARM_USE_OIDC is set to true to indicate that we are using Azure federated identity credentials and this is configured for all Terraform actions.
7. Terraform Validate
- name: Terraform Validate
id: validate
working-directory: ${{ env.WORKING_DIRECTORY }}
run: terraform validate -no-color
Terraform validate checks the syntax and internal consistency of a Terraform configuration, ensuring that it is syntactically valid and internally consistent.
8. Terraform Plan
- name: Terraform Plan
id: plan
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: true
TF_VAR_docker_image_name: '${{ steps.get-image-name.outputs.image-name }}:${{ steps.extract-tag.outputs.tag }}'
working-directory: ${{ env.WORKING_DIRECTORY }}
run: terraform plan -no-color
continue-on-error: false
Terraform plan is a command that previews the changes that Terraform will make to your infrastructure, showing the execution plan before applying any modifications. The new Terraform variable is TF_VAR_docker_image_name, which will set the image and tag for the published containerized Streamlit application and is set on Terraform plan/apply actions.
9. Terraform Apply
- name: Terraform Apply (auto-approve)
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_ENTRA_ID_CLIENT_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_ENTRA_ID_TENANT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_USE_OIDC: true
TF_VAR_docker_image_name: '${{ steps.get-image-name.outputs.image-name }}:${{ steps.extract-tag.outputs.tag }}'
run: terraform apply -auto-approve
Terraform apply is a command that executes the actions defined in a Terraform configuration file to create, update, or delete infrastructure resources. The apply has -auto-approve set which will not prompt confirmation (this is for proof of concepts and if using for production, rather introduce approval step).
Completed Destroy Workflow
name: Terraform Destroy Inrastructure
on: workflow_dispatch
env:
TF_VAR_resource_group_name: 'rg-streamlit-poc'
WORKING_DIRECTORY: './infra'
jobs:
terraform:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Azure login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_AD_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_AD_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.8.0
- name: Terraform Init
id: init
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_AD_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
ARM_USE_OIDC: true
run: terraform init
- name: Terraform Destroy (auto-approve)
id: destroy
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_AD_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
ARM_USE_OIDC: true
run: terraform destroy -auto-approve
Destroy Workflow Walkthrough
For brevity, I will only go through the differences. Otherwise, for the same actions, please refer to the Apply workflow.
Triggers
on: workflow_dispatch
The workflow triggers only uses âworkflow_dispatchâ to allow the workflow to be triggered manually.
Steps
1. Terraform Destroy
- name: Terraform Destroy (auto-approve)
id: destroy
working-directory: ${{ env.WORKING_DIRECTORY }}
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_AD_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
ARM_USE_OIDC: true
run: terraform destroy -auto-approve
Terraform destroy command systematically dismantles all managed infrastructure resources, reverting the environment back to its original, unprovisioned state. The destroy has -auto-approve set which will not prompt confirmation (this is for proof of concepts and if using for production, rather introduce approval step).
Wrap Up
This is an initial starting point for managed the proof of concept infrastructure. When tackling production workloads, the workflows would be more mature and could include, but not be limited to, the following items:
- Terraform Apply Approval: Validate plan before applying
- Pull Request Workflow: Ensure changes are validated, scanned, and tested
- Multi-environment support (i.e., integration, staging, and production)