AWS与Lambda和Golang的自定义资源
#aws #typescript #go #cdk

动机

CDK是AWS的绝佳框架,使您可以将云基础架构定义为代码(IAC)。您可以使用自己喜欢的编程语言,例如打字稿,Python,Java,Golang来定义您的资源。此功能特别方便,因为它以可读性和更易于管理的方式自动化云形式模板的生成。

但是,并非每个AWS资源都可以直接映射到使用CDK的云形式模板。在我的特殊情况下,我必须从CDK内创建 Secure SSM参数。通常,这就是您在云形式中创建SSM参数的方式:

Resources:
  MySSMParameter:
    Type: "AWS::SSM::Parameter"
    Properties:
      Type: "String"     
      Name: "/my/ssm/parameter"
      Value: "myValue"
      Description: "Description"
用于创建SSM参数的示例云形式模板

in¶您指定了SSM参数的类型:

  • 标准String
    • 简单的键值字符串对
    • 不是支持版本
  • 高级StringList
    • 与其他元数据的钥匙值对
    • 确实支持版本
  • 安全字符串SecureString
    • 类似于标准参数,但使用AWS KMS
    • 在REST上加密数据
    • 这用于存储敏感数据,例如密码,API键和其他凭据

根据堆栈操作,CloudFormation将您的功能发送一个创建,更新或删除事件。因为每个事件的处理方式都不同,所以请确保在收到三种事件类型中的任何一个时都没有意外行为。
-Source

自定义资源可以在AWS CloudFormation stack中使用到 create update delete 某些无法作为本机CFN的资源(云形式)资源。这可能是需要以某种方式生成的SSL证书,自定义DNS记录或AWS外部的任何内容。 Lambda功能将负责该特定资源的生命周期管理。

image

工作流序列

在CDK中,您将创建您的自定义资源,该自定义资源具有所谓的provider附加(在我们的情况下,它是Lambda function),旨在在创建,更新或删除资源时实现逻辑。在cdk synth之后,创建了CDK堆栈的新云形式模板。每当创建/更新/删除资源时,都会发生新的云形式事件。此事件将发送到lambda函数,该功能最终将根据事件的属性创建/update/delete ssm参数。

这使您有足够的灵活性来定义某些事件时应该发生什么。让我们深入研究细节。

当然,您可以立即跳到GitHub存储库:

GitHub logo dorneanu / aws-custom-resource-golang

AWS lambda烤制AWS golang的自定义资源POC

AWS custom resources using Golang, Lambda and TypeScript.

Motivation

This is ready-to-deploy proof of concept leveraging Golang to efficiently handle the lifecycle management of so called AWS custom resources。此存储库例证了如何使用这些存储库来创建类型的SSM parameters CDK中的SecureString。当然,您可以将此存储库用作更高级自定义资源的模板。

ð您可以在AWS Custom resources with Lambda and Golang上的博客上阅读更多信息。

部署

确保首先安装所有依赖项:

  • go二进制
  • npm

然后克隆此存储库:

 $ git克隆https://github.com/dorneanu/aws-custom-source-golang 

然后安装所有npm依赖项:

 $ npx 

AWS Lambda

基本模板

如前所述,自定义资源应通过AWS lambda函数为 baked 。这就是您写基本功能结构的方式:

package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/aws/aws-lambda-go/cfn"
)
// Global AWS session variable
var awsSession aws.Config  // ❶

// init will setup the AWS session
func init() {              // ❷
    cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("eu-central-1"))
    if err != nil {
        log.Fatalf("unable to load SDK config, %v", err)
    }
    awsSession = cfg
}

// lambdaHandler handles incoming CloudFormation events
// and is of type cfn.CustomResourceFunction
func lambdaHandler(ctx context.Context, event cfn.Event) (string, map[string]interface{}, error) {
    var physicalResourceID string
    responseData := map[string]interface{}{}

    switch event.ResourceType {    // ❹
    case "AWS::CloudFormation:CustomResource":
        customResourceHandler := NewSSMCustomResourceHandler(awsSession)
        return customResourceHandler.HandleEvent(ctx,event)
    default:
        return "",nil, fmt.Errorf("Unknown resource type: %s", event.ResourceType)
    }
    return physicalResourceID, nil, nil
}

// main function
func main() {
    // From : https://github.com/aws/aws-lambda-go/blob/main/cfn/wrap.go
    //
    // LambdaWrap returns a CustomResourceLambdaFunction which is something lambda.Start()
    // will understand. The purpose of doing this is so that Response Handling boiler
    // plate is taken away from the customer and it makes writing a Custom Resource
    // simpler.
    //
    //  func myLambda(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) {
    //      physicalResourceID = "arn:...."
    //      return
    //  }
    //
    //  func main() {
    //      lambda.Start(cfn.LambdaWrap(myLambda))
    //  }
    lambda.Start(cfn.LambdaWrap(lambdaHandler))  // ➌
}
用于创建SSM参数的示例云形式模板

一些解释:

  • main功能将调用lambda处理程序
  • 在执行main之前,init函数将首先执行。
    • 它将尝试连接到AWS并填充定义的全局变量。
  • lambdaHandler中,我们还必须确保检查正确的CFN自定义资源类型

自定义资源处理程序

// handleSSMCustomResource decides what to do in case of CloudFormation event
func (s SSMCustomResourceHandler) HandleSSMCustomResource(ctx context.Context, event cfn.Event) (string, map[string]interface{}, error) {

    switch event.RequestType {   //  ❶
    case cfn.RequestCreate:
        return s.Create(ctx, event)
    case cfn.RequestUpdate:
        return s.Update(ctx, event)
    case cfn.RequestDelete:
        return s.Delete(ctx, event)
    default:
        return "", nil, fmt.Errorf("Unknown request type: %s", event.RequestType)
    }
}
lambda函数的主处理程序

假设我们使用一种称为SSMCustomResourceHandler的自定义类型,我们可以拥有一个主入口点(在此示例中,称为HandleSSMCustomResource),在其中我们根据事件请求类型“¶。

”调用其他方法。

每种方法将应用触发不同的操作。每当新的自定义资源创建:
时,这就是发生的事情

// Create creates a new SSM parameter
func (s SSMCustomResourceHandler) Create(ctx context.Context, event cfn.Event) (string, map[string]interface{}, error) {
    var physicalResourceID string

    // Get custom resource parameter from event
    ssmPath, err := strProperty(event, "key")    // ❶
    if err != nil {
        return physicalResourceID, nil, fmt.Errorf("Couldn't extract credential's key: %s", err)
    }
    physicalResourceID = ssmPath                 // ❷

    ssmValue, err := strProperty(event, "value") // ❶
    if err != nil {
        return physicalResourceID, nil, fmt.Errorf("Couldn't extract credential's value: %s", err)
    }

    // Put new parameter                            ➌
    _, err = s.ssmClient.PutParameter(context.Background(), &ssm.PutParameterInput{
        Name:      aws.String(ssmPath),
        Value:     aws.String(ssmValue),
        Type:      types.ParameterTypeSecureString,
        Overwrite: aws.Bool(true),
    })
    log.Printf("Put parameter into SSM: %s", physicalResourceID)

    if err != nil {
        return physicalResourceID, nil, fmt.Errorf("Couldn't put parameter (%s): %s\n", ssmPath, err)
    }
    return physicalResourceID, nil, nil
}
创建SSMCUSTOMRESOURCEHANDLER的方法

Create应根据云形式事件中包含的信息创建一个新的SSM参数(类型为SecureString)。在¶¶我使用辅助功能从event提取属性。一旦拥有ssmPath,我们还将physicalResourceID设置为该值。之后,我们将调用PutParameter,该21应该创建一个新的SSM参数。

云形式事件包含很多信息。这就是它的样子:

{
  "RequestType": "Create",
  "RequestID": "b37cee19-f52d-4801-89f0-eed1be454756",
  "ResponseURL": "",
  "ResourceType": "AWS::CloudFormation::CustomResource",
  "PhysicalResourceID": "",
  "LogicalResourceID": "SSMCredential63DBA3F67",
  "StackID": "arn:aws:cloudformation:eu-central-1:xxxx:stack/CustomResourcesGolang/a0de3b10-c3e1-11ed-9d97-02c",
  "ResourceProperties": {
    "ServiceToken": "arn:aws:lambda:eu-central-1:xxxxxxxxxxxx:function:CustomResourcesGolang-ProviderframeworkonEvent83C1-Dt9Jv3RwL9KT",
    "key": "/testing6",
    "value": "some-secret-value"
  },
  "OldResourceProperties": {}
}

CDK

现在,我们知道如何处理云形式事件以及如何管理自定义资源,让我们深入研究DevOps并设置一个小型CDK应用程序。通常,我会在Python中写下CDK部分,但是对于这个项目,我在Typescriptð中设置了我的第一个CDK应用程序。让我们从基本模板开始。

部署堆栈

我定义了应该创建哪些资源/组件的部署堆栈:

import * as cdk from "aws-cdk-lib";
import * as path from "path";
import * as customResources from "aws-cdk-lib/custom-resources";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from 'aws-cdk-lib/aws-iam';
import { spawnSync, SpawnSyncOptions } from "child_process";
import { Construct } from "constructs";
import { SSMCredential } from "./custom-resource";

export class DeploymentsStack extends cdk.Stack {  // ❶
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Build the Golang based Lambda function
    const lambdaPath = path.join(__dirname, "../../");

    // Create IAM role
    const iamRole = new iam.Role(this, 'Role', {...});  // ❷

    // Add further policies to IAM role
    iamRole.addToPolicy(...);                           // ➌

    // Create Lambda function
    const lambdaFunc = new lambda.Function(this, "GolangCustomResources", {...});   // ❹

    // Create a new custom resource provider
    const provider = new customResources.Provider(this, "Provider", {...});   // ❺

    // Create custom resource
    new SSMCredential(this, "SSMCredential1", provider, {...});               // ❻
  }
}
deployments-stack.ts

所以我的CDK应用程序将:

  • 创建一个名为DeploymentsStack的新云形式堆栈
  • 创建一个新的IAM角色 -
    • 用于将其连接到lambda函数
    • 在这里,我们定义了在SSM参数上运行所需的IAM策略
  • 向IAM角色添加几个IAM政策
  • 创建一个新的AWS lambda函数â€
  • 创建一个所谓的提供商 - 负责AWS中自定义资源的生命周期管理
    • 在我们的情况下,这是我们的lambda函数
    • 我不确定这是否会有不同的东西ð

自定义资源

在上一节中,我提到了SSMCredential,这是我们实现类型SecureString的SSM参数的新自定义资源。

import * as path from "path";
import * as cdk from "aws-cdk-lib";
import * as customResources from "aws-cdk-lib/custom-resources";
import { Construct } from "constructs";
import fs = require("fs");

export interface SSMCredentialProps {
  // ❶
  key: string;
  value: string;
}

// SSMCredential is an AWS custom resource
//
// Example code from: https://github.com/aws-samples/aws-cdk-examples/blob/master/typescript/custom-resource/my-custom-resource.ts
export class SSMCredential extends Construct {
  // ❷
  public readonly response: string;

  constructor(
    scope: Construct,
    id: string,
    provider: customResources.Provider,
    props: SSMCredentialProps
  ) {
    super(scope, id);

    const resource = new cdk.CustomResource(this, id, {
      // ➌
      serviceToken: provider.serviceToken, // ❹
      properties: props, // ❺
    });

    this.response = resource.getAtt("Response").toString();
  }
}
custom-resource.ts
  • SSMCredentialProps定义了要传递给自定义资源的参数¶
    • key:参数的名称
    • value:参数应保持的值
  • 自定义资源本身是类型SSMCredential
    • 它有一个constructor
    • 其中一个新的CDK自定义资源正在初始化
    • ServiceToken是提供此自定义资源类型的提供商的ARN。
    • 此外,我们通过参数(作为属性)

这就是与先前定义的DeploymentsStack
一起使用的方式

import { SSMCredential } from "./custom-resource";
...

export class DeploymentsStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    ...

    // Create a new custom resource provider
    const provider = new customResources.Provider(this, "Provider", {
      onEventHandler: lambdaFunc,
    });

    // Create custom resource
    new SSMCredential(this, "SSMCredential1", provider, {
      key: "/test/testing",
      value: "some-secret-value",
    });
  }
}
如何在CDK堆栈中使用SSMCREDENTIAN

屏幕截图

正如图片所说的不仅仅是单词,让我们看看一些屏幕截图,以更好地了解引擎盖下发生的事情。这样做,您可能会更好地了解工作流程以及CDK创建的所有涉及组件。

image

AWS控制台中的云形式堆栈。在这里,我们创建了2种SSMCREDential类型的自定义资源。

image

lambda函数创建的SSM参数是Securestring类型。

image

每个SSM参数都分配了一个标签(堆叠)。

测试

单元测试{#单位测验}

SSMCUSTOMRESOURCEHANDLER structure has a SSM client为了放置和删除参数:

// SSMParameterAPI defines an interface for the SSM API calls
// I use this interface in order to be able to mock out the SSM client and implement unit tests properly.
//
// Also check https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/gov2/ssm
type SSMParameterAPI interface {
    DeleteParameter(ctx context.Context, params *ssm.DeleteParameterInput, optFns ...func(*ssm.Options)) (*ssm.DeleteParameterOutput, error)
    PutParameter(ctx context.Context, params *ssm.PutParameterInput, optFns ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
}

type SSMCustomResourceHandler struct {
    ssmClient SSMParameterAPI
}
aws_custom_resource.go

我将自己的界面用于SSM参数API,因为编写单元测试时可以很容易地嘲笑:

// SSMParameterApiImpl is a mock for SSMParameterAPI
type SSMParameterApiImpl struct{}

// PutParameter
func (s SSMParameterApiImpl) PutParameter(ctx context.Context, params *ssm.PutParameterInput, optFns ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) {
    output := &ssm.PutParameterOutput{}
    return output, nil
}


// DeleteParameter
func (s SSMParameterApiImpl) DeleteParameter(ctx context.Context, params *ssm.DeleteParameterInput, optFns ...func(*ssm.Options)) (*ssm.DeleteParameterOutput, error) {
    output := &ssm.DeleteParameterOutput{}
    return output, nil
}

在AWS_CUSTOM_RESOURCE_TEST.GO

现在,我可以将SSMParameterApiImpl用作模拟客户端,因为它满足了SSMParameterAPI接口:

func TestPutParameter(t *testing.T) {
    mockedAPI := SSMParameterApiImpl{}
    ssmHandler := SSMCustomResourceHandler{
        ssmClient: mockedAPI,
    }
    ...
}
在AWS_CUSTOM_RESOURCE_TEST.GO

我们现在要做的就是创建一个cfn.event,并调用SSMHandLercustomResource类的创建方法:

func TestPutParameter(t *testing.T) {
    mockedAPI := SSMParameterApiImpl{}
    ssmHandler := SSMCustomResourceHandler{
        ssmClient: mockedAPI,
    }

    // Create new SSM parameter
    cfnEvent := cfn.Event{
        RequestType:        "Create",
        RequestID:          "xxx",
        ResponseURL:        "some-url-here",
        ResourceType:       "AWS::CloudFormation::CustomResource",
        PhysicalResourceID: "",
        LogicalResourceID:  "SSMCredentialTesting1",
        StackID:            "arn:aws:cloudformation:eu-central-1:9999999:stack/CustomResourcesGolang",
        ResourceProperties: map[string]interface{}{
            "ServiceToken": "arn:aws:lambda:eu-central-1:9999999:function:CustomResourcesGolang-Function",
            "key":          "/testing3",
            "value":        "some-secret-value",
        },
    }
    _, _, _ = ssmHandler.Create(context.TODO(), cfnEvent)

}

集成测试

我已经使用AWS SAM局部调用通过CDK创建的lambda函数。确保您的计算机上安装了aws-sam-cli

初始通话后,aws-sam将首先下载您功能的Docker映像:

...
Invoking /main (go1.x)
Local image was not found.
Removing rapid images for repo public.ecr.aws/sam/emulation-go1.x
...

之后在本地调用lambda很容易:

$ cdk synth
$ sam local invoke -t cdk.out/CustomResourcesGolang.template.json GolangCustomResources
...
Mounting /home/victor/work/repos/aws-custom-resource-golang/deployments/cdk.out/asset.1ac1b002ba7d09e11c31702e1724d092e837796c2ed40541947abdfc6eb75947 as /var/task:ro,delegated, inside runt
ime container
2023/03/31 11:42:29 Starting lambda
2023/03/31 11:42:29 event: cfn.Event{RequestType:"", RequestID:"", ResponseURL:"", ResourceType:"", PhysicalResourceID:"", LogicalResourceID:"", StackID:"", ResourceProperties:map[string]in
terface {}(nil), OldResourceProperties:map[string]interface {}(nil)}
2023/03/31 11:42:29 sending status failed: Unknown resource type:
Put "": unsupported protocol scheme "": Error
null
{"errorMessage":"Put \"\": unsupported protocol scheme \"\"","errorType":"Error"}END RequestId: 93eed487-2441-4d41-a0b6-d939efeab99f
REPORT RequestId: 93eed487-2441-4d41-a0b6-d939efeab99f  Init Duration: 0.30 ms  Duration: 224.84 ms     Billed Duration: 225 ms Memory Size: 128 MB     Max Memory Used: 128 M

当然,您需要为您的功能指定有效载荷。您可以将有效负载(类型云形式事件)存储为JSON文件:

$ cat tests/create.json
{
  "RequestType": "Create",
  "RequestID": "9bf90339-c6f0-47ff-ad67-e19226facf6e",
  "ResponseURL": "https://some-url",
  "ResourceType": "AWS::CloudFormation::CustomResource",
  "PhysicalResourceID": "",
  "LogicalResourceID": "SSMCredential21D358858",
  "StackID": "arn:aws:cloudformation:eu-central-1:xxxxxxxxxxxx:stack/CustomResourcesGolang/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "ResourceProperties": {
    "ServiceToken": "arn:aws:lambda:eu-central-1:xxxxxxxxxxxx:function:CustomResourcesGolang-ProviderframeworkonEvent83C1-Dt9Jv3RwL9KT",
    "key": "/test/testing12345",
    "value": "some-secret-value"
  },
  "OldResourceProperties": {}
}

然后您可以指定JSON文件

$ sam local invoke -t cdk.out/CustomResourcesGolang.template.json GolangCustomResources -e ../tests/create.json

Invoking /main (go1.x)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/go:1-rapid-x86_64.

Mounting /home/victor/work/repos/aws-custom-resource-golang/deployments/cdk.out/asset.1ac1b002ba7d09e11c31702e1724d092e837796c2ed40541947abdfc6eb75947 as /var/task:ro,delegated, inside runt
ime container
START RequestId: cb8c7882-269c-434e-ace1-f6958940ee2e Version: $LATEST
2023/03/31 11:48:16 Starting lambda
2023/03/31 11:48:16 event: cfn.Event{RequestType:"Create", RequestID:"9bf90339-c6f0-47ff-ad67-e19226facf6e", ResponseURL:"https://some-file", ResourceType:"AWS::CloudFormation::CustomResour
ce", PhysicalResourceID:"", LogicalResourceID:"SSMCredential21D358858", StackID:"arn:aws:cloudformation:eu-central-1:xxxxxxxxxxxx:stack/CustomResourcesGolang/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxx", ResourceProperties:map[string]interface {}{"ServiceToken":"arn:aws:lambda:eu-central-1:xxxxxxxxxxxx:function:CustomResourcesGolang-ProviderframeworkonEvent83C1-Dt9Jv3RwL9KT", "key":
"/test/testing12345", "value":"some-secret-value"}, OldResourceProperties:map[string]interface {}{}}
2023/03/31 11:48:16 Creating SSM parameter
2023/03/31 11:48:16 Put parameter into SSM: /test/testing12345
Put "https://some-file": dial tcp: lookup some-file on 192.168.179.1:53: no such host: Error
null
END RequestId: cb8c7882-269c-434e-ace1-f6958940ee2e
REPORT RequestId: cb8c7882-269c-434e-ace1-f6958940ee2e  Init Duration: 0.13 ms  Duration: 327.24 ms     Billed Duration: 328 ms Memory Size: 128 MB     Max Memory Used: 128 MB
{"errorMessage":"Put \"https://some-file\": dial tcp: lookup some-file on 192.168.179.1:53: no such host","errorType":"Error"}%

这个失败,因为我没有指定任何有效的ResponseURL

结论

我认为这种方法为根据您的需求创建高级自定义资源开辟了很多可能性。例如,您可以使用自定义资源在多个帐户中部署资源。出于安全原因,您可以执行几项合规性策略并监控合规性偏差。或者,您可以使用一些第三方API 来回传递数据(例如用户管理,产品库存等)

由于您控制了AWS lambda函数中实现的逻辑,因此定义了如何管理您的自定义资源,因此可能性是无限的。创建自己的自定义资源很开心!

资源

一般的

安全

戈兰