Atlantis en Google Cloud Run
INFO
Aunque está escrita para Google Cloud Run, esta arquitectura de despliegue también se aplica a AWS Fargate, Azure Container Instances y Kubernetes.
INFO
Esta publicación de blog cubre las partes más importantes del código de Terraform. Para un ejemplo completo y funcional, consulta nuestro atlantis-on-gcp-cloud-run example.
La mayoría de los despliegues de Atlantis se ejecutan en VM autogestionadas. Aunque esta es una opción familiar y directa, viene con desafíos. Las identidades a menudo se vuelven excesivamente poderosas, con acceso directo o indirecto —mediante impersonation— a muchos recursos y proyectos. La alta disponibilidad también falta: Atlantis escribe su backend de locking directamente en disco, así que si la VM deja de funcionar, Atlantis deja de estar disponible. En resumen, las VM autogestionadas crean un único punto de falla, no ofrecen escalado horizontal y exigen mantenimiento continuo, desde parches y actualizaciones del SO hasta backups.
En esta publicación de blog, mostraremos cómo ejecutar Atlantis en plataformas serverless de contenedores como Google Cloud Run. En lugar de una sola instancia, usaremos una base de datos central para locking, y desplegaremos múltiples instancias de Atlantis detrás de un balanceador de carga. Cada instancia se ejecuta con su propia identidad y permisos limitados, administrando solo sus propios proyectos. Esta arquitectura elimina el único punto de falla, permite escalado horizontal y habilita un modelo de seguridad de privilegio mínimo.
Lo que construiremos
Aquí hay una vista general de alto nivel de la arquitectura que construiremos:

- Un balanceador de carga HTTP(S) externo para enrutar solicitudes a múltiples instancias de Atlantis.
- Múltiples instancias de Atlantis ejecutándose en Google Cloud Run.
- Una instancia de Memorystore for Redis para proporcionar un backend central de locking.
INFO
Si quieres adelantarte, puedes encontrar el código de Terraform para esta arquitectura en nuestro atlantis-on-gcp-cloud-run example.
Sin embargo, recomendamos leer el resto de esta publicación de blog para entender cómo funciona todo.
Dejamos cosas afuera
Para mantener esta publicación en una longitud razonable, hemos dejado afuera algunos detalles importantes. Por ejemplo, no cubrimos la configuración de red, DNS, cómo extraer una imagen de Docker o certificados TLS wildcard, ni profundizamos en cada opción y ajuste de Atlantis; nuestro enfoque aquí está en las partes más relevantes para la arquitectura. Dicho eso, recomendamos encarecidamente ejecutar Atlantis en una VPC aislada con Private Service Access habilitado. Esto asegura que Atlantis solo hable con las API de Google para hacer su trabajo, sin llegar nunca a tu otra infraestructura.
BoltDB: Genial, si solo tienes un writer
Atlantis usa BoltDB como su backend de locking predeterminado. BoltDB es un almacén simple de clave-valor embebido que escribe directamente en disco. Esto funciona bien para despliegues de instancia única, pero BoltDB limita Atlantis a una arquitectura de un solo nodo. Para lograr alta disponibilidad y escalado horizontal, necesitas reemplazarlo con una base de datos distribuida y administrada que múltiples instancias de Atlantis puedan compartir de forma segura.
Un Atlantis para gobernarlos a todos
Para facilitar la creación y administración de múltiples instancias de Atlantis, también desplegaremos una instancia dedicada de Atlantis de “management”. Esta instancia será responsable de administrar el ciclo de vida de las otras instancias de Atlantis, incluida su creación, actualización y eliminación según sea necesario. Normalmente mantengo esto en un proyecto separado de Google Cloud llamado atlantis-mgmt, junto con un repositorio Git dedicado para este propósito.
Una vez que tienes una instancia configurada, es directo replicarla y colocarla detrás de un balanceador de carga compartido.
Redis: Un backend distribuido de locking
Desde Atlantis v0.19.0, Redis es un backend de locking compatible. Redis es un almacén en memoria de estructuras de datos que usaremos para proporcionar un backend central de locking para múltiples instancias de Atlantis. Cada instancia se conectará a la misma instancia de Redis, permitiéndoles coordinar locks y evitar conflictos.
Redis también admite persistencia, mediante RDB (Redis Database), que realiza snapshots point-in-time del conjunto de datos en intervalos especificados, y AOF (Append Only File), que registra cada operación de escritura recibida por el servidor Redis. Esto significa que incluso si la instancia de Redis deja de funcionar, no perdemos nuestros locks.
Nuestro primer recurso a crear es una instancia de Redis:
resource "google_redis_instance" "atlantis" {
name = "atlantis"
tier = "STANDARD_HA"
redis_version = "REDIS_7_2"
memory_size_gb = 1
region = "your-region"
authorized_network = "your-network-id"
connect_mode = "PRIVATE_SERVICE_ACCESS"
persistence_config {
persistence_mode = "RDB"
rdb_snapshot_period = "TWENTY_FOUR_HOURS"
}
maintenance_policy {
# ...
}
project = "your-project-id"
lifecycle {
prevent_destroy = true
}
}Esto crea una instancia de Redis con 1 GiB de memoria y habilita RDB, tomando snapshots cada 24 horas. El connect_mode está configurado en PRIVATE_SERVICE_ACCESS, por lo que la instancia de Redis solo es accesible desde dentro de tu VPC. Asegúrate de que Private Service Access esté configurado en tu VPC antes de crear la instancia.
Despliegue en Cloud Run
Cloud Run es una plataforma serverless de contenedores que escala automáticamente tus aplicaciones, maneja solicitudes HTTP y abstrae la infraestructura. Pagas solo por el cómputo que usas, y cada servicio se ejecuta bajo una única cuenta de servicio que define sus permisos. Esto la convierte en una gran opción para una o más instancias de Atlantis.
Comenzaremos creando una configuración de Atlantis del lado del servidor, atlantis/management.yaml:
repos:
- id: github.com/acme/example
apply_requirements: [approved, mergeable]
import_requirements: [approved, mergeable]
allowed_overrides: ["workflow"]
allowed_workflows: ["example"]
delete_source_branch_on_merge: true
workflows:
example:
plan:
steps:
- init
- plan
apply:
steps:
- applyINFO
La configuración de Terraform a continuación destaca solo las variables de entorno de Atlantis que son relevantes para esta publicación de blog. Para un ejemplo completo y funcional, consulta nuestro atlantis-on-gcp-cloud-run example.
Comienza creando la instancia de Atlantis de management. Debajo de esta configuración de Terraform, encontrarás detalles sobre las opciones de configuración —como variables de entorno y otras configuraciones importantes— que configuran Atlantis para esta arquitectura.
resource "google_cloud_run_v2_service" "atlantis_management" {
provider = google-beta
name = "atlantis-management"
location = "your-region"
deletion_protection = false
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
invoker_iam_disabled = true
launch_stage = "GA"
template {
scaling {
min_instance_count = 1
max_instance_count = 1
}
execution_environment = "EXECUTION_ENVIRONMENT_GEN2"
service_account = google_service_account.atlantis_management.email
containers {
image = "ghcr.io/runatlantis/atlantis:v0.35.1"
resources {
limits = {
cpu = "1"
memory = "2Gi"
}
}
volume_mounts {
name = "atlantis"
mount_path = "/app/atlantis"
}
env {
name = "ATLANTIS_PORT"
value = "8080"
}
env {
name = "ATLANTIS_DATA_DIR"
value = "/app/atlantis"
}
env {
name = "ATLANTIS_USE_TF_PLUGIN_CACHE"
value = "true"
}
env {
name = "ATLANTIS_LOCKING_DB_TYPE"
value = "redis"
}
env {
name = "ATLANTIS_REDIS_HOST"
value = google_redis_instance.atlantis.host
}
env {
name = "ATLANTIS_REDIS_DB"
value = "0"
}
env {
name = "ATLANTIS_ATLANTIS_URL"
value = "https://management.atlantis.acme.com"
}
env {
name = "ATLANTIS_REPO_CONFIG_JSON"
value = jsonencode(yamldecode(file("${path.module}/atlantis/management.yaml")))
}
}
vpc_access {
egress = "ALL_TRAFFIC"
network_interfaces {
network = "your-network-id"
subnetwork = "your-subnetwork-id"
}
}
volumes {
name = "atlantis"
empty_dir {
medium = "MEMORY"
size_limit = "5Gi"
}
}
}
project = "your-project-id"
}Almacenamiento efímero
Atlantis es intensivo en I/O: hace checkout de repositorios, ejecuta comandos de Terraform, descarga providers y extrae módulos. Esto requiere un sistema de archivos escribible para almacenar datos temporales. En Cloud Run, esto se maneja con almacenamiento efímero, que se limpia cada vez que una instancia de contenedor se detiene o reinicia.
Debido a cómo opera Atlantis, sus requisitos de almacenamiento efímero son limitados: los datos de pull request se eliminan del sistema de archivos una vez que se hace merge o se cierra, los providers pueden almacenarse en caché usando ATLANTIS_USE_TF_PLUGIN_CACHE, y los binarios de Terraform ya están incluidos en la imagen del contenedor. Por esta razón, configuramos un volumen empty_dir en memoria de 5 GiB, montado en /app/atlantis y configurado como el ATLANTIS_DATA_DIR.
# ...
volumes {
name = "atlantis"
empty_dir {
medium = "MEMORY"
size_limit = "5Gi"
}
}
# ...
volume_mounts {
name = "atlantis"
mount_path = "/app/atlantis"
}
# ...
env {
name = "ATLANTIS_DATA_DIR"
value = "/app/atlantis"
}
env {
name = "ATLANTIS_USE_TF_PLUGIN_CACHE"
value = "true"
}Mantener una instancia caliente
Las instancias de Cloud Run pueden escalar a cero cuando no están en uso, lo que puede provocar cold starts y pérdida del almacenamiento efímero en memoria. Para evitar esto, configuramos min_instance_count en 1, asegurando que al menos una instancia siempre esté ejecutándose y lista para manejar solicitudes.
scaling {
min_instance_count = 1
max_instance_count = 1
}Impersonation y privilegio mínimo
Cada servicio Atlantis Cloud Run desplegado se ejecuta bajo una cuenta de servicio que define su identidad. En lugar de dar a esta cuenta acceso amplio, usamos una cuenta de servicio que solo tiene permiso para hacer impersonation de otras cuentas de servicio más restringidas.
Esto no es algo que necesites hacer para la instancia de Atlantis de management, ya que esa se despliega contra un único proyecto de todos modos, pero es importante para las otras instancias de Atlantis que administrarán múltiples proyectos y entornos. Al usar impersonation, podemos asegurar que cada instancia de Atlantis solo tenga los permisos que necesita para administrar sus proyectos específicos.
Por ejemplo, puedes crear una cuenta de servicio base de Atlantis (atlantis-example) y cuentas de servicio separadas para cada entorno (p. ej. atlantis-example-dev, atlantis-example-prod). A la cuenta base se le concede el rol roles/iam.serviceAccountTokenCreator en esas cuentas de entorno, y las suplanta cuando Atlantis ejecuta comandos de Terraform.
A su vez, a estas cuentas de servicio específicas de entorno se les conceden solo los permisos requeridos sobre los proyectos que administran. Esto asegura que si una instancia de Atlantis se ve comprometida, el blast radius se limite solo a los recursos que administra esa instancia.
Aquí hay un ejemplo de cómo configurar esto en Terraform:
locals {
atlantis_network_service_accounts = [
"atlantis-example-dev",
"atlantis-example-prod",
]
}
# Base Atlantis service account
resource "google_service_account" "atlantis_example" {
account_id = "atlantis-example"
project = local.project_id
}
# Per-environment service accounts
resource "google_service_account" "atlantis_example_service_accounts" {
for_each = toset(local.atlantis_example_service_accounts)
account_id = each.value
project = local.project_id
}
# Allow base SA to impersonate the env-specific ones
resource "google_service_account_iam_member" "atlantis_example_impersonation" {
for_each = google_service_account.atlantis_example_service_accounts
service_account_id = each.value.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:${google_service_account.atlantis_example.email}"
}La propia impersonation se habilita configurando la variable de entorno GOOGLE_IMPERSONATE_SERVICE_ACCOUNT dentro de un workflow atlantis/example.yaml. En esta configuración, Atlantis administra dos entornos separados: dev e prod — cambiando identidades a la cuenta de servicio específica del entorno correspondiente durante las operaciones plan e apply.
repos:
- id: github.com/acme/example
apply_requirements: [approved, mergeable]
import_requirements: [approved, mergeable]
delete_source_branch_on_merge: true
allowed_overrides: ["workflow"]
allowed_workflows: ["example-dev", "example-prod"]
workflows:
example-dev:
plan:
steps:
- env:
name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
value: example-dev@acme-atlantis-mgmt.iam.gserviceaccount.com
- run: rm -rf .terraform
- init:
extra_args:
["-lock=false", "-backend-config=env/dev/backend-config.tfvars"]
- plan:
extra_args: ["-lock=false", "-var-file=env/dev/vars.tfvars"]
apply:
steps:
- env:
name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
value: example-dev@acme-atlantis-mgmt.iam.gserviceaccount.com
- apply:
extra_args: ["-lock=false"]
example-prod:
plan:
steps:
- env:
name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
value: example-prod@acme-atlantis-mgmt.iam.gserviceaccount.com
- run: rm -rf .terraform
- init:
extra_args:
["-lock=false", "-backend-config=env/prod/backend-config.tfvars"]
- plan:
extra_args: ["-lock=false", "-var-file=env/prod/vars.tfvars"]
apply:
steps:
- env:
name: GOOGLE_IMPERSONATE_SERVICE_ACCOUNT
value: example-prod@acme-atlantis-mgmt.iam.gserviceaccount.com
- apply:
extra_args: ["-lock=false"]El balanceador de carga compartido
Al ejecutar múltiples instancias de Atlantis —cada una responsable de un conjunto diferente de proyectos o entornos— es importante enrutar el tráfico a la instancia correcta de Atlantis. En lugar de dar a cada instancia su propio endpoint público, centralizamos el tráfico mediante un único balanceador de carga HTTPS global.
El balanceador de carga a continuación usa enrutamiento basado en host para dirigir solicitudes a la instancia apropiada de Atlantis según el subdominio. Por ejemplo, las solicitudes a network.atlantis.acme.com se enrutan a la instancia de Atlantis que administra los proyectos de red, mientras que las solicitudes a workloads.atlantis.acme.com van a la instancia de Atlantis que administra los proyectos de carga de trabajo.
resource "google_compute_url_map" "atlantis" {
name = "atlantis"
default_url_redirect {
host_redirect = "atlantis.acme.com"
https_redirect = true
redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
strip_query = false
}
host_rule {
hosts = ["network.atlantis.acme.com"]
path_matcher = "atlantis-network-webhooks"
}
host_rule {
hosts = ["workloads.atlantis.acme.com"]
path_matcher = "atlantis-workloads-webhooks"
}
host_rule {
hosts = ["management.atlantis.acme.com"]
path_matcher = "atlantis-management-webhooks"
}
path_matcher {
name = "atlantis-network-webhooks"
default_service = google_compute_backend_service.atlantis_network.id
path_rule {
paths = ["/events"]
service = google_compute_backend_service.atlantis_network_webhooks.id
}
}
path_matcher {
name = "atlantis-workloads-webhooks"
default_service = google_compute_backend_service.atlantis_workloads.id
path_rule {
paths = ["/events"]
service = google_compute_backend_service.atlantis_workloads_webhooks.id
}
}
path_matcher {
name = "atlantis-management-webhooks"
default_service = google_compute_backend_service.atlantis_management.id
path_rule {
paths = ["/events"]
service = google_compute_backend_service.atlantis_management_webhooks.id
}
}
project = "your-project-id"
}
resource "google_compute_ssl_policy" "restricted" {
name = "restricted"
profile = "RESTRICTED"
min_tls_version = "TLS_1_2"
project = "your-project-id"
}
resource "google_compute_target_https_proxy" "atlantis" {
name = "atlantis"
url_map = google_compute_url_map.atlantis.id
ssl_certificates = [
google_compute_managed_ssl_certificate.atlantis_network.id,
google_compute_managed_ssl_certificate.atlantis_workloads.id,
google_compute_managed_ssl_certificate.atlantis_management.id,
]
ssl_policy = google_compute_ssl_policy.restricted.id
project = "your-project-id"
}
resource "google_compute_global_forwarding_rule" "atlantis" {
name = "atlantis"
target = google_compute_target_https_proxy.atlantis.id
port_range = "443"
ip_address = google_compute_global_address.atlantis.address
load_balancing_scheme = "EXTERNAL_MANAGED"
project = "your-project-id"
}Cada instancia de Atlantis se registra como un servicio backend en el balanceador de carga. Es importante destacar que cada instancia requiere dos backends separados: uno para el endpoint HTTP principal de Atlantis y otro para el endpoint de webhook /events. Esta separación nos permite proteger la interfaz principal de Atlantis detrás de Identity-Aware Proxy (IAP), asegurando que solo los usuarios autorizados puedan acceder a ella, mientras se mantiene el endpoint de webhook accesible públicamente para que GitHub o GitLab puedan entregar eventos sin restricción.
Recomendamos encarecidamente proteger el endpoint /events con una security policy para bloquear tráfico no deseado. Como mínimo, restringe el acceso a los rangos de IP usados por tu proveedor Git, y a algunos patrones comunes de vulnerabilidades web. Consulta Cloud Armor preconfigured WAF rules.
resource "google_compute_backend_service" "atlantis_workloads" {
name = "atlantis-workloads"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
load_balancing_scheme = "EXTERNAL_MANAGED"
security_policy = google_compute_security_policy.atlantis.id
backend {
group = google_compute_region_network_endpoint_group.atlantis_workloads.id
}
iap {
enabled = true
}
project = "your-project-id"
}
resource "google_compute_backend_service" "atlantis_workloads_webhooks" {
name = "atlantis-workloads-webhooks"
protocol = "HTTP"
port_name = "http"
timeout_sec = 30
load_balancing_scheme = "EXTERNAL_MANAGED"
security_policy = google_compute_security_policy.atlantis_events_webhook.id
backend {
group = google_compute_region_network_endpoint_group.atlantis_workloads.id
}
project = "your-project-id"
}Conclusión
Al desplegar Atlantis en Google Cloud Run con un backend compartido de locking Redis y un balanceador de carga compartido, proporcionamos un despliegue de Atlantis altamente disponible, con escalado horizontal y seguro. Cada instancia de Atlantis se ejecuta con su propia identidad y permisos limitados, administrando solo sus propios proyectos.
Como hay mucho que cubrir, y queremos mantener esta publicación en una longitud razonable, no hemos cubierto todo. Para un ejemplo completo y funcional, consulta nuestro atlantis-on-gcp-cloud-run example.
Si tienes alguna pregunta o comentario, únete al canal de Slack #atlantis en el espacio de trabajo de Slack de Cloud Native Computing Foundation, o abre un issue en el atlantis-on-gcp-cloud-run example.
También dimos una charla sobre esta arquitectura; las diapositivas están disponibles aquí: Atlantis on Cloud Run.