亚马逊AWS官方博客

自动创建和更新 CloudFront 中的 Lambda@Edge

应用场景

在配置CloudFront的Distribution的时候,往往一个distribution下可能会有多个behavior。每个behavior下可能会用Lambda@Edge来实现修改请求或者边缘计算等功能。在整个Lambda@Edge的部署过程中,需要修改晚Lambda的代码后,在将代码推送到边缘节点,并且需要在CloudFront的每一个behavior的配置下面修改Lambda@Edge的version ARN。为了避免手动误操作的情况,本文主要讨论一个自动化的部署方案,在完成CI阶段后,仅需把程序包放置在对应的存储桶位置时,就可以实现CloudFront的Lambda@Edge的自动化部署方案

技术背景

本文主要应用到了以下AWS的服务及功能

1.S3 Bucket,并启用了版本控制:用于存储Lambda@Edge的代码

2.S3 存储桶触发Lambda:集成服务,实现上传Object后能够自动触发CloudFormation执行。并控制参数传入

(参考文档: https://docs.amazonaws.cn/lambda/latest/dg/with-s3-example.html

3.CloudFormation:创建并负责更新CloudFront, Lambda@Edge, Lambda Version 等组件

事例代码

CloudFormation:

AWSTemplateFormatVersion: 2010-09-09
Parameters:
  FirstLaunch:
    Type: String
    Default: False
    AllowedValues:
      - True
      - False
    ConstraintDescription: True for Fist Launch the template, False for Update.
  LambdaVersion:
    Type: String
    Default: NONE
Conditions:
  FirstLaunch: !Equals [ !Ref FirstLaunch, True ]
Resources:
  edgelambdarole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: edgelambda
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action:
              - 'sts:AssumeRole'
            Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
                - edgelambda.amazonaws.com
  testdistribuationupdate:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          ViewerProtocolPolicy: allow-all
          ForwardedValues:
            QueryString: 'false'
          TargetOriginId: MyOrigin
          LambdaFunctionAssociations:
            - EventType: origin-request
              LambdaFunctionARN: !If [FirstLaunch, !Ref edgelambdaVersion, !Ref LambdaVersion]
        Enabled: true
        Origins:
          - CustomOriginConfig:
              OriginProtocolPolicy: match-viewer
              HTTPPort: 80
            Id: MyOrigin
            DomainName: aws.amazon.com
  edgelambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        S3Bucket: test-cf-lambda-code
        S3Key: index.js.zip
      FunctionName: !Join
        - '-'
        - - testlambda
      Handler: index.handler
      Role: !GetAtt
        - edgelambdarole
        - Arn
      Timeout: 5
      Runtime: nodejs12.x
  edgelambdaVersion:
    Type: 'AWS::Lambda::Version'
    Properties:
      FunctionName: !Ref edgelambda

 

Lambda:

部署函数之前,需要设置Template URL 为CloudFormation 模版存储的s3 地址

import boto3

def update_lambda(functionname, S3Bucket, S3Key, S3ObjectVersion):
    client_lambda = boto3.client('lambda')
    response_lambda = client_lambda.update_function_code(
        FunctionName=functionname,
        S3Bucket=S3Bucket,
        S3Key=S3Key,
        S3ObjectVersion=S3ObjectVersion,
        Publish=True
    )
    lambdaversion = response_lambda['FunctionArn']
    return lambdaversion

def lambda_handler(event, context):
    # Get S3 Object Meta
    S3Bucket = event['Records'][0]['s3']['bucket']['name']
    S3Key = event['Records'][0]['s3']['object']['key']
    S3ObjectVersion = event['Records'][0]['s3']['object']['versionId']
    # The S3 Object key must start up with lambda@edge name
    functionname = S3Key.split('.')[0]
    lambdaversion = update_lambda(functionname, S3Bucket, S3Key, S3ObjectVersion)
    client = boto3.client('cloudformation')
    response_cloudformation = client.update_stack(
        StackName='test',
        TemplateURL='change to your cloudformation template located url',
        Parameters=[
            {
                'ParameterKey': 'FirstLaunch',
                'ParameterValue': 'false'
            },
            {
                'ParameterKey': 'LambdaVersion',
                'ParameterValue': lambdaversion
            }
        ],
        Capabilities=['CAPABILITY_NAMED_IAM']
)

 

Lambda@Edge json code:

exports.handler = (event, context, callback) => {
    const response = {
        status: '200',
        statusDescription: 'OK',
        body: "test",
    };
    callback(null, response);
};

工作原理

资源准备

1.创建S3 存储桶作为Lambda@Edge的代码artifacts。

2.创建S3 存储桶,作为CloudFormation 存储模版的仓库,并将模版上传至存储桶

3.创建deployed lambda执行时所需的IAM Role(具备AWSLambdaBasicExecutionRole 和 自定义策略如下)

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:GetRole",
                "lambda:ListFunctions",
                "lambda:InvokeFunction",
                "lambda:GetFunction",
                "cloudfront:CreateDistribution",
                "lambda:EnableReplication",
                "lambda:UpdateFunctionCode",
                "cloudfront:GetDistribution",
                "s3:GetObject",
                "cloudformation:UpdateStack",
                "lambda:PublishVersion",
                "cloudfront:UpdateDistribution",
                "s3:GetObjectVersion"
            ],
            "Resource": "*"
        }
    ]
}

 

示例场景及代码

场景一: 首次部署CloudFront和Lambda@Edge

 CloudFormation Template解释:

在cloudfront_lambda.yaml,定义了一个参数 FirstLaunch, 并在后面用Conditions 设置一个判断条件。

Parameters:
  FirstLaunch:
    Type: String
    Default: False
    AllowedValues:
      - True
      - False
    ConstraintDescription: True for Fist Launch the template, False for Update.
  LambdaVersion:
    Type: String
    Default: NONE
Conditions:
  FirstLaunch: !Equals [ !Ref FirstLaunch, True ]

在创建distribution的resource中,关联Lambda@Edge部分的时候会使用这个判断条件,如果FirstLaunch 为True的时候则这里的参数引用本模版中创建的Lambda Version,如果为False,则引用从Template外传入的参数。

testdistribuationupdate:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          ViewerProtocolPolicy: allow-all
          ForwardedValues:
            QueryString: 'false'
          TargetOriginId: MyOrigin
          LambdaFunctionAssociations:
            - EventType: origin-request
              LambdaFunctionARN: !If [FirstLaunch, !Ref edgelambdaVersion, !Ref LambdaVersion]

 

创建资源

参考官方文档,创建堆栈

https://docs.aws.amazon.com/zh_cn/AWSCloudFormation/latest/UserGuide/cfn-using-console.html

注意:在首次执行的时候,FirstLaunch 请选择true。

验证:

1.验证堆栈

在CloudFormation创建完毕后,以下资源被创建

2.登入CloudFront界面查看Distribution的Behavior

3.访问CloudFront的页面,并与Lambda@Edge功能核对

 

场景二: 更新CloudFront上的Lambda@Edge

在运行完首次创建后,在后期正常运维的情况下,可以直接将代码更新到S3 的Artifacts中,后续部分自动由deploy_lambda.py来执行。

deploy_lambda.py代码解释:

update_lambda() 负责根据event中的捕获的信息,完成对应的Lambda@Edge的更新,并生成新的版本。并作为更新CloudFormation的输入参数进行后续操作

def lambda_handler(event, context):
    # Get S3 Object Meta
    S3Bucket = event['Records'][0]['s3']['bucket']['name']
    S3Key = event['Records'][0]['s3']['object']['key']
    S3ObjectVersion = event['Records'][0]['s3']['object']['versionId']
    # The S3 Object key must start up with lambda@edge name
    functionname = S3Key.split('.')[0]
    lambdaversion = update_lambda(functionname, S3Bucket, S3Key, S3ObjectVersion)
    client = boto3.client('cloudformation')
    response_cloudformation = client.update_stack(
        StackName='test',
        TemplateURL='your template url',
        Parameters=[
            {
                'ParameterKey': 'FirstLaunch',
                'ParameterValue': 'false'
            },
            {
                'ParameterKey': 'LambdaVersion',
                'ParameterValue': lambdaversion
            }
        ],
        Capabilities=['CAPABILITY_NAMED_IAM']
)

注意:

  • py 的权限请参照文章资源准备中的权限部分
  • 您上传的Lambda@Edge的zip包的名字需要跟Lambda@Edge函数的名字保持一致。
  • StackName 要与CloudFormation的Stack名称一致,如果考虑参数联动,可以后期将py与API Gateway继承,进行参数传递

验证:

当您具备以上的前置条件和注意事项后

1.上传代码到S3 的artifacts中,并查看版本信息

2.检查CloudFormation是否处于更新状态,并等待更新完成(注意:因为更新CloudFront资源本身涉及到更新edge端的配置,因此可能耗时较长)

3.检查CloudFront状态,直至状态变更为“已部署”

4.检查Lambda@Edge效果(由于可能存在缓存,请清理缓存后验证)

 

总结

  • 如果在管理多个Lambda@Edge场景中,可以将CloudFormation模版修改为Nested Stack方式执行。
  • 在后续运维过程中,用户只需要将Lambda@Edge的code上传至S3即可,CD部分自动化实施
  • CI部分可以集成Code系列工具,只需将代码成功上传至S3即可

 

本篇作者

田原

AWS 解决方案架构师,专注于DevOps, 加入AWS之前一直从事运维相关的工作。在自动化运维方面有丰富的经验