diff --git a/terraform/pipeline/azure-pipelines.yml b/terraform/pipeline/azure-pipelines.yml index 9ec60c77..16b7aa3c 100644 --- a/terraform/pipeline/azure-pipelines.yml +++ b/terraform/pipeline/azure-pipelines.yml @@ -1,26 +1,40 @@ trigger: - # automatically runs on pull requests - # https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#pr-triggers branches: include: - dev - test - prod + tags: + include: + - 20??.??.?*-rc?* + - 20??.??.?* # only run for changes to Terraform files paths: include: - terraform/* + +pr: + branches: + include: + - "*" + paths: + include: + - terraform/* + +pool: + vmImage: ubuntu-latest + stages: - - stage: terraform - pool: - vmImage: ubuntu-latest + - stage: TerraformPlan jobs: - - job: terraform + - job: Plan variables: - name: OTHER_SOURCE value: $[variables['System.PullRequest.SourceBranch']] - name: INDIVIDUAL_SOURCE value: $[variables['Build.SourceBranchName']] + - name: IS_TAG + value: $[startsWith(variables['Build.SourceBranch'], 'refs/tags/')] - name: TARGET value: $[variables['System.PullRequest.TargetBranch']] steps: @@ -28,7 +42,11 @@ stages: # https://learn.microsoft.com/en-us/azure/devops/pipelines/process/set-variables-scripts?view=azure-devops&tabs=bash#about-tasksetvariable - bash: | WORKSPACE=$(python terraform/pipeline/workspace.py) - echo "##vso[task.setvariable variable=workspace]$WORKSPACE" + echo "##vso[task.setvariable variable=workspace;isOutput=true]$WORKSPACE" + + TAG_TYPE=$(python terraform/pipeline/tag.py) + echo "##vso[task.setvariable variable=tag_type;isOutput=true]$TAG_TYPE" + name: setvars displayName: Determine deployment environment env: REASON: $(Build.Reason) @@ -59,7 +77,7 @@ stages: provider: azurerm command: custom customCommand: workspace - commandOptions: select $(workspace) + commandOptions: select $(setvars.workspace) workingDirectory: "$(System.DefaultWorkingDirectory)/terraform" # service connection environmentServiceNameAzureRM: deployer @@ -70,21 +88,88 @@ stages: command: plan # wait for lock to be released, in case being used by another pipeline run # https://discuss.hashicorp.com/t/terraform-plan-wait-for-lock-to-be-released/6870/2 - commandOptions: -input=false -lock-timeout=5m - workingDirectory: "$(System.DefaultWorkingDirectory)/terraform" - # service connection - environmentServiceNameAzureRM: deployer - # the plan is done as part of the apply (below), so don't bother doing it twice - condition: notIn(variables['Build.SourceBranchName'], 'dev', 'test', 'prod') - - task: TerraformTaskV3@3 - displayName: Terraform apply - inputs: - provider: azurerm - command: apply - # (ditto the lock comment above) - commandOptions: -input=false -lock-timeout=5m + commandOptions: -input=false -lock-timeout=5m -out=$(Build.ArtifactStagingDirectory)/tfplan workingDirectory: "$(System.DefaultWorkingDirectory)/terraform" # service connection environmentServiceNameAzureRM: deployer - # only run on certain branches - condition: in(variables['Build.SourceBranchName'], 'dev', 'test', 'prod') + # need to publish the tfplan to used by next stage if it's going to run + - publish: "$(Build.ArtifactStagingDirectory)" + displayName: "Publish tfplan for use in TerraformApply" + artifact: savedPlan + condition: | + or( + in(variables['Build.SourceBranchName'], 'dev', 'test', 'prod'), + eq(variables['setvars.tag_type'], 'test'), + eq(variables['setvars.tag_type'], 'prod') + ) + - stage: TerraformApply + dependsOn: TerraformPlan + variables: + - name: workspace + value: $[ dependencies.TerraformPlan.outputs['Plan.setvars.workspace'] ] + - name: tag_type + value: $[ dependencies.TerraformPlan.outputs['Plan.setvars.tag_type'] ] + # only run on dev, test, or prod branches OR if it's a tag for test or prod + condition: | + or( + in(variables['Build.SourceBranchName'], 'dev', 'test', 'prod'), + eq(variables['tag_type'], 'test'), + eq(variables['tag_type'], 'prod') + ) + jobs: + - deployment: Apply + condition: succeeded() + environment: Approval + variables: + - name: workspace + value: $[ stageDependencies.TerraformPlan.Plan.outputs['setvars.workspace'] ] + - name: tag_type + value: $[ stageDependencies.TerraformPlan.Plan.outputs['setvars.tag_type'] ] + strategy: + runOnce: + deploy: + steps: + - checkout: self + - download: current + displayName: "Download plan file published from TerraformPlan" + artifact: savedPlan + - task: TerraformInstaller@0 + displayName: Install Terraform + inputs: + terraformVersion: 1.3.1 + # https://github.com/microsoft/azure-pipelines-terraform/tree/main/Tasks/TerraformTask/TerraformTaskV3#readme + - task: TerraformTaskV3@3 + displayName: Terraform init + inputs: + provider: azurerm + command: init + workingDirectory: "$(System.DefaultWorkingDirectory)/terraform" + # https://developer.hashicorp.com/terraform/tutorials/automation/automate-terraform#automated-terraform-cli-workflow + commandOptions: -input=false + # service connection + backendServiceArm: deployer + # needs to match main.tf + backendAzureRmResourceGroupName: courtesy-cards-eligibility-terraform + backendAzureRmStorageAccountName: courtesycardsterraform + backendAzureRmContainerName: tfstate + backendAzureRmKey: terraform.tfstate + - task: TerraformTaskV3@3 + displayName: Select environment + inputs: + provider: azurerm + command: custom + customCommand: workspace + commandOptions: select $(workspace) + workingDirectory: "$(System.DefaultWorkingDirectory)/terraform" + # service connection + environmentServiceNameAzureRM: deployer + - task: TerraformTaskV3@3 + displayName: Terraform apply + inputs: + provider: azurerm + command: apply + # (ditto the lock comment above) + commandOptions: -input=false -lock-timeout=5m $(Pipeline.Workspace)/savedPlan/tfplan + workingDirectory: "$(System.DefaultWorkingDirectory)/terraform" + # service connection + environmentServiceNameAzureRM: deployer diff --git a/terraform/pipeline/tag.py b/terraform/pipeline/tag.py new file mode 100644 index 00000000..4c33f209 --- /dev/null +++ b/terraform/pipeline/tag.py @@ -0,0 +1,19 @@ +import os +import re + +REASON = os.environ["REASON"] +# use variable corresponding to tag triggers +SOURCE = os.environ["INDIVIDUAL_SOURCE"] +IS_TAG = os.environ["IS_TAG"].lower() == "true" + +if REASON == "IndividualCI" and IS_TAG: + if re.fullmatch(r"20\d\d.\d\d.\d+-rc\d+", SOURCE): + tag_type = "test" + elif re.fullmatch(r"20\d\d.\d\d.\d+", SOURCE): + tag_type = "prod" + else: + tag_type = None +else: + tag_type = None + +print(tag_type) diff --git a/terraform/pipeline/workspace.py b/terraform/pipeline/workspace.py index 526296e8..b63d23ca 100644 --- a/terraform/pipeline/workspace.py +++ b/terraform/pipeline/workspace.py @@ -1,10 +1,12 @@ import os +import re import sys REASON = os.environ["REASON"] # the name of the variable that Azure Pipelines uses for the source branch depends on the type of run, so need to check both SOURCE = os.environ.get("OTHER_SOURCE") or os.environ["INDIVIDUAL_SOURCE"] TARGET = os.environ["TARGET"] +IS_TAG = os.environ["IS_TAG"].lower() == "true" # the branches that correspond to environments ENV_BRANCHES = ["dev", "test", "prod"] @@ -15,6 +17,10 @@ elif REASON in ["IndividualCI", "Manual"] and SOURCE in ENV_BRANCHES: # it's being run on one of the environment branches, so use that environment = SOURCE +elif REASON in ["IndividualCI"] and IS_TAG and re.fullmatch(r"20\d\d.\d\d.\d+-rc\d+", SOURCE): + environment = "test" +elif REASON in ["IndividualCI"] and IS_TAG and re.fullmatch(r"20\d\d.\d\d.\d+", SOURCE): + environment = "prod" else: # default to running against dev environment = "dev"