GitOps: Gitlab + Packer + Ansible + Terraform + AWS

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 editionself 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).

EnvironmentAvailability Zone
Test us-east-2
Productionaf-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

Deixe uma Resposta

Preencha os seus detalhes abaixo ou clique num ícone para iniciar sessão:

Logótipo da WordPress.com

Está a comentar usando a sua conta WordPress.com Terminar Sessão /  Alterar )

Google photo

Está a comentar usando a sua conta Google Terminar Sessão /  Alterar )

Imagem do Twitter

Está a comentar usando a sua conta Twitter Terminar Sessão /  Alterar )

Facebook photo

Está a comentar usando a sua conta Facebook Terminar Sessão /  Alterar )

Connecting to %s