AWS lambda复制RDS快照
#aws #serverless #devops #go

几天前,我遇到了AWS RDS服务的问题,特别是使用AWS备份服务无法备份的SQL Server数据库。当前,请求RDS备份数据库存在局限性。但是,仍然可以使用RDS API为我们的数据库创建备份请求。

因此,我想删除必须通过API或Web控制台手动创建备份的工程师的活动,而是开发无服务器服务以自动化此过程。这种方法也可以扩展以解决其他问题。

让我们从这个准则

准备

在创建任何lambda函数之前,我们需要阅读一个文档,以说明如何开发它们。您可以找到它here。然后,您应该有以下内容。

  1. Golang版本1.16或更高版本
  2. 用于确保您将经过验证的源代码推向SCM
  3. Pre-commit
  4. Taskfile可选的,用于减少开发中的常规步骤。

概述解决方案是

我们专注于创建快照时RDS生成的事件。这些事件是由AWS EventBridge触发的,该事件在AWS Lambda中提供了一个函数以执行必要的操作。

Lambda with EventBridge

第一件事

您可以提供一个文件夹结构以在工作区中开发lambda功能。

  1. 创建新的GitHub存储库并将其克隆到您的本地计算机中。

    git clone <your_repo_url> 
    
  2. 转到您的文件夹,然后启动一个新的Golang项目。它将创建一个新的go.mod文件。

    go mod init <your_repo_url>
    #Example
    go mod init github.com/StartloJ/lambda-rds-utils
    
  3. (可选)创建一个预先承诺的策略文件,以帮助您提高开发质量。

    #.pre-commit-config.yaml
    repos:
    - repo: https://github.com/dnephin/pre-commit-golang
      rev: master
      hooks:
      - id: go-fmt
      - id: go-vet
      - id: go-imports
      - id: go-mod-tidy
    

创建第一个功能

现在,我们准备开发lambda功能来处理事件。您可以按照下面的步骤来创建一个真实的用例函数,该功能复制跨区域的RDS快照。

记住AWS EventBridge的活动结构

您可以在官方文档here上看到更多活动。

{
    "version": "0",
    "id": "844e2571-85d4-695f-b930-0153b71dcb42",
    "detail-type": "RDS DB Snapshot Event",
    "source": "aws.rds",
    "account": "123456789012",
    "time": "2018-10-06T12:26:13Z",
    "region": "us-east-1",
    "resources": ["arn:aws:rds:us-east-1:123456789012:snapshot:rds:snapshot-replica-2018-10-06-12-24"],
    "detail": {
      "EventCategories": ["creation"],
      "SourceType": "SNAPSHOT",
      "SourceArn": "arn:aws:rds:us-east-1:123456789012:snapshot:rds:snapshot-replica-2018-10-06-12-24",
      "Date": "2018-10-06T12:26:13.882Z",
      "SourceIdentifier": "rds:snapshot-replica-2018-10-06-12-24",
      "Message": "Automated snapshot created",
      "EventID": "RDS-EVENT-0091"
    }
}

定义功能的配置

我将定义参数以在下面重复此功能。

  • OPT_SRC_REGION是复制的RDS快照的来源区域。
  • OPT_TARGET_REGION是我们需要存储快照的目标区域。
  • OPT_DB_NAME是我们需要复制快照的RDS DB名称的身份。
  • OPT_OPTION_GROUP_NAME是一个目标选项组,用于附加以复制目标区域中的快照。
  • OPT_KMS_KEY_ID是一个KMS密钥,用于加密目标区域的快照。

定义主要功能

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/sirupsen/logrus"
)

func main() {
    // Make the hangler available for remote procedure call by Lambda
    logrus.Info("we starting handle lambda...")
    lambda.Start(HandleEvents)
}

通过lambda创建功能句柄

// Define func to receive a variable from `BridgeEvent`
// and it should return `error` if any mistake.
func HandleEvents(events BridgeEvent) error {
    return nil
}

定义新的AWS会话

// define AWS session for source and target region to copy snapshot
func HandleEvents(event BridgeEvent) error {
    // start copy code
    des_sess := session.Must(session.NewSessionWithOptions(session.Options{
        Config: aws.Config{
            Region: aws.String(viper.GetString("target_region")),
        },
    }))
    src_sess := session.Must(session.NewSessionWithOptions(session.Options{
        Config: aws.Config{
            Region: aws.String(viper.GetString("src_region")),
        },
    }))
    // end copy code
    return nil
}

创建功能以获取所有现有快照

// This func input session of RDS service. Then, it get all list of database
// snapshot that already in your resources in List of Snapshot name.
func ListAllSnapshot(svc *rds.RDS) ([]string, error) {

    db := viper.GetString("DB_NAME")

    // This line will return a list of snapshots included snapshot name.
    out, err := svc.DescribeDBSnapshots(&rds.DescribeDBSnapshotsInput{
        DBInstanceIdentifier: &db,
    })
    if err != nil {
        panic(err)
    }

    var snapShotName []string

    // this loop is extract a snapshot name only.
    for _, b := range out.DBSnapshots {
        logrus.Infof("We have %s", ConvertToString(*b.DBSnapshotArn))
        snapShotName = append(snapShotName, ConvertToString(*b.DBSnapshotArn))
    }

    return snapShotName, nil
}

从源和目标区域获取RDS快照名称的列表

func HandleEvents(event BridgeEvent) error {
    ...
    // start copy code
    targetSvc := rds.New(des_sess)
    rep_dbSnapshots, err := ListAllSnapshot(targetSvc)
    if err != nil {
        return fmt.Errorf("we can't get any Snapshot name")
    }

    srcSvc := rds.New(src_sess)
    src_dbSnapshots, err := ListAllSnapshot(srcSvc)
    if err != nil {
        return fmt.Errorf("we can't get any Snapshot name")
    }
    // End copy code
    ...
}

创建一个功能以删除重复快照作为源和目标

// this func use to remove duplicate name source if found in target
// you shouldn't switch position input to this func.
// `t` is mean a target that use double check to `s`
// it will remove a value in `s` if found it in `t`
func GetUniqueSnapShots(t, s []string) ([]string, error) {
    //Create a map to keep track a unique strings
    uniqueMap := make(map[string]bool)

    //Iterate over `s` and add each string to map
    for _, str := range s {
        uniqueMap[str] = true
    }

    //Iterate over `t` and remove any string that are already in uniqueMap
    for _, str2 := range t {
        delete(uniqueMap, str2)
    }

    //Convert the unique string from Map to slice string
    result := make([]string, 0, len(uniqueMap))
    for str := range uniqueMap {
        result = append(result, str)
    }

    if len(result) == 0 {
        return nil, fmt.Errorf("not any Snapshot unique between source and target region")
    }

    return result, nil
}

定义独特的快照

func HandleEvents(event BridgeEvent) error {
    ...
    // start copy code
    dbSnapShots2Copy, err := GetUniqueSnapShots(rep_dbSnapshots, src_dbSnapshots)
    if err != nil {
        logrus.Warnf("it doesn't any task copy snapshot to %s", viper.GetString("target_region"))
        return nil
    }
    // End copy code
    ...
}

创建一个跨区域的Func复制RDS快照

// Request to AWS RDS API for create event copy a specific snapshot
// into across region and encrypt it by KMS key multi-region
func CopySnapshotToTarget(svc *rds.RDS, snap string) (string, error) {
    targetSnapArn := strings.Split(snap, ":")
    targetSnapName := targetSnapArn[len(targetSnapArn)-1]

    // Copy the snapshot to the target region
    copySnapshotInput := &rds.CopyDBSnapshotInput{
        OptionGroupName:            aws.String(viper.GetString("option_group_name")),
        KmsKeyId:                   aws.String(viper.GetString("kms_key_id")),
        CopyTags:                   aws.Bool(true),
        SourceDBSnapshotIdentifier: aws.String(snap),
        TargetDBSnapshotIdentifier: aws.String(targetSnapName),
        SourceRegion:               aws.String(viper.GetString("src_region")),
    }

    _, err := svc.CopyDBSnapshot(copySnapshotInput)
    if err != nil {
        logrus.Errorf("Copy request %s is failed", snap)
        return "", err
    }

    logrus.Infof("Copy %s is created", snap)
    return fmt.Sprintf("Copy %s is created", snap), nil
}

定义循环以执行所有快照值

func HandleEvents(event BridgeEvent) error {
    ...
    // start copy code
    for s := range dbSnapShots2Copy {
        logrus.Infof("trying to copy DBSnapshot to %s...", viper.GetString("target_region"))
        _, err := CopySnapshotToTarget(targetSvc, dbSnapShots2Copy[s])
        if err != nil {
            logrus.Error(err.Error())
        }
    }
    // End copy code
    ...
}

如果所有工作流程都完成,则它将从AWS lambda控制器中正常结束功能。

最后步骤

  1. 如果您完成了开发功能,则将用zip格式构建并打包它。

    export BINARY_NAME="lambda-rds-util" #you can change this name.
    go build -o $BINARY_NAME main.go
    zip lambda-$BINARY_NAME.zip $BINARY_NAME
    
  2. 您可以将压缩文件(也可以zip文件)上传到S3。

    #you can push it with below command
    export S3_NAME="<your_s3_bucket_name>"
    aws s3api put-object --bucket $S3_NAME --key lambda-$BINARY_NAME.zip --body ./lambda-$BINARY_NAME.zip
    
  3. 在AWS lambda的Web控制台中,您可以定义一个新资源,然后从S3中选择源。

  4. 在Web控制台中,您可以在AWS lambda中测试功能,以确保您的代码工作!。

结论

您可以使用Serverless服务(例如AWS Lambda)在AWS或其他云提供商上自动化某些活动。它可以增强您的发展经验,并使您能够探索新的可能性。您甚至可以扩展此想法以在您的工作中开发新的工作流程。

我希望此博客能激发您考虑在开发旅程中使用Serverless