在 CodePipeline 中的管道中调用 AWS Lambda 函数 - AWS CodePipeline

在 CodePipeline 中的管道中调用 AWS Lambda 函数

AWS Lambda 是一项计算服务,可使您无需调配或管理服务器即可运行代码。您可以创建 Lambda 函数并将其作为操作添加到管道中。由于 Lambda 允许您编写函数来执行几乎任何任务,因此您可以自定义管道的工作方式。

重要

请勿记录 CodePipeline 发送到 Lambda 的 JSON 事件,因为这可能会导致用户凭证被记录在 CloudWatch Logs 中。CodePipeline 角色使用 JSON 事件,将 artifactCredentials 字段中的临时凭证传递给 Lambda。有关示例事件,请参阅JSON 事件示例

以下是可在管道中使用 Lambda 函数的一些方法:

  • 使用 AWS CloudFormation 在管道的一个阶段按需创建资源,并在另一个阶段将其删除。

  • 使用将交换 CNAME 值的 Lambda 函数,在 AWS Elastic Beanstalk 中零停机时间部署应用程序版本。

  • 部署到 Amazon ECS Docker 实例。

  • 通过创建 AMI 快照在生成或部署之前备份资源。

  • 将与第三方产品的集成添加到您的管道中,例如将消息发布到 IRC 客户端。

注意

创建和运行 Lambda 函数可能会导致您的 AWS 账户产生费用。有关更多信息,请参阅定价

本主题假定您熟悉 AWS CodePipeline 和 AWS Lambda 并且知道如何创建管道、函数和 IAM 策略以及它们所依赖的角色。此主题向您演示如何:

  • 创建一个 Lambda 函数以测试网页是否已成功部署。

  • 配置 CodePipeline 和 Lambda 执行角色,以及将函数作为管道的一部分运行所需的权限。

  • 编辑管道,将 Lambda 函数作为一个操作添加到其中。

  • 通过手动发布更改来测试操作。

注意

在 CodePipeline 中使用跨区域 Lambda 调用操作时,应将使用 PutJobSuccessResultPutJobFailureResult 的 Lambda 执行的状态发送到 Lambda 函数所在的 AWS 区域,而不是 CodePipeline 所在的区域。

本主题包含一些示例函数,用于展示在 CodePipeline 中使用 Lambda 函数的灵活性:

  • Basic Lambda function

    • 创建基本 Lambda 函数以便与 CodePipeline 一起使用。

    • 在操作的详细信息链接中,将成功或失败结果返回到 CodePipeline。

  • 使用 AWS CloudFormation 模板的 Python 函数示例

    • 使用 JSON 编码的用户参数向函数 (get_user_params) 传递多个配置值。

    • 与项目存储桶 (get_template) 中的 .zip 项目交互。

    • 使用延续令牌监控长期异步过程 (continue_job_later)。这将允许操作继续进行,使函数取得成功,即使已超过十五分钟的运行时(Lambda 中的限制)也不例外。

每个示例函数都包含有关您必须添加到角色的权限的信息。有关 AWS Lambda 中的限制的更多信息,请参阅 AWS Lambda 开发者指南 中的限制

重要

本主题中包含的示例代码、角色和策略仅作为示例,并按原样提供。

步骤 1:创建管道

在此步骤中,您将创建一个管道,稍后您将向其中添加 Lambda 函数。这与您在CodePipeline 教程中创建的管道相同。如果该管道仍然配置用于您的账户,并且属于您计划创建 Lambda 函数的相同区域,则可以跳过此步骤。

创建管道
  1. 按照 教程:创建一个简单的管道(S3 存储桶) 中的前三个步骤操作,创建一个 Amazon S3 桶、CodeDeploy 资源和一个两阶段管道。为您的实例类型选择 Amazon Linux 选项。您可以对管道使用任意名称,但此主题中的步骤使用的是 MyLambdaTestPipeline。

  2. 在管道的状态页上,在 CodeDeploy 操作中选择详细信息。在部署组的部署详细信息页中,从列表中选择一个实例 ID。

  3. 在 Amazon EC2 控制台上,在实例的详细信息选项卡中,复制公有 IPv4 地址中的 IP 地址(例如,192.0.2.4)。您应将此地址用作 AWS Lambda 中的函数的目标。

注意

CodePipeline 的默认服务角色策略包含调用函数所需的 Lambda 权限。但是,如果您修改了该默认服务角色或选择了另一个角色,请确保该角色的策略允许 lambda:InvokeFunctionlambda:ListFunctions 权限。否则,包含 Lambda 操作的管道将会失败。

第 2 步:创建 Lambda 函数

在此步骤中,您将创建一个 Lambda 函数,该函数会发出 HTTP 请求,并检查网页上的一行文本。作为此步骤的一部分,您还必须创建 IAM 策略和 IAM 执行角色。有关更多信息,请参阅 AWS Lambda 开发者指南 中的权限模型

创建执行角色
  1. 登录 AWS Management Console,单击 https://console.aws.amazon.com/iam/ 打开 IAM 控制台。

  2. 选择 Policies,然后选择 Create Policy。选择 JSON 选项卡,然后将以下策略粘贴到字段中。

    { "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:*" ], "Effect": "Allow", "Resource": "arn:aws:logs:*:*:*" }, { "Action": [ "codepipeline:PutJobSuccessResult", "codepipeline:PutJobFailureResult" ], "Effect": "Allow", "Resource": "*" } ] }
  3. 选择查看策略

  4. Review policy (查看策略) 页面上,在 Name (名称) 中,键入策略的名称(例如,CodePipelineLambdaExecPolicy)。在 Description (描述) 中,输入 Enables Lambda to execute code

    选择创建策略

    注意

    这些是 Lambda 函数与 CodePipeline 和 Amazon CloudWatch 进行交互时所需的最低权限。如果要扩展此策略,以便允许使用与其他 AWS 资源交互的函数,则您应修改此策略,允许执行这些 Lambda 函数所请求的操作。

  5. 在策略控制面板页上,选择 Roles (角色),然后选择 Create role (创建角色)

  6. 创建角色页面上,选择 AWS 服务。选择 Lambda,然后选择 Next: Permissions (下一步: 权限)

  7. 附加权限策略页面上,选中 CodePipelineLambdaExecPolicy 旁边的复选框,然后选择下一步: 标签。选择 下一步: 审核

  8. Review (审核) 页面上的 Role name (角色名称) 中,输入名称,然后选择 Create role (创建角色)

创建示例 Lambda 函数以便与 CodePipeline 一起使用
  1. 登录到 AWS Management Console,然后通过以下网址打开 AWS Lambda 控制台:https://console.aws.amazon.com/lambda/

  2. Functions (LAM 函数) 页面上,选择 Create function (创建函数)

    注意

    如果您看到欢迎页面而不是 Lambda 页面,请选择立即开始使用

  3. 创建函数页面上,选择从头开始创作。在函数名称中,输入 Lambda 函数的名称(例如 MyLambdaFunctionForAWSCodePipeline)。在运行时中,选择 Node.js 20.x

  4. Role (角色) 下,选择 Choose an existing role (选择现有角色)。在 Existing role (现有角色) 中,选择角色,然后选择 Create function (创建函数)

    此时将打开供您创建函数的详细信息页面。

  5. Function code (函数代码) 框中,复制贴以下代码:

    注意

    CodePipeline.job 密钥下的事件对象包含 job details。有关 CodePipeline 返回到 Lambda 的 JSON 事件的完整示例,请参阅 JSON 事件示例

    import { CodePipelineClient, PutJobSuccessResultCommand, PutJobFailureResultCommand } from "@aws-sdk/client-codepipeline"; import http from 'http'; import assert from 'assert'; export const handler = (event, context) => { const codepipeline = new CodePipelineClient(); // Retrieve the Job ID from the Lambda action const jobId = event["CodePipeline.job"].id; // Retrieve the value of UserParameters from the Lambda action configuration in CodePipeline, in this case a URL which will be // health checked by this function. const url = event["CodePipeline.job"].data.actionConfiguration.configuration.UserParameters; // Notify CodePipeline of a successful job const putJobSuccess = async function(message) { const command = new PutJobSuccessResultCommand({ jobId: jobId }); try { await codepipeline.send(command); context.succeed(message); } catch (err) { context.fail(err); } }; // Notify CodePipeline of a failed job const putJobFailure = async function(message) { const command = new PutJobFailureResultCommand({ jobId: jobId, failureDetails: { message: JSON.stringify(message), type: 'JobFailed', externalExecutionId: context.awsRequestId } }); await codepipeline.send(command); context.fail(message); }; // Validate the URL passed in UserParameters if(!url || url.indexOf('http://') === -1) { putJobFailure('The UserParameters field must contain a valid URL address to test, including http:// or https://'); return; } // Helper function to make a HTTP GET request to the page. // The helper will test the response and succeed or fail the job accordingly const getPage = function(url, callback) { var pageObject = { body: '', statusCode: 0, contains: function(search) { return this.body.indexOf(search) > -1; } }; http.get(url, function(response) { pageObject.body = ''; pageObject.statusCode = response.statusCode; response.on('data', function (chunk) { pageObject.body += chunk; }); response.on('end', function () { callback(pageObject); }); response.resume(); }).on('error', function(error) { // Fail the job if our request failed putJobFailure(error); }); }; getPage(url, function(returnedPage) { try { // Check if the HTTP response has a 200 status assert(returnedPage.statusCode === 200); // Check if the page contains the text "Congratulations" // You can change this to check for different text, or add other tests as required assert(returnedPage.contains('Congratulations')); // Succeed the job putJobSuccess("Tests passed."); } catch (ex) { // If any of the assertions failed then fail the job putJobFailure(ex); } }); };
  6. 保留 Handler (处理程序)Role (角色) 的默认值 CodePipelineLambdaExecRole

  7. Basic settings (基本设置) 中,对于 Timeout (超时),输入 20 秒。

  8. 选择保存

步骤 3:在 CodePipeline 控制台中向管道添加 Lambda 函数

在此步骤中,您将在管道中添加一个新阶段,然后向该阶段添加调用您的函数的 Lambda 操作。

添加阶段
  1. 通过以下网址登录AWS Management Console并打开 CodePipeline 控制台:http://console.aws.amazon.com/codesuite/codepipeline/home

  2. Welcome (欢迎使用) 页面上,选择您创建的管道。

  3. 在管道视图页中,选择 Edit

  4. 编辑页面上,选择 + 添加阶段,以便在 CodeDeploy 部署阶段之后添加一个阶段。输入阶段的名称(例如,LambdaStage),然后选择 Add stage (添加阶段)

    注意

    您还可以选择将您的 Lambda 操作添加到现有阶段。出于演示目的,我们将 Lambda 函数作为阶段中的唯一操作进行添加,以便您可以轻松地在构件通过管道时查看该操作的进度。

  5. 选择 + 添加操作组。在编辑操作操作名称中,键入 Lambda 操作的名称(例如,MyLambdaAction)。在 Provider (提供方) 中,选择 AWS Lambda。在函数名称中,选择或输入 Lambda 函数的名称(例如,MyLambdaFunctionForAWSCodePipeline)。在用户参数中,指定前面复制的 Amazon EC2 实例的 IP 地址(例如,http://192.0.2.4),然后选择完成

    注意

    本主题使用 IP 地址,但在真实场景中,您可以提供您注册的网站名称(例如,http://www.example.com)。有关 AWS Lambda 中事件数据和处理程序的更多信息,请参阅 AWS Lambda 开发者指南 中的编程模型

  6. Edit action (编辑操作) 页面上,选择 Save (保存)

步骤 4:使用 Lambda 函数测试管道

要测试函数,请通过管道发布最近的更改。

使用控制台,通过管道运行最新版本的项目
  1. 在管道详细信息页中,选择发布更改。这会通过管道运行在源操作中指定的每个源位置中提供的最新修订。

  2. Lambda 操作完成后,选择详细信息链接,以查看 Amazon CloudWatch 中的函数日志流,包括事件的计费持续时间。如果函数失败,CloudWatch 日志将提供有关原因的信息。

步骤 5:后续步骤

既然已成功创建 Lambda 函数并将其作为操作添加到管道中,接下来您便可以尝试以下操作:

  • 向阶段中添加更多 Lambda 操作以检查其他网页。

  • 修改 Lambda 函数以检查其他文本字符串。

  • 探究 Lambda 函数并创建您自己的 Lambda 函数,然后将其添加到管道中。

在管道中运行的 AWS Lambda 操作。

试验完 Lambda 函数后,请考虑将其从管道中移除,并从 AWS Lambda 中删除,然后从 IAM 中删除角色,以避免可能产生的费用。有关更多信息,请参阅在 CodePipeline 中编辑管道在 CodePipeline 中删除管道删除角色或实例配置文件

JSON 事件示例

以下示例显示了 CodePipeline 发送给 Lambda 的示例 JSON 事件。此事件的结构类似于对 GetJobDetails API 的响应,但没有 actionTypeIdpipelineContext 数据类型。JSON 事件和对 GetJobDetails API 的响应中都包含两个操作配置详细信息 FunctionNameUserParameters红色斜体文字 中的值是示例或解释,而不是实际值。

{ "CodePipeline.job": { "id": "11111111-abcd-1111-abcd-111111abcdef", "accountId": "111111111111", "data": { "actionConfiguration": { "configuration": { "FunctionName": "MyLambdaFunctionForAWSCodePipeline", "UserParameters": "some-input-such-as-a-URL" } }, "inputArtifacts": [ { "location": { "s3Location": { "bucketName": "the name of the bucket configured as the pipeline artifact store in Amazon S3, for example codepipeline-us-east-2-1234567890", "objectKey": "the name of the application, for example CodePipelineDemoApplication.zip" }, "type": "S3" }, "revision": null, "name": "ArtifactName" } ], "outputArtifacts": [], "artifactCredentials": { "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "sessionToken": "MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w 0BAQUFADCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZ WF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIw EAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5 jb20wHhcNMTEwNDI1MjA0NTIxWhcNMTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBh MCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBb WF6b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMx HzAdBgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wgZ8wDQYJKoZIhvcNAQE BBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ21uUSfwfEvySWtC2XADZ4nB+BLYgVI k60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9TrDHudUZg3qX4waLG5M43q7Wgc/MbQ ITxOUSQv7c7ugFFDzQGBzZswY6786m86gpEIbb3OhjZnzcvQAaRHhdlQWIMm2nr AgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4nUhVVxYUntneD9+h8Mg9q6q+auN KyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0FkbFFBjvSfpJIlJ00zbhNYS5f6Guo EDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTbNYiytVbZPQUQ5Yaxu2jXnimvw 3rrszlaEXAMPLE=", "accessKeyId": "AKIAIOSFODNN7EXAMPLE" }, "continuationToken": "A continuation token if continuing job", "encryptionKey": { "id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", "type": "KMS" } } } }

其他示例函数

以下示例 Lambda 函数演示了您可以在 CodePipeline 中用于您的管道的其他功能。要使用这些函数,您可能必须修改 Lambda 执行角色的策略,如每个示例的介绍中所述。

使用 AWS CloudFormation 模板的 Python 函数示例

以下示例显示了基于提供的 AWS CloudFormation 模板创建或更新堆栈的函数。此模板将创建一个 Amazon S3 桶。它只用于演示用途,以将成本降至最低。理想情况下,您应该在向存储桶上传任何内容之前删除堆栈。如果您将文件上传到存储桶,那么在删除堆栈时将无法删除存储桶。您必须手动删除存储桶中的所有内容,然后才能删除存储桶本身。

该 Python 示例假设您有一个使用 Amazon S3 桶作为源操作的管道,或者您有权访问可与管道一起使用的版本控制型 Amazon S3 桶。您创建 AWS CloudFormation 模板,压缩它,并将其作为 .zip 文件上传到该存储桶。然后,您必须向管道中添加一个源操作,以从存储桶中检索此 .zip 文件。

注意

当 Amazon S3 是您的管道的源提供程序时,您可以将一个或多个源文件压缩到单个 .zip 文件中,然后将 .zip 文件上传到源桶。您也可以上传单个解压缩的文件;但是,需要 .zip 文件的下游操作将失败。

此示例演示:

  • 使用 JSON 编码的用户参数向函数 (get_user_params) 传递多个配置值。

  • 与项目存储桶 (get_template) 中的 .zip 项目交互。

  • 使用延续令牌监控长期异步过程 (continue_job_later)。这将允许操作继续进行,使函数取得成功,即使已超过十五分钟的运行时(Lambda 中的限制)也不例外。

要使用此示例 Lambda 函数,Lambda 执行角色的策略在 AWS CloudFormation、Amazon S3 和 CodePipeline 中必须具有 Allow 权限,如此示例策略中所示:

{ "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:*" ], "Effect": "Allow", "Resource": "arn:aws:logs:*:*:*" }, { "Action": [ "codepipeline:PutJobSuccessResult", "codepipeline:PutJobFailureResult" ], "Effect": "Allow", "Resource": "*" }, { "Action": [ "cloudformation:DescribeStacks", "cloudformation:CreateStack", "cloudformation:UpdateStack" ], "Effect": "Allow", "Resource": "*" }, { "Action": [ "s3:*" ], "Effect": "Allow", "Resource": "*" } ] }

要创建 AWS CloudFormation 模板,请打开任何纯文本编辑器,然后复制并粘贴以下代码:

{ "AWSTemplateFormatVersion" : "2010-09-09", "Description" : "CloudFormation template which creates an S3 bucket", "Resources" : { "MySampleBucket" : { "Type" : "AWS::S3::Bucket", "Properties" : { } } }, "Outputs" : { "BucketName" : { "Value" : { "Ref" : "MySampleBucket" }, "Description" : "The name of the S3 bucket" } } }

使用名称 template.json 将此内容保存为名为 template-package 目录中的一个 JSON 文件。创建此目录和文件的压缩 (.zip) 文件并命名为 template-package.zip,然后将压缩文件上传到受版本控制的 Amazon S3 桶。如果您已经为管道配置了存储桶,则可以使用该存储桶。接下来,编辑您的管道以添加检索 .zip 文件的源操作。将此操作的输出命名为 MyTemplate。有关更多信息,请参阅 在 CodePipeline 中编辑管道

注意

示例 Lambda 函数期望使用这些文件名和压缩结构。但是,您可以使用您自己的 AWS CloudFormation 模板替换此示例。如果您使用自己的模板,请确保修改 Lambda 执行角色的策略,以允许使用您的 AWS CloudFormation 模板所需的任何额外功能。

添加以下代码以作为 Lambda 中的函数
  1. 打开 Lambda 控制台,选择创建函数

  2. 创建函数页面上,选择从头开始创作。在函数名称中,为您的 Lambda 函数输入一个名称。

  3. Runtime (运行时) 中,选择 Python 2.7

  4. 选择或创建执行角色下,选择使用现有角色。在 Existing role (现有角色) 中,选择角色,然后选择 Create function (创建函数)

    此时将打开供您创建函数的详细信息页面。

  5. Function code (函数代码) 框中,复制贴以下代码:

    from __future__ import print_function from boto3.session import Session import json import urllib import boto3 import zipfile import tempfile import botocore import traceback print('Loading function') cf = boto3.client('cloudformation') code_pipeline = boto3.client('codepipeline') def find_artifact(artifacts, name): """Finds the artifact 'name' among the 'artifacts' Args: artifacts: The list of artifacts available to the function name: The artifact we wish to use Returns: The artifact dictionary found Raises: Exception: If no matching artifact is found """ for artifact in artifacts: if artifact['name'] == name: return artifact raise Exception('Input artifact named "{0}" not found in event'.format(name)) def get_template(s3, artifact, file_in_zip): """Gets the template artifact Downloads the artifact from the S3 artifact store to a temporary file then extracts the zip and returns the file containing the CloudFormation template. Args: artifact: The artifact to download file_in_zip: The path to the file within the zip containing the template Returns: The CloudFormation template as a string Raises: Exception: Any exception thrown while downloading the artifact or unzipping it """ tmp_file = tempfile.NamedTemporaryFile() bucket = artifact['location']['s3Location']['bucketName'] key = artifact['location']['s3Location']['objectKey'] with tempfile.NamedTemporaryFile() as tmp_file: s3.download_file(bucket, key, tmp_file.name) with zipfile.ZipFile(tmp_file.name, 'r') as zip: return zip.read(file_in_zip) def update_stack(stack, template): """Start a CloudFormation stack update Args: stack: The stack to update template: The template to apply Returns: True if an update was started, false if there were no changes to the template since the last update. Raises: Exception: Any exception besides "No updates are to be performed." """ try: cf.update_stack(StackName=stack, TemplateBody=template) return True except botocore.exceptions.ClientError as e: if e.response['Error']['Message'] == 'No updates are to be performed.': return False else: raise Exception('Error updating CloudFormation stack "{0}"'.format(stack), e) def stack_exists(stack): """Check if a stack exists or not Args: stack: The stack to check Returns: True or False depending on whether the stack exists Raises: Any exceptions raised .describe_stacks() besides that the stack doesn't exist. """ try: cf.describe_stacks(StackName=stack) return True except botocore.exceptions.ClientError as e: if "does not exist" in e.response['Error']['Message']: return False else: raise e def create_stack(stack, template): """Starts a new CloudFormation stack creation Args: stack: The stack to be created template: The template for the stack to be created with Throws: Exception: Any exception thrown by .create_stack() """ cf.create_stack(StackName=stack, TemplateBody=template) def get_stack_status(stack): """Get the status of an existing CloudFormation stack Args: stack: The name of the stack to check Returns: The CloudFormation status string of the stack such as CREATE_COMPLETE Raises: Exception: Any exception thrown by .describe_stacks() """ stack_description = cf.describe_stacks(StackName=stack) return stack_description['Stacks'][0]['StackStatus'] def put_job_success(job, message): """Notify CodePipeline of a successful job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_success_result() """ print('Putting job success') print(message) code_pipeline.put_job_success_result(jobId=job) def put_job_failure(job, message): """Notify CodePipeline of a failed job Args: job: The CodePipeline job ID message: A message to be logged relating to the job status Raises: Exception: Any exception thrown by .put_job_failure_result() """ print('Putting job failure') print(message) code_pipeline.put_job_failure_result(jobId=job, failureDetails={'message': message, 'type': 'JobFailed'}) def continue_job_later(job, message): """Notify CodePipeline of a continuing job This will cause CodePipeline to invoke the function again with the supplied continuation token. Args: job: The JobID message: A message to be logged relating to the job status continuation_token: The continuation token Raises: Exception: Any exception thrown by .put_job_success_result() """ # Use the continuation token to keep track of any job execution state # This data will be available when a new job is scheduled to continue the current execution continuation_token = json.dumps({'previous_job_id': job}) print('Putting job continuation') print(message) code_pipeline.put_job_success_result(jobId=job, continuationToken=continuation_token) def start_update_or_create(job_id, stack, template): """Starts the stack update or create process If the stack exists then update, otherwise create. Args: job_id: The ID of the CodePipeline job stack: The stack to create or update template: The template to create/update the stack with """ if stack_exists(stack): status = get_stack_status(stack) if status not in ['CREATE_COMPLETE', 'ROLLBACK_COMPLETE', 'UPDATE_COMPLETE']: # If the CloudFormation stack is not in a state where # it can be updated again then fail the job right away. put_job_failure(job_id, 'Stack cannot be updated when status is: ' + status) return were_updates = update_stack(stack, template) if were_updates: # If there were updates then continue the job so it can monitor # the progress of the update. continue_job_later(job_id, 'Stack update started') else: # If there were no updates then succeed the job immediately put_job_success(job_id, 'There were no stack updates') else: # If the stack doesn't already exist then create it instead # of updating it. create_stack(stack, template) # Continue the job so the pipeline will wait for the CloudFormation # stack to be created. continue_job_later(job_id, 'Stack create started') def check_stack_update_status(job_id, stack): """Monitor an already-running CloudFormation update/create Succeeds, fails or continues the job depending on the stack status. Args: job_id: The CodePipeline job ID stack: The stack to monitor """ status = get_stack_status(stack) if status in ['UPDATE_COMPLETE', 'CREATE_COMPLETE']: # If the update/create finished successfully then # succeed the job and don't continue. put_job_success(job_id, 'Stack update complete') elif status in ['UPDATE_IN_PROGRESS', 'UPDATE_ROLLBACK_IN_PROGRESS', 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', 'CREATE_IN_PROGRESS', 'ROLLBACK_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS']: # If the job isn't finished yet then continue it continue_job_later(job_id, 'Stack update still in progress') else: # If the Stack is a state which isn't "in progress" or "complete" # then the stack update/create has failed so end the job with # a failed result. put_job_failure(job_id, 'Update failed: ' + status) def get_user_params(job_data): """Decodes the JSON user parameters and validates the required properties. Args: job_data: The job data structure containing the UserParameters string which should be a valid JSON structure Returns: The JSON parameters decoded as a dictionary. Raises: Exception: The JSON can't be decoded or a property is missing. """ try: # Get the user parameters which contain the stack, artifact and file settings user_parameters = job_data['actionConfiguration']['configuration']['UserParameters'] decoded_parameters = json.loads(user_parameters) except Exception as e: # We're expecting the user parameters to be encoded as JSON # so we can pass multiple values. If the JSON can't be decoded # then fail the job with a helpful message. raise Exception('UserParameters could not be decoded as JSON') if 'stack' not in decoded_parameters: # Validate that the stack is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the stack name') if 'artifact' not in decoded_parameters: # Validate that the artifact name is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the artifact name') if 'file' not in decoded_parameters: # Validate that the template file is provided, otherwise fail the job # with a helpful message. raise Exception('Your UserParameters JSON must include the template file name') return decoded_parameters def setup_s3_client(job_data): """Creates an S3 client Uses the credentials passed in the event by CodePipeline. These credentials can be used to access the artifact bucket. Args: job_data: The job data structure Returns: An S3 client with the appropriate credentials """ key_id = job_data['artifactCredentials']['accessKeyId'] key_secret = job_data['artifactCredentials']['secretAccessKey'] session_token = job_data['artifactCredentials']['sessionToken'] session = Session(aws_access_key_id=key_id, aws_secret_access_key=key_secret, aws_session_token=session_token) return session.client('s3', config=botocore.client.Config(signature_version='s3v4')) def lambda_handler(event, context): """The Lambda function handler If a continuing job then checks the CloudFormation stack status and updates the job accordingly. If a new job then kick of an update or creation of the target CloudFormation stack. Args: event: The event passed by Lambda context: The context passed by Lambda """ try: # Extract the Job ID job_id = event['CodePipeline.job']['id'] # Extract the Job Data job_data = event['CodePipeline.job']['data'] # Extract the params params = get_user_params(job_data) # Get the list of artifacts passed to the function artifacts = job_data['inputArtifacts'] stack = params['stack'] artifact = params['artifact'] template_file = params['file'] if 'continuationToken' in job_data: # If we're continuing then the create/update has already been triggered # we just need to check if it has finished. check_stack_update_status(job_id, stack) else: # Get the artifact details artifact_data = find_artifact(artifacts, artifact) # Get S3 client to access artifact with s3 = setup_s3_client(job_data) # Get the JSON template file out of the artifact template = get_template(s3, artifact_data, template_file) # Kick off a stack update or create start_update_or_create(job_id, stack, template) except Exception as e: # If any other exceptions which we didn't expect are raised # then fail the job and log the exception message. print('Function failed due to exception.') print(e) traceback.print_exc() put_job_failure(job_id, 'Function exception: ' + str(e)) print('Function complete.') return "Complete."
  6. 处理程序保留为默认值,将角色保留为您之前选择或创建的名称 CodePipelineLambdaExecRole

  7. Basic settings (基本设置) 中,对于 Timeout (超时),将默认值 3 秒替换为 20 秒。

  8. 选择保存

  9. 在 CodePipeline 控制台中,编辑管道以添加函数,将其作为管道中一个阶段的操作。对要更改的管道选择编辑,然后选择添加操作组。在编辑操作页面的操作名称中,为您的操作输入一个名称。在操作提供程序中,选择 Lambda

    输入构件中,选择 MyTemplate。在 UserParameters 中,您必须提供具有三个参数的 JSON 字符串:

    • 堆栈名称

    • AWS CloudFormation 模板名称和文件的路径

    • 输入构件

    使用大括号 ({}),并使用逗号分隔各个参数。例如,要创建名为 MyTestStack 的堆栈,对于输入构件为 MyTemplate 的管道,在 UserParameters 中,输入:{"stack":"MyTestStack","file":"template-package/template.json","artifact":"MyTemplate"}。

    注意

    即使您在 UserParameters 中指定了输入项目,您也必须在 Input artifacts 中为操作指定此输入项目。

  10. 保存您对管道所做的更改,然后手动发布更改,以便测试操作和 Lambda 函数。