前言

过去一年在前公司(电信)写了很多基础设施的自动化的实现,主要是通过 Ansible 将以往手动部署的中间件代码化,实现中间件的高效部署和持续运维。但像云服务器,域名等云资源,还是通过人工在控制台点点点来完成的。过去新机房的建设,近万台服务器,全是靠人工点出来的。

业界已经有成熟的工具:Terraform,国内的云公司比如:阿里云和腾讯云,也都有完善的支持了。这里记录一下 Arch Linux DevOps 团队的实现和我本地的实践。

本文会包含:

  • 云资源如何代码化
  • Terraform 代码如何实现本地 AK、SK 等加密信息的加密存储

IaC & Terraform

Terraform 介绍

Terraform 是声明式的(类似 K8s),基于自己的描述语言:HCL。比如有 10 台服务器,那么在存放 HCL 的基础设施代码仓库里,就会有 10 台服务器对应的安全组/规格/镜像的声明(当然,为了避免重复代码,会使用模块进行抽象)。

Terraform HCL 只声明必要的项,一些非必要的项,比如实例的 ID,镜像的 ID,会作为状态,存放在一个叫 state backend 的地方,通过本地的 HCL 代码配合 state backend 中的数据一起,将云资源所有信息声明出来。

默认 state backend 是执行者本地的一份 tfstate 结尾的文件。本地文件会导致一个问题,如果其他人想要获得当前所有云设施的最新状态,就需要跟之前的执行者索取他本地最新的 state 文件。因此,团队内一般会使用 remote state backend,这可以是一个数据库或者 S3(AWS 对象存储服务)。比如我本地就是用 PostgreSQL 来作为 remote state backend。所有基础设施代码的贡献者,确保能安全的访问到 PG 和对象存储即可。

即使使用了 remote state backend,本地还是会有 tfstate 文件,但将只包含 remote state backend 的连接信息,不包含云资源的补充信息。本地 tfstate 文件的信息在初始化 Terraform 时写入,比如我本地的 Terraform 首次初始化的命令如下:

# 我本地的私密信息是通过 Ansible Vault 来管理的,`get_key.py` 是 Arch Linux DevOps Team 写的一份脚本,用于实现 Ansible Vault 加密内容的读取和格式化输出)
terraform init -backend-config="conn_str=postgres://terraform:$(../misc/get_key.py ../group_vars/all/vault_terraform.yml vault_terraform_db_password)@state.jinmiaoluo.com?sslmode=verify-full"

本地生成一个 .terraform 文件夹(记得添加到 .gitignore 内),里面就是 tfstate 文件。

我的云设施

我的主要云设施有这些:

  • AWS
    • 一个 DNS 域名:jinmiaoluo.com
    • 一个子账户用于实现 letsencrypt DNS-01 证书自动刷新
    • 一个子账户,用于实现 Terraform 管理云资源
  • Aliyun
    • 有一台香港的轻量应用服务器,部署了 Xray 和 Trojan-go 解决代理问题
    • 一个子账户,用于实现 Terraform 管理云资源
  • Tencent Cloud
    • 有一台香港的轻量应用服务器,部署了 FRPS 实现博客的公网暴露(高带宽但也高延迟)
    • 有一台广州的轻量应用服务器,部署了 FRPS 用于实现 WireGuard 的公网暴露(低带宽但也低延迟),让自己可以在咖啡店,也能访问我家局域网内的物理机/虚拟机。常见的比如:访问我自己的 Jira/Gitlab/Samba,通过 VSCode Remote 远程开发调试(类似 GitPod)等.
    • 一个子账户,用于实现 Terraform 管理云资源

如何使用 Terraform

我使用的 remote state backend 是 PG,PG 部署在我的虚拟化内,基于 WireGuard 网络确保我在任何地方工作都可以访问到:

terraform {
  backend "pg" {
    schema_name = "terraform_remote_state_aws"
  }
}

我借鉴了 Archlinux Infrastructure 的方案,像 AWS/Aliyun/Tencent Cloud 的 AK 和 SK 我是通过 ansible-vault 加密后各自存放在一个 yml 文件内的。类似这样:ArchLinux Terraform Vault

然后我复用了他们的:get_key.py 脚本,这个脚本会被 Terraform 中的 external provider 调用(初始化 Terraform 也会用到),如下:

data "external" "vault_aws" {
  program = [
    "${path.module}/../misc/get_key.py","${path.module}/../misc/vaults/vault_aws.yml",
    "aws_ak",
    "aws_sk",
    "--format","json"
  ]
}

data "external" "vault_alicloud" {
  program = [
    "${path.module}/../misc/get_key.py","${path.module}/../misc/vaults/vault_alicloud.yml",
    "alicloud_ak",
    "alicloud_sk",
    "--format","json"
  ]
}

data "external" "vault_tencentcloud" {
  program = [
    "${path.module}/../misc/get_key.py","${path.module}/../misc/vaults/vault_tencentcloud.yml",
    "tencentcloud_ak",
    "tencentcloud_sk",
    "--format","json"
  ]
}

provider "aws" {
  region = "us-east-1"
  access_key = data.external.vault_aws.result.aws_ak
  secret_key = data.external.vault_aws.result.aws_sk
}

provider "alicloud" {
  region = "cn-hongkong"
  access_key = data.external.vault_alicloud.result.alicloud_ak
  secret_key = data.external.vault_alicloud.result.alicloud_sk
}

provider "tencentcloud" {
  region = "ap-guangzhou"
  secret_id = data.external.vault_tencentcloud.result.tencentcloud_ak
  secret_key = data.external.vault_tencentcloud.result.tencentcloud_sk
}

实际的代码执行流程如下:

  1. Terraform 命令行工具访问 .terraform 中的 tfstate 文件,获取 remote state backend 的连接信息(账号、密码、数据库名字)
  2. external provider 调用这个脚本
  3. 脚本会调起 GnuPG 以交互式的要求输入 GnuPG 的密码来解密密文
  4. 解密密文得到 ansible vault 的密钥
  5. 通过 ansible vault 密钥和对应的加密文件,解密得到云平台的 AK 和 SK
  6. AK 和 SK 打印到标准输出,被 external provider 捕获
  7. external 将 AK 和 SK 信息存放到 remote backend 内(PG 内)

因此,其他开发者只要执行初始化的命令并确保能访问到 pg 数据库,可以访问和管理我的所有云资源的信息了。

常用命令

Terraform 依赖 .terraform 目录中的信息,因此所有的命令要到 .terraform 所在的文件夹执行

  • 列出 state backend 中的信息:terrafrom state list
  • 查看 state backend 中某个资源的信息:terrafrom state show data.external.vault_alicloud
  • 导入存量的资源到 state backend:terrafrom import
  • 更新云资源信息(比如:新建域名解析,更新域名解析等):terrafrom apply