Salta el contingut

Integració amb CI/CD i GitOps

10. Integració amb CI/CD i GitOps

Terraform cobra tot el seu potencial quan s'integra amb pipelines de CI/CD, permetent gestionar la infraestructura amb el mateix rigor que el codi de les aplicacions.

GitOps: Infraestructura com a Codi en Pràctica

GitOps és una metodologia que utilitza Git com a única font de veritat per a la infraestructura. Cada canvi a la infraestructura comença amb un commit a Git, passa per revisió de codi (pull request), i només s'aplica després d'aprovació.

El workflow típic és:

  1. Desenvolupador fa canvis: Modifica fitxers .tf en una branca
  2. Pull Request: Crea una PR amb els canvis
  3. CI executa terraform plan: Automàticament mostra quins canvis es farien
  4. Revisió: L'equip revisa els canvis proposats
  5. Aprovació: Després d'aprovar la PR
  6. Merge: Es fa merge a la branca principal
  7. CD executa terraform apply: Automàticament aplica els canvis

Integració amb GitHub Actions

Exemple de workflow complet (.github/workflows/terraform.yml):

name: Terraform CI/CD

on:
  pull_request:
    branches: [main]
    paths:
      - 'terraform/**'
      - '.github/workflows/terraform.yml'
  push:
    branches: [main]
    paths:
      - 'terraform/**'

env:
  TF_VERSION: '1.9.0'
  AWS_REGION: 'eu-west-1'

jobs:
  terraform-validate:
    name: Validate Terraform
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Format Check
        working-directory: ./terraform/environments/production
        run: terraform fmt -check -recursive

      - name: Terraform Init
        working-directory: ./terraform/environments/production
        run: terraform init -backend=false

      - name: Terraform Validate
        working-directory: ./terraform/environments/production
        run: terraform validate

  terraform-security:
    name: Security Scan
    runs-on: ubuntu-latest
    needs: terraform-validate

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.0
        with:
          working_directory: ./terraform
          soft_fail: false

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: ./terraform
          framework: terraform
          soft_fail: false

  terraform-plan:
    name: Terraform Plan
    runs-on: ubuntu-latest
    needs: [terraform-validate, terraform-security]
    if: github.event_name == 'pull_request'

    permissions:
      contents: read
      pull-requests: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        working-directory: ./terraform/environments/production
        run: terraform init

      - name: Terraform Plan
        working-directory: ./terraform/environments/production
        id: plan
        run: |
          terraform plan -no-color -out=tfplan
          terraform show -no-color tfplan > plan.txt

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('./terraform/environments/production/plan.txt', 'utf8');
            const output = `#### Terraform Plan 📝

            <details><summary>Show Plan</summary>

            \`\`\`terraform
            ${plan}
            \`\`\`

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Workflow: \`${{ github.workflow }}\`*`;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            });

      - name: Upload Plan Artifact
        uses: actions/upload-artifact@v3
        with:
          name: tfplan
          path: ./terraform/environments/production/tfplan

  terraform-apply:
    name: Terraform Apply
    runs-on: ubuntu-latest
    needs: [terraform-validate, terraform-security]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'

    environment:
      name: production
      url: https://app.techstart.com

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        working-directory: ./terraform/environments/production
        run: terraform init

      - name: Terraform Apply
        working-directory: ./terraform/environments/production
        run: terraform apply -auto-approve

      - name: Notify Slack on Success
        if: success()
        uses: 8398a7/action-slack@v3
        with:
          status: success
          text: '✅ Terraform apply completat amb èxit a producció'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

      - name: Notify Slack on Failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: failure
          text: '❌ Terraform apply ha fallat a producció'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

Integració amb GitLab CI/CD

Exemple de .gitlab-ci.yml:

stages:
  - validate
  - plan
  - apply

variables:
  TF_VERSION: "1.9.0"
  TF_ROOT: ${CI_PROJECT_DIR}/terraform/environments/production

.terraform-base:
  image: hashicorp/terraform:$TF_VERSION
  before_script:
    - cd $TF_ROOT
    - terraform --version
    - terraform init

validate:
  extends: .terraform-base
  stage: validate
  script:
    - terraform fmt -check -recursive
    - terraform validate
  only:
    changes:
      - terraform/**/*

security-scan:
  stage: validate
  image: aquasec/tfsec:latest
  script:
    - tfsec terraform/ --format json > tfsec-report.json
  artifacts:
    reports:
      sast: tfsec-report.json
    paths:
      - tfsec-report.json
    when: always
  only:
    changes:
      - terraform/**/*

plan:
  extends: .terraform-base
  stage: plan
  script:
    - terraform plan -out=tfplan
    - terraform show -json tfplan > plan.json
  artifacts:
    paths:
      - $TF_ROOT/tfplan
      - $TF_ROOT/plan.json
    expire_in: 1 week
  only:
    - merge_requests
    - main

apply:
  extends: .terraform-base
  stage: apply
  script:
    - terraform apply -auto-approve
  dependencies:
    - plan
  environment:
    name: production
    url: https://app.techstart.com
  only:
    - main
  when: manual

Terraform Cloud i Automation

Terraform Cloud és una plataforma SaaS de HashiCorp que proporciona execució remota, gestió d'estat, i col·laboració en equip:

terraform {
  cloud {
    organization = "techstart"

    workspaces {
      name = "production-infrastructure"
    }
  }

  required_version = ">= 1.0"
}

Característiques de Terraform Cloud:

  • Remote State Management: State emmagatzemat de manera segura i encriptat
  • Remote Execution: Terraform s'executa en entorn gestionat, no en el teu portàtil
  • VCS Integration: Integració directa amb GitHub/GitLab/Bitbucket
  • Policy as Code: Sentinel policies per assegurar conformitat
  • Cost Estimation: Estimació de costos abans d'aplicar canvis
  • Private Registry: Mòduls i providers privats

Referència oficial: Documentació de Terraform Cloud: https://developer.hashicorp.com/terraform/cloud-docs

Policy as Code amb Sentinel

Sentinel permet definir polítiques que s'executen abans d'aplicar canvis:

# Política: Tots els recursos S3 han de tenir encriptació
import "tfplan/v2" as tfplan

# Obtenir tots els buckets S3
s3_buckets = filter tfplan.resource_changes as _, rc {
    rc.type is "aws_s3_bucket" and
    rc.mode is "managed" and
    (rc.change.actions contains "create" or rc.change.actions contains "update")
}

# Regla: server_side_encryption_configuration ha d'estar configurat
bucket_encryption = rule {
    all s3_buckets as _, bucket {
        bucket.change.after.server_side_encryption_configuration is not null
    }
}

main = rule {
    bucket_encryption
}

Testing d'Infraestructura

Pots testejar el teu codi de Terraform amb Terratest:

// test/terraform_test.go
package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestTerraformVPC(t *testing.T) {
    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../terraform/modules/networking",
        Vars: map[string]interface{}{
            "project_name": "test",
            "environment":  "test",
            "vpc_cidr":     "10.0.0.0/16",
            "availability_zones": []string{"eu-west-1a", "eu-west-1b"},
        },
    })

    defer terraform.Destroy(t, terraformOptions)

    terraform.InitAndApply(t, terraformOptions)

    // Verificar outputs
    vpcID := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcID)

    publicSubnets := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    assert.Equal(t, 2, len(publicSubnets))
}