☰
90DaysOfDevOps 第 60 天:用 Terraform 管理 Docker 容器、Provisioners 与 Modules
2026/10/7 19:44:03 网站建设 项目流程
  • 文档/教程

【免费下载链接】90DaysOfDevOps

This repository started out as a learning in public project for myself and has now become a structured learning map for many in the community. We have 3 years under our belt covering all things DevOps, including Principles, Processes, Tooling and Use Cases surrounding this vast topic.

项目地址:https://gitcode.com/gh_mirrors/90/90DaysOfDevOps
点击查看免费下载

本文是 90DaysOfDevOps 开源学习路线图中「IaC(基础设施即代码)」阶段的实战篇:在 Day 59 用 Terraform + VirtualBox 创建虚拟机之后,本篇将把同样的声明式思路迁移到本地 Docker 环境——先用kreuzwerker/docker社区 Provider 部署一个对外暴露的 NGINX 容器,再进一步把 docker-compose 风格的 WordPress + MySQL 架构完整改写为 Terraform 配置,并梳理 Provisioners 与 Modules 两大组织机制。读完本文你将掌握:用 Terraform 声明 Docker 镜像、容器、网络与卷资源,通过terraform init/apply/destroy管理容器生命周期,以及何时用local-exec、remote-exec等 Provisioner 兜底非声明式逻辑、如何用 Modules 拆分可复用基础设施组件。

前置回顾:从 VirtualBox 虚拟机到 Docker 容器

Day 60 是 IaC 系列的第 3 站。在 Day 59 中,我们已经使用terra-farm/virtualbox社区 Provider 在本地 VirtualBox 中声明式地创建了 2 台 Ubuntu 虚拟机(对应源码 virtualbox.tf),并体验了terraform init、terraform plan、terraform apply与terraform destroy的完整生命周期,还引入了变量与输出的概念。

本节的核心思路不变:写一份描述「期望状态」的.tf配置,交给 Terraform 去执行差异比对(drift)与资源编排。区别仅在于这次的目标不是虚拟机,而是本地 Docker 守护进程——Terraform 通过 Docker Provider 与dockersocket 通信,把镜像拉取、容器创建、端口映射、网络与卷挂载全部纳入 Terraform 状态(terraform.tfstate)管理。

定位说明:在真实生产环境中,Terraform 管理 Docker 并不常见(Kubernetes 才是更合适的容器编排层),但作为学习 IaC 抽象模型、体验 Provider 与资源声明的过渡实验,它是成本最低、反馈最快的演练环境。

第一个实验:用 Terraform 部署 NGINX 容器并暴露到 8000 端口

1. 编写配置:镜像资源 + 容器资源

在项目里新建一个目录(例如Docker/),创建docker.tf,内容如下。仓库中已有完整可用的示例文件 docker.tf,建议直接对照阅读:

terraform { required_providers { docker = { source = "kreuzwerker/docker" version = "2.16.0" } } } provider "docker" {} resource "docker_image" "nginx" { name = "nginx:latest" keep_locally = false } resource "docker_container" "nginx" { image = docker_image.nginx.latest name = "tutorial" ports { internal = 80 external = 8000 } }

逐段解读这份配置:

  • terraform块:声明 Provider 来源与版本。这里用的是社区维护的kreuzwerker/dockerProvider(非 HashiCorp 官方),固定版本2.16.0可保证行为可复现,避免上游更新带来的隐性变更。
  • provider "docker" {}:无参实例化 Provider。默认情况下它直接使用本机的 Docker 守护进程(与dockerCLI 同一 socket)。
  • docker_image.nginx:声明期望存在的镜像。name = "nginx:latest"指定镜像标签;keep_locally = false表示当资源被销毁时不保留本地镜像副本(若设为true则terraform destroy不会删除已拉取的镜像)。
  • docker_container.nginx:声明期望运行的容器。image = docker_image.nginx.latest引用了上方镜像资源的latest属性,形成资源间依赖(Terraform 会先创建镜像再创建容器);name = "tutorial"指定容器名;ports块把容器内部 80 端口映射到宿主机的 8000 端口。

2. 初始化:terraform init 拉取 Provider

首次执行前必须先初始化,下载kreuzwerker/dockerProvider 插件并生成.terraform目录与锁定文件:

terraform init

执行成功后的终端输出如图(来自 Day60_IAC1.png):Terraform 初始化后端、安装 docker Provider 插件并提示初始化完成。

3. 应用与验证:terraform apply + docker ps

执行应用命令,Terraform 会先拉取nginx:latest镜像、再创建容器:

terraform apply

从源码与运行截屏(Day60_IAC2.png)可以看到,apply完成后立即执行docker ps,即出现名为tutorial的 nginx 容器处于运行状态。

此时打开浏览器访问http://localhost:8000/,即可看到 NGINX 默认欢迎页(Day60_IAC3.png),说明容器内部 80 端口已通过宿主 8000 端口对外提供服务:

4. 清理

不需要时执行terraform destroy,Terraform 会按依赖逆序移除容器与镜像(keep_locally = false时镜像一并删除)。

小结:这个最小实验证明了「镜像 + 容器」这两个资源类型,以及terraform state对容器生命周期的接管能力——这正是 docker-compose 与 Terraform 的交叉地带。

进阶实验:把 docker-compose 的 WordPress + MySQL 改写为 Terraform

为了让演示更有说服力,接下来把容器章节中用 docker-compose 编排的 WordPress + MySQL 架构,完整改写为 Terraform 配置。仓库中的对应文件为 docker-wordpress.tf。

完整配置

terraform { required_providers { docker = { source = "kreuzwerker/docker" version = "2.16.0" } } } provider "docker" {} variable wordpress_port { default = "8080" } resource "docker_volume" "db_data" { name = "db_data" } resource "docker_network" "wordpress_net" { name = "wordpress_net" } resource "docker_container" "db" { name = "db" image = "mysql:5.7" restart = "always" network_mode = "wordpress_net" env = [ "MYSQL_ROOT_PASSWORD=wordpress", "MYSQL_PASSWORD=wordpress", "MYSQL_USER=wordpress", "MYSQL_DATABASE=wordpress" ] mounts { type = "volume" target = "/var/lib/mysql" source = "db_data" } } resource "docker_container" "wordpress" { name = "wordpress" image = "wordpress:latest" restart = "always" network_mode = "wordpress_net" env = [ "WORDPRESS_DB_HOST=db:3306", "WORDPRESS_DB_USER=wordpress", "WORDPRESS_DB_NAME=wordpress", "WORDPRESS_DB_PASSWORD=wordpress" ] ports { internal = "80" external = "${var.wordpress_port}" } }

这份配置引入了比上一示例更丰富的资源类型,逐个说明:

  • variable wordpress_port:声明输入变量,default = "8080"提供默认值。WordPress 容器把 80 端口映射到external = "${var.wordpress_port}",意味着可以通过变量(或-var、TF_VAR_*、terraform.tfvars等方式,见 Day 59 的变量章节)灵活调整对外端口。
  • docker_volume "db_data":声明命名卷,对应 compose 中的db_datavolume。它是 MySQL 数据持久化的载体。
  • docker_network "wordpress_net":声明自定义网络,对应 compose 中的网络定义,用于让db与wordpress两个容器通过容器名互相解析。
  • docker_container "db"(MySQL):镜像mysql:5.7;restart = "always"让守护进程在容器退出后自动重启;network_mode = "wordpress_net"加入自定义网络;env数组等价于 compose 的environment,注入MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD、MYSQL_USER、MYSQL_DATABASE;mounts块把db_data卷挂载到容器内/var/lib/mysql(MySQL 数据目录),实现数据持久化。
  • docker_container "wordpress":镜像wordpress:latest;env中WORDPRESS_DB_HOST=db:3306让 WordPress 通过容器名db访问 MySQL 服务(依赖wordpress_net网络的 DNS 解析);其余变量对应数据库账号、库名与密码,与db容器的env保持一致。

执行流程与验证

把配置放入新目录后,依次执行:

terraform init # 拉取 docker Provider terraform apply # 创建卷、网络、两个容器 docker ps # 查看运行中的容器

apply完成后执行docker ps,可以看到mysql:5.7镜像的db容器与wordpress:latest镜像的wordpress容器均处于运行状态,如图 Day60_IAC5.png:

随后在浏览器打开http://localhost:8080/,即可进入 WordPress 安装向导。整个初始化流程与之前用 docker-compose 走通的过程完全一致:完成安装后,文章、页面等数据都写入 MySQL 的db_data卷中,数据随容器重建而保留。

与 docker-compose 的对照

能力docker-composeTerraform + Docker Provider
镜像image:docker_image资源
容器services:docker_container资源
网络networks:docker_network资源
卷volumes:docker_volume资源
环境变量environment:env = [...]
端口映射ports:ports { internal / external }
重启策略restart:restart = "always"
状态管理无状态文件terraform.tfstate记录全部资源

两者的声明式思想一致,但 Terraform 会把资源生命周期纳入统一的 state 管理,并能与其他云资源(VPC、VM、K8s 集群)编排在同一个配置中。

生产环境提示:原文档明确指出,容器直接运行方式的验证价值大于生产价值。若真的要对外提供网站服务,应优先考虑 Kubernetes 编排(下一篇将演示 kubernetes.tf 用 Terraform 声明 Namespace、Deployment 与 Service),容器方案只适合测试与本地演练。

Provisioners:声明式之外的命令执行兜底

为什么需要 Provisioners

Terraform 的核心哲学是声明式:配置只描述「最终状态」,Terraform 负责计算执行计划。但现实总有些动作无法被任何资源类型表达(例如初始化应用配置、执行系统级脚本、注册外部服务)。此时引入Provisioners——它们在资源创建(或销毁)后执行命令,是「把命令式逻辑塞进声明式配置」的兜底通道。

原文档也给出明确的取舍建议:只有当你没有其他替代方案时才使用,因为它会把无法追踪的副作用带进配置,降低可预测性。

local-exec 与 remote-exec

原文档给出的local-exec示例——在本地机器上执行命令(可用于打印信息、调用本地脚本、触发后续工具链):

resource "docker_container" "db" { # ... provisioner "local-exec" { command = "echo The server's IP address is ${self.private_ip}" } }

其中${self.private_ip}引用当前资源自身的属性,local-exec会在本地 shell 中执行这条 echo 命令。

remote-exec则是在远程资源创建完成后,通过 SSH/WinRM 等通道在目标机器上执行脚本。它适合处理操作系统特定的初始化,或作为包装器把 Ansible、Chef、Puppet 等配置管理工具串进资源创建流程。

Provisioner 类型总览

原文档列举的常用 Provisioner 及使用场景:

  • file:把文件或目录复制到远程资源(常配合remote-exec使用)。
  • local-exec:在 Terraform 运行的本地机器上执行命令,无远程访问要求。
  • remote-exec:在远程资源上执行脚本,需先打通 SSH/WinRM 连接。
  • vendor 类集成:通过封装调用外部配置管理工具完成更复杂的配置下发:
    • Ansible
    • Chef
    • Puppet

Modules:把基础设施拆成可复用组件

什么是 Modules

Modules 是多个资源共同组成的容器:一个模块就是同一目录下的一组.tf文件的集合(通常包含main.tf、variables.tf、outputs.tf)。Terraform 默认把当前工作目录视为根模块,而你可以在根模块中通过module块引用其他模块(本地目录或第三方注册中心)。

为什么要用 Modules

原文档给出了两个核心动机:

  1. 职责分离:当同一个项目需要同时构建 VMs、VPC、Security Groups 和 Kubernetes 集群时,把不同类型的资源拆进各自模块,可以更清晰地定义资源边界与组织方式,避免单个目录堆积大量互不相关的资源声明。
  2. 复用与共享:模块可以跨项目复用,也可以发布到公开注册中心供社区使用——"不用重新发明轮子"。团队把通用组件(如网络、安全组、集群)封装成模块后,新项目只需声明模块引用并传参即可。

结合仓库结构可以直观理解模块化思想:IaC 目录下 Docker/、Docker-Wordpress/、Virtualbox/、Kubernetes/ 各自独立成目录,每个目录就是一个自包含的模块——每个模块只负责一类基础设施,可单独init/apply,也可被上层模块引用。这正是「把基础设施分解为组件,组件即模块」的落地形态。

本篇小结与下一步

  • 用kreuzwerker/dockerProvider 声明镜像与容器资源,terraform init/apply/destroy即可完成 NGINX 的部署、验证与清理;
  • 通过 volume、network、env、mounts、restart 等参数,可将 docker-compose 的 WordPress + MySQL 架构 1:1 改写为 Terraform 配置并纳入 state 管理;
  • Provisioners(file / local-exec / remote-exec,以及 Ansible、Chef、Puppet 等 vendor 集成)是声明式模型的兜底手段,仅在无替代方案时使用;
  • Modules 是资源分组的复用单元,帮助拆分复杂基础设施并支持跨项目共享。

下一篇将进入「Terraform + Kubernetes」:使用 kubernetes.tf 示例,声明 Namespace、Deployment(含 replicas、selector、容器端口)与 NodePort Service,把同样的声明式能力延伸到集群编排层。

  • 文档/教程

【免费下载链接】90DaysOfDevOps

This repository started out as a learning in public project for myself and has now become a structured learning map for many in the community. We have 3 years under our belt covering all things DevOps, including Principles, Processes, Tooling and Use Cases surrounding this vast topic.

项目地址:https://gitcode.com/gh_mirrors/90/90DaysOfDevOps
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询