
O post de hoje é sobre uma trend denominada GitOps e assim dar continuidade aos posts de automation que já vinha abordando no blog.
Uma nova forma de provisionar e administrar infraestrutura.
GitOps?
Uso de git repository como única fonte de verdade “single source of truth” onde toda as operações de criação, alteração passam pelo git, desde deploy, configuração de apps e infraestrutura é tudo feito via code “everything as code” armazenado no git. Basta um commit no repo para disparar o fluxo de configuração/deployment que pode ser validado e aprovado via merge request MR, “git based workflow”.
Esta modelo operacional vem sendo adotado muito”trend”, e eu acho bastante interessante pois permite integrar, consolidar as várias tools “IaC”, processos usados por dev and ops teams e assim ter um fluxo de teste, deployment e configuração de infraestrutura automatizado, integrado e controlado.
Portanto, uma mistura de praticas devops “colaboração”, git para control de versão e “Merge Request”, Infrastructure as a Code (IaC) “infraestrutura declarativa”, mecanismo de sincronização de estado desejado “desired state” com pipeline CI/CD para automação de infraestrutura , eu chamaria de gitops.
O modelo operacional GitOps tem mais afinidade com k8s pelo seu modelo declarativo e por estes oferecem mais flexibilidade, escalabilidade, controle de clusters, imutabilidadee etc. Por isso é normal gitops estar sempre associado a k8s e tools operators como Flux e ArgoCD.
No entanto acho melhor sublinhar que Gitops não é somente para k8s, mas sim para tudo aquilo que podemos definir ou declarar por code e termos um mecanismo de sincronização de desired state .
Cenário
No cenário que demonstrarei, será usado gitlab community edition “self hosted” como ferramenta CI/CD para criar o meu pipeline. Lembrar que existem várias ferramentas CI/CD no mercado como Jenkins, Travis CI, CircleCI, VMware Code Stream. Ferramentas como essas permitem criar pipeline e automatizar release, build, test, deploy.
Pipeline é apenas uma série de jobs controlados, sequenciados.
Neste pipeline irei integrar:
– Packer para criação de image “AMI”
– Ansible para Configuração da image/OS.
– Terraform para provision de infraestrura, amazon EC2.
– Gitlab para guardar código nos repositórios e CI/CD pipeline, MR.
Workflow

O pipeline terá as seguintes etapas:
Validate: Validar o code (Packer, Terraform).
Build: via Packer cria a image AMI com apache e efectua hardening via Ansible CM e logo em seguida PLAN que cria terraform execution plan.
Test: : Deploy instâncias EC2 baseadas na imagem criada pelo packer e configuração de users no ambiente de Teste (us-east-2).
E só depois do deploy for efectuado com sucesso no ambiente de teste, submeter Merge Request que deve ser validado e aprovado, após aprovação e merge, inicia o processo de ‘build’ criação da AMI image e deploy em produção (af-south-1).
Production: : Deploy instâncias EC2 baseadas na imagem criada pelo packer e configuração de users no ambiente de prod(af-south-1).
Environment | Availability Zone |
Test | us-east-2 |
Production | af-south-1 |
Hands-On
Gitlab
Definir variáveis de ambiente
As variáveis aqui definidas serão usados no código declarativo do packer e terraform quando o pipeline estiver a ser executado.

Nota: Não é boa prática definir credenciais como variaveis expostas no gitlab, o recomendável é usar um vault como por exemplo hashicorp vault.
Também existe outra forma de controlar environment com terraform workspace que não irei abordar aqui.
Instalar e Configurar Runners
Runners são processos/agentes que executam jobs no gitlab, trabalham com CI/CD para correr os jobs do pipeline. E necessário ter runner instalado e configurado para poder usar a feature de pipeline do Gitlab.
O runner pode ser instalado num host separado/isolado do gitlab ou em um container. Neste eceplo vou instalar na maquina local separada do gitlab.
#install
curl -LJO “https://gitlab-runner-downloads.s3.amazonaws.com/latest/deb/gitlab-runner_amd64.deb“
sudo dpkg -i gitlab-runner_amd64.deb
#Registar runner
$ sudo gitlab-runner register
introduzir url do gitlab

#introduzir token para registar runner
Abrir interface gitlab para obter o toke,
Admin Area → Runners, Copiar o token e colar

#introduzir descricao para o runner.
introduzir tags para o runner.
Escolher o tipo de executor

Nota: E Recomendável usar docker executor, eu selecionei shell porque não quero usar imagem ou imagens para excutar meus jobs e tenho todas as tools necessárias para executar o pipeline na minha maquina.
#validar runner

IaC Code
Packer config file
ami_ansible.json
"variables": {
"aws_access_key": "{{env `AWS_ACCESS_KEY`}}",
"aws_secret_key": "{{env `AWS_SECRET_KEY`}}",
"aws_region": "{{env `AWS_DEFAULT_REGION`}}",
"aws_ami": "{{env `AWS_SOURCE_AMI`}}"
},
"builders": [
{
"type": "amazon-ebs",
"access_key": "{{user `aws_access_key`}}",
"secret_key": "{{user `aws_secret_key`}}",
"region": "{{user `aws_region`}}",
"source_ami": "{{user `aws_ami`}}",
"instance_type": "t3.micro",
"ssh_username": "ec2-user",
"ami_name": "webserver {{timestamp}}",
"ami_description": "Apache",
"tags": {
"Name": "Apache_latest",
"Environment": "Test",
"OS_Version": "Amazon Linux"
}
}
],
"provisioners": [
{
"type": "shell",
"script": "update.sh"
},
{
"type": "ansible",
"user": "ec2-user",
"playbook_file": "apache.yml",
"extra_arguments": [
"-v"
]
},
{
"type": "ansible",
"user": "ec2-user",
"playbook_file": "hardening.yml",
"extra_arguments": [
"-v"
]
}
]
}
Terraform
main.tf
##################################################################################
# PROVIDERS
##################################################################################
provider "aws" {}
# access_key = var.aws_access_key
# secret_key = var.aws_secret_key
# region = var.region
##################################################################################
# DATA
##################################################################################
data "aws_ami" "aws-linux" {
most_recent = true
owners = ["self"]
name_regex = "web*"
filter {
name = "name"
values = ["webs*"]
}
# filter {
# name = "name"
# values = ["webserver"]
#}
# filter {
# name = "Description"
# values = ["Apache"]
# }
tags = {
"Name" = "Apache_latest"
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
##################################################################################
# RESOURCES
##################################################################################
resource "aws_default_vpc" "default" {
}
resource "aws_security_group" "allow_ssh" {
name = var.sec_name
description = "Allow ports 22 & 80 aws"
vpc_id = aws_default_vpc.default.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "apache_terraform" {
count = var.vm_num
ami = data.aws_ami.aws-linux.id
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.allow_ssh.id]
tags = {
Name = "${var.tag_Name} ${count.index}"
# Role = var.tag_Role [count.index]
}
connection {
type = "ssh"
user = var.user
host = self.public_ip
private_key = file(var.private_key_path)
}
##################################################################################
# PROVISIONER
##################################################################################
provisioner "remote-exec" {
inline = [
"sudo yum update -y",
]
}
provisioner "local-exec" {
command = "chmod 400 ./PrivateSvr.pem"
}
#Creates inventory for ansible
provisioner "local-exec" {
command = <<EOD
cat <<EOF >> inv.ini
[web]
${self.public_ip}
[web:vars]
ansible_user=${var.user}
ansible_ssh_private_key_file=${var.private_key_path}
EOF
EOD
}
#executes ansible playbook for user e group creation
provisioner "local-exec" {
command = "ansible-playbook -i inv.ini users.yml"
}
}
variables.tf
##################################################################################
# VARIABLES
##################################################################################
variable "private_key_path" {}
variable "key_name" {}
variable "user" {
description = "user for ssh"
}
variable "vm_num" {}
variable "tag_Name" {}
variable "tag_Role" {}
variable "instance_type" {}
variable "region" {}
variable "sec_name" {}
variable "aws_ami" {}
terraform.tfvars
user = "ec2-user"
key_name = "PrivateSvr"
private_key_path ="./PrivateSvr.pem"
#region = "af-south-1"
instance_type = "t3.micro"
vm_num = 2
#ami
aws_ami = "aaaa"
#Tags
tag_Name = "apache"
tag_Role = "web"
#sec group
sec_name = "SSH_HTTP_ALLOW13"
Apache.yml
---
- name: webserver configuration
hosts: all
become: true
tasks:
- name: Install Apache
yum: name=httpd state=present
- name: Enable Apache Service on boot
service: name=httpd enabled=yes state=started
- name: Setting up web page
copy:
src: ./index.html
dest: /var/www/html/index.html
hardening.yml
---
- name: HARDENING
hosts: all
become: true
tasks:
- name: Disable SSH root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#PermitRootLogin'
line: 'PermitRootLogin no'
- name: Disable SSH password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#PasswordAuthentication yes'
line: 'PasswordAuthentication no'
user.yml
---
- name: Group & users Creation
hosts: all
vars_files:
- users_var.yml
become: true
tasks:
- name: Add allowed users group
group:
name: allowed_users
state: present
- name: Create users
user:
name: "{{item.name}}"
groups: "{{item.groups}}"
shell: "{{item.shell}}"
comment: "{{item.comment}}"
with_items: "{{ users }}"
users_var.yml
---
#defautl file for input user details
users:
- {name: ops01, groups: allowed_users, comment: "local user", shell: /sbin/login}
- {name: ap01, groups: allowed_users, comment: "service account", shell: /sbin/nologin}
A configuração do pipeline fica guardada no ficheiro .gitla-ci.yml
é nesse ficheiro que fica definida as fases stages e jobs
.gtlab-ci.yml
# Default output file for Packer and Terraform
variables:
PLAN: plan.tfplan
JSON_PLAN_FILE: tfplan.json
STATE_APPLY: to.tfstate
PACKER_OUTPUT: image.log
cache:
paths:
- .terraform
before_script:
- shopt -s expand_aliases
- alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
# - terraform init
stages:
- validate
- build
- test
- production
validate:
stage: validate
tags:
- local
environment:
name: test
script:
- echo $AWS_ACCESS_KEY_ID
- echo $AWS_SECRET_ACCESS_KEY
- echo $AWS_DEFAULT_REGION
- echo $CI_JOB_ID
- export
- terraform --version
- packer --version
- ansible --version
- echo "-------------- Validating Packer Code --------------"
- packer validate ami_ansible.json
- echo "------------------------- Terraform validate Init -----------------------"
- terraform init
- terraform validate
Test_Image:
stage: build
environment:
name: test
only:
- test
tags:
- packer
- local
script:
- packer build ami_ansible.json | tee -a $PACKER_OUTPUT
artifacts:
paths:
- $PACKER_OUTPUT
plan:
stage: build
tags:
- local
environment:
name: test
script:
- echo "Building..... terraform plan"
- terraform plan -state=$STATE_APPLY -out=$PLAN
- terraform show --json $PLAN | convert_report > $JSON_PLAN_FILE
artifacts:
paths:
- $PLAN
- $STATE_APPLY
reports:
terraform: $JSON_PLAN_FILE
Packer_pro:
stage: build
environment:
name: production
only:
- production
tags:
- packer
- local
script:
- packer build ami_ansible.json
# rules:
# - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# when: manual
apply test:
stage: test
tags:
- local
environment:
name: test
script:
- terraform apply -state-out=$STATE_APPLY -input=false $PLAN
artifacts:
paths:
- $STATE_APPLY
dependencies:
- plan
only:
- test
apply:
stage: production
# needs: ["Packer_pro"]
only:
- production
tags:
- local
environment:
name: production
script:
- echo ".....in Production"
- terraform plan -state=$STATE -out=$PLAN
- terraform apply -state-out=$STATE_APPLY -input=false $PLAN
artifacts:
paths:
- $STATE_APPLY
when: manual
# rules:
# - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
# when: manual
Depois de estar definido os jobs do pipeline, podemos efetuar o teste.
Nota: Sempre que for efectuado 1 commit ou push ao repositório que contem o ficheiro .gitlab-ci.yml automaticamente irá disparar o pipeline que vai passar pela fases já mencionadas anteriormente. Os jobs de deploy em produção só serão executados quando a alteração ou commit acontecer no branch production ou ambiente de produção.
Para começar vou efectuar commit e de seguida push ao remote branch test.

Após o git push, o pipeline é disparado:
Podemos verificar que levou 8 min para executar os 4 jobs definidos no pipeline para este environment.

Vamos entrar em cada fase do pipeline para validar:
Stage: Validate
Job: Validate

Stage: Build
Job: Test_Image

Verificando na consola AWS podemos validar que foi criada uma imagem AMI

Stage: Build
Job: Plan

Stage: Test
Job: apply test

E assim posso confirmar que foram criadas 2 instâncias apache no test environment “us-east”

Com sucesso do deploy em test, já pode se avançar para produção e isso vai acontecer através de 1 Merge Request do branch test para production.
Merge Request (MR)

Nessa janela definimos o source branch e target,
O objectivo é efectuar Merge do branch do test com production

Preencher descriçãodo MR

Aqui já coneguimos ter resumo dos commits feitos, verificar se o pipeline no branch test foi executado com sucesso, report do terraform com os resources a serem adicionados.
Um dos pontos chaves do MR é a aprovação, o processo exige revisão e aprovação. Por estar a utilizar versão free CE, a aprovação aparece como opcional, mas continua sendo necessária intervenção para permitir o merge.

Pós aprovação

Logo apos aprovar o MR o pipeline começa a criação da AMI no ambiente prod, a fase de apply deploy em prod é manual, por questão de segurança esse job foi definido como manual portamto, é necessária intervenção humana para começar.

Validando na consola AWS, AMI criada em prod “af-south-1”

Apply prod

Assim que o job apply termina a sua execução, podemos validar as instâncias criadas.

Confirmação do deploy em prod

O pipeline ainda esta sendo melhorado e o code pode ser encontrado no meu repo https://github.com/manuh-L/CICD_AT