通過 Amazon API Gateway 和 Amazon Lambda 實現基於 Restful API 的 CloudFront Distribution 複製 / 克隆功能
背景
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 策略,注意需要將 <S3_bucket_name> 與 <account_id> 分別替換成 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:::<S3_bucket_name >",
"arn:aws:logs:us-east-1:<account_id>:*"
]
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1: :<account_id>: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.au...' -H 'Content-Type: application/x-www-form-urlencoded'
執行完命令後會得到訪問令牌,如下圖所示:
這裡,API 測試工具選擇 Postman,開啟工具後新增 header(key 為 Authorization,value 為上圖中的 access_token),輸入 API 連結,並加上查詢字串後傳送請求。
連結示例:https://5xx44xx6x0.execute-ap...
如上圖所示,CloudFront Distribution 複製成功,返回新建立的 CloudFront Distribution ID。
總結
本文介紹了一種通過 Serverless 服務 Amazon API Gateway 和 AWS Lambda 以及Amazon CloudFront SDK 來複制/克隆 CloudFront Distribution 的 Restful API 實現,並通過 Cognito 在 API Gateway 訪問請求中提供了安全的訪問介面。該方案適用於需要將相同配置項應用於多個 Distribution 域名的情形,能實際有效地簡化客戶的配置管理工作量,減少人工操作出錯概率,並且可以達到快速一鍵部署的目的。
本篇作者
馬宇紅
AWS技術客戶經理,負責企業級大客戶的運維與架構優化、成本管理、專案交付、技術諮詢等。加入AWS前曾供職於IBM中國軟體開發中心,擁有分散式軟體開發經驗。目前致力於Edge、DevOps、Serverless 等方向的研究和實踐。
史天
亞馬遜雲科技資深解決方案架構師。擁有豐富的雲端計算、資料分析和機器學習經驗,目前致力於資料科學、機器學習、無伺服器等領域的研究和實踐。譯有《機器學習即服務》《基於Kubernetes的DevOps實踐》《Kubernetes微服務實戰》《Prometheus監控實戰》《雲原生時代的CoreDNS學習指南》等。