如何在 azure devops CI/CD 管道中动态创建 s3 存储桶

问题描述

我想根据其中一个 yaml 文件中提到的数据,通过 CI/CD 管道自动执行存储桶创建过程。所以,我得到了包含所有存储桶名称的 bucket.yaml 文件。这个文件不断变化,因为将来会添加更多的存储桶名称。目前,bucket.yaml 是这样的

BucketName:
    - test-bucket
    - test-bucket2
    - test-bucket3

我有一个 template.yaml 文件,它是一个用于创建 s3 存储桶的 cloudformation 模板。这是它的外观:

Resources:
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    DeletionPolicy: Retain
    Properties:
      BucketName: This will come from bucket.yaml

现在,template.yaml 将从bucket.yaml 文件获取bucket 名称,并且应该创建bucket.yaml 中提到的3 个bucket。如果有人在 bucket.yaml 中再添加 2 个桶,那么 template.yaml 也应该创建这 2 个新桶。此外,如果有人从 bucket.yaml 中删除了任何存储桶名称,那么这些存储桶也应该被删除。我在我的研究中找不到过程,只是找到了点点滴滴的信息。所以,在这里我有一些具体的问题,如果可能的话:

  1. 如何从bucket.yaml 和template.yaml 中获取bucket 名称应该创建所有bucket。
  2. 如果有人在 bucket.yaml 中更新/添加/删除存储桶名称,template.yaml 应相应地更新这些名称。 另外,请解释我将如何通过 Azure DevOps 中的 CI/CD 管道来实现。

解决方法

  1. 关于您的第一个问题:

如何从 bucket.yaml 和 template.yaml 中获取桶名应该创建所有的桶。

bucket.yaml 中,您可以使用 Parameters 来设置 BucketName

例如:

parameters:
- name: BucketName
  type: object
  default:
  - test-bucket
  - test-bucket2
  - test-bucket3

steps:
- ${{ each value in parameters.BucketName }}:
  - script: echo ${{ value }}

此处的步骤可以遍历参数 BucketName 的值。

template.yaml 中,您可以像下面这样调用 bucket.yaml

trigger:
- main

extends:
  template: bucket.yaml
  1. 对于您的第二个问题:

如果有人在 bucket.yaml 中更新/添加/删除存储桶名称,template.yaml 应相应地更新这些名称。

没有任何简单的方法可以做到这一点。可以尝试写一个脚本在pipeline中运行来做以下事情:

  • 列出所有已创建的存储桶。这是现有存储分区的列表。
  • 将现有存储桶列表与参数 BucketName 的值列表进行比较,以检查哪些存储桶需要添加,哪些需要删除。
  • 如果某个存储桶在参数中列出但不在现有存储桶中,则应将此存储桶创建为新存储桶。
  • 如果一个bucket在现有buckets中列出,但没有在参数中,这个bucket应该被删除。
,
BucketName:
                  - test-bucket
                  - test-bucket2
                  - test-bucket3

要求意味着所有 S3 存储桶都将以相同的方式创建,并且不需要偏离给定的 Cloudformation 模板 (AWS::S3::Bucket)。

要求我们跟踪需要删除哪些 S3 存储桶。 Cloudformation 不会删除 S3 存储桶,因为 Cloudformation 模板片段包含 DeletionPolicy 的 Retain。

解决方案:

可以以特定方式标记 S3 存储桶,以将它们标识为由当前 CI/CD 管道拥有。可以列出 S3 存储桶,然后可以删除所有以正确方式标记但在 bucket.yaml 中不存在的 S3 存储桶。


我个人只会使用 AWS 开发工具包创建 CI/CD 管道所需的 S3 存储桶,并手动管理 S3 存储桶的删除。如果应用程序需要 S3 存储桶,那么他们应该在其应用程序的 Cloudformation 堆栈中自行创建它,以便他们可以!Ref 并按照他们想要的方式对其进行自定义(例如静态加密、版本控制、生命周期规则等)。


技术说明:

要删除 S3 存储桶,还需要删除其内容。这将要求我们列出 S3 存储桶中的所有对象,然后删除它们。 Java SDK [here] 的一些文档。 之后,删除 S3 存储桶的 API 调用才会成功。

您可以让 Cloudformation 使用 custom resource 删除您的 S3 对象。也就是说,我觉得使用自定义资源并不有趣 - 因此,如果您可以在 CI/CD 管道中使用 AWS 开发工具包,我可能只会使用它。

用于删除存储桶内容的自定义资源在 Cloudformation 中可能如下所示:(它是启动 Lambda 的自定义资源。如果自定义资源被取消配置,Lambda 将删除 S3 存储桶内容)

 # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html
  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-custom-resources-lambda-lookup-amiids.html
  ExampleBucketOperationCustomResource:
    Type: AWS::CloudFormation::CustomResource
    DependsOn: [Bucket,ExampleBucketOperationLambdaFunction]
    Properties:
      ServiceToken: !GetAtt ExampleBucketOperationLambdaFunction.Arn
      # Custom properties
      BucketToUse: !Ref S3BucketName
  ExampleBucketOperationLambdaFunctionExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "ExampleBucketOperationLambda-ExecutionRole"
      Path: "/"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action:
              - sts:AssumeRole
            Principal:
              Service:
                - lambda.amazonaws.com
      Policies:
        - PolicyName: "ExampleBucketOperationLambda-CanAccessCloudwatchLogs"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*
        - PolicyName: "ExampleBucketOperationLambda-S3BucketLevelPermissions"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource:
                  - !Sub "arn:aws:s3:::${S3BucketName}"
        - PolicyName: "ExampleBucketOperationLambda-S3ObjectLevelPermissions"
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - s3:DeleteObject
                  - s3:PutObject
                Resource:
                  - !Sub "arn:aws:s3:::${S3BucketName}/*"
  # Test payload:
  # {"RequestType":"Create","ResourceProperties":{"BucketToUse":"your-bucket-name"}}
  ExampleBucketOperationLambdaFunction:
    Type: AWS::Lambda::Function
    DependsOn: ExampleBucketOperationLambdaFunctionExecutionRole
    # DeletionPolicy: Retain
    Properties:
      FunctionName: "ExampleBucketOperationLambda"
      Role: !GetAtt ExampleBucketOperationLambdaFunctionExecutionRole.Arn
      Runtime: python3.8
      Handler: index.handler
      Timeout: 30
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          def handler(event,context):
              eventType = event["RequestType"]
              print("The event type is: " + str(eventType));
              bucketToUse = event["ResourceProperties"]["BucketToUse"]
              print("The bucket to use: " + str(bucketToUse));
              try:
                  # Requires s3:ListBucket permission
                  if (eventType in ["Delete"]):
                      print("Deleting everyting in bucket: " + str(bucketToUse));
                      s3Client = boto3.client("s3")
                      s3Bucket = boto3.resource("s3").Bucket(bucketToUse)
                      for currFile in s3Bucket.objects.all():
                          print("Deleting file: " + currFile.key);
                          s3Client.delete_object(Bucket=bucketToUse,Key=currFile.key)
                  
                  print("All done")
                  responseData = {}
                  cfnresponse.send(event,context,cfnresponse.SUCCESS,responseData)
              except Exception as e:
                  responseData = {}
                  errorDetail = "Exception: " + str(e)
                  errorDetail = errorDetail + "\n\t More detail can be found in CloudWatch Log Stream: " + context.log_stream_name
                  print(errorDetail)
                  cfnresponse.send(event=event,context=context,responseStatus=cfnresponse.FAILED,responseData=responseData,reason=errorDetail)
,

感谢以上回答。我采取了不同的方法来解决这个问题。我使用 AWS CDK 来实现我真正想要的。我个人将 AWS CDK 用于 Python 并使用它创建了基础设施。