Gestionando una organización de GitHub con Infraestructura como Código

Esta publicación comparte nuestra experiencia adoptando infraestructura como código (IaC) para gestionar los recursos de una organización de GitHub.

Gestionando una organización de GitHub con Infraestructura como Código

Gestionar los recursos de una organización de GitHub puede ser complejo, sin importar el tamaño de la empresa. Esto ocurre especialmente cuando hay una gran cantidad de equipos y repositorios que administrar, niveles de acceso que asignar y altas/bajas de usuarios.

Esta publicación comparte nuestra experiencia adoptando infraestructura como código (IaC) para gestionar los recursos de una organización de GitHub.

La organización donde se implementó esta iniciativa es una empresa estadounidense de ventas en línea con sede en la ciudad de Nueva York, compuesta por más de 3000 personas, más de 150 equipos y más de 500 repositorios. Hace unos meses el equipo SRE decidió gestionar los recursos de GitHub usando IaC con terraform; en la fase de investigación/prueba de concepto (PoC) encontramos un completo proveedor de GitHub para terraform que podía ayudarnos a lograr nuestro objetivo, ya que ofrece la capacidad de gestionar de forma programática repositorios, organización, equipos, permisos y proyectos.

Tras los buenos resultados obtenidos con la PoC, iniciamos nuestro camino teniendo presente el reto del cambio: pasar de la creación o actualización fácil y manual de recursos de GitHub a una forma controlada, estandarizada y programática. El cambio implicará sin duda resolver múltiples retos técnicos, así como comunicación y evangelización hacia los equipos de toda la organización, pero creemos que el esfuerzo invertido a largo plazo ayudará a evolucionar con facilidad y aportará transparencia sobre las configuraciones de los repositorios, los miembros de los equipos, los roles y los permisos. También acelerará el proceso de incorporación y salida de personas relacionado con la base de código de la organización.

Asumiendo que la persona lectora conoce terraform y sus proveedores, esa parte no se cubrirá en esta publicación, pero se comparten algunos enlaces útiles por si se desea obtener más contexto:

Definir recursos estándar con módulos

Definir recursos estándar con módulos permite construir código de infraestructura reutilizable y modular que puede gestionarse como una sola unidad. Esto ayuda a aumentar la eficiencia y reducir errores, además de facilitar el mantenimiento y la actualización de la infraestructura.

Pensando en facilitar el uso de los recursos de GitHub, definimos módulos de terraform para agrupar un conjunto de recursos en términos de repositorios y equipos. Por ejemplo, el módulo de repositorio de GitHub se compone de 4 recursos principales: github_repository, github_branch_default, github_branch_protection y github_team_repository.

Composición del módulo de repositorio
Figura 1: Composición del módulo de repositorio

Para encapsular esos recursos definimos un único módulo que contiene todas las propiedades requeridas, de modo que cada vez que alguien dentro de la organización quiere crear un repositorio solo necesita usar un recurso y completar todas las propiedades requeridas.

#notifications.tf

module "repository_notifications" {
  source = "git::https://github.com/herrera-luis/infra-modules.git//github-repository?ref=v0.0.10"
  #source                 = "../../../infra-modules/github-repository"
  name               = "notifications"
  description        = "The notification service is a platform that sends timely and relevant notifications or messages to users via different communication channels such as emails, SMS and push notifications"
  allow_merge_commit = false
  auto_init          = false
  topics             = ["notifications", "platform", "python"]
  homepage_url       = "https://notifications.inhouse-service.com"
  visibility         = "private"
  default_branch     = "main"
  archived           = false
  lock_branch        = local.lock_branch
  # Permission options are: pull, triage, push, maintain, admin
  team_access = [
    {
      team_id    = local.teams["notifications"].team_id
      permission = local.permissions.admin
    },
    {
      team_id    = local.teams["sre"].team_id
      permission = local.permissions.pull
    },
  ]
  deploy_branch_protection         = true
  branch_protection_enforce_admins = true
  branch_protection_required_pull_request_reviews = {
    dismiss_stale_reviews           = false
    require_code_owner_reviews      = true
    required_approving_review_count = 1
  }
  delete_branch_on_merge     = true
  enable_issues              = true
  enable_downloads           = true
  enable_wiki                = true
  enable_projects            = true
  enable_vulnerabiliy_alerts = true
}

Al trabajar con el módulo, es fundamental prestar atención a las propiedades que soporta. Durante la fase de definición tomamos la decisión de estandarizar las configuraciones que todos los repositorios de la organización admitirían. Esta estandarización garantiza consistencia y simplifica el proceso de gestión de los repositorios.

Configuraciones como otorgar permisos a usuarios individuales fueron eliminadas y, en su lugar, mantuvimos solo los permisos de los equipos, lo que significa que cada usuario debe pertenecer a un equipo para obtener acceso a los repositorios. Otra configuración que eliminamos fueron las GitHub pages, porque se consideró un riesgo de seguridad (podría ser una forma de exponer información confidencial), ya que la mayoría de los repositorios se crearon con visibilidad interna o privada, y los repositorios que contienen aplicaciones web (apps de frontend) en fase de desarrollo se despliegan en una red privada a la que solo se puede acceder a través de una VPN.

El equipo de GitHub se compone de 2 recursos principales: github_team y github_team_members. Encapsulamos esos 2 recursos en 1 módulo que llamamos github-team.

#sre.tf

module "sre_team" {
  source           = "git::https://github.com/herrera-luis/infra-modules.git//github-team?ref=v0.0.10"
  team_name        = "SRE"
  team_description = "The Site Reliability Engineering Team"
  team_privacy     = "closed"
  parent_team_id   = "3118942" # Infrastructure
  team_members = [
    {
      username = github_membership.member["herrera-luis"].username
      role     = "maintainer"
    },
    {
      username = github_membership.member["teammate-1"].username
      role     = "member"
    },
    {
      username = github_membership.member["teammate-2"].username
      role     = "member"
    },
  ]
}

En las definiciones de equipos había un requisito de implementar equipos anidados para reflejar el organigrama y simplificar la gestión de permisos de grupos grandes; en el módulo de equipos que definimos hicimos uso de la propiedad parent_team_id, que nos permitió construir equipos anidados y otorga a los equipos hijos la capacidad de heredar los permisos de acceso del equipo padre.

Equipos anidados
Figura 2: Equipos anidados

Un factor importante a considerar al trabajar con el recurso de equipo de GitHub es que, antes de añadir usuarios a los equipos, estos deben formar parte de la organización de GitHub, así que hay que encontrar una manera de mapear los nombres de usuario de GitHub con el rol que tendrán y agregarlos como miembros de la organización; en el siguiente apartado compartimos cómo lo logramos.

Usuario miembro y equipo
Figura 3: Usuario miembro y equipo

Mapa de membresía de usuarios de GitHub

Un usuario se referencia en el código de membresía de la organización y en uno o más equipos; esta parte de la configuración es un proceso manual, ya que necesitas solicitar el nombre de usuario de GitHub y luego añadirlo a la lista de recursos. Para facilitar la gestión de la membresía de usuarios generamos un mapa de objetos —una estructura de datos que asocia claves con valores—; como clave usamos los nombres de usuario y como valores usamos el rol. Después de desplegarlos expusimos su slug e id para que los recursos del módulo de equipos pudieran reutilizarlos. Veamos cómo luce el objeto del mapa de membresía de usuarios:

#users.auto.tfvars

users = {
  "herrera-luis" = {
    org_role = "admin"
  }
  "admin-teammate-1" = {
    org_role = "admin"
  }
  "teammate-1" = {
    org_role = "member"
  }
  "teammate-2" = {
    org_role = "member"
  }
}

Dado que definimos un mapa de objetos, tenemos la capacidad de iterarlo sobre un recurso de terraform para evitar declararlo varias veces; en terraform, para iterar un objeto está disponible el meta-argumento for_each, así que hicimos uso de él y así luce la implementación:

#user.tf

resource "github_membership" "member" {
  for_each = var.users
  username = each.key
  role     = each.value.org_role
}

#output

output "users" {
  value = {
    for user, userinfo in var.users : user =>
    {
      login = user
      membership = {
        role = userinfo.org_role
      }
    }
  }
}

Desarrollando un script de importación

Después de definir el módulo con las propiedades estándar, otro reto técnico que tuvimos que resolver fue mantener el negocio en funcionamiento sin romper nada, lo que significa que necesitábamos importar todos los equipos de GitHub junto con los repositorios y sus configuraciones actuales al código de terraform, de modo que en el paso final, tras completar la ejecución del script de importación y si ejecutamos terraform apply, todo debería quedar sincronizado. Teníamos alrededor de 500+ repositorios y 150+ equipos; pensamos que importarlos uno por uno sería una pesadilla, por esa razón optamos por usar un script que pudiera automatizar esa tarea. Dado que la mayor parte del equipo tiene experiencia con el lenguaje python, decidimos usarlo y complementarlo con librerías.

La primera tarea del script era obtener todas las propiedades de cada repositorio de la organización de GitHub. Así que buscamos las mejores librerías que pudieran facilitar ese proceso y encontramos PyGithub, una librería completa que tiene una buena integración con las APIs de GitHub.

La segunda tarea del script era generar un archivo de terraform por repositorio, de modo que pudiéramos gestionar cada repositorio con sus configuraciones por separado. Para lograr ese objetivo usamos la librería del motor de plantillas Jinja; la implementación de esta solución resultó muy ventajosa para nosotros y generó beneficios significativos, porque era sencilla de usar y la generación de archivos era transparente. Esto fue así porque definimos una plantilla basada en el módulo de terraform y la plantilla iteraba todas las propiedades usando un objeto de python que le enviábamos como parámetro.

La última tarea de nuestro script era instalar los archivos de terraform generados e importarlos al estado de terraform, lo que significa que el script tenía que ejecutar los comandos terraform init y terraform import tras bambalinas; para lograr ese objetivo usamos la librería python-terraform, que proporciona un wrapper de la herramienta de línea de comandos de terraform.

Las tareas del script de importación
Figura 4: Las tareas del script de importación

Una vez completada y validada la definición del script con un grupo de repositorios de ejemplo, lo ejecutamos para importar todos los repositorios de la organización. Encontramos que el tiempo para completar el proceso de generar los archivos de terraform e importarlos al estado de terraform toma alrededor de 30 minutos. Esto podría interpretarse como mucho tiempo, pero este comportamiento solo ocurrió la primera vez que importamos todos los recursos. Tras incorporar los nuevos repositorios y realizar cambios posteriores, el tiempo necesario para ejecutar los comandos terraform plan y terraform apply se redujo a la mitad, generando un ahorro de tiempo significativo.

Pipeline para automatizar el despliegue de los recursos de la organización de GitHub

Implementar un pipeline automatizado para validar y desplegar cambios en los recursos de GitHub es una forma práctica de reducir errores manuales y hacer que el proceso de despliegue sea más eficiente, consistente y confiable. Al automatizar estas tareas, las organizaciones pueden mejorar su proceso de desarrollo en general, aumentar la productividad de las personas desarrolladoras y reducir el riesgo de generar cambios no deseados durante los despliegues. Nuestro objetivo siempre ha sido fomentar la adopción de IaC, permitiendo que cualquier miembro de la organización cree o actualice recursos de GitHub mediante un pull-request (PR). Al implementar un pipeline automatizado, pudimos impulsar esa adopción y hacer que el proceso fuera aún más ágil y eficiente.

En nuestro contexto, habíamos estado usando el proveedor de pipelines circle-ci para el CI/CD de los servicios y los recursos de infraestructura, así que usamos el mismo proveedor para crear un pipeline que gestiona los recursos de la organización de GitHub.

Pipeline de repositorio
Figura 5: Pipeline de repositorio

El proceso establecido para realizar cambios en los recursos de la organización de GitHub se compuso de 3 pasos. En el primer paso, la persona usuaria debe hacer los cambios deseados sobre los archivos de terraform que representan los recursos de GitHub. Después, debe crear un PR para fusionar su rama de git con la rama principal. Cuando se crea un PR se dispara un pipeline que ejecuta el comando terraform plan para validar que los cambios realizados no romperán nada. El PR estará listo para fusionarse si tiene al menos 1 aprobación y si el comando terraform plan devuelve un estado exitoso. Después de fusionar el PR, el último paso era hacer una aprobación manual en el workflow del pipeline para desplegar los cambios.

Implementamos una aprobación manual ya que no existe un entorno de pruebas o de staging para GitHub. Todos los cambios se envían directamente al único entorno que proporciona GitHub, que podría considerarse el entorno de producción. Así, con este enfoque creemos que podemos reducir cambios no deseados o evitar romper los recursos de la organización de GitHub.

Proceso para realizar cambios en los recursos de github
Figura 6: Proceso para realizar cambios en los recursos de github

La estandarización de la organización para gestionar recursos de GitHub con IaC

Cuando comenzamos nuestro camino para gestionar los recursos de GitHub con IaC, todos dentro de la organización podían crear/actualizar/eliminar repositorios. Además, algunos miembros con rol de administrador podían otorgar permisos a usuarios o equipos sobre los repositorios. En otras palabras, no había un estándar para gestionar los recursos de GitHub. Tras completar la migración de todos los recursos de GitHub a archivos de terraform, solicitamos cambios para gestionarlos de una forma estandarizada y transparente con IaC.

Antes de aplicar los cambios a la gestión de los recursos, compartimos nuestro recorrido del proceso de migración y los casos de uso soportados en una sesión interna de la organización llamada: “Tech Team Demo”. Es un espacio donde los equipos técnicos internos comparten nuevas funcionalidades entregadas, retos y beneficios. Para nosotros fue un buen espacio para empatizar con los equipos técnicos y compartir la nueva forma de gestionar los recursos de GitHub.

Tras nuestra presentación en el tech team demo, quisimos estandarizar los permisos sobre los repositorios ofreciendo acceso exclusivo a los equipos y no a usuarios específicos. Para lograr ese objetivo, compartimos múltiples comunicaciones con los tech leads solicitando los nombres de los repositorios a los que sus equipos necesitan acceso y qué permisos requieren sobre ellos. Después de unas semanas, configuramos los equipos con los permisos requeridos que los tech leads nos compartieron y eliminamos los permisos a usuarios. Con esos cambios, pudimos estandarizar los permisos sobre los repositorios.

La otra parte que quisimos estandarizar fue el proceso para añadir o eliminar a un usuario dentro de la organización de GitHub y como miembro de un equipo de GitHub usando IaC. Compartimos el nuevo proceso con las personas responsables de esta tarea y les mostramos los pasos que debían seguir para lograrlo. En las primeras semanas, cuando empezaron a usar el nuevo proceso, recibimos algunas solicitudes de aclaración, pero con el tiempo pudieron hacerlo por su cuenta y, por supuesto, lo celebramos, porque con eso pudimos estandarizar las altas/bajas de usuarios y equipos de la organización de GitHub.

Con los dos nuevos estándares implementados, solicitamos eliminar los permisos para crear/actualizar/eliminar repositorios, equipos y usuarios, de modo que cualquier cambio tenga que hacerse únicamente usando IaC. En las primeras semanas de la eliminación de permisos recibimos muchísimas solicitudes de acceso, ya que varios usuarios tenían acceso directo a repositorios o no formaban parte del equipo correcto, así que durante unas semanas estuvimos ocupados configurando los permisos y equipos correctos, pero después de eso todo fue transparente para los miembros técnicos de la organización, de modo que actualmente saben de qué equipos forman parte y qué repositorios poseen.

Hallazgos y próximos pasos

Después de implementar los nuevos estándares para gestionar los recursos de GitHub, pudimos encontrar algunas áreas que podríamos mejorar. Una de ellas estaba relacionada con los repositorios: por el nombre o la descripción usados, encontramos que algunos repositorios se crearon con fines de PoC y la persona propietaria olvidó eliminarlos. También encontramos que algunos repositorios no se habían actualizado en el último año, así que nadie estaba trabajando en ellos. Con base en esos hallazgos, podríamos archivar los repositorios identificados y solicitar confirmación a los equipos técnicos correspondientes sobre la necesidad de mantenimiento. Si no se requiere mantenimiento, podríamos proceder a eliminarlos.

En cuanto a los equipos, encontramos que algunos estaban compuestos por uno o dos miembros, y que además estos miembros forman parte de múltiples equipos, lo que significa que podríamos refactorizar la composición de los miembros de los equipos en la organización de GitHub. Si queremos ir un paso más allá, también podríamos configurar los equipos según los grupos del proveedor de identidad (Okta, Auth0, OneLogin, etc.) que se usa en la organización.

Dado que gestionamos cientos de recursos de GitHub con IaC, el pipeline puede ser lento al ejecutar los comandos terraform plan y terraform apply, así que podríamos encontrar mejores mecánicas para mejorar el rendimiento agrupando los repositorios más usados, los menos usados o los archivados, y configurando otro pipeline para gestionarlos por separado.

A veces, en periodos críticos relacionados con las ventas, el negocio necesita aplicar un congelamiento de código (code freeze) a toda la organización, lo que significa que nadie puede fusionar un PR a la rama principal ni desplegar cambios al entorno de producción. Sería bueno aprovechar las configuraciones de repositorio que tenemos disponibles para habilitar y deshabilitar esa funcionalidad cuando el negocio lo requiera.

En el pipeline, como prioridad, se ejecuta el comando terraform plan y luego esperamos la aprobación manual para desplegar los cambios. A veces olvidábamos presionar la aprobación en el pipeline porque comenzábamos con otra actividad. Sería bueno implementar una notificación que se envíe al chat grupal del equipo, avisándoles que la aprobación en el pipeline está esperando ser presionada.

Reflexiones finales

Este recorrido de adoptar IaC para gestionar los recursos de una organización de GitHub nos ha dejado múltiples lecciones. Entre ellas está el desarrollo de la capacidad de iterar constantemente. En concreto, cuando estábamos diseñando nuestros módulos para el repositorio y los equipos, nos encontramos con diferentes configuraciones y, para cubrir todos los recursos de GitHub, tuvimos que modificar nuestros módulos varias veces.

Otra lección importante fue la comunicación y la evangelización hacia otros equipos técnicos que quizás al principio se resistían a la nueva forma de modificar los recursos de GitHub. Hoy en día han adoptado los estándares propuestos por la transparencia y el control que también les proporciona.

Espero que esta publicación pueda guiarte o brindarte ideas si estás pensando en gestionar los recursos de tu organización de GitHub con IaC.