Published

- 8 min read

Streamlit Deployment Guide Part 4: Terraform Apply & Destroy

img of 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:

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

  1. Navigate to your repository on GitHub.
  2. Go to “Settings” > “Environments”.
  3. Create a new environment named ‘production’

Add Secrets

To add secrets:

  1. Navigate to your repository on GitHub.
  2. Go to “Settings” > “Environments” > Select “production”.
  3. Go to “Environment secrets” and select “Add environment secret”
  4. 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)