Template de pipeline Azure DevOps générique pour Terraform

Template de pipeline Azure DevOps générique pour Terraform

Voici un template de pipeline Azure DevOps que j'ai conçu pour simplifier le déploiement d'infrastructures sur Microsoft Azure en utilisant Terraform.

Le template Terraform

Voici un modèle prêt à l'emploi fournit une approche cohérente et automatisée pour gérer l'infrastructure en tant que code, tout en exploitant les fonctionnalités d'Azure DevOps.

Ce template repose sur les principes de l'intégration continue et du déploiement continu (CI/CD) pour offrir une méthode robuste et fiable de déploiement d'infrastructures Azure. Il permet aux équipes de développement et d'exploitation de collaborer de manière transparente, en automatisant le provisionnement, la configuration et la mise à jour des ressources Azure.

💡
Que vous soyez une petite équipe ou une entreprise de grande envergure, ce template de pipeline Azure DevOps générique pour Terraform facilite grandement l'adoption de bonnes pratiques DevOps et accélère votre cycle de déploiement.

En utilisant les capacités de gestion de version d'Azure DevOps, vous pouvez également suivre et contrôler les changements apportés à votre infrastructure au fil du temps, assurant ainsi la traçabilité et la gouvernance nécessaires.

parameters:
- name: terraformPath
  type: string
- name: action
  displayName: Action
  type: string
  default: validate
  values:
  - validate
  - plan
  - apply
  - destroy
- name: terraformVersion
  displayName: Version
  type: string
  default: '1.5.1'
- name: 'backendServiceArm'
  displayName: 'Azure service connection name'
  type: string
- name: 'backendAzureRmResourceGroupName'
  displayName: 'Ressource group name for the storage account'
  type: string
- name: 'backendAzureRmStorageAccountName'
  displayName: 'Storage account name'
  type: string
- name: 'backendAzureRmContainerName'
  displayName: 'Container name'
  type: string
  default: 'tfstates'
- name: 'backendAzureRmKey'
  displayName: 'Terraform state filename'
  type: string

jobs:
- job: "Terraform"
  displayName: "Terraform ${{ parameters.action}} 🚀"
  steps:
  - task: TerraformInstaller@0
    displayName: "Install Terraform"
    inputs:
      terraformVersion: ${{ parameters.terraformVersion }}
      terraformDownloadLocation: 'https://releases.hashicorp.com/terraform'
  - task: TerraformTaskV4@4
    displayName: 'Terrafomr init'
    inputs:
      provider: 'azurerm'
      command: 'init'
      backendServiceArm: ${{ parameters.backendServiceArm }}
      backendAzureRmResourceGroupName: ${{ parameters.backendAzureRmResourceGroupName }}
      backendAzureRmStorageAccountName: ${{ parameters.backendAzureRmStorageAccountName }}
      backendAzureRmContainerName: ${{ parameters.backendAzureRmContainerName}}
      backendAzureRmKey: ${{ parameters.backendAzureRmKey}}
      workingDirectory: $(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}
      ensureBackend: true
  - task: TerraformTaskV4@4
    condition: eq('${{ parameters.action }}', 'validate')
    displayName: 'Terraform validate'
    inputs:
      command: validate
  - task: TerraformTaskV4@4
    condition: |
      or(
          eq('${{ parameters.action }}', 'apply'),
          eq('${{ parameters.action }}', 'plan')
      )
    name: terraformPlan
    displayName: 'Terraform plan'
    inputs:
      command: plan
      commandOptions: "-out=tfplan"
      environmentServiceNameAzureRM: ${{ parameters.backendServiceArm }}
      publishPlanResults: 'Terraform Plan'
      workingDirectory: $(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}
  - task: TerraformTaskV4@4
    condition: |
      or(
          eq('${{ parameters.action }}', 'apply'),
          eq('${{ parameters.action }}', 'plan')
      )
    displayName: 'Terraform show'
    inputs:
      provider: 'azurerm'
      command: 'show'
      commandOptions: '$(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}/tfplan'
      outputTo: 'file'
      outputFormat: 'json'
      fileName: '$(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}/tfplan.json'
      environmentServiceNameAzureRM: ${{ parameters.backendServiceArm }}
      workingDirectory: $(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}    
  - task: PublishPipelineArtifact@1
    condition: |
      or(
          eq('${{ parameters.action }}', 'apply'),
          eq('${{ parameters.action }}', 'plan')
      )
    inputs:
      targetPath: $(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}/tfplan.json
      artifact: 'Plan'
      publishLocation: 'pipeline' 
  - task: TerraformTaskV4@4
    condition: eq('${{ parameters.action }}', 'apply')
    displayName: 'Terraform apply'
    inputs:
      command: apply
      commandOptions: "tfplan"
      condition: eq(variables['terraformPlan.changesPresent'], 'true')
      environmentServiceNameAzureRM: ${{ parameters.backendServiceArm }}
      workingDirectory: $(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}
  - task: TerraformTaskV4@4
    condition: eq('${{ parameters.action }}', 'destroy')
    displayName: 'Terraform destroy'
    inputs:
      command: destroy   
      environmentServiceNameAzureRM: ${{ parameters.backendServiceArm }}
      workingDirectory: $(System.DefaultWorkingDirectory)/${{ parameters.terraformPath}}

Comment l'exploiter

Voici un exemple simple pour appeler le template précédent depuis un pipeline Azure DevOps. Dans ma solution, j'ai positionné le template dans une arborescence "./templates/terraform.yaml" et mes scripts d'IaC Terraform sont présents dans le répertoire "./terraform".

name: Deploy IaC $(BuildDefinitionName)_$(date:yyyyMMdd)$(rev:.r)
trigger:
  branches:
    include:
      - main
      - feature/*
  paths:
    exclude:
      - pipelines/*
pool:
  vmImage: ubuntu-latest # This is the default if you don't specify a pool or vmImage.
variables:
  - name: TFSource-Path
    value: "./terraform/"
stages:        
  - stage: "Apply"
    displayName: "DEV - Deploy Infrasctruture using Terraform ✅"
    jobs:
      - template: templates/terraform.yaml
        parameters:
          action: apply
          terraformPath: $(TFSource-Path)
          backendServiceArm: 'Dev-Demo-Azdotemplate'
          backendAzureRmResourceGroupName: 'rg-demo-azdotemplate'
          backendAzureRmStorageAccountName: 'sastatesdemoazdotemplatetf'
          backendAzureRmContainerName: 'tfstate'
          backendAzureRmKey: 'demoazdotemplate.dev.tfstate'

Cette solution me permet d'enchainer mes différents environnements en exécutant le même modèle de déploiement et même me permet de faire appel à d'autre processus.

Par exemple:

Je peux ajouter une brique SAST (Checkov) dont voici le template :

parameters:
- name: terraformPath
  type: string

jobs:
- job: "Checkov"
  displayName: "Checkov > Pull, run and publish results of Checkov scan"
  steps:
  - bash: |
      docker pull bridgecrew/checkov
      docker run --volume $(pwd):/tf bridgecrew/checkov --directory /tf --output junitxml --soft-fail > $(pwd)/Checkov-Report.xml
    workingDirectory: '${{ parameters.terraformPath }}'
    failOnStderr: false
    displayName: "Pull & Run > bridgecrew/checkov"
    name: CheckovScan
    condition: always()        

  - task: PublishTestResults@2
    condition: succeededOrFailed()
    inputs:
      testResultsFormat: "JUnit"
      testResultsFiles: "Checkov-Report.xml"
      searchFolder: "${{ parameters.terraformPath }}"
      testRunTitle: "Checkov Results"
      mergeTestResults: false
      failTaskOnFailedTests: false
      publishRunAttachments: true            
    displayName: "Publish > Checkov scan results"

  # Clean up any of the containers / images that were used for quality checks
  - bash: |
      docker rmi "bridgecrew/checkov:latest" -f | true
    displayName: 'Clean > Remove Checkvov docker image'
    condition: always()

Que je référence en amont de mon déploiement terraform dans mon pipeline

name: Deploy IaC $(BuildDefinitionName)_$(date:yyyyMMdd)$(rev:.r)
trigger:
  branches:
    include:
      - main
      - feature/*
  paths:
    exclude:
      - pipelines/*
pool:
  vmImage: ubuntu-latest # This is the default if you don't specify a pool or vmImage.
variables:
  - name: TFSource-Path
    value: "./terraform/"
stages:        
  - stage: "SAST"
    displayName: "Static code analysis using Checkov 👮‍♀️"
    jobs:
      - template: templates/checkov.yaml
        parameters:
          terraformPath: $(TFSource-Path)
  - stage: "Apply"
    displayName: "DEV - Deploy Infrasctruture using Terraform ✅"
    jobs:
      - template: templates/terraform.yaml
        parameters:
          action: apply
          terraformPath: $(TFSource-Path)
          backendServiceArm: 'Dev-Demo-Azdotemplate'
          backendAzureRmResourceGroupName: 'rg-demo-azdotemplate'
          backendAzureRmStorageAccountName: 'sastatesdemoazdotemplatetf'
          backendAzureRmContainerName: 'tfstate'
          backendAzureRmKey: 'demoazdotemplate.dev.tfstate'

J'espére que cet article vous a fourni des informations précieuses et vous a aidé à mieux comprendre l'importance d'une approche automatisée et cohérente pour le déploiement d'infrastructures sur Azure. Je suis convaincus que l'utilisation de ce template de pipeline facilitera vos processus de déploiement, tout en vous offrant une plus grande agilité et une meilleure gouvernance.

Si vous avez des questions supplémentaires ou si vous souhaitez explorer davantage le sujet, n'hésitez pas à me contacter via linkedIn ou en laissant un commentaire.

💡
Encore une fois, merci d'avoir choisi de lire mon article. Votre intérêt et votre soutien m'encourage à continuer à partager des connaissances et à vous fournir des ressources utiles pour vos projets techniques.

Did you find this article valuable?

Support Antoine LOIZEAU by becoming a sponsor. Any amount is appreciated!