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:
- Desenvolupador fa canvis: Modifica fitxers
.tfen una branca - Pull Request: Crea una PR amb els canvis
- CI executa terraform plan: Automàticament mostra quins canvis es farien
- Revisió: L'equip revisa els canvis proposats
- Aprovació: Després d'aprovar la PR
- Merge: Es fa merge a la branca principal
- 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))
}