You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Terraform销毁AWS资源受阻:Python添加的DNS记录未托管的解决方案咨询

问题描述

我通过Terraform配置部署Web应用所需的AWS基础设施,Terraform执行完成后,Jenkins会运行Python脚本完成两个操作:

  • 将Porkbun的域名服务器更新为AWS Route53的域名服务器
  • 在Route53托管区中添加ACM证书验证所需的CNAME记录(Terraform已生成ACM证书)

当前遇到的问题:执行terraform destroy时,由于上述CNAME记录由Python脚本创建、未纳入Terraform管理范围,Route53托管区因存在未被Terraform跟踪的记录而无法销毁。手动删除该CNAME记录后,Terraform即可正常销毁所有资源。

我需要确认:

  1. 当前的实现方式是否存在问题?
  2. 有没有合适的解决方案?比如能否在Python脚本中动态将该CNAME记录导入Terraform管理?

相关配置及脚本

dns.tf配置

resource "aws_route53_zone" "main" {
  name = var.domain_name
}

resource "aws_route53_record" "www" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "www.${var.domain_name}"
  type    = "A"
  ttl     = "300"
  records = [aws_instance.resume-app-ec2-instance.public_ip]
}

resource "aws_route53_record" "root" {
  zone_id = aws_route53_zone.main.zone_id
  name    = var.domain_name
  type    = "A"
  ttl     = "300"
  records = [aws_instance.resume-app-ec2-instance.public_ip]
}

自定义Python脚本

import requests
import json
import boto3
import time

print("Firing up domain changes...")
time.sleep(3)

# AWS Secret Manager details
secret_name = "aws-pork-dns-ns"
region_name = "us-east-1"

# Porkbun Domain Name
domain_name = "redacted :)"

# Initialize a Secrets Manager client
client = boto3.client('secretsmanager', region_name=region_name)

# Retrieve Porkbun API credentials from AWS Secret Manager
response = client.get_secret_value(SecretId=secret_name)
secret_dict = json.loads(response['SecretString'])
pork_api_key = secret_dict['pork_api_key']
pork_api_secret = secret_dict['pork_api_secret']

# Retrieve name servers from Route 53
route53_client = boto3.client('route53')
response = route53_client.list_hosted_zones_by_name(DNSName=domain_name)
hosted_zone_id = response['HostedZones'][0]['Id'].split('/')[-1]
response = route53_client.get_hosted_zone(Id=hosted_zone_id)
name_servers = response['DelegationSet']['NameServers']

# Construct the request body to update name servers according to the Porkbun API documentation
request_body = {
    "apikey": pork_api_key,
    "secretapikey": pork_api_secret,
    "ns": name_servers
}

# Update name servers on Porkbun
url = f"https://porkbun.com/api/json/v3/domain/updateNs/{domain_name}"
headers = {"Content-Type": "application/json"}
response = requests.post(url, headers=headers, json=request_body)

# Check the response for name server update
if response.status_code == 200:
    print("Name servers updated successfully.")
    print(response.json())
else:
    print("Failed to update name servers.")
    print("Status Code:", response.status_code)
    print("Response:", response.json())

# Wait for a few seconds for DNS propagation
print("Hold on, waiting for records to propagate before continuing...")
time.sleep(30)

# Get ACM certificate ARN
acm_client = boto3.client('acm')
response = acm_client.list_certificates()
certificate_arn = None
for certificate in response['CertificateSummaryList']:
    if certificate['DomainName'] == domain_name:
        certificate_arn = certificate['CertificateArn']
        break

if certificate_arn:
    # Get DNS validation options
    response = acm_client.describe_certificate(CertificateArn=certificate_arn)
    validation_options = response['Certificate']['DomainValidationOptions']
    for option in validation_options:
        if option['ValidationMethod'] == 'DNS':
            dns_record_name = option['ResourceRecord']['Name']
            dns_record_value = option['ResourceRecord']['Value']

            # Create CNAME record in Route 53
            response = route53_client.change_resource_record_sets(
                HostedZoneId=hosted_zone_id,
                ChangeBatch={
                    'Changes': [{
                        'Action': 'UPSERT',
                        'ResourceRecordSet': {
                            'Name': dns_record_name,
                            'Type': 'CNAME',
                            'TTL': 300,
                            'ResourceRecords': [{'Value': dns_record_value}]
                        }
                    }]
                }
            )

            print("CNAME record created successfully.")
            print("DNS Record Name:", dns_record_name)
            print("DNS Record Value:", dns_record_value)
            break
else:
    print("ACM certificate not found for domain:", domain_name)

acm.tf配置

resource "aws_acm_certificate" "resume-app-cert" {
  domain_name       = var.domain_name
  validation_method = "DNS"

  tags = {
    Name = "resume-app-ssl-cert"
  }
}

问题分析与解决方案

当前方法的问题

当前实现的核心问题是Terraform状态与实际AWS资源不一致:Terraform的设计逻辑是通过状态文件跟踪所有由其创建的资源,而Python脚本手动创建的CNAME记录不在状态文件中,导致Terraform在销毁托管区时,发现存在未管理的资源,从而报错终止。这种混合管理方式会破坏Terraform的状态一致性,是不推荐的实践。

推荐解决方案

方案1:让Terraform自动管理ACM验证CNAME(最优)

完全抛弃Python脚本中创建CNAME的逻辑,改用Terraform自动生成并管理ACM证书的验证记录,这样销毁时Terraform会自动清理该记录,无需手动干预。

修改dns.tf,添加以下资源配置:

# 自动创建ACM证书验证所需的CNAME记录
resource "aws_route53_record" "acm_validation" {
  for_each = {
    for dvo in aws_acm_certificate.resume-app-cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = aws_route53_zone.main.zone_id
  name    = each.value.name
  type    = each.value.type
  ttl     = 300
  records = [each.value.record]
}

# 等待ACM证书完成验证
resource "aws_acm_certificate_validation" "resume-app-cert" {
  certificate_arn         = aws_acm_certificate.resume-app-cert.arn
  validation_record_fqdns = [for record in aws_route53_record.acm_validation : record.fqdn]
}

优势:

  • 完全由Terraform管理所有资源,状态一致,destroy时自动清理验证记录
  • 无需额外脚本,减少维护成本
  • 自动等待证书验证完成,避免手动sleep的不可靠性

方案2:在销毁流程中添加CNAME清理逻辑

如果需要保留Python脚本的逻辑,可以在执行terraform destroy前,先运行一个清理脚本删除该CNAME记录。

示例清理脚本(可整合到Jenkins的destroy步骤):

import boto3

domain_name = "redacted :)"
region_name = "us-east-1"

# 获取Route53托管区ID
route53_client = boto3.client('route53', region_name=region_name)
response = route53_client.list_hosted_zones_by_name(DNSName=domain_name)
hosted_zone_id = response['HostedZones'][0]['Id'].split('/')[-1]

# 获取ACM证书验证的CNAME记录
acm_client = boto3.client('acm', region_name=region_name)
response = acm_client.list_certificates()
certificate_arn = None
for cert in response['CertificateSummaryList']:
    if cert['DomainName'] == domain_name:
        certificate_arn = cert['CertificateArn']
        break

if certificate_arn:
    response = acm_client.describe_certificate(CertificateArn=certificate_arn)
    for option in response['Certificate']['DomainValidationOptions']:
        if option['ValidationMethod'] == 'DNS':
            # 删除CNAME记录
            change_batch = {
                'Changes': [{
                    'Action': 'DELETE',
                    'ResourceRecordSet': {
                        'Name': option['ResourceRecord']['Name'],
                        'Type': option['ResourceRecord']['Type'],
                        'TTL': 300,
                        'ResourceRecords': [{'Value': option['ResourceRecord']['Value']}]
                    }
                }]
            }
            route53_client.change_resource_record_sets(HostedZoneId=hosted_zone_id, ChangeBatch=change_batch)
            print("验证CNAME记录已删除")
            break

方案3:动态将CNAME导入Terraform(不推荐)

可以在Python脚本创建CNAME后,执行terraform import命令将该记录导入Terraform状态,但该方式复杂度高,容易引发状态冲突,仅适合特殊场景。

步骤:

  1. 提前在dns.tf中定义占位的资源:
    resource "aws_route53_record" "acm_validation" {
      zone_id = aws_route53_zone.main.zone_id
      name    = "" # 占位,导入后会被覆盖
      type    = "CNAME"
      ttl     = 300
      records = [""]
    }
    
  2. 在Python脚本创建CNAME后,添加以下代码执行导入:
    import subprocess
    
    # 构造导入命令,注意记录名中的点需替换为下划线
    import_id = f"{hosted_zone_id}_{dns_record_name.replace('.', '_')}_CNAME"
    import_command = f"terraform import aws_route53_record.acm_validation {import_id}"
    subprocess.run(import_command, shell=True, check=True)
    

劣势:

  • 需要维护Terraform状态的一致性,多次执行可能导致重复导入错误
  • 依赖本地Terraform环境配置,增加Jenkins流程复杂度

内容的提问来源于stack exchange,提问作者Assaf

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 00:54:54