使用 Terraform 在阿里云 ESS 中自动加入 Docker 节点

解读

国内云原生面试中,这道题考察的是“把 Docker 节点当成弹性资源”的闭环能力:

  1. 用 Terraform 声明式管理 IaaS 资源(ESS、ECS、VPC、SLB、RAM、AutoScaling 生命周期钩子);
  2. 让新弹出的 ECS 实例自动完成 Docker 安装、守护进程加固、Swarm 或 Agent 注册,并无损地接入线上集群
  3. 缩容时优雅下线容器、排空节点、从集群摘除,再释放实例,保证业务无损与成本最优
    面试官最在意的是“ESS 生命周期钩子 + Terraform 本地执行器 + Docker 节点初始化”这一条链路的可靠性、幂等性与可观测性”,而不是单纯把 ECS 装个 Docker。

知识点

  • Terraform 阿里云 Provider 资源alicloud_ess_scaling_groupalicloud_ess_scaling_configurationalicloud_ess_lifecycle_hookalicloud_ess_notificationalicloud_ram_rolealicloud_oss_bucket_object
  • ESS 生命周期钩子SCALE_OUTSCALE_IN 事件,MNS 主题 + RocketMQ 或 FunctionCompute 作为通知通道。
  • Terraform local-exec / null_resource:在收到钩子消息时,本地或跳板机执行 ansible/ansible-playbook,完成 Docker 节点初始化。
  • Docker 20.10+ 国内源优化yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.reposystemd 级 cgroup 驱动统一为 systemd
  • Swarm join-token 或 ACK/ASK 节点接入:通过OSS 临时凭证拉取 swarm-worker-tokenkubelet-bootstrap-kubeconfig防止敏感信息写入镜像或 user-data
  • 节点无损缩容docker node update --availability drain + kubectl drain 前置脚本,等待 Pod 重调度完成后,再向 ESS 发送 CONTINUE 信号。
  • Terraform 状态锁:使用阿里云 OSS Backend + DynamoDB 模拟锁(或 Terraform Cloud),保证多工程师并发安全
  • 成本治理alicloud_ess_scaling_rule 绑定按量+Spot 混合策略Terraform 输出节点标签,供 Prometheus 做实时成本核算

答案

  1. 目录结构
terraform-ess-docker/
├── main.tf
├── variables.tf
├── templates/
│   └── worker_init.sh
├── ansible/
│   └── docker-worker.yml
└── backend.tf
  1. 核心 Terraform 片段(main.tf 节选)
# 1) ESS 启动模板指定实例规格、镜像、UserData 只做“最小 OS+云助手”
resource "alicloud_ess_scaling_configuration" "docker_worker" {
  scaling_group_id        = alicloud_ess_scaling_group.docker.id
  image_id                = data.alicloud_images.centos.images[0].id
  instance_type           = var.instance_type
  security_group_id       = alicloud_security_group.docker.id
  user_data               = base64encode(templatefile("${path.module}/templates/worker_init.sh", {}))
  key_name                = alicloud_key_pair.docker.key_name
  active                  = true
  force_delete            = true
  spot_strategy           = "SpotWithPriceLimit"
  spot_price_limit        = var.spot_price_limit
}

# 2) 生命周期钩子:SCALE_OUT 事件发送到 MNS 主题
resource "alicloud_ess_lifecycle_hook" "scale_out" {
  scaling_group_id      = alicloud_ess_scaling_group.docker.id
  name                  = "docker-node-join"
  lifecycle_transition  = "SCALE_OUT"
  notification_metadata = jsonencode({
    terraform_workspace = terraform.workspace
    hook_type           = "docker_swarm_join"
  })
  notification_arn      = "acs:mns:cn-shanghai:${data.alicloud_account.current.id}:topic/ess-lifecycle"
  heartbeat_timeout     = 600
  default_result        = "CONTINUE"
}

# 3) 本地接收 MNS 消息并触发 Ansible
resource "null_resource" "docker_join" {
  triggers = {
    always_run = timestamp()
  }
  provisioner "local-exec" {
    command = <<EOF
      aliyun-mns-cli receive --topic ess-lifecycle --wait > /tmp/msg.json
      instance_id=$(jq -r '.Message.instanceId' /tmp/msg.json)
      private_ip=$(jq -r '.Message.privateIp' /tmp/msg.json)
      ansible-playbook ansible/docker-worker.yml -e instance_id=$instance_id -e private_ip=$private_ip
      aliyun-mns-cli ack --receiptHandle $(jq -r '.ReceiptHandle' /tmp/msg.json)
    EOF
    working_dir = path.module
  }
}
  1. Ansible Playbook 关键任务(ansible/docker-worker.yml 节选)
- hosts: new_instances
  gather_facts: no
  vars:
    swarm_manager_ip: "{{ hostvars['manager']['private_ip'] }}"
  tasks:
    - name: 安装 Docker CE(国内源)
      yum:
        name:
          - docker-ce-20.10.24
          - docker-ce-cli-20.10.24
          - containerd.io
        enablerepo: mirrors.aliyun.com_docker-ce
        state: present
    - name: 配置 systemd 驱动
      copy:
        dest: /etc/docker/daemon.json
        content: |
          {
            "exec-opts": ["native.cgroupdriver=systemd"],
            "registry-mirrors": ["https://registry.cn-hangzhou.aliyuncs.com"],
            "log-driver": "json-file",
            "log-opts": { "max-size": "50m", "max-file": "3" }
          }
      notify: restart docker
    - name: 获取 swarm join-token
      uri:
        url: "http://oss-{{ region }}.aliyuncs.com/swarm-worker-token"
        method: GET
        headers:
          x-oss-security-token: "{{ sts_token }}"
      register: token
      no_log: true
    - name: 加入 swarm 集群
      command: "docker swarm join --token {{ token.content }} {{ swarm_manager_ip }}:2377"
  1. 缩容反向流程
  • 生命周期钩子 SCALE_IN 触发 FunctionCompute,函数内调用 Terraform Taint + Ansible 对目标节点执行
    docker node update --availability drain && docker node rm --force
    完成后回调 ESS CompleteLifecycleAction
  • Terraform 侧使用 null_resource 监听缩容事件,自动从 state 中移除对应资源,保证状态一致。
  1. 验证
  • terraform apply 后手动调大 min_size3 分钟内新节点在 Swarm 显示 Ready
  • 调小 min_sizePod 重调度完成时间 < 30 s,节点安全释放;
  • terraform state list | grep docker 无残留地址,OSS backend 状态文件 MD5 一致。

拓展思考

  • 多地域容灾:把 ESS 与 Swarm Manager 做跨地域对等连接,Terraform 用 workspace 区分 cn-shanghaicn-beijing通过 DNS 权重实现就近接入
  • 混合云场景:线下 IDC 裸金属通过 AliCloud Express Connect 加入同一个 Swarm 覆盖网络,Terraform 用 external data source 动态读取线下节点信息,统一纳管
  • 安全加固
    – 用 RAM 角色+STS 临时凭证替代 AccessKey,Terraform 端开启 assume_role
    – Docker Daemon 仅监听 Unix Socket,通过 AliCloud Bastionhost + ansible_ssh_common_args 跳板运维;
    – 镜像签名采用 Harbor+Notary,Terraform 在 user_data 里注入 docker trust 公钥,拒绝未签名镜像启动
  • GitOps 闭环:PR Merge 触发 GitHub Action → Terraform Cloud Run → ESS 弹性伸缩Prometheus 监控节点 CPU 利用率 < 20% 持续 5 min 自动缩容实现无人值守