Trunk Based Development, Kubernetes y GCP: Un pipeline con la ayuda de Gitlab, Kaniko, Skaffold y Terraform

Trunk Based Development, Kubernetes y GCP: Un pipeline con la ayuda de Gitlab, Kaniko, Skaffold y Terraform

Esta es la segunda entrega del articulo k8s desde dev hasta prod con Gitlab, Skaffold, Kustomize y Kaniko donde arme el skeleton base para esta segunda entrega, al igual que el anterior al final del artículo estará el enlace al repositorio con todo el código

Premisa

Ya tenemos un pipeline que cumple con las siguientes pasos:

  • Test test suites ejecutadas por PHPUnit
  • Build con Kaniko construimos las imágenes, las tagueamos y las enviamos a nuestro registro de imágenes (utilizaremos el mismo de gitlab en este caso), Kaniko se encarga de guardar una capa de cache en el mismo registro.
  • Deploy con Skaffold se construyen dinámicamente los manifiestos junto con las imágenes construidas en el paso anterior generando un manifiesto final que es aplicado en el cluster
  • Destroy con Skaffold se puede también eliminar del manifiesto aplicado actualmente.

Lo que queremos ahora es, eliminar un stage Destroy, y agregar un par de pasos más para aprovisionamiento de infraestructura con Terraform, el ciclo quedaría más o menos así:

emr-base.jpeg

Vamos a ello

Como esta es la continuación de la primera parte , no los voy a aburrir en la creación del pipeline, sino que nos enfocaremos en la mejora del del mismo y de los archivos de instrucciones de Terraform.

Estructura de Archivos

Vamos a crear una nueva carpeta infrastructure y una sub carpeta terraform a nivel del root dir, donde irán colocados todos los archivos de terraform

Luego de esto vamos a crear los archivos declarativos para aprovisionar un cluster en nuestra infraestructura (en mi caso estoy utilizando Google Cloud). La idea principal es que adicionalmente a los clusters permanentes (staging y producción) estos archivos de terraform se encarguen de crear cluster temporales que permitan que los equipos cuenten dinámicamente con un cluster para probar sus features y que este mismo cluster temporal pueda eliminarse una vez se haga el merge de esa branch a la rama main del repositorio.

Para esto vamos a crear los archivos de terraform y aparte vamos a colocar un webhook en GitLab al momento del merge request para eliminar dicho cluster temporal y el environment temporal de gitlab.

Archivos de Skaffold y Kustomize para ambientes review

En un ínterin, como estamos utilizando skaffold para los ciclos de CI/CD, vamos a crear rápidamente un nuevo profile llamado review para nuestras aplicaciones de ambientes temporales/dinámicos para el manejo de los manifiestos de kubernetes para esto podemos hacer una copiar del folder staging que esta dentro de deployments/k8s/environments teniendo en cuenta cambiar los keys de staging a review

Screen Shot 2021-08-30 at 14.27.04.png

Vamos con Terraform

En lugar de hacer el típico archivo main.tf en este caso y para mejor visualización vamos a separarlos por su función.

Empecemos con el 0-provider.tf donde, como su nombre lo indica, vamos a generar las instrucciones para que terraform sepa con que proveedor de servicios en la nube estamos trabajando (en nuestro caso Google Cloud), previamente necesitarás haber creado previamente tus credenciales de service account para que puedas comunicarte con tu proveedor (GCP -> IAM & Admin > Service Accounts, y luego hacer click en Create Service Account.)

Los valores var.* serán definidos mas adelante en un archivo de terraform conocido como tfvars esto nos permitirá en ese archivo escribir las variables dinámicas antes de inicializar el plan y aplicarlo.

# Google Cloud Platform Provider
# https://registry.terraform.io/providers/hashicorp/google/latest/docs
provider "google" {
  credentials = file("../configs/service-account.json")

  project     = var.project_id
  region      = var.region
}

terraform {
  required_providers {
    google = {
      source = "hashicorp/google"
      version = "3.5.0"
    }
  }
}

En tu consola para ir probando localmente puedes ejecutar el siguiente comando para inicializar terraform, esto descargara las primeras dependencias y las instalará en un folder .terraform del mismo directorio de trabajo y creará un archivo .lock.hcl que, como buena practica, deberías guardarlo en tu repo.

$: terraform init

Adicionalmente a esto vamos a crear un archivo 1-cluster.tf para definir las instrucciones de terraform para crear un cluster en nuestra infraestructura de Google Cloud Platform junto con la instrucción para crear una VPC para este cluster.

resource "google_compute_network" "vpc_network" {
  name = var.cluster_network
}

resource "google_container_cluster" "gke-cluster" {
  name               = var.cluster_name
  network            = var.cluster_network
  location           = var.region
  initial_node_count = 1
}

Luego estarán los dos últimos archivos, vars.tf y terraform.tfvars dónde estarán las variables globales y sus valores y placeholders para que sean accesibles para terraform.

variable "project_id" {
  type  = string
}

variable "region" {
  type  = string
}

variable "cluster_name" {
  type  = string
}

variable "network_name" {
  type  = string
}
project_id = "YOUR_GCP_PROJECT_ID"
region     = "us-central-1c"
cluster_name = "PLACEHOLDER_CLUSTER"
network_name = "PLACEHOLDER_NETWORK"

Luego de esto nos queda completar nuestro pipeline de GitLab con los jobs que debemos sumar para terraform (incluye 3 validate, init, plan y apply) y para el deploy del ambiente dinámico tomando en cuenta que solo debe crear dichos jobs si el branch es un feature branch y que no sea la rama main.

Si ves en detalle, hemos creado una variable de CI/CD de GitLab que contiene las credenciales de la service-account ( SERVICE_ACCOUNT_GCP) de Google Cloud para que pueda estar accesible para el pipeline y pasarla a los jobs de terraform en forma de archivo json.

.terraform:
  image:
    name: hashicorp/terraform:1.0.5
    entrypoint:
      - '/usr/bin/env'
      - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
  before_script:
    - cd infrastructure && mkdir -p configs && cd configs
    - echo $SERVICE_ACCOUNT_GCP | base64 -d > service-account.json
    - cd ../terraform
    - sed -i "s/\PLACEHOLDER_CLUSTER/$CI_COMMIT_REF_SLUG" terraform.tfvars
    - sed -i "s/\PLACEHOLDER_NETWORK/$CI_COMMIT_REF_SLUG" terraform.tfvars
    - terraform init
  cache:
    key: terraform-cache
    paths:
      - .terraform
  rules:
    - if: '$CI_COMMIT_BRANCH =~ /feature/'
    - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'

validate:
  extends: .terraform
  stage: provision
  script:
    - terraform validate

plan:
  extends: .terraform
  stage: provision
  script:
    - terraform plan -out plan.tfplan
  needs:
    - validate
  artifacts:
    paths:
      - plan.tfplan

apply:
  extends: .terraform
  stage: provision
  script:
    - terraform apply -input=false plan.tfplan
  needs:
    - plan
  rules:
    - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
      when: manual

apply:
  extends: .terraform
  stage: provision
  script:
    - cp $CI_PROJECT_DIR/planfile ./
    - terraform apply -input=false "planfile"
  needs:
    - plan
  rules:
    - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
      when: manual

deploy:feature:
  extends: .skaffold
  stage: deploy
  environment:
    name: "$CI_COMMIT_REF_NAME"
    url: $CI_COMMIT_SHORT_SHA.features.equatonlabs.io
  needs:
    - build:api
  script:
    - skaffold deploy -f deployment/skaffold.yaml -p feature -a latest-build.json --status-check=true
  rules:
    - if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
      when: manual

destroy:feature:
  extends: .terraform
  stage: destroy
  environment:
    name: $CI_COMMIT_REF_NAME
    url: $CI_COMMIT_SHORT_SHA.features.equatonlabs.io
    action: stop
  script:
    - terraform destroy
  rules:
    - if: $CI_MERGE_REQUEST_APPROVED
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'

Ahora juntemos todo

Una vez tengamos todos nuestros archivos en lugar es momento de hacer nuestro commit & push al repositorio y seguir la ejecución del pipeline de GitLab y sus salidas para asegurarnos que esta todo en orden.

Tendríamos este escenario:

  • Tener 2 nuevos stages provision y deploy cuando la rama comienza con feature/
  • Dinámicamente nuestro cluster y la vpc tendrán el nombre de la variable runtime CI_COMMIT_REF_SLUG (ver mas acá )
  • En GitLab se creara un environment dinámico basado en la variable CI_COMMIT_REF_NAME
  • Una vez aprobado el MR a la rama main, se dispara un job que elimina el cluster temporal que se utilizo para desarrollar y probar la feature y así no incurrir en costos innecesarios.
  • Una vez aprobado el MR a la rama main se eliminará también el environment creado en GitLab
  • En la rama main no debe aparecer el stage de provision ya que esos cluster no son permanentes en el tiempo y se manejan fuera de este pipeline.

Pipeline en Features

Screenshot 2021-09-30 at 20.27.40.png

Environment dinámico y deploy dashboard

Screen Shot 2021-08-30 at 21.01.53.png

Google Cloud con el Cluster Creado

Screen Shot 2021-08-30 at 19.45.14.png

Ahora nos queda probar, para completar el circuito, que al aprobar el MR, se ejecute el paso de destroy:feature que elimine el cluster y el environment de el deploy dashboard de GitLab

Pipeline con el MR aprobado

Screenshot 2021-10-06 at 17.35.05.png

Y esto es todo por ahora amigos, como siempre compartan, que compartir hace bien. Les dejo más abajo el repo con todos los files que vimos hoy para que los puedan ojear mas a detalles.