Chapter 03

Provider 配置:AWS/GCP/Azure

provider 块配置、AWS EC2/VPC/S3 资源创建、多云认证方式、环境变量认证

Provider 是什么?工作原理解析

Provider(提供者)
Terraform 与外部系统(AWS、GCP、Azure、GitHub、Datadog 等)交互的插件。每个 Provider 是一个独立的二进制文件,封装了特定平台的 API 客户端和资源 CRUD 逻辑。执行 terraform init 时,Terraform 会根据 required_providers 声明从 Terraform Registry 下载对应的 Provider 二进制文件。
Terraform Registry
HashiCorp 官方维护的 Provider 和模块仓库(registry.terraform.io),托管了超过 3000 个 Provider,包括 HashiCorp 官方维护的(如 AWS、GCP、Azure)和社区维护的第三方 Provider(如 Cloudflare、PagerDuty、Snowflake)。
required_providers 块
terraform 块内声明当前配置所需的所有 Provider,包括 source(注册表路径)和 version(版本约束)。这是 Terraform 0.13+ 的标准做法,比旧版在 provider 块内声明版本更明确、更规范。
provider 块
配置 Provider 的运行时参数,如 region、认证凭证、重试策略、默认标签等。每个 Provider 类型至多有一个默认 provider 块(无 alias),可以有多个带 alias 的额外 provider 块(用于多区域/多账号)。
# terraform.tf — 统一声明所需 Provider(推荐单独文件)
terraform {
  required_version = ">= 1.5"   # 指定最低 Terraform 版本
  required_providers {
    # AWS Provider:最常用,source 格式为 "命名空间/名称"
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"   # ~> 5.0 意为 >= 5.0, < 6.0
    }
    # GCP Provider
    google = {
      source  = "hashicorp/google"
      version = ">= 5.0, < 6.0"
    }
    # random Provider:生成随机值(不与外部 API 通信)
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
    # 社区 Provider 示例(Cloudflare)
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
  }
}

版本约束语法详解

理解版本约束符号对于维护稳定的 Terraform 项目至关重要。版本号遵循语义化版本(SemVer):MAJOR.MINOR.PATCH,其中 MAJOR 变更意味着不兼容的 API 改动。

= 5.50.0(或不加符号)
精确锁定到 5.50.0,不允许任何其他版本。最严格,升级需手动改版本号。适合对稳定性要求极高的生产环境。
!= 5.50.0
排除特定版本,通常用于绕过已知有 Bug 的版本。例如 != 5.45.0 表示跳过这个有问题的版本。
>= 5.0
5.0 或更高版本,没有上限。最宽松,但可能引入 MAJOR 破坏性变更。单独使用时风险较高。
~> 5.50
允许 5.50.x(只允许 patch 更新),但不允许 5.51 及以上。两位版本号时只锁定 minor,三位版本号时只锁定 patch。这是最常用的约束,既能获得 bug 修复,又不引入破坏性变更。
~> 5.0
允许 5.x(minor 级别更新),但不允许 6.x。适合愿意接受新功能但防止主版本破坏性变更的场景。
>= 5.0, < 6.0
等价于 ~> 5.0,但更明确易读。可组合多个约束,用逗号分隔,所有约束必须同时满足。
最佳实践:使用 ~> 而非 >= 单独使用

团队项目中推荐使用 ~> 5.0~> 5.50 这样的约束。>= 5.0 没有上限,6.0 发布后 terraform init -upgrade 可能拉取带有破坏性变更的新版本。执行 init 后,.terraform.lock.hcl 会锁定精确版本,应提交到 Git。

AWS Provider 配置

# provider.tf — AWS Provider 完整配置示例
provider "aws" {
  region  = var.aws_region   # 使用变量,不硬编码

  # 方式1:使用 Named Profile(~/.aws/credentials 或 ~/.aws/config 中定义)
  profile = "my-work-account"

  # 方式2:assume role(跨账号访问,推荐用于 CI/CD)
  assume_role {
    role_arn     = "arn:aws:iam::123456789:role/TerraformRole"
    session_name = "TerraformSession"
    duration     = "1h"   # 临时凭证有效期
  }

  # default_tags:自动给所有支持 tags 的资源添加全局标签
  # 无需在每个资源中重复声明这些标签
  default_tags {
    tags = {
      ManagedBy   = "Terraform"
      Environment = var.environment
      Team        = "platform"
      Repository  = "github.com/my-org/infra"
    }
  }

  # 重试配置:API 限速时自动重试
  retry_mode     = "adaptive"
  max_retries    = 3
}
# 多区域 Provider(使用 alias)
# 默认 provider:us-east-1(不加 alias)
provider "aws" {
  region = "us-east-1"
}

# 别名 provider:us-west-2
provider "aws" {
  alias  = "us_west"
  region = "us-west-2"
}

# 别名 provider:eu-west-1(欧洲合规数据存储)
provider "aws" {
  alias  = "eu_west"
  region = "eu-west-1"
}

# 资源中通过 provider 参数指定使用哪个 provider 实例
resource "aws_s3_bucket" "west_bucket" {
  provider = aws.us_west    # 格式:provider_type.alias
  bucket   = "my-west-bucket"
}

resource "aws_s3_bucket" "eu_bucket" {
  provider = aws.eu_west
  bucket   = "my-eu-data-bucket"
}

AWS 常用资源创建

以下示例展示了如何用 Terraform 创建一个完整的基础网络和计算层,包括 VPC、子网、安全组和 EC2 实例。这些资源之间存在严格的依赖关系,Terraform 会自动推断依赖顺序。

VPC 网络基础设施

# 1. VPC:虚拟私有网络,是所有网络资源的容器
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"   # 整个 VPC 的 IP 地址范围
  enable_dns_hostnames = true             # 允许实例获取 DNS 名称
  enable_dns_support   = true             # 启用 VPC DNS 解析
  tags = { Name = "main-vpc" }
}

# 2. 公有子网:路由到 Internet Gateway,实例可访问互联网
resource "aws_subnet" "public" {
  count             = 2   # 在 2 个可用区各建一个子网,实现高可用
  vpc_id            = aws_vpc.main.id   # 引用上面 VPC 的 ID
  cidr_block        = "10.0.${count.index + 1}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true   # 实例启动时自动分配公网 IP
  tags = { Name = "public-${count.index + 1}", Type = "Public" }
}

# 3. 私有子网:无直接出站路由,通过 NAT Gateway 访问互联网
resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 10}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = { Name = "private-${count.index + 1}", Type = "Private" }
}

# 4. Internet Gateway:VPC 连接互联网的门户
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags = { Name = "main-igw" }
}

# 5. 路由表:定义子网流量走向
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"                # 所有公网流量
    gateway_id = aws_internet_gateway.main.id  # 路由到 IGW
  }
  tags = { Name = "public-rt" }
}

# 6. 关联路由表与公有子网
resource "aws_route_table_association" "public" {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

# 数据源:动态获取当前 region 的可用区列表(避免硬编码)
data "aws_availability_zones" "available" {
  state = "available"
}

安全组与 EC2 实例

# 安全组:虚拟防火墙,控制进出实例的流量
resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web 服务器安全组"
  vpc_id      = aws_vpc.main.id

  # ingress:入站规则(允许哪些流量进入实例)
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # HTTP:允许所有来源
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]   # HTTPS:允许所有来源
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]  # SSH:只允许内网访问(生产不开放公网 SSH)
  }

  # egress:出站规则(实例发出的流量)
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"             # -1 表示所有协议
    cidr_blocks = ["0.0.0.0/0"]   # 允许所有出站流量
  }

  tags = { Name = "web-sg" }
}

# 数据源:动态查找最新的 Amazon Linux 2023 AMI
# 避免硬编码 AMI ID(不同区域 AMI ID 不同,且会定期更新)
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "state"
    values = ["available"]
  }
}

# EC2 实例
resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id  # 使用数据源动态获取的 AMI
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.public[0].id
  vpc_security_group_ids = [aws_security_group.web.id]  # 列表,可附加多个安全组
  key_name               = aws_key_pair.deployer.key_name

  # user_data:实例首次启动时执行的 shell 脚本(Bash heredoc)
  user_data = <<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y nginx
    systemctl enable --now nginx
    echo "<h1>Hello from $(hostname)</h1>" > /usr/share/nginx/html/index.html
  EOF

  # 启用终止保护(防止意外删除生产实例)
  disable_api_termination = var.environment == "prod" ? true : false

  # 根卷配置
  root_block_device {
    volume_type = "gp3"
    volume_size = 20      # GB
    encrypted   = true   # 加密根卷
  }

  tags = { Name = "web-server" }
}

# 导入 SSH 公钥(密钥对)
resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = file("~/.ssh/id_rsa.pub")  # 读取本地公钥文件
}

S3 存储桶完整配置

# 生成随机后缀,避免 S3 bucket 名称全局冲突
resource "random_id" "suffix" {
  byte_length = 4   # 生成 8 位十六进制字符串(如 "a1b2c3d4")
}

# S3 Bucket 主体
resource "aws_s3_bucket" "assets" {
  bucket = "my-assets-${random_id.suffix.hex}"   # 全局唯一的 bucket 名称
  tags   = { Purpose = "static-assets" }
}

# 版本控制:保存对象的历史版本,支持恢复误删除的文件
resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id
  versioning_configuration {
    status = "Enabled"   # Enabled / Suspended / Disabled
  }
}

# 服务端加密:静态加密所有存储的对象
resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"   # 使用 AWS KMS 管理密钥(更安全)
      kms_master_key_id = aws_kms_key.s3.arn  # 指定自定义 KMS 密钥
    }
    bucket_key_enabled = true   # 减少 KMS API 调用次数,降低成本
  }
}

# 阻止公共访问(所有 S3 bucket 默认应该阻止公共访问)
resource "aws_s3_bucket_public_access_block" "assets" {
  bucket = aws_s3_bucket.assets.id

  block_public_acls       = true   # 阻止设置公共 ACL
  block_public_policy     = true   # 阻止公共 bucket 策略
  ignore_public_acls      = true   # 忽略现有公共 ACL
  restrict_public_buckets = true   # 限制公共 bucket 访问
}

# 生命周期策略:自动管理对象存储层级和过期
resource "aws_s3_bucket_lifecycle_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id

  rule {
    id     = "archive-old-versions"
    status = "Enabled"

    # 非当前版本(旧版本)在 30 天后转到 Glacier 节省成本
    noncurrent_version_transition {
      noncurrent_days = 30
      storage_class   = "GLACIER"
    }

    # 非当前版本在 90 天后彻底删除
    noncurrent_version_expiration {
      noncurrent_days = 90
    }
  }
}

AWS 认证方式详解

环境变量(CI/CD 推荐)
设置 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_DEFAULT_REGION 等环境变量。AWS Provider 自动读取,无需在 provider 块中配置。GitHub Actions 等 CI 平台将密钥存在 Secrets 中,运行时注入为环境变量。
~/.aws/credentials(本地开发)
通过 aws configure 生成的凭证文件,支持多个 Named Profile(如 [work][personal])。在 provider 块中通过 profile = "work" 指定。
IAM Instance Profile / Task Role(AWS 上的 CI/CD)
在 EC2、ECS、Lambda 等 AWS 服务上运行 Terraform 时,附加 IAM 角色后无需管理静态密钥。AWS SDK 通过 IMDS(实例元数据服务)自动获取临时凭证。优点:零密钥管理。
OIDC / Web Identity(最安全的 CI/CD 方式)
GitHub Actions、GitLab CI、CircleCI 等现代 CI 平台支持通过 OpenID Connect 协议向 AWS 证明身份,无需存储任何静态 Access Key。AWS 配置 OIDC 身份提供商后,CI 工作流获得短期临时凭证,即使工作流代码被泄露也不会暴露长期密钥。
Assume Role(跨账号访问)
通过 assume_role 块让 Terraform 临时扮演另一个 IAM 角色。常用于:CI 账号承担生产账号的只读角色,或一个 Terraform 工作流管理多个 AWS 账号的资源。
# ===== 环境变量认证(CI/CD 中常用)=====
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="us-east-1"
# 可选:指定 Profile(覆盖 profile 配置)
export AWS_PROFILE="my-work-account"

# ===== GCP 认证 =====
# 方式1:服务账号密钥文件(适合 CI/CD,不推荐在本地使用)
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"

# 方式2:ADC(Application Default Credentials)本地开发推荐
gcloud auth application-default login
# 这会在 ~/.config/gcloud/application_default_credentials.json 生成临时凭证
# Terraform google provider 会自动使用 ADC,无需任何 provider 块配置

# ===== Azure 认证 =====
# 方式1:Azure CLI 登录(本地开发)
az login
# 选择订阅
az account set --subscription "00000000-0000-0000-0000-000000000000"

# 方式2:Service Principal(CI/CD 使用)
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
export ARM_TENANT_ID="11111111-1111-1111-1111-111111111111"
export ARM_CLIENT_ID="22222222-2222-2222-2222-222222222222"
export ARM_CLIENT_SECRET="your-client-secret"
绝对不要将密钥提交到 Git

不要在 .tf 文件中硬编码 Access Key 或密码。应使用环境变量、AWS Secrets Manager、HashiCorp Vault 或 CI 平台内置的 Secrets 管理。

.gitignore 中始终添加:
*.tfvars(可能包含密码)
terraform.tfstateterraform.tfstate.backup(包含敏感输出,见第4章)
*.tfvars.json(JSON 格式的变量文件,同样可能含密码)

多 Provider 配置:跨账号、跨云部署

# 场景:应用资源在账号 A,日志/监控资源在账号 B(隔离安全域)

# 账号 A 的 provider(默认,无 alias)
provider "aws" {
  region = "us-east-1"
  assume_role {
    role_arn = "arn:aws:iam::111111111111:role/TerraformRole"
  }
}

# 账号 B 的 provider(带 alias)
provider "aws" {
  alias  = "logging"
  region = "us-east-1"
  assume_role {
    role_arn = "arn:aws:iam::222222222222:role/LoggingRole"
  }
}

# 在账号 A 创建应用资源(省略 provider 参数,使用默认 provider)
resource "aws_ecs_cluster" "app" {
  name = "production"
}

# 在账号 B 创建日志存储(显式指定别名 provider)
resource "aws_s3_bucket" "logs" {
  provider = aws.logging   # 这个资源在账号 B 创建
  bucket   = "central-logs-bucket"
}

# 将 provider 传递给模块(模块内使用别名 provider)
module "monitoring" {
  source = "../../modules/monitoring"

  providers = {
    aws = aws.logging  # 模块内的所有 aws 资源使用 aws.logging provider
  }
}

# 混合多云:同一 Terraform 配置使用 AWS + GCP
resource "aws_ses_domain_identity" "email" {
  domain = "example.com"
}

# 将 AWS SES 验证记录自动添加到 GCP Cloud DNS
resource "google_dns_record_set" "ses_verification" {
  name         = "_amazonses.example.com."
  type         = "TXT"
  managed_zone = "example-com-zone"
  rrdatas      = [aws_ses_domain_identity.email.verification_token]
}

GCP 和 Azure Provider 完整示例

# GCP Provider 配置
provider "google" {
  project = "my-gcp-project-id"
  region  = "us-central1"
  zone    = "us-central1-a"
  # 认证:优先读 GOOGLE_APPLICATION_CREDENTIALS 环境变量
  # 若未设置则使用 Application Default Credentials(本地 gcloud 登录)
}

# GCP Compute Engine VM 实例
resource "google_compute_instance" "web" {
  name         = "web-server"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
      size  = 20   # GB
      type  = "pd-ssd"
    }
  }

  network_interface {
    network    = "default"
    subnetwork = "default"
    access_config {}  # 空 access_config 块表示分配临时公网 IP
  }

  # 服务账号(最小权限原则)
  service_account {
    email  = google_service_account.vm.email
    scopes = ["cloud-platform"]
  }

  tags = ["web", "http-server"]
}

# GCP 防火墙规则(类似 AWS 安全组)
resource "google_compute_firewall" "allow_http" {
  name    = "allow-http"
  network = "default"

  allow {
    protocol = "tcp"
    ports    = ["80", "443"]
  }

  target_tags   = ["web"]         # 规则应用到带 "web" 标签的实例
  source_ranges = ["0.0.0.0/0"]   # 来源:所有 IP
}
# Azure Provider 配置
provider "azurerm" {
  features {
    # features 块是必须的,即使为空;可配置删除行为
    resource_group {
      prevent_deletion_if_contains_resources = true  # 防止误删含资源的 RG
    }
    virtual_machine {
      delete_os_disk_on_deletion     = true   # 删除 VM 时同时删除系统盘
      graceful_shutdown              = false  # 不等待 OS 优雅关机
    }
  }
  # 认证:从 ARM_ 环境变量读取(推荐),或从 Azure CLI 登录状态读取
  subscription_id = var.azure_subscription_id   # 也可通过变量传入
}

# Azure Resource Group:所有 Azure 资源必须归属一个 Resource Group
resource "azurerm_resource_group" "main" {
  name     = "production-rg"
  location = "East US"
}

# Azure Virtual Network(类似 AWS VPC)
resource "azurerm_virtual_network" "main" {
  name                = "main-vnet"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  address_space       = ["10.0.0.0/16"]
}

# Azure Linux VM
resource "azurerm_linux_virtual_machine" "web" {
  name                = "web-vm"
  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  size                = "Standard_B2s"
  admin_username      = "adminuser"

  admin_ssh_key {
    username   = "adminuser"
    public_key = file("~/.ssh/id_rsa.pub")
  }

  network_interface_ids = [azurerm_network_interface.web.id]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Premium_LRS"  # SSD 类型
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }
}

Data Source:读取已有资源

Data Source(数据源)允许 Terraform 查询已有的云资源信息,而不创建新资源。这对于读取不由当前 Terraform 管理的资源(如共享的 VPC、其他团队创建的资源)非常有用。

# 查询已有的 VPC(由另一个 Terraform 工作区管理)
data "aws_vpc" "shared" {
  tags = {
    Name = "shared-vpc"
  }
}

# 查询已有 VPC 的所有私有子网
data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.shared.id]
  }
  tags = { Type = "Private" }
}

# 读取 AWS SSM Parameter Store 中存储的配置值
data "aws_ssm_parameter" "db_password" {
  name            = "/myapp/prod/db-password"
  with_decryption = true   # 解密 SecureString 类型的参数
}

# 使用数据源返回的值
resource "aws_db_instance" "main" {
  db_subnet_group_name = aws_db_subnet_group.main.name
  password             = data.aws_ssm_parameter.db_password.value
  # ...
}

# 读取当前 AWS 账号 ID 和 Region(无需硬编码)
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

# 在资源中引用
locals {
  account_id = data.aws_caller_identity.current.account_id
  region     = data.aws_region.current.name
  # 动态构建 ARN,无需硬编码账号 ID
  role_arn   = "arn:aws:iam::${local.account_id}:role/MyRole"
}

认证的优先级顺序(AWS)

AWS Provider 按以下顺序依次尝试认证方式,找到第一个有效的凭证后停止:

  1. provider 块中直接配置的 access_key / secret_key(不推荐,安全风险高)
  2. 环境变量:AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
  3. 环境变量:AWS_PROFILE(或 provider 块中的 profile 参数)
  4. ~/.aws/credentials 文件中的 [default] profile
  5. ~/.aws/config 文件中的配置
  6. EC2 Instance Metadata(IAM Instance Profile)/ ECS Task Role / Lambda 执行角色
  7. Web Identity(OIDC)Token(GitHub Actions 等 CI 系统)
AWS default_tags 的工作原理

AWS Provider 的 default_tags 块会自动将指定的标签应用到所有支持 tags 属性的资源上。资源级别的 tagsdefault_tags 会合并,若有同名键则资源级别优先。这个功能在 Terraform plan 输出中会以 tags_all 属性展示合并后的最终标签。注意:并非所有资源都支持标签,如 aws_iam_policy_attachment 等资源不支持。

本章小结

本章核心要点