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)可以实现多云部署,轻松实现一个入口,一键部署。