From 5cb4b0af1108ea855330cfbff3d605a0143a4286 Mon Sep 17 00:00:00 2001 From: Karissa Jacobsen Date: Wed, 14 Jan 2026 14:29:51 -0800 Subject: [PATCH] Add CI/CD updates --- .github/workflows/terraform-deploy.yml | 104 ++++++++++++++++++ terraform/azure/main.tf | 1 + terraform/azure/variables.tf | 6 + terraform/common/modules/template/main.tf | 8 +- terraform/common/modules/template/outputs.tf | 2 +- .../common/modules/template/variables.tf | 11 ++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/terraform-deploy.yml diff --git a/.github/workflows/terraform-deploy.yml b/.github/workflows/terraform-deploy.yml new file mode 100644 index 0000000..0569adc --- /dev/null +++ b/.github/workflows/terraform-deploy.yml @@ -0,0 +1,104 @@ +name: Terraform CI/CD + +on: + pull_request: + branches: [deploy] + push: + branches: [deploy] + +permissions: + id-token: write + contents: write + pull-requests: write + +env: + TF_VAR_application_name: ${{ vars.APPLICATION_NAME }} + TF_VAR_application_oauth_client_id: ${{ secrets.APPLICATION_OAUTH_CLIENT_ID }} + TF_VAR_application_oauth_client_secret: ${{ secrets.APPLICATION_OAUTH_CLIENT_SECRET }} + TF_VAR_location: ${{ vars.AZURE_LOCATION }} + TF_VAR_execution_mode: ci + ACR_NAME: ${{ vars.ACR_NAME }} + +jobs: + terraform: + runs-on: ubuntu-latest + + steps: + # Checkout repo + - name: Checkout repository + uses: actions/checkout@v4 + + # Install Terraform + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: 1.10.5 + + # Temporarily remove repo provider configuration + - name: Disable repo azurerm provider + working-directory: terraform/azure + run: mv providers.tf providers.tf.bak + + # Create CI-only backend + OIDC provider + - name: Create CI Azure backend/provider + working-directory: terraform/azure + run: | + cat < azure_ci.tf + terraform { + backend "azurerm" {} + } + + provider "azurerm" { + features {} + } + EOF + + - name: Configure Azure OIDC environment + run: | + echo "ARM_USE_OIDC=true" >> $GITHUB_ENV + echo "ARM_CLIENT_ID=${{ secrets.AZURE_CLIENT_ID }}" >> $GITHUB_ENV + echo "ARM_TENANT_ID=${{ secrets.AZURE_TENANT_ID }}" >> $GITHUB_ENV + echo "ARM_SUBSCRIPTION_ID=${{ secrets.AZURE_SUBSCRIPTION_ID }}" >> $GITHUB_ENV + + - name: Azure Login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Login to ACR + run: | + TOKEN=$(az acr login --name ${{ env.ACR_NAME }} --expose-token --output tsv --query accessToken) + docker login -u 00000000-0000-0000-0000-000000000000 --password-stdin ${{ env.ACR_NAME }}.azurecr.io <<< $TOKEN + + - name: Terraform Init + working-directory: terraform/azure + run: | + terraform init -upgrade -reconfigure\ + -backend-config="resource_group_name=${{ vars.AZURE_RG }}" \ + -backend-config="storage_account_name=${{ vars.AZURE_STORAGE_ACCOUNT }}" \ + -backend-config="container_name=${{ vars.AZURE_CONTAINER }}" \ + -backend-config="key=${{ vars.AZURE_TFSTATE_KEY }}" \ + -backend-config="use_oidc=true" + + # Terraform plan (PR only) + - name: Terraform Plan + if: github.event_name == 'pull_request' + working-directory: terraform/azure + run: terraform plan -out=tfplan + + # Terraform apply (merge to main only) + - name: Terraform Apply + if: github.event_name == 'push' + working-directory: terraform/azure + run: | + terraform plan -out=tfplan + terraform apply -auto-approve tfplan + + # Cleanup CI-only files + - name: Cleanup CI Terraform files + working-directory: terraform/azure + run: | + rm -f azure_ci.tf + mv providers.tf.bak providers.tf \ No newline at end of file diff --git a/terraform/azure/main.tf b/terraform/azure/main.tf index a52ca5d..9e5b0bf 100644 --- a/terraform/azure/main.tf +++ b/terraform/azure/main.tf @@ -61,6 +61,7 @@ module "manifest" { "${basename(dirname(var.manifest_files_paths[count.index]))}.${basename(var.manifest_files_paths[count.index])}" ]) + execution_mode = var.execution_mode client_id = local.application_oauth_client_id client_secret = local.application_oauth_client_secret base_url = local.application_service_url diff --git a/terraform/azure/variables.tf b/terraform/azure/variables.tf index f3ae5fa..cde74d7 100644 --- a/terraform/azure/variables.tf +++ b/terraform/azure/variables.tf @@ -274,3 +274,9 @@ variable "tags" { description = "A map of the tags to apply to various resources" default = {} } + +variable "execution_mode" { + description = "Execution mode: 'local' or 'ci'" + type = string + default = "local" +} \ No newline at end of file diff --git a/terraform/common/modules/template/main.tf b/terraform/common/modules/template/main.tf index 9842f95..e5b06d1 100644 --- a/terraform/common/modules/template/main.tf +++ b/terraform/common/modules/template/main.tf @@ -25,11 +25,15 @@ locals { ]... ) ]) - - output_file_path = nonsensitive(local_sensitive_file.this.filename != "/dev/null" ? local_sensitive_file.this.filename : "") } resource "local_sensitive_file" "this" { + count = var.execution_mode == "local" ? 1 : 0 content = local.output_file_content filename = local.enabled ? var.output_file_path : "/dev/null" } + +output "manifest_file_path" { + description = "The intended output file path" + value = var.output_file_path +} diff --git a/terraform/common/modules/template/outputs.tf b/terraform/common/modules/template/outputs.tf index c9a5050..8e32e66 100644 --- a/terraform/common/modules/template/outputs.tf +++ b/terraform/common/modules/template/outputs.tf @@ -1,4 +1,4 @@ output "output_file_path" { description = "The absolute path to the output file" - value = local.output_file_path + value = var.output_file_path } diff --git a/terraform/common/modules/template/variables.tf b/terraform/common/modules/template/variables.tf index 53d62ee..34da19a 100644 --- a/terraform/common/modules/template/variables.tf +++ b/terraform/common/modules/template/variables.tf @@ -1,3 +1,14 @@ +variable "execution_mode" { + description = "Controls whether Terraform writes manifests to disk (local) or only renders them (ci)" + type = string + default = "local" + + validation { + condition = contains(["local", "ci"], var.execution_mode) + error_message = "execution_mode must be 'local' or 'ci'." + } +} + variable "input_file_path" { description = "The absolute path to the input file. If it doesn't exist, the module will not do anything" type = string