背景
Amazon CloudFront 是一个全球性的内容分发网络 (CDN),您可以借助 CloudFront 以低延迟和高可用性向查看者或者最终用户分发内容。通常来讲,Amazon CloudFront 的客户都拥有多个 CloudFront Distribution,每个 Distribution 都包含了一组待加速的内容以及相关的配置信息,例如源站、加速域名、缓存策略、证书、日志、访问控制等。
以下主题说明了一些有关 CloudFront Distribution 的基础知识,并提供了有关可选择用来配置 Distribution 以满足业务需求的设置的信息。在 CloudFront 自动化操作的过程中,一个常见的任务是创建多个 Distribution,这些 Distribution 的配置在同一类型作业下配置参数完全相同,例如动态加速场景下缓存 TTL 均设置为0。在该场景下,客户往往需要一个克隆已存在的 Distribution 的配置项,并希望通过标准 Restful API 接口调用以实现借助程序快速创建多个域名 Distribution 的目的。而目前亚马逊云科技支持的新建 Distribution 的 API :CreateDistribution,需要提供完整的配置参数信息,暂时不支持复制或者克隆 Distribution 的功能。
本文介绍的方案是利用 Amazon API Gateway 和 Amazon Lambda,后端基于Amazon SDK for Python (Boto3),实现基于一个参考 Distribution的配置信息,仅修改域名与源站等变更项,来复制一个新 Distribution 的 Restful API。
解决方案简介
CloudFront Distribution 复制/克隆功能的解决方案基本实现思路如下:
1)复制已存在有一个参考 Distribution 的配置信息;
2)改造可变项,例如新 Distribution 的加速域名 CNAME 和源站的域名,组成新的完整配置信息;
3)执行新建 Distribution 的操作。
在此过程中,会调用 Amazon Certificate Manager 的相关 API 查找到新 CNAME 所对应的 ACM 证书的 ARN,从而完成加速域名的证书关联操作。其中,查询证书 ARN 的过程对用户是透明的。
解决方案的详细实现流程如下:
1)获取参考域名的配置信息做为基准配置(函数:get_reference_config)
调用Amazon Boto3 API get_distribution_config,输入 distribution id,以 Json 形式获取 distribution 的配置信息 DistributionConfig;
2)获取账号下 ACM 证书与域名的对应列表(函数get_certificate_mapping)
调用Amazon Boto3 API list_certificates,输入 CertificateStatuses=’ISSUED’,查询账号下 ACM 中的已签发的证书与域名的对应列表;
3)获取该 CNAME 对应的 ACM 证书(函数get_certificate_arn)
从步骤3中查出的列表里取得源站 origin 所对应的 ACM 证书的 ARN(Amazon Resource Names,资源名的唯一标识符),用于下一步创建 Distribution 时的证书参数;
4)构造新Distribution的配置信息(函数set_config_based_on_ref)
根据步骤一获取的基准 DistributionConfig,修改域名和源站,添加证书 ARN,创建新的 DistributionConfig
5)创建新Distribution(函数create_distribution)
调用 Amazon Boto3 API create_distribution, 输入步骤4构造的 DistributionConfig,创建所需Distribution。
解决方案架构
方案用户接口通过 API Gateway 和 Lambda 函数 cf_distribution_clone 生成一个 Restful API clone_distribution, 函数 cf_distribution_clone 会依据触发 event 中的 queryStringParameters 创建克隆的 Distribution。为了进一步加强安全管理限制 API 的访问,此例中 API Gateway 中将开启 Cognito 授权,访问接口的用户需携带 Cognito 令牌才能正常请求 API。方案的架构图如下所示。
解决方案部署
1. 部署 Lambda 函数
创建一个IAM 角色 cf-clone-distribution-role 供 Lambda 执行时 Assume,为该角色创建如下 IAM 策略,注意需要将与分别替换成 CloudFront 日志所在的 S3 Bucket 的桶名与账号 ID。该策略具有对已有 Distribution 的配置查询权限,新建 Distribution 权限,ACM 证书的列出权限,以及对 CloudFront 日志所在 S3 存储桶的 Bucket ACL 的查询与修改权限。示例以美东区域 us-east-1 作为参考,可以根据实际情况进行替换。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"acm:ListCertificates",
"cloudfront:CreateDistribution",
"cloudfront:GetDistributionConfig"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"s3:PutBucketAcl",
"s3:GetBucketAcl",
"logs:CreateLogGroup"
],
"Resource": [
"arn:aws:s3:::",
"arn:aws:logs:us-east-1::*"
]
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1: ::log-group:/aws/lambda/cf_distribution_clone:*"
}
]
}
创建 Lambda函数 cf_distribution_clone,设置 Lambda 执行角色为上文创建的 cf-clone-distribution-role。解决方案使用的运行时环境为 Python 3.9,其对应的完整 Lambda 代码如下所示。
import boto3
from botocore.config import Config
import botocore.exceptions
from datetime import datetime, timezone
import logging
import json
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger()
cf_client = boto3.client('cloudfront')
my_config = Config(
region_name = 'us-east-1'
)
acm_client = boto3.client('acm', config=my_config)
def lambda_handler(event, context):
conf_domain = event['queryStringParameters']['domain']
conf_origin = event['queryStringParameters']['origin']
conf_ref_dist = event['queryStringParameters']['ref_dist']
# 1) 获取参考域名的配置信息做为基准配置
ref_config = get_reference_config(conf_ref_dist)
# 2) 获取账号下ACM证书与域名的对应列表
certs = get_certificate_mapping()
# 3) 获取该CNAME对应的ACM证书
certArn = get_certificate_arn(certs, conf_domain)
# 4) 构造新Distribution的配置信息
new_config = set_config_based_on_ref(ref_config, conf_domain, conf_origin, certArn)
# 5) 创建新Distribution
distribution = create_distribution(new_config) response_body = {}
response_body['requestId'] = context.aws_request_id
response_body['distributionId'] = distribution['Distribution']['Id']
responseObject = {}
responseObject['statusCode'] = 200
responseObject['body'] = json.dumps(response_body)
return(responseObject)
def get_reference_config(ref_dist):
try:
return cf_client.get_distribution_config(Id=ref_dist)
except botocore.exceptions.ClientError as error:
logger.exception(f"{format(error)}")
raise error
def get_certificate_mapping():
try:
response = acm_client.list_certificates(
CertificateStatuses=[
'ISSUED'
],
MaxItems=1000
)
certs = response['CertificateSummaryList']
while "NextToken" in response:
response = acm_client.list_certificates(
CertificateStatuses=[
'ISSUED'
],
MaxItems=1000,
NextToken= response['NextToken']
)
certs.extend(response["CertificateSummaryList"])
cert_dict = {}
for cert in certs:
cert_dict[cert['DomainName']] = cert['CertificateArn']
return(cert_dict)
except botocore.exceptions.ClientError as error:
logger.exception(f"{format(error)}")
raise error
def get_certificate_arn(certs, domain):
if domain in certs:
cert = certs[domain]
else:
cert_domain = '*.' + domain.split(".", 1)[-1]
if cert_domain in certs:
cert = certs[cert_domain]
else:
logger.info(f"No certificate for domain - {format(domain)} in ACM. Please create or import one.") exit(1) logger.info(f"Use ACM certificate for domain \'{format(domain)}\': {format(cert)}.") return cert
def set_config_based_on_ref(ref_config, conf_domain, conf_origin, certArn):
ref_config['DistributionConfig']['Aliases'] = {
'Quantity': 1,
'Items': [
conf_domain
]
}
new_config = ref_config['DistributionConfig']
new_config['CallerReference'] = str(datetime.now(tz=None).timestamp())
new_config['Origins']['Items'][0]['Id'] = conf_origin
new_config['Origins']['Items'][0]['DomainName'] = conf_origin
new_config['DefaultCacheBehavior']['TargetOriginId'] = conf_origin
new_config['Comment'] = conf_domain
new_config['ViewerCertificate']['ACMCertificateArn'] = certArn
return new_config
def create_distribution(config):
try:
distribution = cf_client.create_distribution(DistributionConfig=config)
logger.info(f"Done! Created distribution {format(distribution['Distribution']['Id'])}.") except botocore.exceptions.ClientError as error:
logger.exception(f"{format(error)}")
raise error
return(distribution)
Lambda 函数部分的操作可以参考下图中的示例。
2. 创建 API Gateway
创建 API Gateway 执行方法,添加 URL 查询字符串参数(*注意:以下参数均为小写)。
-
domain:创建的 Distribution 所关联的 CNAME,即加速域名;
-
origin:新建的 Distribution 指向的源站域名;
-
ref_dist:参考 Distribution,新建 Distribution 参数参考 Ref_dist 的参数配置,仅修改 CNAME 和 Origin 域名。
API Gateway 部分的操作可以参考下图中的示例。
3. 部署身份认证服务 Amazon Cognito
默认 API Gateway 创建的 API 是公开的,所有人都可以访问,缺少身份认证部分。本方案会创建一个 Cognito 用户池、域名、资源服务器和应用程序客户端用来实现鉴权,即只有鉴权通过后才能访问 API。
Cognito 部分的操作可以参考下图中的示例。
然后返回到 API Gateway 页面,在 API Gateway 中创建一个 Cognito 授权方,将其配置到相应 API 资源中, Cognito 的令牌需要配置在 Authorization 标头中,如下图所示。
4 测试验证
curl -X POST -u <应用程序客户端ID>:<应用程序客户端密钥>
'https://clone-distribution.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=client_credentials' -H 'Content-Type: application/x-www-form-urlencoded'
执行完命令后会得到访问令牌,如下图所示:
这里,API 测试工具选择 Postman,打开工具后添加 header(key 为 Authorization,value 为上图中的 access_token),输入 API 链接,并加上查询字符串后发送请求。
链接示例:
https://5xx44xx6x0.execute-api.us-east-1.amazonaws.com/prod/?domain=service.yuhong.com&origin=ec2-ip.compute-1.amazonaws.com&ref_dist=E2Z4DXXXXXXXXX
如上图所示,CloudFront Distribution 复制成功,返回新创建的 CloudFront Distribution ID。
总结
本文介绍了一种通过 Serverless 服务 Amazon API Gateway 和 Amazon Lambda 以及Amazon CloudFront SDK 来复制/克隆 CloudFront Distribution 的 Restful API 实现,并通过 Cognito 在 API Gateway 访问请求中提供了安全的访问接口。该方案适用于需要将相同配置项应用于多个 Distribution 域名的情形,能实际有效地简化客户的配置管理工作量,减少人工操作出错概率,并且可以达到快速一键部署的目的。
本篇作者
马宇红
亚马逊云科技技术客户经理,负责企业级大客户的运维与架构优化、成本管理、项目交付、技术咨询等。
加入亚马逊云科技前曾供职于IBM中国软件开发中心,拥有分布式软件开发经验。目前致力于Edge、DevOps、Serverless 等方向的研究和实践。
史天
亚马逊云科技资深解决方案架构师。拥有丰富的云计算、数据分析和机器学习经验。
目前致力于数据科学、机器学习、无服务器等领域的研究和实践。译有《机器学习即服务》《基于Kubernetes的DevOps实践》《Kubernetes微服务实战》《Prometheus监控实战》《云原生时代的CoreDNS学习指南》等。
听说,点完下面4个按钮
就不会碰到bug了!