Administra la infraestructura como código con Terraform, Jenkins y GitOps

En este instructivo, se explica cómo administrar la infraestructura como código con Terraform y Jenkins mediante la metodología popular GitOps. El instructivo es para desarrolladores y operadores que buscan prácticas recomendadas a fin de administrar la infraestructura de la manera en que administran las aplicaciones de software. En el artículo, se supone que estás familiarizado con Terraform, Jenkins, GitHub, Google Kubernetes Engine (GKE) y Google Cloud.

Arquitectura

La arquitectura que se usa en este instructivo usa ramas de GitHub (dev y prod) para representar entornos de desarrollo y producción reales. Estos entornos se definen mediante redes de nube privada virtual (VPC) (dev y prod) en un proyecto de Google Cloud.

Propuesta de infraestructura

Como se muestra en el siguiente diagrama de arquitectura, el proceso comienza cuando un desarrollador o un operador hacen una propuesta de infraestructura a una rama no protegida de GitHub, por lo general, una rama de funciones. Cuando corresponda, esta rama de funciones se puede promover al entorno de desarrollo a través de una solicitud de extracción (PR) a la rama dev. Luego, Jenkins activa de forma automática un trabajo para ejecutar la canalización de validación. Este trabajo ejecuta el comando terraform plan e informa el resultado de la validación a GitHub y, además, incluye un vínculo a un informe detallado de cambios en la infraestructura. Este paso es fundamental porque te permite revisar posibles cambios con los colaboradores y agregar confirmaciones de seguimiento antes de que los cambios se combinen en la rama dev.

Arquitectura en la que se muestran las prácticas de GitOps para administrar ejecuciones de Terraform.

Implementación Dev

Si el proceso de validación se realiza de forma correcta y apruebas los cambios propuestos en la infraestructura, puedes combinar la solicitud de extracción en la rama dev. En el diagrama a continuación, se muestra este proceso.

Combinación de la solicitud de extracción a la rama “dev”.

Cuando se completa la combinación, Jenkins activa otro trabajo para ejecutar la canalización de implementación. En esta situación, el trabajo aplica los manifiestos de Terraform en el entorno de desarrollo para lograr el estado que deseas. Este paso es importante porque te permite probar el código de Terraform antes de ascenderlo a producción.

Implementación Prod

Después de realizar las pruebas y estar listo para promover los cambios a la producción, debes combinar la rama dev con la rama prod a fin de activar la instalación de la infraestructura en el entorno de producción. En el diagrama a continuación, se muestra este proceso.

Combinación de la rama “dev” con la rama “prod” para activar la instalación de la infraestructura en el entorno de producción.

El mismo proceso de validación se ejecuta cuando creas la solicitud de extracción. Este proceso permite que el equipo de operaciones revise y apruebe los cambios propuestos para la producción.

Objetivos

  • Configurar el repositorio de GitHub
  • Crear el almacenamiento de estado remoto de Terraform
  • Crear un clúster de GKE e instalar Jenkins
  • Cambiar la configuración del entorno en una rama de funciones
  • Promover los cambios en el entorno de desarrollo.
  • Promover los cambios en el entorno de producción.

Costos

En este instructivo, se usan los siguientes componentes facturables de Google Cloud:

Para generar una estimación de costos en función del uso previsto, usa la calculadora de precios. Es posible que los usuarios nuevos de Google Cloud sean aptos para obtener una prueba gratuita.

Cuando finalices este instructivo, podrás borrar los recursos creados para evitar que se te siga facturando. Para obtener más información, consulta cómo hacer una limpieza.

Antes de comenzar

  1. Accede a tu Cuenta de Google.

    Si todavía no tienes una cuenta, regístrate para obtener una nueva.

  2. En la página de selección de proyectos de Cloud Console, selecciona o crea un proyecto de Cloud.

    Ir a la página Selector de proyectos

  3. Asegúrate de que la facturación esté habilitada para tu proyecto de Google Cloud. Obtén información sobre cómo confirmar que tienes habilitada la facturación para tu proyecto.

  4. En Cloud Console, activa Cloud Shell.

    Activar Cloud Shell

    En la parte inferior de Cloud Console, se inicia una sesión de Cloud Shell en la que se muestra una ventana de línea de comandos. Cloud Shell es un entorno de shell que tiene el SDK de Cloud preinstalado, incluida la herramienta de línea de comandos de gcloud, y valores ya establecidos para el proyecto actual. La inicialización de la sesión puede tomar unos minutos.

  5. En Cloud Shell, configura el ID del proyecto y el nombre de usuario y la dirección de correo electrónico de GitHub:
    PROJECT_ID=PROJECT_ID
    GITHUB_USER=YOUR_GITHUB_USER
    GITHUB_EMAIL=YOUR_EMAIL_ADDRESS
    gcloud config set project $PROJECT_ID
    

    Si no accediste a GitHub desde Cloud Shell antes, puedes configurarlo con tu nombre de usuario y dirección de correo electrónico:

    git config --global user.email "$GITHUB_EMAIL"
    git config --global user.name "$GITHUB_USER"
    

    GitHub usa esta información para identificarte como el autor de las confirmaciones que creas en Cloud Shell.

Configura un repositorio de GitHub

En este instructivo, usarás un único repositorio de GitHub para definir la infraestructura de nube. Para organizar esta infraestructura debes tener diferentes ramas que correspondan a diferentes entornos:

  • La rama dev contiene los cambios más recientes que se aplican al entorno de desarrollo.
  • La rama prod contiene los cambios más recientes que se aplican al entorno de producción.

Con esta infraestructura, siempre se puede hacer referencia al repositorio a fin de saber qué configuración se espera en cada entorno y combinar cambios nuevos en el entorno dev para proponerlos. A continuación, debes combinar la rama dev con la rama prod para promover los cambios.

Para comenzar, debes bifurcar el repositorio solutions-terraform-jenkins-gitops.

  1. En GitHub, ve al repositorio solutions-terraform-jenkins-gitops.
  2. Haz clic en Fork.

    Bifurca un repositorio en GitHub.

    Ahora tienes una copia del repositorio solutions-terraform-jenkins-gitops con archivos de origen.

  3. En Cloud Shell, clona este repositorio bifurcado:

    cd ~
    git clone https://github.com/$GITHUB_USER/solutions-terraform-jenkins-gitops.git
    cd ~/solutions-terraform-jenkins-gitops
    

    El código de este repositorio está estructurado de la siguiente manera:

    • Carpeta example-pipelines/: Contiene subcarpetas con la canalización de ejemplo que se usó en este instructivo.
    • example-create/: Contiene el código de Terraform para crear una máquina virtual en el entorno.
    • environments/: Contiene carpetas del entorno dev y prod con configuraciones de backend y vínculos a archivos de la carpeta example-create/.
    • Carpeta jenkins-gke/: Contiene secuencias de comandos necesarias para implementar Jenkins en un clúster de GKE nuevo.
    • tf-gke/: Contiene el código de Terraform para implementar en GKE e instalar Jenkins y sus recursos dependientes.

Crea el almacenamiento de estado remoto de Terraform

De forma predeterminada, el estado de Terraform se almacena de forma local. Sin embargo, te recomendamos almacenar el estado en el almacenamiento central remoto al que puedes acceder desde cualquier sistema. Este enfoque te ayuda a evitar crear varias copias en sistemas diferentes, lo que puede provocar que la configuración y los estados de la infraestructura no coincidan.

En esta sección, configurarás un depósito de Cloud Storage que almacena el estado remoto de Terraform.

  1. En Cloud Shell, crea un depósito de Cloud Storage:

    gsutil mb gs://${PROJECT_ID}-tfstate
    
  2. Habilita el control de versiones de objetos para mantener el historial de los estados:

    gsutil versioning set on gs://${PROJECT_ID}-tfstate
    
  3. Reemplaza el marcador de posición PROJECT_ID por el ID del proyecto en los archivos terraform.tfvars y backend.tf:

    sed -i.bak "s/PROJECT_ID/${PROJECT_ID}/g" ./example-pipelines/environments/*/terraform.tfvars
    sed -i.bak "s/PROJECT_ID/${PROJECT_ID}/g" ./example-pipelines/environments/*/backend.tf
    
    sed -i.bak "s/PROJECT_ID/${PROJECT_ID}/g" ./jenkins-gke/tf-gke/terraform.tfvars
    sed -i.bak "s/PROJECT_ID/${PROJECT_ID}/g" ./jenkins-gke/tf-gke/backend.tf
    
  4. Verifica si todos los archivos se actualizaron:

    git status
    

    El resultado luce de la siguiente manera:

    On branch dev
    Your branch is up-to-date with 'origin/dev'.
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git checkout -- <file>..." to discard changes in working directory)
    
            modified:   example-pipelines/environments/dev/backend.tf
            modified:   example-pipelines/environments/dev/terraform.tfvars
            modified:   example-pipelines/environments/prod/backend.tf
            modified:   example-pipelines/environments/prod/terraform.tfvars
            modified:   jenkins-gke/tf-gke/backend.tf
            modified:   jenkins-gke/tf-gke/terraform.tfvars
    
  5. Confirma y envía los cambios:

    git add --all
    git commit -m "Update project IDs and buckets"
    git push origin dev
    

Según la configuración de GitHub, es posible que debas autenticarte para enviar los cambios anteriores.

Crea un clúster de GKE e instala Jenkins

En esta sección, usas Terraform y Helm para configurar el entorno a fin de administrar la infraestructura como código. Primero, usa Terraform y Cloud Foundations Toolkit para configurar una nube privada virtual, un clúster de GKE y Workload Identity. Luego, usa helm para instalar Jenkins sobre este entorno.

Antes de comenzar a ejecutar comandos de Terraform, debes crear un token de acceso personal de GitHub. Este token es necesario para permitir que Jenkins acceda al repositorio bifurcado.

Crea un token de acceso personal de GitHub

  1. Accede a GitHub.
  2. Haz clic en tu foto de perfil y, luego, en Settings.
  3. Haz clic en Developer settings y, luego, en Personal access tokens.
  4. Haz clic en Generate new token y agrega una descripción del token en el campo Note, luego, selecciona el alcance de repo.
  5. Haz clic en Generate token y copia el token recién creado en el portapapeles.

    Genera y copia un token en el portapapeles.

  6. En Cloud Shell, guarda el token en la variable GITHUB_TOKEN. Este contenido de la variable se almacena más tarde como un Secret en el clúster de GKE.

    GITHUB_TOKEN="NEWLY_CREATED_TOKEN"
    

Crea un clúster de GKE e instala Jenkins

Ahora crearás el clúster de GKE. Este clúster incluye una Workload Identity (jenkins-wi-jenkins@PROJECT_ID.iam.gserviceaccount.com), que te permite darle a Jenkins el permiso que necesita en la página Cuentas de servicio en Cloud Console. Debido a las propiedades de seguridad y la capacidad de administración mejoradas, recomendamos usar Workload Identity para acceder a los servicios de Google Cloud desde GKE.

Para administrar la infraestructura de Google Cloud como código, Jenkins debe autenticarse a fin de usar las API de Google Cloud. En los siguientes pasos, Terraform configura la cuenta de servicio de Kubernetes (KSA) que Jenkins usa para que actúe como una cuenta de servicio de Google (GSA). Esta configuración permite que Jenkins se autentique de forma automática como GSA cuando accede a las API de Google Cloud.

Para hacerlo más simple, en este instructivo, otorgas acceso de editor de proyectos. Sin embargo, debido a que esta función tiene un amplio conjunto de permisos, en entornos de producción, debes seguir las prácticas recomendadas de seguridad de TI de tu empresa, lo que implica proporcionar un acceso de mínimo privilegio.

  1. En Cloud Shell, instala Terraform:

    wget https://releases.hashicorp.com/terraform/0.12.24/terraform_0.12.24_linux_amd64.zip
    unzip terraform_0.12.24_linux_amd64.zip
    sudo mv terraform /usr/local/bin/
    rm terraform_0.12.24_linux_amd64.zip
    
  2. Crea el clúster de GKE e instala Jenkins:

    cd jenkins-gke/tf-gke/
    terraform init
    terraform plan --var "github_username=$GITHUB_USER" --var "github_token=$GITHUB_TOKEN"
    terraform apply --auto-approve --var "github_username=$GITHUB_USER" --var "github_token=$GITHUB_TOKEN"
    

    Este proceso puede llevar unos minutos para completarse. El resultado es similar al siguiente:

    Apply complete! Resources: 28 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    ca_certificate = LS0tLS1CRU..
    client_token = <sensitive>
    cluster_name = jenkins
    gcp_service_account_email = jenkins-wi-jenkins@PROJECT_ID.iam.gserviceaccount.com
    jenkins_k8s_config_secrets = jenkins-k8s-config
    jenkins_project_id = PROJECT_ID
    k8s_service_account_name = jenkins-wi-jenkins
    kubernetes_endpoint = <sensitive>
    service_account = tf-gke-jenkins-k253@PROJECT_ID.iam.gserviceaccount.com
    zone = us-east4-a
    

    Cuando se implementa Jenkins en el clúster de GKE que se acaba de crear, el directorio de la página principal de Jenkins se almacena en un volumen persistente según la documentación del gráfico de Helm. Esta implementación también incluye una canalización de varias ramas preconfigurada con example-pipelines/environments/Jenkinsfile, que se activa en solicitudes de extracción y se combina con las ramas dev y prod.

  3. Regresa a la carpeta principal:

    cd ../..
    
  4. Recupera las credenciales de clúster que acabas de crear:

    gcloud container clusters get-credentials jenkins --zone=us-east4-a --project=${PROJECT_ID}
    
  5. Recupera las credenciales y la URL de Jenkins:

    JENKINS_IP=$(kubectl get service jenkins -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    JENKINS_PASSWORD=$(kubectl get secret jenkins -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo
    printf "Jenkins url: http://$JENKINS_IP\nJenkins user: admin\nJenkins password: $JENKINS_PASSWORD\n"
    
  6. Accede a Jenkins mediante la información del resultado del paso anterior.

  7. Configura la ubicación de Jenkins para que GitHub pueda crear vínculos que vayan directamente a las compilaciones. Haz clic en Administrar Jenkins > Configurar sistema, y en el campo URL de Jenkins, configura la URL de Jenkins.

Cambia la configuración del entorno en una rama de funciones nueva

Ya se configuró la mayor parte del entorno. Por lo tanto, es hora de hacer algunos cambios en el código.

  1. En Cloud Shell, crea una rama de funciones nueva en la que puedas trabajar sin afectar a otras del equipo:

    git checkout -b change-vm-name
    
  2. Cambia el nombre de la máquina virtual:

    cd example-pipelines/example-create
    sed -i.bak "s/\${var.environment}-001/\${var.environment}-new/g" main.tf
    

    Estás cambiando el archivo main.tf en la carpeta example-create. Este archivo está vinculado por las carpetas del entorno dev y prod, lo que significa que el cambio se propaga en ambos entornos.

  3. Envía el cambio de código a la rama de funciones de GitHub:

    git commit -am "change vm name"
    git push --set-upstream origin change-vm-name
    
  4. En GitHub, ve a la página principal del repositorio bifurcado.

  5. Haz clic en la pestaña Pull requests del repositorio y, luego, en New pull request.

  6. En base repository, selecciona el repositorio bifurcado.

    Crea una solicitud de extracción para el repositorio base.

  7. En base, selecciona dev y, en compare, selecciona change-vm-name.

    Selecciona la base y compara las bifurcaciones.

  8. Haz clic en Create pull request.

  9. Cuando la solicitud de extracción está abierta, un trabajo de Jenkins se inicia de forma automática (Jenkins puede tomarse un minuto aproximadamente para confirmar la recepción de la solicitud de extracción nueva). Haz clic en Show all checks y espera a que la marca se vuelva verde.

    Espera a que la marca se vuelva verde.

  10. Haz clic en Details para ver más información, incluido el resultado del comando terraform plan.

    Más detalles sobre los resultados, incluido el resultado de “terraform plan”.

El trabajo de Jenkins ejecutó la canalización definida en Jenkinsfile. Esta canalización tiene comportamientos diferentes según la rama que se recupera. La compilación verifica si la variable TARGET_ENV coincide con alguna carpeta de entorno. Si coincide, Jenkins ejecuta terraform plan para ese entorno. De lo contrario, Jenkins ejecuta terraform plan para todos los entornos a fin de garantizar que el cambio propuesto sea válido en todos ellos. Si alguno de estos planes no se ejecuta, falla la compilación.

terraform plan se ejecuta para todas las subcarpetas example-pipelines/environments a fin de garantizar que los cambios propuestos se apliquen a cada entorno. De esta manera, antes de combinar la solicitud de extracción, puedes revisar los planes para asegurarte de no otorgar acceso a una entidad no autorizada, por ejemplo.

Este es el código terraform plan en JENKINSFILE:

stage('TF plan') {
  when { anyOf {branch "prod";branch "dev";changeRequest() } }
  steps {
    container('terraform') {
      sh '''
      if [[ $CHANGE_TARGET ]]; then
        TARGET_ENV=$CHANGE_TARGET
      else
        TARGET_ENV=$BRANCH_NAME
      fi

      if [ -d "example-pipelines/environments/${TARGET_ENV}/" ]; then
        cd example-pipelines/environments/${TARGET_ENV}
        terraform plan
      else
        for dir in example-pipelines/environments/*/
        do
          cd ${dir}
          env=${dir%*/}
          env=${env#*/}
          echo ""
          echo "*************** TERRAFOM PLAN ******************"
          echo "******* At environment: ${env} ********"
          echo "*************************************************"
          terraform plan || exit 1
          cd ../../../
        done
      fi'''
    }
  }
}

De manera similar, el comando terraform apply se ejecuta para las ramas de entorno, pero se ignora en cualquier otro caso. En esta sección, enviaste un cambio de código a una rama nueva, por lo que no se aplicaron implementaciones de infraestructura en el proyecto de Cloud.

Este es el código terraform apply en JENKINSFILE:

stage('TF Apply') {
  when { anyOf {branch "prod";branch "dev" } }
  steps {
    container('terraform') {
      sh '''
      TARGET_ENV=$BRANCH_NAME

      if [ -d "example-pipelines/environments/${TARGET_ENV}/" ]; then
        cd example-pipelines/environments/${TARGET_ENV}
        terraform apply -input=false -auto-approve
      else
        echo "*************** SKIPPING APPLY ******************"
        echo "Branch '$TARGET_ENV' does not represent an official environment."
        echo "*************************************************"
      fi'''
    }
  }
}

Aplica de forma forzosa la ejecución exitosa de Jenkins antes de combinar las ramas

Puedes asegurarte de que las combinaciones solo se apliquen cuando las ejecuciones de trabajos de Jenkins sean exitosas.

  1. En GitHub, ve a la página principal del repositorio bifurcado.
  2. En el nombre del repositorio, haz clic en Settings.
  3. Haz clic en Branches.
  4. En Branch protection rules, haz clic en Add rule.
  5. En Branch name pattern, ingresa dev.
  6. En Protect matching branches, selecciona Require status checks to pass before merging y, luego, continuous-integration/jenkins/pr-merge.

    Considera habilitar las opciones Require pull request reviews before merging y Include administrators para evitar que las solicitudes de extracción no revisadas y no autorizadas se combinen con el entorno de producción (opcional).

  7. Haz clic en Create.

  8. Repite los pasos del 4 al 7 y establece el Branch name pattern en prod.

Esta configuración es importante para proteger las ramas dev y prod, lo que significa que primero debes enviar las confirmaciones a otra rama y, después, combinarlas con la rama protegida. En este instructivo, la protección requiere que la ejecución del trabajo de Jenkins sea exitosa para que se permita la combinación. Puedes ver si la configuración se aplicó en la solicitud de extracción recién creada. Busca las marcas de verificación verdes.

Verifica que se aplicó la configuración.

Promueve los cambios en el entorno de desarrollo

Tienes una solicitud de extracción en espera para combinarse. Ahora puedes aplicar el estado que deseas en el entorno dev.

  1. En GitHub, ve a la página principal del repositorio bifurcado.
  2. Para el nombre del repositorio, haz clic en Pull requests.
  3. Haz clic en la solicitud de extracción que creaste.
  4. Haz clic en Merge pull request y, luego, en Confirm merge.

    Combina y confirma una solicitud de extracción.

  5. En Jenkins, haz clic en Open Blue Ocean. Luego, en el proyecto de varias ramas terraform-jenkins-create-demo, en la pestaña Branches, verifica el ícono de estado para ver si un trabajo dev nuevo se activó. Este proceso puede llevar aproximadamente un minuto.

    Verifica que se activó un trabajo **dev** nuevo.

  6. En Cloud Console, ve a la página Instancias de VM (VM instances) y verifica si tienes la VM con el nombre nuevo.

    Ir a Instancias de VM

    Verifica si tienes la VM con el nombre nuevo.

Promueve los cambios en el entorno de producción

Ahora que probaste el entorno de desarrollo, puedes promover el código de infraestructura a la producción.

  1. En GitHub, ve a la página principal del repositorio bifurcado.
  2. Haz clic en New pull request.
  3. En el base repository, selecciona el repositorio que bifurcaste.

    Selecciona el repositorio que bifurcaste.

  4. En base, selecciona prod y, en compare, selecciona dev.

    Repositorios bifurcados para base y comparar.

  5. Haz clic en Create pull request.

  6. Ingresa un título, como Promoting vm name change y, luego, haz clic en Create pull request.

  7. Espera a que el verificador se vuelva verde (puede llevar uno o dos minutos) y, luego, haz clic en el vínculo Details junto a continuous-integration/jenkins/pr-merge.

    Espera a que el verificador se vuelva verde.

  8. En Jenkins, selecciona TF Plan y revisa los cambios propuestos en los registros.

    Revisa los cambios propuestos en los registros.

  9. Si los cambios propuestos tienen un aspecto correcto, en GitHub haz clic en Merge pull request y, luego, en Confirm merge.

  10. En Cloud Console, abre la página Instancias de VM (VM instances) y verifica si la VM de producción se implementó.

    Instancias de VM

    Verifica si se implementó la VM de producción.

Configuraste una canalización de infraestructura como código en Jenkins. En el futuro, se recomienda probar lo siguiente:

  • Agregar implementaciones para casos de uso independientes
  • Crear entornos adicionales que reflejen tus necesidades
  • Usar un proyecto por entorno en lugar de una VPC por entorno

Realiza una limpieza

Sigue estos pasos para evitar que se apliquen cargos a tu cuenta de Google Cloud Platform por los recursos que usaste en este instructivo:

Borra el proyecto

  1. En Cloud Console, ve a la página Administrar recursos.

    Ir a la página Administrar recursos

  2. En la lista de proyectos, selecciona el proyecto que deseas borrar y haz clic en Borrar .
  3. En el cuadro de diálogo, escribe el ID del proyecto y haz clic en Cerrar para borrar el proyecto.

Próximos pasos