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即可正常销毁所有资源。
我需要确认:
- 当前的实现方式是否存在问题?
- 有没有合适的解决方案?比如能否在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状态,但该方式复杂度高,容易引发状态冲突,仅适合特殊场景。
步骤:
- 提前在
dns.tf中定义占位的资源:resource "aws_route53_record" "acm_validation" { zone_id = aws_route53_zone.main.zone_id name = "" # 占位,导入后会被覆盖 type = "CNAME" ttl = 300 records = [""] } - 在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

