EC2实例停止重启后无法获取公网IP的Terraform配置问题
使用Terraform创建带有3个额外私有网络接口的EC2实例,首次执行plan/apply操作后,实例正常创建并获取到公网IP。但停止该EC2实例再重启后,AWS不再为其分配公网IP;再次执行Terraform plan/apply时,EC2实例会被替换销毁。已开启子网自动分配公网IPv4地址,Terraform版本为v1.6.6,AWS Provider版本为~> 4.0。
相关配置
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } } resource "aws_instance" "monitoring" { ami = var.ami_id instance_type = var.instance_type subnet_id = var.subnet_id associate_public_ip_address = true key_name = var.key # iam_instance_profile = var.aws_iam_instance_profile # security_groups = [aws_security_group.common.id] tags = merge( local.common_tags, { "Name" = "${var.monitoring}" } ) lifecycle { # ignore_changes = [network_interface, associate_public_ip_address, vpc_security_group_ids, security_groups] prevent_destroy = false } } resource "aws_network_interface" "private1" { subnet_id = var.subnet_id # security_groups = [aws_security_group.common.id] attachment { instance = aws_instance.monitoring.id device_index = 1 } tags = merge( local.common_tags, { "Name" = "${var.monitoring}" } ) } # 另外两个私有网卡配置类似,省略...
Terraform Plan输出
Terraform will perform the following actions: # aws_instance.monitoring must be replaced -/+ resource "aws_instance" "monitoring" { ~ arn = "arn:aws:ec2:eu-west-1:335094912020:instance/i-04dbad35419e5403d" -> (known after apply) ~ associate_public_ip_address = false -> true # forces replacement ~ availability_zone = "eu-west-1a" -> (known after apply) ~ cpu_core_count = 2 -> (known after apply) ~ cpu_threads_per_core = 2 -> (known after apply) ~ disable_api_stop = false -> (known after apply) ~ disable_api_termination = false -> (known after apply) ~ ebs_optimized = false -> (known after apply) - hibernation = false -> null + host_id = (known after apply) + host_resource_group_arn = (known after apply) + iam_instance_profile = (known after apply) ~ id = "i-04dbad35419e5403d" -> (known after apply) ~ instance_initiated_shutdown_behavior = "stop" -> (known after apply) ~ instance_state = "running" -> (known after apply) ~ ipv6_address_count = 0 -> (known after apply) ~ ipv6_addresses = [] -> (known after apply) ~ monitoring = false -> (known after apply) + outpost_arn = (known after apply) + password_data = (known after apply) + placement_group = (known after apply) ~ placement_partition_number = 0 -> (known after apply) ~ primary_network_interface_id = "eni-067ddc45b42b3bc58" -> (known after apply) ~ private_dns = "ip-10-1-4-200.eu-west-1.compute.internal" -> (known after apply) ~ private_ip = "10.1.4.200" -> (known after apply) + public_dns = (known after apply) + public_ip = (known after apply) ~ secondary_private_ips = [] -> (known after apply) ~ security_groups = [] -> (known after apply) tags = { "Name" = "advertisement-monitoring" "Owner" = "devops-team" "Project" = "advertising" "Role" = "monitoring" "Terraform" = "true" } ~ tenancy = "default" -> (known after apply) + user_data = (known after apply) + user_data_base64 = (known after apply) ~ vpc_security_group_ids = [ - "sg-058c8236c532c5a43", ] -> (known after apply) # (8 unchanged attributes hidden) - capacity_reservation_specification { - capacity_reservation_preference = "open" -> null } - cpu_options { - core_count = 2 -> null - threads_per_core = 2 -> null } - credit_specification { - cpu_credits = "unlimited" -> null } - enclave_options { - enabled = false -> null } - maintenance_options { - auto_recovery = "default" -> null } - metadata_options { - http_endpoint = "enabled" -> null - http_put_response_hop_limit = 2 -> null - http_tokens = "required" -> null - instance_metadata_tags = "disabled" -> null } - private_dns_name_options { - enable_resource_name_dns_a_record = false -> null - enable_resource_name_dns_aaaa_record = false -> null - hostname_type = "ip-name" -> null } - root_block_device { - delete_on_termination = true -> null - device_name = "/dev/xvda" -> null - encrypted = false -> null - iops = 3000 -> null - tags = {} -> null - throughput = 125 -> null - volume_id = "vol-05544aeda21e9408c" -> null - volume_size = 8 -> null - volume_type = "gp3" -> null } } # aws_network_interface.private1 will be updated in-place ~ resource "aws_network_interface" "private1" { id = "eni-0f66a33626a3a3f66" tags = { "Name" = "advertisement-monitoring" "Owner" = "devops-team" "Project" = "advertising" "Role" = "monitoring" "Terraform" = "true" } # (22 unchanged attributes hidden) - attachment { - attachment_id = "eni-attach-0740bf5f5dfab0d38" -> null - device_index = 1 -> null - instance = "i-04dbad35419e5403d" -> null } + attachment { + attachment_id = (known after apply) + device_index = 1 + instance = (known after apply) } }
从Plan输出可以明确看到,associate_public_ip_address属性的实际值从false变为配置中的true,而该属性是Terraform AWS Provider中的强制替换属性,因此触发了EC2实例的销毁重建。
导致状态不一致的核心原因是:
- 当EC2实例附加了额外弹性网卡(ENI)时,停止并重启实例后,AWS会自动将
associate_public_ip_address的实际状态重置为false,即使子网已开启自动分配公网IP - Terraform同步资源状态时,检测到实际值与配置值不符,进而触发替换逻辑
方案1:忽略associate_public_ip_address的状态变化
修改aws_instance资源的lifecycle块,添加ignore_changes规则,让Terraform忽略该属性的差异,避免触发实例替换:
resource "aws_instance" "monitoring" { ami = var.ami_id instance_type = var.instance_type subnet_id = var.subnet_id associate_public_ip_address = true key_name = var.key tags = merge( local.common_tags, { "Name" = "${var.monitoring}" } ) lifecycle { ignore_changes = [associate_public_ip_address] prevent_destroy = false } }
此方案适用于接受临时公网IP的场景,重启后若需要公网IP,可手动在AWS控制台为实例分配,或依赖子网的自动分配规则。
方案2:绑定弹性公网IP(EIP)
如果需要固定公网IP,建议创建并绑定弹性公网IP,彻底避免依赖实例的临时公网IP分配机制:
# 创建弹性公网IP resource "aws_eip" "monitoring" { vpc = true tags = merge(local.common_tags, { "Name" = "${var.monitoring}-eip" }) } # 将EIP绑定到EC2实例 resource "aws_eip_association" "monitoring" { instance_id = aws_instance.monitoring.id allocation_id = aws_eip.monitoring.id }
弹性公网IP在实例停止重启后会自动重新绑定,确保公网IP不变。
方案3:调整网卡配置方式
避免在独立的aws_network_interface资源中定义attachment块,而是在aws_instance资源内部通过network_interface块统一管理所有网卡,让Terraform更好地维护资源关联状态:
resource "aws_instance" "monitoring" { ami = var.ami_id instance_type = var.instance_type subnet_id = var.subnet_id associate_public_ip_address = true key_name = var.key # 主网卡(device_index=0) network_interface { device_index = 0 subnet_id = var.subnet_id # security_groups = [aws_security_group.common.id] } # 额外私有网卡1 network_interface { device_index = 1 subnet_id = var.subnet_id # security_groups = [aws_security_group.common.id] } # 额外私有网卡2 network_interface { device_index = 2 subnet_id = var.subnet_id # security_groups = [aws_security_group.common.id] } # 额外私有网卡3 network_interface { device_index = 3 subnet_id = var.subnet_id # security_groups = [aws_security_group.common.id] } tags = merge( local.common_tags, { "Name" = "${var.monitoring}" } ) }
此方式能让Terraform更精准地控制网卡与实例的绑定关系,减少状态不一致的概率。
内容的提问来源于stack exchange,提问作者Steve Angelo

