资源编排ROS之自定制资源(多云部署AWS篇)

简介: 资源编排服务(Resource Orchestration Service, 简称ROS)是阿里云提供的一项简化云计算资源管理的服务。您可以遵循ROS定义的模板规范编写资源栈模板,在模板中定义所需的云计算资源(例如ECS实例、RDS数据库实例)、资源间的依赖关系等。

1.背景

资源编排服务(Resource Orchestration Service, 简称ROS)是阿里云提供的一项简化云计算资源管理的服务。您可以遵循ROS定义的模板规范编写资源栈模板,在模板中定义所需的云计算资源(例如ECS实例、RDS数据库实例)、资源间的依赖关系等。ROS的编排引擎将根据模板自动完成所有资源的创建和配置,实现自动化部署及运维。

ROS资源编排接入了大量的阿里云资源,目前涉及38个服务,近200个资源,而且还在持续增长中。对于尚未提供的资源,或无法提供的资源或功能,ROS提供了自定义资源 (ALIYUN::ROS::CustomResource)作为解决方案。如果您还不了解自定义资源,可以参考资源编排ROS之自定制资源(基础篇)

本篇为ROS多云部署AWS篇。

其他进阶篇:多云部署Terraform篇

2.目标

通过ROS成功部署AWS云资源。推而广之,使用类似的方法,可以对所有其他云厂商的云资源进行部署,从而达到多云部署的目标。

3.准备工作

ROS模板

为了简化,我们使用如下模板在ROS中进行部署。这个模板创建了3个资源:

  • Service:FC(函数计算)的Service,用于创建Function。
  • Function:FC(函数计算)的Function,其代码实现了在AWS CloudFormation管理堆栈的逻辑。模板中,通过与ROS资源栈同一地域的OSS(对象存储) Bucket和Object指定代码。为了示例,这里的BucketName为ros-demo,ObjectName为test-custom-resource-aws.zip。
  • TestAwsCloudFormationStack:自定义资源,用于传递参数和接收输出。
    • 传递参数:包括AWS的AK信息,地域信息,CloudFormation堆栈信息(包括名称、模板和参数)。用于测试的CloudFormation的模板由于较为简单,已内嵌到ROS模板当中。为了方便示例,这个模板只创建了一个AWS::S3::Bucket资源。
    • 接收输出:为了简单,内嵌的CloudFormation模板的输出与自定义资源的输出一致。为了方便示例,输出只有一个,为AWS::S3::Bucket资源的ARN。

ROSTemplateFormatVersion: '2015-09-01'
Parameters:
  ServiceName:
    Type: String
    Default: test-service
  FunctionName:
    Type: String
    Default: test-function
  Timeout:
    Type: Number
    Default: 600
    MaxValue: 600
  AwsAccessKeyId:
    Type: String
    NoEcho: true
  AwsAccessKeySecret:
    Type: String
    NoEcho: true
  AwsRegionId:
    Type: String
    Default: us-east-1
Resources:
  Service:
    Type: ALIYUN::FC::Service
    Properties:
      ServiceName:
        Ref: ServiceName
  Function:
    Type: ALIYUN::FC::Function
    Properties:
      ServiceName:
        Fn::GetAtt: [Service, ServiceName]
      FunctionName: 
        Ref: FunctionName
      Handler: index.handler
      Runtime: python3
      Timeout:
        Ref: Timeout
      Code:
        OssBucketName: ros-demo
        OssObjectName: test-custom-resource-aws.zip
  TestAwsCloudFormationStack:
    Type: Custom::AwsCloudFormationStack
    Properties:
      ServiceToken:
        Fn::GetAtt: [Function, ARN]
      Parameters:
        AccessKeyId:
          Ref: AwsAccessKeyId
        AccessKeySecret:
          Ref: AwsAccessKeySecret
        RegionId:
          Ref: AwsRegionId
        Stack:
          Name: stack-by-ros
          Template: |
            AWSTemplateFormatVersion: '2010-09-09'

            Parameters:

              BucketName:
                Type: String

            Resources:
              MyBucket:
                Type: AWS::S3::Bucket
                Properties:
                  BucketName:
                    Ref: BucketName

            Outputs:
              MyBucketArn:
                Value:
                  Fn::GetAtt: [MyBucket, Arn]
          Parameters:
            BucketName: bucket-by-ros
      Timeout:
        Ref: Timeout
Outputs:
  AwsMyBucketArn:
    Value:
      Fn::GetAtt: [TestAwsCloudFormationStack, MyBucketArn]

函数代码

模板中的Function资源的主要实现代码如下。这个FC函数的主要功能如下:

  • 按照ALIYUN::ROS::CustomResource的规范接受输入,返回输出。
  • 按照在模板中自定义的资源属性,对AWS CloudFormation的堆栈进行管理。这是一个简单的示例实现,使用AWS提供的boto3库,对CloudFormation的堆栈执行操作(创建、更新、删除),并使用轮询的方式等待其完成。

# -*- coding: utf-8 -*-

import time
import json
import urllib.request
import urllib.error
import logging

import six
import boto3


class AwsError(Exception):
    pass


class AwsStack(object):

    def __init__(self, client, logger, event):
        self._client = client
        self._logger = logger
        self._event = event

    def _get_stack_args(self):
        event = self._event
        client_token = '-'.join([event['StackId'], event['RequestId'],
                                 event['LogicalResourceId']])[:128]
        stack_info = event['ResourceProperties']['Stack']
        stack_name = event.get('PhysicalResourceId') or stack_info['Name']
        return (stack_name, stack_info['Template'],
                [dict(ParameterKey=k, ParameterValue=v)
                 for k, v in six.iteritems(stack_info['Parameters'])], client_token)

    def _wait(self, stack_id):
        self._logger.info('begin to wait for stack %s.', stack_id)
        while True:
            resp = self._client.describe_stacks(StackName=stack_id)
            stack = resp['Stacks'][0]
            status = stack['StackStatus']
            if status.endswith('_IN_PROGRESS'):
                time.sleep(3)
                continue
            self._logger.info('end to wait for stack %s: %s.', stack_id, stack)
            if status.endswith('_COMPLETE'):
                return {item['OutputKey']: item['OutputValue']
                        for item in stack.get('Outputs', [])}
            raise AwsError('Stack operation failed, status: {}.'.format(status))

    def create(self):
        stack_name, template_body, parameters, client_token = self._get_stack_args()
        resp = self._client.create_stack(
            StackName=stack_name,
            TemplateBody=template_body,
            Parameters=parameters,
            ClientRequestToken=client_token
        )
        self._logger.info('create stack: %s.', resp)
        stack_id = resp['StackId']
        return stack_id, self._wait(stack_id)

    def update(self):
        stack_name, template_body, parameters, client_token = self._get_stack_args()
        resp = self._client.update_stack(
            StackName=stack_name,
            TemplateBody=template_body,
            Parameters=parameters,
            ClientRequestToken=client_token
        )
        self._logger.info('update stack: %s.', resp)
        return self._wait(stack_name)

    def delete(self):
        stack_name, _, _, client_token = self._get_stack_args()
        resp = self._client.delete_stack(
            StackName=stack_name,
            ClientRequestToken=client_token
        )
        self._logger.info('delete stack: %s.', resp)
        self._wait(stack_name)

    @classmethod
    def from_event(cls, event, logger):
        res_props = event['ResourceProperties']
        session = boto3.Session(aws_access_key_id=res_props['AccessKeyId'],
                                aws_secret_access_key=res_props['AccessKeySecret'],
                                region_name=res_props['RegionId'])
        client = session.client('cloudformation')
        return cls(client, logger, event)


def handler(event, context):
    logger = logging.getLogger()
    event = json.loads(event)
    req_type = event['RequestType']
    aws_stack = AwsStack.from_event(event, logger)

    result = dict(
        RequestId=event['RequestId'],
        LogicalResourceId=event['LogicalResourceId'],
        StackId=event['StackId'],
    )
    try:
        aws_stack_id = aws_stack_outputs = None
        if req_type == 'Create':
            aws_stack_id, aws_stack_outputs = aws_stack.create()
        elif req_type == 'Update':
            aws_stack_outputs = aws_stack.update()
        else:
            aws_stack.delete()

        result['Status'] = 'SUCCESS'
        result['PhysicalResourceId'] = aws_stack_id or event.get('PhysicalResourceId')
        if aws_stack_outputs:
            result['Data'] = aws_stack_outputs
    except Exception as ex:
        result['Status'] = 'FAILED'
        result['Reason'] = str(ex)

    headers = {
      'Content-type': 'application/json',
      'Accept': 'application/json',
      'Date': time.strftime('%a, %d %b %Y %X GMT', time.gmtime())
    }
    data = json.dumps(result).encode('utf-8')
    req = urllib.request.Request(event['ResponseURL'], data=data, headers=headers)
    with urllib.request.urlopen(req) as response:
        resp_content = response.read().decode('utf-8')
    logger.info('response: %s %s', result, resp_content)

由于这个FC函数使用了非内置库boto3,所以不内直接使用上述代码,需要进行打包。具体可以参考使用自定义的模块。大致流程如下:


# 环境信息:MAC, Python 3.8

mkdir /tmp/code
# 写入函数代码
cat > /tmp/code/index.py<

然后把/tmp/test-custom-resource-aws.zip文件上传到OSS上即可。模板中上传到的OSS Bucket为ros-demo、Object为test-custom-resource-aws.zip,可按需调整,但要与ROS资源栈在同一地域。

示例中的压缩包也可以通过这个地址进行下载。

4.测试验证

我们在ROS对上述模板和代码进行测试验证。

创建资源栈。在模板中,AwsAccessKeyId和AwsAccessKeySecret都被设置成了加密参数。

观察资源列表,ALIYUN::ROS::CustomResource资源已成功创建。

观察输出列表,获取到AWS S3 Bucket的ARN。

观察AWS CloudFormation控制台,可以看到stack-by-ros堆栈,以及相关的AWS::S3::Bucket资源。

5.总结

通过自定义资源 (ALIYUN::ROS::CustomResource)可以实现多云部署,轻松实现一个入口,一键部署。

作者介绍
目录