- DevOps
- CI/CD
- 基础设施
【免费下载链接】atlantis
Terraform Pull Request Automation
本文基于 runatlantis.io 官方博客《Atlantis on Google Cloud Run》整理并辅以当前仓库源码佐证,完整讲解如何将 Terraform Pull Request Automation 工具 Atlantis 从自管 VM 迁移到 Google Cloud Run 这类无服务器容器平台:以 Redis 作为集中式锁后端、以共享负载均衡器统一路由多个实例、并用服务账号模拟(impersonation)实现最小权限隔离。读完本文,你将掌握一套可复制的、消除单点故障并支持横向扩容的 Atlantis 部署方案,并理解其底层锁数据库切换机制。
::: info 虽然本文以 Google Cloud Run 为例编写,该部署架构同样适用于 AWS Fargate、Azure Container Instances 与 Kubernetes。 :::
本文覆盖 Terraform 代码中最关键的部分;完整的可运行示例位于 runatlantis 社区的 atlantis-contrib 仓库(atlantis-on-cloud-run目录),建议配合阅读。
为什么不再用自管 VM
大多数 Atlantis 部署运行在自管 VM 上。这一方案虽然熟悉且直接,却伴随一系列挑战:
- 身份权限过强:VM 服务账号往往通过直接授权或模拟(impersonation)间接拥有大量资源与项目的访问权;
- 缺少高可用:Atlantis 默认把锁后端直接写到磁盘(BoltDB),VM 一旦宕机,Atlantis 便不可用;
- 单点故障:单一实例既无法横向扩容,也带来单点风险;
- 持续运维负担:打补丁、操作系统升级、备份等运维工作长期存在。
本文展示的替代方案是:在 Google Cloud Run 这类无服务器容器平台上运行多个 Atlantis 实例,使用集中式数据库(Redis)存放锁,并把多个实例放到负载均衡器后面。每个实例以自己的身份、有限的权限运行,只管理属于自己的项目。这一架构消除了单点故障、支持水平伸缩,并落地了最小权限(least-privilege)安全模型。
我们要构建什么
整体架构如下:
- 外部 HTTP(S) 负载均衡器:负责把请求路由到多个 Atlantis 实例;
- 多个 Atlantis 实例:运行在 Google Cloud Run 上;
- Memorystore for Redis 实例:提供集中式锁后端,让多个实例协调锁、避免冲突。
我们略过的内容
为控制篇幅,原文刻意省略了部分重要细节:网络搭建、DNS、Docker 镜像拉取、通配符 TLS 证书,以及 Atlantis 的每一个配置旋钮——本文聚焦于与架构最相关的部分。
需要特别强调的是:强烈建议把 Atlantis 部署在隔离的 VPC 中,并开启 Private Service Access。这样 Atlantis 只与 Google API 通信来完成工作,不会触达你的其他基础设施。
BoltDB:只有一个写入者时很棒
Atlantis 默认使用 BoltDB 作为锁后端。BoltDB 是一个简单、内嵌的键值存储,直接写入磁盘(server/server.go中boltdb.New(userConfig.DataDir)即把数据目录作为参数传入)。它适合单实例部署,但把 Atlantis 锁死在单节点架构上——要获得高可用与水平伸缩,必须把它替换为多个 Atlantis 实例可以安全共享的托管分布式数据库。
从当前仓库源码可以确认默认值:cmd/server.go中DefaultLockingDBType = "boltdb",即不显式配置时 Atlantis 走 BoltDB 分支。服务端在启动时按--locking-db-type的值做分支选择(见 server/server.go):
"redis":创建 Redis 数据库客户端(支持单节点与集群两种模式);"boltdb":在数据目录下创建内嵌 BoltDB。
一个“管理一切”的 Atlantis
为了便于创建和管理多个 Atlantis 实例,可以额外部署一个专用的“管理(management)”Atlantis 实例,负责其他 Atlantis 实例的生命周期管理(创建、更新、删除)。作者通常把它放在独立的 Google Cloud 项目(如atlantis-mgmt)中,并配一个专门的 Git 仓库。
只要搭好一个实例,复制它并放到共享负载均衡器之后即可,后续复制成本很低。
Redis:分布式锁后端
自 Atlantis v0.19.0 起,Redis 就是受支持的锁后端。Redis 是内存数据结构存储,这里用作多个 Atlantis 实例的中央锁后端:每个实例连接同一个 Redis 实例,从而协调锁、避免冲突。
Redis 还支持持久化:
- RDB(Redis Database):按指定间隔对数据集做时间点快照;
- AOF(Append Only File):记录 Redis 服务器收到的每次写操作。
这意味着即使 Redis 实例重启,锁也不会丢失。
要创建的第一个资源就是 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 } }关键点:
- 该实例拥有1 GiB内存,开启RDB持久化,每24 小时做一次快照;
connect_mode = "PRIVATE_SERVICE_ACCESS"使 Redis 只能在 VPC 内部访问——创建实例前必须先为 VPC 配置好 Private Service Access;tier = "STANDARD_HA"提供高可用副本;prevent_destroy = true保护锁数据不被意外销毁。
源码视角:Atlantis 如何连接 Redis
从源码看,Redis 后端完整实现位于 server/core/redis/redis.go。其Config结构体支持Hostname、Port、Password、Username、TLSEnabled、InsecureSkipVerify、DB与ClusterAddresses(redis.go)。NewWithConfig会做如下选择:
- 若
ClusterAddresses非空 → 使用 Redis Cluster 客户端; - 否则 → 使用单节点客户端(
host:port),并执行Ping校验连接可用性。
server/server.go在switch userConfig.LockingDBType中把ATLANTIS_REDIS_HOST、ATLANTIS_REDIS_PORT、ATLANTIS_REDIS_PASSWORD、ATLANTIS_REDIS_USERNAME、ATLANTIS_REDIS_TLS_ENABLED、ATLANTIS_REDIS_INSECURE_SKIP_VERIFY、ATLANTIS_REDIS_DB、ATLANTIS_REDIS_CLUSTER_ADDRESSES等配置装配进redis.NewWithConfig(server/server.go)。
这些 flag 的定义与默认值位于 cmd/server.go:
| 配置项 | 环境变量 / flag | 默认值 | 说明 |
|---|---|---|---|
--locking-db-type | ATLANTIS_LOCKING_DB_TYPE | boltdb | 锁数据库类型(boltdb/redis) |
--redis-host | ATLANTIS_REDIS_HOST | 无 | Redis 主机名(锁类型为 redis 时使用) |
--redis-port | ATLANTIS_REDIS_PORT | 6379 | Redis 端口 |
--redis-db | ATLANTIS_REDIS_DB | 0 | 使用的 Redis 逻辑数据库编号 |
--redis-password | ATLANTIS_REDIS_PASSWORD | 无 | Redis 密码 |
--redis-username | ATLANTIS_REDIS_USERNAME | 无 | Redis 用户名 |
--redis-tls-enabled | ATLANTIS_REDIS_TLS_ENABLED | false | 是否启用 TLS 连接 |
--redis-insecure-skip-verify | ATLANTIS_REDIS_INSECURE_SKIP_VERIFY | false | 是否跳过 TLS 证书校验 |
--redis-cluster-addresses | ATLANTIS_REDIS_CLUSTER_ADDRESSES | 无 | 逗号分隔的集群节点地址(host:port),设置后启用集群模式 |
从 cmd/server.go 的 flag 描述可以看到:--locking-db-type用于“存储 plan/apply 锁的锁数据库类型”,--redis-host等仅在锁类型为redis时生效。
锁键的存储设计也值得了解:项目锁键格式为pr/{repoFullName}/{path}/{workspace}/{projectName}(由models.GenerateLockKey生成,见 redis.go);TryLock通过GET+SET实现原子加锁(redis.go);UnlockIfOwnedByPull用一段 Lua 脚本保证“只有该 PR 持有的锁才被删除”的原子性(redis.go)。此外,启动时NewWithConfig还会执行一次旧格式锁键到新格式的迁移(使用Scan而非Keys,以兼容 Redis Cluster),迁移失败不会阻塞启动,会在下次启动重试(redis.go)。
部署到 Cloud Run
Cloud Run 是无服务器容器平台:自动伸缩应用、处理 HTTP 请求、抽象底层基础设施,只按实际使用的计算资源计费;每个服务运行在单一服务账号下,由该账号定义其权限——非常适合部署一个或多个 Atlantis 实例。
服务端配置 atlantis/management.yaml
先创建服务端 Atlantis 配置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: - apply这里用allowed_workflows把仓库限定到名为example的工作流,并设置了apply_requirements: [approved, mergeable](需批准且可合并)等门槛。
创建管理实例的 Cloud Run 服务
下面的 Terraform 配置只突出与本文架构相关的 Atlantis 环境变量;完整可运行示例见 atlantis-contrib 仓库的atlantis-on-cloud-run目录。
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" }这些环境变量与源码中的配置项一一对应:
ATLANTIS_LOCKING_DB_TYPE=redis:等价于--locking-db-type redis,触发 server/server.go 中的 Redis 分支,使锁后端从 BoltDB 切换到 Redis;ATLANTIS_REDIS_HOST/ATLANTIS_REDIS_DB:指定集中式锁数据库的地址与逻辑库(--redis-host/--redis-db,默认库为 0,见 cmd/server.go);ATLANTIS_REPO_CONFIG_JSON:把management.yaml先yamldecode再jsonencode注入为服务端仓库配置(对应源码中的--repo-config-json,user_config.go);ATLANTIS_DATA_DIR=/app/atlantis:数据目录指向挂载的临时卷;ATLANTIS_USE_TF_PLUGIN_CACHE=true:开启 Terraform provider 插件缓存(对应--use-tf-plugin-cache,cmd/server.go),减少重复下载。
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"让服务只接受来自内部负载均衡器的流量;vpc_access把出站流量全部导向 VPC 网络,确保 Atlantis 只按需访问 Google API。
临时存储(Ephemeral Storage)
Atlantis 是 I/O 密集型应用:它会 checkout 仓库、运行 Terraform 命令、下载 provider、拉取模块,因此需要可写的文件系统存放临时数据。在 Cloud Run 上,这由临时存储承担——容器实例停止或重启时会被清空。
好在 Atlantis 的运行方式决定了其临时存储需求有限:
- PR 数据在合并或关闭后即从文件系统移除;
- provider 可以用
ATLANTIS_USE_TF_PLUGIN_CACHE缓存; - Terraform 二进制已包含在容器镜像中。
因此配置一个5 GiB 的内存empty_dir卷,挂载到/app/atlantis并设为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" }内存卷速度快且不依赖持久磁盘;由于锁等持久状态已迁往 Redis,临时存储清空不会影响可用性。
让实例保持“温热”
Cloud Run 实例在不使用时可以缩容到零,这会导致冷启动以及内存临时存储的丢失。为避免这一点,设置min_instance_count = 1,保证至少一个实例始终运行、随时可以处理请求:
scaling { min_instance_count = 1 max_instance_count = 1 }模拟身份与最小权限(Impersonation and Least Privilege)
每个 Cloud Run 服务都运行在一个定义其身份的服务账号下。与其给这个账号过大的权限,不如使用一个只拥有“模拟其他更受限服务账号”权限的服务账号。
对管理 Atlantis 实例来说不必如此(它本来就只部署在单个项目上),但对那些管理多个项目/环境的其他 Atlantis 实例来说,这一点至关重要。通过模拟身份,可以确保每个 Atlantis 实例只拥有管理其特定项目所需的权限。
典型做法:创建一个基础 Atlantis 服务账号(atlantis-example),再为每个环境创建独立服务账号(如atlantis-example-dev、atlantis-example-prod)。基础账号在这些环境账号上被授予roles/iam.serviceAccountTokenCreator角色,并在 Atlantis 运行 Terraform 命令时模拟它们;反过来,这些环境专属账号只被授予其管理项目所需的权限。这样即使某个 Atlantis 实例被攻破,爆炸半径也仅限该实例管理的资源。
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}" }模拟本身通过在atlantis/example.yaml工作流中设置GOOGLE_IMPERSONATE_SERVICE_ACCOUNT环境变量启用。在该配置中,Atlantis 管理dev与prod两个环境——在plan和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"]工作流中env步骤对应 Atlantis 自定义工作流的envstep(实现见 server/core/runtime/env_step_runner.go),它会在执行 Terraform 命令前注入环境变量。这里还演示了init/plan/apply的extra_args用法:通过-backend-config与-var-file为不同环境传入不同的后端配置与变量文件,并用-lock=false避免在多实例架构下与集中锁产生冗余交互。
共享负载均衡器
运行多个 Atlantis 实例(各自负责不同的项目或环境)时,关键是把流量路由到正确的实例。与其给每个实例独立的公网端点,不如通过一个全局 HTTPS 负载均衡器集中流量。
下面的负载均衡器使用基于主机的路由,按子域名把请求分发到对应实例。例如,network.atlantis.acme.com的请求被路由到管理 network 项目的 Atlantis 实例,workloads.atlantis.acme.com则进入管理 workload 项目的实例。
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" }配置要点:
default_url_redirect把未知主机重定向到atlantis.acme.com并强制 HTTPS;- 每个主机规则对应一个
path_matcher,其中把/events路径单独路由到 webhook 专用后端; - 使用RESTRICTED配置文件、
TLS_1_2最低版本的 SSL 策略; - 全局转发规则监听 443,绑定托管证书与固定 IP。
为什么每个实例需要两个后端
每个 Atlantis 实例在负载均衡器中被注册为两个独立的 backend service:一个服务主 HTTP 端点,另一个服务/eventswebhook 端点。这种分离的意义在于:
- 主界面可以放在 Identity-Aware Proxy(IAP)后面,只有授权用户才能访问;
- webhook 端点保持公网可达,让 GitHub 或 GitLab 能无阻碍地投递事件。
示例(workloads 实例):
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" }可见主后端开启了iap { enabled = true },webhook 后端则不开启。
保护 /events 端点
强烈建议用安全策略(security policy)保护/events端点,阻挡非法流量:
- 至少限制为 Git 提供商使用的 IP 网段;
- 并拦截一些常见的 Web 漏洞模式(可参考 Cloud Armor 预置 WAF 规则)。
Atlantis 的事件路由入口在仓库中对应 server/controllers/events/events_controller.go(/events端点由 server/router.go 注册),它会校验 webhook 请求并把 GitHub、GitLab 等平台的事件转发给命令执行管线,因此该端点必须既能被 Git 平台访问,又要受到安全策略约束。
总结
把 Atlantis 部署在 Google Cloud Run 上,配合共享的 Redis 锁后端与共享负载均衡器,可以交付一个高可用、可水平伸缩、安全的 Atlantis 部署:
- 高可用:锁与状态存于托管 Redis(带持久化与高可用副本),不再依赖单机磁盘;
- 水平伸缩:多个实例共享同一锁后端,负载均衡器按子域名分发请求;
- 最小权限:每个实例以独立服务账号运行,通过 impersonation 只获得管理自身项目所需的最小权限,缩小爆炸半径;
- 无服务器运维:Cloud Run 自动扩缩容、按用量计费,无需维护虚拟机。
由于篇幅所限,本文并未覆盖全部细节(网络、DNS、证书等)。完整的可运行 Terraform 示例请查阅 atlantis-contrib 仓库的atlantis-on-cloud-run目录,社区作者也曾在公开演讲中介绍过该架构。相关讨论可以到 Cloud Native Computing Foundation Slack 工作区的 #atlantis 频道进行。
- DevOps
- CI/CD
- 基础设施
【免费下载链接】atlantis
Terraform Pull Request Automation
相关推荐
如何用Genkit和Google Cloud Run构建高扩展AI服务:容器化部署终极指南
如何用Genkit和Google Cloud Run构建高扩展AI服务:容器化部署终极指南 Genkit是一款开源AI应用框架,它让开发者能够使用熟悉的代码模式
人工智能大模型后端AI AgentRAG工具调用企业级Kubernetes安全防护:Kubescape高可用部署架构全解析
企业级Kubernetes安全防护:Kubescape高可用部署架构全解析 Kubescape作为开源Kubernetes安全平台,提供从IDE到CI/CD流水
网络安全云原生应用安全运维StreamEx高级特性完全手册:掌握前缀操作、折叠和自定义收集器的终极指南
StreamEx高级特性完全手册:掌握前缀操作、折叠和自定义收集器的终极指南 StreamEx作为Java Stream API的增强库,为开发者提供了更强大、
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考