GitOps e Qualidade: Infraestrutura como Código Testável

Introdução a Qualidade de Software · 6 de julho de 2026

📖 6 min de leitura

O Que é GitOps?

GitOps usa Git como fonte única de verdade para infraestrutura e configurações. Changes são feitos via Git commits, não manualmente.

Princípios

  1. Tudo no Git – Código, configs, infraestrutura
  2. Automação – CI/CD reconcile com Git
  3. Idempotência – mesmo resultado sempre
  4. Auditoria – histórico completo de mudanças

Terraform – Infraestrutura como Código

Setup

# main.tf
terraform {
  required_version = ">= 1.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}

backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}

provider "aws" {
region = var.aws_region
}

# Variables
variable "aws_region" {
description = "AWS region"
type = string
default = "us-east-1"
}

variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}

Recursos

# modules/api-service/main.tf
variable "environment" {}
variable "app_name" {}
variable "desired_count" { default = 2 }
variable "instance_type" { default = "t3.micro" }

resource "aws_ecs_cluster" "main" {
name = "${var.app_name}-${var.environment}"

setting {
name = "containerInsights"
value = "enabled"
}
}

resource "aws_ecs_task_definition" "api" {
family = "${var.app_name}-api"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"

container_definitions = jsonencode([
{
name = "api"
image = "${var.app_name}:latest"
essential = true

portMappings = [{
containerPort = 8080
protocol = "tcp"
}]

environment = [
{ name = "ENV", value = var.environment }
]

logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/${var.app_name}"
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "ecs"
}
}
}
])
}

resource "aws_ecs_service" "api" {
name = "${var.app_name}-api-${var.environment}"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = var.desired_count

deployment_controller {
type = "ECS"
}

deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100

load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}

depends_on = [aws_lb.api]
}


Testando Infraestrutura

Terratest (Go)

// api_service_test.go
package test

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

func TestAPIService(t *testing.T) {
// Setup
terraformOptions := &terraform.Options{
TerraformDir: "../modules/api-service",
Vars: map[string]interface{}{
"environment": "test",
"app_name": "myapp",
"desired_count": 2,
},
BackendConfig: map[string]interface{}{
"bucket": "my-test-tfstate",
"key": "test/terraform.tfstate",
},
}

defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)

// Get outputs
clusterName := terraform.Output(t, terraformOptions, "cluster_name")
serviceName := terraform.Output(t, terraformOptions, "service_name")

// Assertions
assert.NotEmpty(t, clusterName)
assert.NotEmpty(t, serviceName)

// Wait for service to be stable
aws.WaitForServiceToStabilize(t, "us-east-1", clusterName, serviceName, 10*time.Minute)

// Get service details
service := aws.GetEcsServiceDetails(t, "us-east-1", clusterName, serviceName)
assert.Equal(t, int32(2), *service.DesiredCount)

// Verify load balancer
lbArn := terraform.Output(t, terraformOptions, "lb_arn")
assert.NotEmpty(t, lbArn)
}

Checkov (Security Scanning)

# .github/workflows/security-scan.yml
name: Infrastructure Security Scan

on:
pull_request:
paths:
- '**.tf'
- '/terrform/'

jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: .
framework: terraform
output_format: sarif
output_file_path: results.sarif

- name: Upload to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif

# CLI
checkov -d ./infrastructure --framework terraform

# Com fail
checkov -d ./infrastructure --check CK_VPC_SG_1

# Skip específico
checkov -d ./infrastructure --skip-check CK_VPC_SG_1

Sentinel (Policy as Code)

# policies/api-service.sentinel
import "tfplan/v2" as tfplan

# Validate environment
validate_environment = func() {
violations = []

for tfplan.resource_changes as rc {
if rc.type is "aws_ecs_service" {
environment = rc.change.after.environment
if environment is "production" {
desired_count = rc.change.after.desired_count
if desired_count < 2 {
append(violations, rc.name)
}
}
}
}

return violations
}

main = rule {
validate_environment() is []
}


Ansible – Configuração

Playbook

# playbooks/deploy-api.yml

  • name: Deploy API Service
hosts: api_servers become: yes vars: app_version: "1.2.3" deploy_path: "/opt/myapp"

tasks:
- name: Ensure deploy directory exists
file:
path: "{{ deploy_path }}"
state: directory
owner: www-data
group: www-data
mode: '0755'

- name: Deploy application
template:
src: "app.conf.j2"
dest: "{{ deploy_path }}/config.conf"
owner: www-data
group: www-data
mode: '0644'
notify: Restart API

- name: Pull latest image
docker_image:
name: myapp/api
tag: "{{ app_version }}"
source: pull
force_source: yes

- name: Ensure API is running
docker_container:
name: api
image: "myapp/api:{{ app_version }}"
state: started
restart_policy: always
ports:
- "8080:8080"
env:
ENV: "{{ app_environment }}"
DATABASE_URL: "{{ db_connection_string }}"
volumes:
- "{{ deploy_path }}/config.conf:/app/config.conf:ro"

- name: Wait for API to be healthy
uri:
url: "http://localhost:8080/health"
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 5

handlers:
- name: Restart API
docker_container:
name: api
state: restarted

Molecule (Testing Ansible)

# molecule/default/molecule.yml

dependency: name: galaxy driver: name: docker platforms: - name: instance image: "geerlingguy/docker-${MOLECULE_DISTRO:-centos7}" command: "" volumes: - /sys/fs/cgroup:/sys/fs/cgroup:ro privileged: true provisioner: name: ansible lint: | ansible-lint verifier: name: testinfra lint: - flake8
# molecule/default/tests/test_default.py
import os
import pytest

def test_api_installed(host):
api = host.file("/opt/myapp")
assert api.exists
assert api.is_directory

def test_api_config(host):
config = host.file("/opt/myapp/config.conf")
assert config.exists
assert config.user == "www-data"

def test_api_service(host):
service = host.service("api")
assert service.is_running
assert service.is_enabled

def test_api_health(host):
response = host.run("curl -f http://localhost:8080/health")
assert response.rc == 0
assert "healthy" in response.stdout


ArgoCD – GitOps

Application

# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-api
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/manifests.git
    targetRevision: HEAD
    path: apps/api-service/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
      - PruneLast=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas

Conclusão

GitOps garante infraestrutura versionada e testável. As chaves são:

  1. Tudo no Git – Audit trail completo
  2. Testar infraestrutura – Terratest, Checkov, Molecule
  3. Automação – ArgoCD reconcile
  4. Policy as Code – Validate with Sentinel
  5. Drift detection – Detect unauthorized changes