locals {
project = "xxxxx"
container_image = "${data.terraform_remote_state.global.outputs.ecr_servicexxxx_url}:${var.service_tag}"
....
}
Actualmente, estoy "ejerciendo" las labores de DevOps/Infra en un equipo multidisciplinar, o sea lo que viene siendo varias personas trabajando en varios microservicios que se despliegan en un mismo sitio, en este caso y por "imperativo legal" un cluster ECS de Amazon (yo hubiera preferido un EKS porque me manejo mejor con kubernetes).
Desde el principio hemos aplicado que todo el tema de infra tiene que ser bajo Terraform y hemos reducido las intervenciones manuales (tanto consola como web) al mínimo.
Así nuestro proyecto terraform despliega todo lo relativo a la red, DNS, bases de datos, secretos y cómo no, para bien o para mal, la definición de todos los microservicios.
Ignorando los detalles, básicamente tenemos:
Un proyecto de terraform que define los elementos "globales" entre ellos el repositorio Docker de cada servicio, exponiendo los arn como outputs
Diferentes proyectos de terraform con los recursos necesarios para desplegar cada servicio dentro del cluster, diferenciando además entre staging y prod
A cada proyecto terraform de cada servicio, entre otras cosas, se le define la imagen Docker y el tag de la misma a desplegar junto con otras configuraciones como la lista de secretos y environments a usar.
De esta forma, y como la arquitectura en esta fase está "viva", cada servicio se despliega de forma independiente y si es necesario se elimina sin interferir en los otros proyectos.
Así, cada servicio cuenta con su locals.tf
locals {
project = "xxxxx"
container_image = "${data.terraform_remote_state.global.outputs.ecr_servicexxxx_url}:${var.service_tag}"
....
}
y en un fichero versions.auto.tfvars definimos el tag a desplegar:
service_tag = 1234567
Desplegar una nueva version es "tan fácil" como editar este fichero con el nuevo commit y ejecutar un
terraform apply
Obviamente, una vez que este sistema está funcionando y desplegar versiones se convierte en algo tan fácil
como esperar a que una versión nueva se encuentre subida al ECR, cambiar el fichero y ejecutar el terraform,
surge la "necesidad" de esto hay que automatizarlo
La primera parte para la automatización es "trivial": cada vez que en un servicio se mergee a main, un pipeline
construirá y publicará la imagen.
Como estamos usando Github, esto lo resolvemos mediante una GH Action similar a:
name: CD - Deploy Develop
on:
push:
branches: [main]
env:
AWS_REGION: us-east-1
ECR_REPOSITORY: servicexxxx
jobs:
....
build:
name: Build & Push
runs-on: ubuntu-latest
needs: test
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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Get short SHA
id: shortsha
run: echo "sha7=$(echo ${GITHUB_SHA} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ steps.shortsha.outputs.sha7 }}
run: |
docker build --build-arg GITHUB_TOKEN=${{ steps.generate_token.outputs.token }} -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
IMAGE_URI="$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
echo "image=$IMAGE_URI" >> $GITHUB_OUTPUT
echo "✅ Built and pushed image: $IMAGE_URI"
Ahora que tenemos la imagen creada y subida (recuerda, esto es en cada servicio/proyecto) lo que querríamos es poder "notificar" al repositorio de Terraform que existe una versión nueva para desplegar.
Como esta parte es "común" a todos los servicios hemos creado un repositorio platform-ci donde, entre otros
pipelines, tenemos uno para crear una PR en el repositorio de Terraform que modifique únicamente el versions
correspondiente. De esta forma "reutilizamos" pipelines y no nos repetimos
El pipeline en este repo es similar a:
name: Deploy service
on:
workflow_call:
secrets:
GH_TOKEN_APP_ID:
required: true
GH_TOKEN_PRIVATE_KEY:
required: true
inputs:
environment:
required: true
type: string
project:
required: true
type: string
variable:
required: true
type: string
value:
required: true
type: string
jobs:
update-version:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App Token
id: generate_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.GH_TOKEN_APP_ID }}
private-key: ${{ secrets.GH_TOKEN_PRIVATE_KEY }}
- name: Checkout Infra Repo
uses: actions/checkout@v4
with:
repository: OUR_ORGANIZATION/tr-infra
token: ${{ steps.generate_token.outputs.token }}
- name: Update Version File
run: |
cd environments/${{ inputs.environment }}/${{ inputs.project }}
sed -i 's/${{ inputs.variable }} = .*/${{ inputs.variable }} = "${{ inputs.value }}"/' versions.auto.tfvars
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ steps.generate_token.outputs.token }}
commit-message: "deploy: update ${{ inputs.variable }} (${{ inputs.environment }}) to ${{ inputs.value }}"
title: "🚀 Deploy ${{ inputs.project }} (${{ inputs.environment }})"
body: "Deploy new version of **${{ inputs.project }}** \n ${{inputs.variable}}=${{inputs.value}}"
branch: "gitops/${{ inputs.variable }}-${{ inputs.value }}"
base: main
Este pipeline requiere 4 parámetros:
environment (staging o production), referencia a una de las dos carpetas "padres"
project (service1, service2), referencia a una de las multiples subcarpetas donde tenemos definidos los servicios
variable, por ahora usamos "service_tag" pero lo dejamos abierto para futuros usos
value, el valor a usar que en este caso será el tag pero asi esta abierto para futuros usos
El pipeline simplemente clona el repo terraform, se ubica en la carpeta {environment}/{project}, modifica el valor
proporcionado usando un simple sed y crea una PR
Por ahora las PR que se crean deben ser aprobadas de forma manual hasta que todo vaya más rodado
Así pues, simplemente tenemos que incluir en el pipeline de cada servicio una invocación a este una vez que la imagen se encuentra subida:
notify-infra:
needs: build
uses: OUR_ORGANIZATION/platform-ci/.github/workflows/deploy-version.yml@main
secrets:
GH_TOKEN_APP_ID: ${{ secrets.GH_TOKEN_APP_ID }}
GH_TOKEN_PRIVATE_KEY: ${{ secrets.GH_TOKEN_PRIVATE_KEY }}
with:
environment: staging (1)
project: service1
variable: service_tag
value: ${{ needs.build.outputs.image_tag }}
| 1 | En este caso desplegamos en staging, pero cuando es un merge versionado usamos production |
Cuando la imagen ha sido publicada y este último steps se ejecuta, tenemos una PR creada en el repo de Terraform. En un proceso típico simplemente se mergea y el pipeline del proyecto Terraform despliega la última versión.
Este mecanismo nos permite de una forma fácil hacer rollback a la versión anterior, pues simplemente es hacer otra PR (esta vez manual) indicando el tag a usar, pero lo más importante (en mi opinión) permite al equipo ser "dueño" del proceso de despliegue sin depender de nadie.
La propuesta primera era incluir en cada servicio los comandos aws necesarios para modificar la Task Definition proporcionándole el tag a usar, pero esto creaba que el proyecto terraform se quedará "desactualizado".
La otra opción era "mover" la parte terraform a cada servicio, pero esto nos dificulta la reutilización de módulos de terraform y de alguna manera tendríamos la infra "distribuida" en varios proyectos. Por ahora nos sentimos más cómodos teniéndolo unificado en un único repo
En cualquier caso la idea es conseguir proporcionar herramientas y procedimientos simples al equipo lo más automatizados posible para que los despliegues sean fluídos
2019 - 2026 | Mixed with Bootstrap | Baked with JBake v2.6.7 | Terminos Terminos y Privacidad