Automatiser avec GitHub Actions

Les GitHub actions permettent d’automatiser des workflows et notamment les CI/CD. Dans le cadre de Terraform les objectifs sont :

  1. Valider le style et le format des fichiers

  2. Effectuer l’analyse du code

  3. Générer la planification

  4. Déployer la configuration

Les actions effectuées par le workflow dépendent du contexte.

  1. Un push sur une branche

  2. Une création de Pull Requests

  3. Un merge sur la branche main

Un push sur main déclencherait la mise à jour de l’infrastructure sans validation. Le fait de rendre les PR obligatoires pour cette branche permet d’éviter cela (Settings  Branches  Add Branch protection rule puis Require a pull request before merging).

Push

Lors d’un push, le but est de vérifier que les modifications du code répondent aux standards de développement et ne cassent pas l’existant.

Workflow de _push_
Figure 1. Workflow de push
  • Checkout : récupération du repo dans l’espace de travail

        - name: Checkout
          uses: actions/checkout@v3
  • Analyse de code statique : L’analyse statique du code peut être effectuée directement sur le code de configuration Terraform. Elle est utile pour détecter des problèmes de sécurité ou des incohérences. Ces tests ne nécessitent pas la création d’un plan d’exécution ou d’un déploiement, et s’exécutent rapidement. Ils sont généralement exécutés en premier dans le processus d’intégration continue. Différentes solutions existent : Checkov, Terrascan, tfsec, Deepsource

        - name: Checkov
          id: checkov
          uses: bridgecrewio/checkov-action@master (1)
          with:
            quiet: true
            framework: terraform
            output_format: github_failed_only
            soft_fail: false
            skip_check: CKV_AZURE_35,CKV2_AZURE_8,CKV2_AZURE_18,CKV2_AZURE_1,CKV2_AZURE_21 (2)
    1 Checkov GitHub action
    2 Ignorer certaines recommandations
  • Installation de la CLI Terraform

        - name: Setup Terraform
          uses: hashicorp/setup-terraform@v2
          with:
            terraform_version: '>=1.2.1'
  • Mise en forme : Vérifie que les fichiers sont correctement formatés

        - name: Terraform Format
          id: fmt
          run: terraform fmt -check (1)
    1 -check retourne un code d’erreur si le format n’est pas correct
  • Init : Initialisation de la configuration

        - name: Terraform Init
          id: init
          env:
            ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
            ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
            ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
            ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
          run: terraform init -input=false -no-color (1) (2)
    1 -input=false Lève une erreur si une entrée est attendue
    2 -no-color évite la mise en forme qui est mal gérée par GitHub
  • Validate : Validation de la configuration (utilisation d’une propriété obsolète…​)

        - name: Terraform Validate
          id: validate
          run: terraform validate -no-color (1)

Pull request

Pour une Pull request on ajoute la génération du plan ainsi que la génération d’une synthèse directement dans la PR.

Workflow d’une _PR_
Figure 2. Workflow d’une PR
  • Plan : génération du plan

        - name: Terraform Plan
          id: plan
          env:
            ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
            ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
            ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
            ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
          if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' && github.event_name == 'push' (1)
          run: terraform plan -out=tfplan -no-color -input=false
    <.> Etape exécutée lors d'une _PR_ ou d'un _push_ sur `main`
  • Commenter la PR : génération d’une synthèse dans les commentaires de la PR

        - name: add-plan-comment
          id: comment
          uses: actions/github-script@v3
          if: github.event_name == 'pull_request' && (success() || failure()) (1)
          env:
            PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" (2)
          with:
            github-token: ${{ secrets.GITHUB_TOKEN }} (3)
            script: |
              const output =`#### Checkov 🧪\`${{ steps.checkov.outcome }}\`
              #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
              #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
              #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
              #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
    
              <details><summary>Show Checkov Results</summary>
    
              \`\`\`\n
              ${process.env.CHECKOV_RESULTS}
              \`\`\`
    
              </details>
    
              <details><summary>Show Plan</summary>
    
              \`\`\`\n
              ${process.env.PLAN}
              \`\`\`
    
              </details>
    
              *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Workflow: \`${{ github.workflow }}\`*`;
    
              github.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: output
              })
    1 Etape exécutée même si les étapes précédentes ont échoué
    2 Sortie de l’étape plan
    3 Authentification avec token pré existant pour pouvoir commenter la PR

Merge sur main

La validation de la PR génère un push sur la branche cible. Dans le cas de la branche main, on effectue la mise à jour de l’infrastructure.

Workflow d’un merge sur `main`
Figure 3. Workflow d’un merge sur main
  • Apply : mise à jour de l’infrastructure

        - name: Terraform Apply
          if: github.ref == 'refs/heads/main' && github.event_name == 'push'
          env:
            ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
            ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
            ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
            ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
          run: terraform apply -input=false tfplan
En production il est conseillé de mettre en place une règle de protection sur la branche pour confirmer les déploiements.