跳至主要内容

Terraform Docker:全面指南

通过在 Docker 上使用 Terraform 构建可复现的容器部署,精通基础设施即代码。从配置提供商到自动化 CI/CD,掌握完整流程。
· 13分钟 阅读

使用 AI 探索

我曾花费无数时间在开发和生产环境中手动管理 Docker 容器。配置不一致、状态漂移,以及本地环境是否与生产一致的种种不确定,几乎困扰着每一次部署。直到我发现了 Terraform 的 Docker 提供商,一切才发生了改变。

在本教程中,我将带您将 Terraform 与 Docker 集成,创建可复现、可版本化的基础设施。您将学会如何创建容器、管理网络和卷、实施资源限制,并在团队中维护一致的状态。

无论您是在管理构建服务器还是编排微服务,请打开终端,让我们一起改变管理 Docker 基础设施的方式。

如果您是 Docker 新手,建议先从我们的 Introduction to Docker 课程开始。

什么是 Terraform 与 Docker 的集成?

Terraform 是一种基础设施即代码工具,通过声明式流程管理资源的生命周期。您无需运行单独的 Docker 命令,而是在配置文件中定义期望的基础设施状态。随后 Terraform 按照 plan-apply-destroy 的生命周期执行:计算所需更改、展示预览,并在获得批准后才应用。

Terraform Docker provider 是 HCL(HashiCorp 配置语言)与 Docker API 之间的翻译层。当您在配置中编写 resource “docker_container" 时,提供商会将该声明转换为相应的 Docker API 调用。这种抽象意味着可以用与云基础设施相同的工作流来管理 Docker 资源。

下面是 Terraform 与 Docker Compose 的不同之处。

Compose 通过基于项目的 YAML 文件来管理容器,但它不会维护一个跟踪实际存在资源的状态文件。 

相比之下,Terraform 会将其创建的每个资源记录在 terraform.tfstate 中,使其能够检测配置与实际之间的漂移。如果有人手动修改了容器,Terraform 会发现并将其恢复到定义的状态。

Docker Compose vs Terraform Docker

这种方法在多种场景中大放异彩:

  • 对于可复现的构建服务器,您可以用一条命令销毁并重建整个环境,从而确保一致性。 
  • 在测试环境中,您可以为每次测试运行启动隔离的基础设施。 
  • 对于不可变部署,您会完全替换容器而不是就地修改,从而降低部署风险。

先决条件与安装

在深入 Terraform 配置之前,让我们先确认所需工具已正确安装和配置。

满足系统要求

首先,确认您的机器上 Docker Engine 正在运行。 

  • Linux: 使用 docker ps 检查

  • macOS 或 Windows: 确保 Docker Desktop 已安装并正在运行

Terraform 提供商需要访问 Docker 守护进程,因此您需要确认您的用户拥有访问 Linux 上 /var/run/docker.sock(Unix 套接字)或 Windows 上命名管道的权限。

接下来,从 HashiCorp 官方网站安装 Terraform CLI。Installing Terraform 会因操作系统而异:

  • Linux: 下载合适的软件包或使用发行版的包管理器。 

  • macOS: 使用 Homebrew。先通过 brew tap hashicorp/tap 安装 HashiCorp tap,再使用 brew install hashicorp/tap/terraform 安装 Terraform。 

  • Windows: 从 hashicorp.com 下载二进制文件并添加到 PATH,或使用 Chocolatey:choco install terraform。 

安装完成后,运行 terraform version 验证安装。 

在验证了 Docker 和 Terraform 之后,下一步是组织项目文件以便维护。

配置项目结构

清晰有序的结构可以让 Terraform 项目受益匪浅。通常我会为 Terraform 项目创建一个专用目录并初始化版本控制:

mkdir terraform-docker-project
cd terraform-docker-project
git init

对于基础设施即代码,版本控制至关重要。对 Docker 基础设施的每一次更改都应被跟踪、审查并可回滚。我通常会将 Terraform 的工作目录和状态文件添加到 .gitignore 文件中,以将其排除在版本控制之外:

.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl

最后,我会创建一个空白的 main.tf 文件来定义基础设施。此文件将包含提供商配置和资源定义。

Terraform Docker 提供商与核心资源

在项目结构就绪后,是时候配置 Docker 提供商并开始定义资源了。

初始化 Docker 提供商

首先,在 main.tf 中添加提供商配置:

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
}

provider "docker" {
  host = "unix:///var/run/docker.sock"
}

required_providers 块指定 Terraform 应从何处下载提供商插件。使用版本约束(~> 3.0)可允许小版本更新,同时避免破坏性变更。对于远程 Docker 主机,您可以将 host 参数更改为类似 tcp://192.168.1.100:2376 的值。

现在在项目根目录运行 terraform init。这会下载 Docker 提供商插件并存储在 .terraform/providers/ 中。该命令会输出安装的确切版本,同时 Terraform 会创建一个锁定文件,确保团队成员间的提供商版本一致。

Terraform Init

管理容器镜像

在完成提供商初始化后,下一步是定义容器镜像。在创建容器之前,需要先管理它们所使用的镜像,因此需要定义一个镜像资源:

resource "docker_image" "nginx" {
  name         = "nginx:1.29"
  keep_locally = true
}

将该资源直接添加在提供商块之后。keep_locally 参数很重要,它可以防止我在运行 terraform destroy 时移除镜像。当多个容器共享同一镜像,或我希望在部署之间保留缓存镜像时,这非常有用。

在生产部署中,请使用 SHA256 摘要固定镜像以确保不可变性:

resource "docker_image" "nginx" {
 name = "nginx@sha256:b60f25eb5acdd79172de1a289891d9cfe4fc3669daddeeb68619240cbe839d9f"
}

该摘要可从容器镜像仓库获取。 对于 nginx 这类来自 Docker Hub 的镜像,您可以在 Docker 官方网站的 Tags 部分找到。使用摘要可保证每次部署的镜像二进制完全一致,即使有人用相同的标签推送了新镜像也不受影响。

配置 Docker 容器

定义镜像后,下一步是创建引用这些镜像的容器:

resource "docker_container" "web" {
  name  = "nginx-web"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = 8080
  }

  env = [
    "NGINX_HOST=example.com",
    "NGINX_PORT=80"
  ]
}

通过 docker_image.nginx.image_id 来引用镜像。这会创建一个隐式依赖关系:在镜像存在之前,Terraform 不会创建容器。ports 块将容器的 80 端口映射到主机的 8080 端口,相当于 docker run -p 8080:80。

环境变量以字符串形式放在 env 列表中。如果任何参数发生变化,Terraform 会重新创建该容器,从而确保运行中的基础设施始终与配置一致。

此时,运行 terraform plan 查看 Terraform 将创建的内容,然后运行 terraform apply 来配置基础设施。Terraform 会创建镜像和容器。您可以在 Docker Desktop 中查看资源,或在终端运行 docker ps 进行验证。

在 Terraform 中配置网络、存储与资源限制

现在您已经运行了一个基础容器并理解了核心工作流,让我们通过生产可用的特性扩展基础设施。接下来的部分将在此基础上添加网络、持久化存储和资源约束,这些都是实际部署中不可或缺的。

管理网络

为了在保持隔离的同时实现容器间通信,创建专用网络至关重要。下面演示如何创建隔离网络来控制容器连通性。将以下资源添加到 main.tf 文件:

resource "docker_network" "app_network" {
  name   = "application_net"
  driver = "bridge"
}

现在添加一个使用该网络的第二个容器。在 web 容器之后添加:

resource "docker_container" "api" {
  name  = "api-server"
  image = docker_image.nginx.image_id

  networks_advanced {
    name = docker_network.app_network.name
  }
}

这将创建一个专用的 bridge 网络,并将 api 容器连接到该网络。同一网络上的容器可通过容器名作为主机名进行通信,非常适合微服务架构。 

通过运行 docker network inspect application_net 并确认仅有预期的容器连接来验证网络隔离。请确保先运行 terraform plan 和 terraform apply 创建资源后再执行该命令。

Network Creation Verification

在为容器间通信配置了网络之后,下一个关键方面是数据持久化。

配置持久化卷

对于需要在容器重启后仍保留的数据,添加卷资源。在容器定义之前插入以下内容:

resource "docker_volume" "db_data" {
  name = "postgres_data"
}

现在添加一个使用该卷的数据库容器:

resource "docker_container" "database" {
  name  = "postgres"
  image = "postgres:15"

 env = [
    "POSTGRES_PASSWORD=mysecretpassword"
  ]

  volumes {
    volume_name    = docker_volume.db_data.name
    container_path = "/var/lib/postgresql/data"
  }
}

这类受管卷由 Docker 创建和控制,存储在 Docker 的数据目录中。若需从主机共享配置文件,请使用 bind mounts,通过指定 host_path 而非 volume_name:

volumes {
  host_path      = "/opt/app/config"
  container_path = "/etc/app/config"
  read_only      = true
}

在确保数据在容器重启后仍然保留之后,同样重要的是防止任何单个容器独占系统资源。

设置资源限制

除了存储之外,控制每个容器可消耗的 CPU 和内存对系统稳定性至关重要。为防止容器过度消耗资源,请应用资源限制。

虽然 Docker 允许就地更新部分资源限制,但 Terraform 的声明式模型通常需要替换容器以确保状态与配置一致。为防止在替换过程中出现停机,我们使用 create_before_destroy 生命周期规则。 

重要提示: 必须移除固定的 name 参数,以避免在替换过程中发生命名冲突。可以选择使用 name_prefix 参数。

resource "docker_container" "api" {
  name_prefix       = "api-server-"
  image      = docker_image.nginx.image_id
  memory     = 512
  cpu_shares = 1024

  networks_advanced {
    name = docker_network.app_network.name
  }

 lifecycle {
    create_before_destroy = true
  }
}

memory 参数以 MB 为单位限制内存。cpu_shares 的工作方式不同,它是在发生 CPU 争用时使用的相对权重。拥有 1024 份额的容器将获得 512 份额容器两倍的 CPU 时间,但仅在主机负载较高时生效。 

对于硬 CPU 限制,您还可以使用 cpu_set 将容器固定到特定 CPU 核心:

cpu_set = "0,1"
}

通过运行 docker inspect background-worker --format='{{.HostConfig.Memory}} {{.HostConfig.CpuShares}} 验证这些限制,并检查 Memory 与 NanoCpus 的值是否与配置一致。 

同样如上所述,您需要先运行 terraform plan 和 terraform apply 创建资源。

此时,main.tf 文件包含了提供商配置、一个镜像、一个网络、一个卷,以及三个具有不同配置的容器。 

Terraform 中的变量、模块与机密

随着基础设施的增长,硬编码值会变得不灵活。本节将展示如何使用变量、模块和正确的机密处理,使配置具备可复用性。

使用输入变量

让配置更灵活的第一步,是将硬编码值抽取为变量,从而无需修改主代码即可轻松变更。在同一目录下创建一个名为 variables.tf 的新文件:

variable "web_port" {
  description = "External port for web container"
  type        = number
  default     = 8080

  validation {
    condition     = var.web_port > 1024 && var.web_port < 65535
    error_message = "Port must be between 1024 and 65535."
  }
}

现在在 main.tf 中更新 web 容器以使用该变量:

resource "docker_container" "web" {
  name  = "nginx-web"
  image = docker_image.nginx.image_id

  ports {
    internal = 80
    external = var.web_port
  }

  env = [
    "NGINX_HOST=example.com",
    "NGINX_PORT=80"
  ]
}

验证块可防止使用特权端口。若要覆盖默认值,请创建 terraform.tfvars 文件:

web_port = 9000

运行 terraform apply 时,Terraform 会自动从该文件读取值。或者,您也可以通过命令行传参:terraform apply -var="web_port=9000"。

创建可复用模块

变量解决了硬编码值的问题,但如果您需要多次部署包含所有资源的同一套模式,该怎么办? 

这正是模块的强项。可以将模块看作蓝图,它将分散的部分打包为一个整体,使您能够像“盖章”一样一次性部署整套复杂配置。

首先创建模块目录结构:

mkdir -p modules/web-app

现在创建文件 modules/web-app/variables.tf 并添加以下内容:

variable "environment" {
  description = "Environment name"
  type        = string
}

variable "port" {
  description = "External port"
  type        = number
}

接着创建主文件 modules/web-app/main.tf,内容如下:

resource "docker_container" "app" {
  name  = "app-${var.environment}"
  image = var.image_id

  ports {
    internal = 80
    external = var.port
  }
}

最后创建输出文件 modules/web-app/outputs.tf,主要用于在运行 terraform apply 后在终端显示所需的值:

output "container_ip" {
  value       = docker_container.app.network_data[0].ip_address
  description = "IP address of the application container"
}

添加模块定义后,运行 terraform init 安装模块,然后运行 terraform apply,即可从同一模板创建两个容器。您应能在 Docker Desktop 中看到全部资源。

Terraform Modules在通过模块建立了可复用的基础设施模式之后,配置管理还有一个至关重要的方面:安全地处理密码和 API 密钥等敏感信息。

处理敏感数据

数据库密码等敏感值不应被硬编码。您可以在 variables.tf 文件中使用变量并设置 sensitive = true:

variable "db_password" {
  description = "Database root password"
  type        = string
  sensitive   = true
}

现在在 main.tf 中更新数据库容器,引用该数据库密码:

resource "docker_container" "database" {
  name  = "postgres"
  image = "postgres:15"

  env = [
    "POSTGRES_PASSWORD=${var.db_password}"
  ]

  volumes {
    volume_name    = docker_volume.db_data.name
    container_path = "/var/lib/postgresql/data"
  }
}

不要将密码写入 terraform.tfvars 并硬编码,改为将其设置为环境变量:

export TF_VAR_db_password="your-secure-password"
terraform apply

sensitive = true 标志可防止 Terraform 在控制台输出中显示该值。 

不过,这些值会以明文存储在 terraform.tfstate 中,因此为确保该文件安全,我们可以使用带加密的远程后端进行保护。下面来看看如何实现。

Terraform 状态管理与 CI/CD 集成

本节涵盖生产部署所必需的状态机制与自动化工作流。但在实施自动化之前,务必理解 Terraform 如何通过状态文件跟踪您的基础设施。 

理解状态机制

状态是 Terraform 管理基础设施能力的基石。terraform.tfstate 文件将配置映射到实际的 Docker 容器 ID 与资源属性。运行 terraform apply 时,Terraform 会将该状态与配置进行对比,计算出所需的最小变更集。

使用 terraform show 可检查当前状态,它会显示每个受管资源。对于团队而言,本地状态文件会带来问题、冲突与潜在的数据丢失。解决方案是使用远程后端。

对于不使用云基建、专注于 Docker 的工作流,Terraform Cloud 的免费层非常理想。在 main.tf 的 terraform 部分中添加一个后端块:

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 3.0"
    }
  }
  
  backend "remote" {
    organization = "your-org-name"
    
    workspaces {
      name = "docker-infrastructure"
    }
  }
}

在 Terraform 网站创建一个免费账户。然后运行 terraform login 使用提供的令牌进行认证,再运行 terraform init 迁移状态。这将在远程工作区提供状态锁定与团队协作,而无需云基础设施。 

当然,也可以在 AWS 或 GCP 等云提供商上创建远程后端。

Terraform Cloud Remote Backend

自动化工作流

在配置并远程存储状态管理后,基础设施已为团队协作做好准备。最后一块拼图是通过 CI/CD 流水线实现部署流程自动化,确保变更一致且可审查。

首先,创建 .github/workflows/terraform.yml:

name: Terraform Docker Deploy

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        
      - name: Terraform Init
        run: terraform init
        
      - name: Terraform Plan
        if: github.event_name == 'pull_request'
        run: terraform plan
        
      - name: Terraform Apply
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve

该工作流会在每个拉取请求上运行 terraform plan,向审阅者展示将发生哪些基础设施变更。当更改合并到 main 分支时,会自动执行 terraform apply,确保每次基础设施变更都经过与代码变更相同的审查流程。

排查 Terraform Docker 问题

即使配置谨慎,问题仍可能出现。了解常见问题以及何时考虑替代方案,有助于做出更优的架构决策。 

“Permission denied”

Linux 上最常见的错误是在连接 Docker 套接字时出现 "permission denied"。这是由于用户缺乏访问 /var/run/docker.sock 的权限。通过将用户加入 docker 组来修复:

sudo usermod -aG docker $USER

然后注销并重新登录。

WSL2 连接问题

在 Windows 的 WSL2 环境中,连接问题常见于套接字路径不同。请确保启用 Docker Desktop 的“Expose daemon on tcp://localhost:2375 without TLS”设置,然后在 main.tf 中更新提供商:

provider "docker" {
  host = "tcp://localhost:2375"
}

私有仓库认证失败

对于私有 Docker 镜像仓库,请在提供商块中添加认证,将凭据定义为变量,并通过环境变量设置,以避免因硬编码导致的问题。

hclprovider "docker" {
  host = "unix:///var/run/docker.sock"
  
  registry_auth {
    address  = "registry.mycompany.com"
    username = var.registry_username
    password = var.registry_password
  }
}

Terraform 的替代方案

除了排查问题,同样重要的是了解在何种情况下,其他工具可能比 Terraform 更适合您的特定用例。

Terraform vs Kubernetes

Kubernetes 提供了更为成熟的编排功能:

  • 自愈
  • 高级调度
  • 服务发现
  • 内置负载均衡

对于需要高可用性的复杂多节点部署,Kubernetes 是更好的选择。Terraform 可用于配置 Kubernetes 基础设施,而 Helm 或 Kubernetes 清单则负责应用部署。

Terraform vs Ansible

Ansible 在容器内部的配置管理方面表现出色。Terraform 负责配置基础设施,而 Ansible 管理软件配置和软件包安装。这两种工具常常配合使用:Terraform 创建 Docker 容器,Ansible 配置其中运行的应用程序。

Terraform vs Docker Compose

对于简单的本地开发环境,Docker Compose 仍是最务实的选择。Terraform 则擅长跨环境的可复现部署,以及当需要与其他基础设施组件集成时的场景。

为帮助您做出选择,我将最重要的差异总结在下表中:

工具

核心优势

最佳使用场景

与 Terraform 的对比

Kubernetes

复杂的编排能力(自愈、高级调度、负载均衡)。

需要高可用性的复杂多节点部署。

互补:Terraform 负责配置基础设施(集群),Kubernetes 负责应用部署。

Ansible

配置管理、软件包安装与软件设置。

管理容器内部的软件与配置。

互补:Terraform 创建 Docker 容器,Ansible 配置其中运行的应用。

Docker Compose

简单、务实的基于项目的管理。

简单的本地开发环境。

替代:Compose 更易用于本地开发,但 Terraform 更适合跨环境的可复现部署。

如果您正在寻找可替代 Docker 的工具,请查看我们的 Docker 替代方案精选 指南。

结语

将 Docker 纳入 Terraform 的生命周期管理,可将容器部署从手动流程转变为可复现、可版本控制的基础设施。在本教程中,我演示了如何循序渐进地构建完整的基础设施:从提供商与单个容器入手,再逐步加入网络、存储、变量、模块与自动化。

关键区别在于,Terraform 根据声明式配置来处理资源的配置、创建、更新与销毁。要实现自动伸缩与自愈等完整编排功能,需要使用 Kubernetes 或 Docker Swarm 等工具。Terraform 可用于配置这些编排器,但并不能替代它们。

下一步,建议查阅 Terraform Registry 上的 Docker 提供商官方文档,了解诸如 Swarm 服务、机密管理与插件配置等高级资源选项。

如果您希望亲手实践容器技术,建议报名我们的互动技能路径 使用 Docker 与 Kubernetes 的容器化与虚拟化。

Terraform Docker 常见问答

运行 terraform destroy 时,如何防止 Terraform 删除 Docker 镜像?

在 docker_image 资源中使用 keep_locally = true 参数。这会告诉 Terraform 即使在销毁基础设施后也保留本地镜像缓存,这在多个容器共享同一镜像或您希望在部署之间保留缓存镜像时很有用。

为什么 Terraform Cloud 报错“Cannot connect to Docker daemon”?

默认情况下,Terraform Cloud 以远程执行模式运行,即命令在 Terraform Cloud 的服务器上执行,而该环境无法访问 Docker。您需要将工作区配置为本地执行模式:进入Settings > General > Execution Mode > Local。这样可以在本地机器(可访问 Docker)上运行 Terraform 命令,同时将状态远程存储。

如何在 Terraform 中管理数据库密码等敏感值?

在变量定义中使用 sensitive = true 将变量标记为敏感。对于 Terraform Cloud,请在工作区 UI 中设置敏感变量并标记为“Sensitive”。在本地执行时,使用带有 TF_VAR_ 前缀的环境变量(例如,export TF_VAR_db_password="password")。请注意,这些值仍以明文存储在 terraform.tfstate 中,因此务必确保状态文件安全。

使用 Terraform 与 Docker Compose 管理容器有何区别?

Docker Compose 通过基于项目的 YAML 方式管理容器,但不维护状态文件。Terraform 会将每个资源记录在 terraform.tfstate 中,从而能够检测配置与现实之间的漂移。Terraform 擅长跨环境的可复现部署与与其他基础设施的集成,而 Docker Compose 更适合本地开发。Terraform 提供显式的状态管理与生命周期控制(plan/apply/destroy)。

如何在不出错的情况下更新 Docker 容器的资源限制?

虽然 Docker CLI 支持就地更新,但 Terraform 的 Docker 提供商通常会为了确保状态一致性而强制替换资源。为避免在替换过程中出现停机,请添加 lifecycle { create_before_destroy = true } 块。关键是,您还必须移除固定的 name 参数(或使用 name_prefix)。如果保留类似 name = "web-server" 这样的固定名称,新容器会在旧容器销毁前因“name already in use”错误而无法启动。

Benito Martin

作者

Benito Martin

作为 Martin Data Solutions 的创始人兼自由职业数据科学家、ML 与 AI 工程师,我在回归、分类、NLP、LLM、RAG、神经网络、集成方法和计算机视觉等领域拥有多元项目经验。

  • 成功交付多项端到端机器学习项目,覆盖数据清洗、分析、建模与在 AWS 和 GCP 上的部署,提供具备影响力且可扩展的解决方案。
  • 使用 Streamlit 和 Gradio 构建交互式、可扩展的网页应用,服务于不同行业场景。
  • 教授并指导数据科学与数据分析学员,通过个性化学习路径促进其职业成长。
  • 为企业级需求设计检索增强生成(RAG)应用课程内容。
  • 撰写高影响力的 AI 与 ML 技术博客,涵盖 MLOps、向量数据库和 LLM 等主题,并获得显著关注。

在我参与的每个项目中,我都会应用最新的软件工程与 DevOps 实践,如 CI/CD、代码规范检查、格式化、模型监控、实验追踪以及健壮的错误处理。我致力于交付完整的解决方案,将数据洞察转化为切实可行的策略,帮助企业成长,并充分发挥数据科学、机器学习与 AI 的价值。

主题