阿里云CDN-边缘脚本EdgeScript的CI/CD实践
阿里云CDN-ES脚本CI/CD实践
- 背景
- 环境
- 项目代码结构及发布脚本代码
- 1. 项目结构
- 2. 发布工具代码
- 流水线配置
- 1. 流程配置
- 2. 脚本代码
- 发布脚本说明
- 0. 配置账户
- 1. 清空测试环境(回滚测试环境)
- 2. 执行脚本发布
- 3. 发布(测试环境推送到生产环境)
- 4. 查询生产环境规则(可选)
背景
最近通过阿里云CDN,参照七牛的智能多媒体协议,实用阿里云CDN的ES脚本实现了视频元数据(avinfo)和缩略图(vframe)功能。
但是上述2个功能脚本需要部署到数十个域名中,一个一个复制非常困难。
查阅ES功能文档后,设计了CI/CD方案,方便日后迭代和代码管理。
环境
需要准备的环境如下:
- 阿里云CDN:本方案以阿里云CDN为基础,基于其边缘脚本EdgeScript功能实现。
- 阿里云云效-流水线:CI/CD工具,在这里不限制工具类型。主要以可实现功能为主。
- 代码仓库:用于管理代码,并作为CI/CD工具发布时获取源码的地方,不再赘述。
- Python3.x:发布基于Python3脚本。下方会给出。CI/CD工具内需要支持Python3.x环境
项目代码结构及发布脚本代码
1. 项目结构
项目结构指存放于Git代码仓库中的项目结构。
目录 | 说明 |
---|---|
./src/cicd/ | 用于流水线执行发布的脚本,在流水线中负责将es脚本发布至对应域名下。 |
./src/edgeScript/ | ES的脚本代码 |
2. 发布工具代码
./src/cicd/cdn_es.py是基于阿里云帮助文档中的CLI工具代码改造而来。优化点如下:
- 修改原始代码为python3.x语法。
- 支持命令行直接传递
--id=${AK} --secret=${SK}
,无需先执行config命令,再执行部署命令。 - 修复原代码中的Bug。
代码如下:
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
import logging
import sys
import os
import urllib.parse
import urllib.request
import base64
import hmac
import hashlib
import time
import uuid
import json
from optparse import OptionParser
import configparser
import tracebackaccess_key_id = ''
access_key_secret = ''
cdn_server_address = 'https://cdn.aliyuncs.com'
CONFIGFILE = os.getcwd() + '/aliyun.ini'
CONFIGSECTION = 'Credentials'
cmdlist = '''1. Publish the ES rule to the simulated environment or production environment./es.py action=push_test_env domain=<domain> rule='{"pos":"<head|foot>","pri":"0-999","rule_path":"<the es code path>","enable":"<on|off>"}'./es.py action=push_product_env domain=<domain> rule='{"pos":"<head|foot>","pri":"0-999","rule_path":"<the es code path>","enable":"<on|off>","configid":"<configid>"}'2. Query the ES rule in the simulated environment or production environment./es.py action=query_test_env domain=<domain>./es.py action=query_product_env domain=<domain>3. Delete the ES rule in the simulated environment or production environment./es.py action=del_test_env domain=<domain> configid=<configid>./es.py action=del_product_env domain=<domain> configid=<configid>4. Publish the ES rule from the simulated to production environment, or Rollback the ES rule in the simulated environment./es.py action=publish_test_env domain=<domain>./es.py action=rollback_test_env domain=<domain>
'''def percent_encode(s):res = urllib.parse.quote(s.encode('utf8'), safe='')res = res.replace('+', '%20')res = res.replace('*', '%2A')res = res.replace('%7E', '~')return resdef compute_signature(parameters, access_key_secret):sortedParameters = sorted(parameters.items(), key=lambda x: x[0])canonicalizedQueryString = ''for k, v in sortedParameters:canonicalizedQueryString += '&' + percent_encode(k) + '=' + percent_encode(v)stringToSign = 'GET&%2F&' + percent_encode(canonicalizedQueryString[1:])h = hmac.new((access_key_secret + "&").encode('utf-8'), stringToSign.encode('utf-8'), hashlib.sha1)signature = base64.b64encode(h.digest()).decode('utf-8').strip()return signaturedef compose_url(user_params):timestamp = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())parameters = {'Format': 'JSON','Version': '2018-05-10','AccessKeyId': access_key_id,'SignatureVersion': '1.0','SignatureMethod': 'HMAC-SHA1','SignatureNonce': str(uuid.uuid1()),'Timestamp': timestamp,}parameters.update(user_params)signature = compute_signature(parameters, access_key_secret)parameters['Signature'] = signatureurl = cdn_server_address + "/?" + urllib.parse.urlencode(parameters)return urldef make_request(user_params, quiet=False):url = compose_url(user_params)try:req = urllib.request.Request(url)with urllib.request.urlopen(req) as r:if r.getcode() == 200:print("Response Code:\n=============\n200 OK")print("\nResponse Info:\n==============")body = r.read()body_json = json.loads(body)body_str = json.dumps(body_json, indent=4)print(body_str)except urllib.error.HTTPError as err:print("Response Code:\n=============")print(err)body = err.read()body_json = json.loads(body)body_str = json.dumps(body_json, indent=4)print("\nResponse Info:\n==============")print(body_str)def configure_accesskeypair(args, options):if options.accesskeyid is None or options.accesskeysecret is None:print("config miss parameters, use --id=[accesskeyid] --secret=[accesskeysecret]")sys.exit(1)config = configparser.ConfigParser()config.add_section(CONFIGSECTION)config.set(CONFIGSECTION, 'accesskeyid', options.accesskeyid)config.set(CONFIGSECTION, 'accesskeysecret', options.accesskeysecret)with open(CONFIGFILE, 'w+') as cfgfile:config.write(cfgfile)def setup_credentials(args, options):config = configparser.ConfigParser()global access_key_idglobal access_key_secretif options.accesskeyid is None or options.accesskeysecret is None:# 在这条分支下,命令中没有ak和sktry:config.read(CONFIGFILE)access_key_id = config.get(CONFIGSECTION, 'accesskeyid')access_key_secret = config.get(CONFIGSECTION, 'accesskeysecret')except Exception as e:print(traceback.format_exc())print("can't get access key pair, use config --id=[accesskeyid] --secret=[accesskeysecret] to setup, or add --id=[accesskeyid] --secret=[accesskeysecret] after this cmd")sys.exit(1)else:# 在这条分支下,直接使用命令中的ak和skaccess_key_id = options.accesskeyidaccess_key_secret = options.accesskeysecretdef parse_args(user_params):req_args = {}if user_params['action'] == 'push_test_env' or user_params['action'] == 'push_product_env':if 'domain' not in user_params or 'rule' not in user_params:parser.print_help()sys.exit(0)data = []for rule in user_params['rule']:rule_cfg = {# 'functionId': 180,'functionName': 'edge_function','functionArgs': []}for k in rule:arg_cfg = {}if k == 'configid':rule_cfg['configId'] = int(rule[k])elif k == 'rule_path':try:with open(rule[k], "r", encoding='utf-8') as f:code = f.read()except IOError:print("io error")sys.exit(0)arg_cfg['argName'] = 'rule'arg_cfg['argValue'] = coderule_cfg['functionArgs'].append(arg_cfg)else:arg_cfg['argName'] = karg_cfg['argValue'] = rule[k]rule_cfg['functionArgs'].append(arg_cfg)data.append(rule_cfg)rule_str = json.dumps(data)if user_params['action'] == 'push_test_env':req_args = {'Action': 'SetCdnDomainStagingConfig', 'DomainName': user_params['domain'],'Functions': rule_str}else:req_args = {'Action': 'BatchSetCdnDomainConfig', 'DomainNames': user_params['domain'],'Functions': rule_str}elif user_params['action'] == 'query_test_env':if 'domain' not in user_params:parser.print_help()sys.exit(0)req_args = {'Action': 'DescribeCdnDomainStagingConfig', 'DomainName': user_params['domain'],'FunctionNames': 'edge_function'}elif user_params['action'] == 'query_product_env':if 'domain' not in user_params:parser.print_help()sys.exit(0)req_args = {'Action': 'DescribeCdnDomainConfigs', 'DomainName': user_params['domain'],'FunctionNames': 'edge_function'}elif user_params['action'] == 'del_test_env':if 'domain' not in user_params or 'configid' not in user_params:parser.print_help()sys.exit(0)req_args = {'Action': 'DeleteSpecificStagingConfig', 'DomainName': user_params['domain'],'ConfigId': user_params['configid']}elif user_params['action'] == 'del_product_env':if 'domain' not in user_params or 'configid' not in user_params:parser.print_help()sys.exit(0)req_args = {'Action': 'DeleteSpecificConfig', 'DomainName': user_params['domain'],'ConfigId': user_params['configid']}elif user_params['action'] == 'publish_test_env':if 'domain' not in user_params:parser.print_help()sys.exit(0)req_args = {'Action': 'PublishStagingConfigToProduction', 'DomainName': user_params['domain'],'FunctionName': 'edge_function'}elif user_params['action'] == 'rollback_test_env':if 'domain' not in user_params:parser.print_help()sys.exit(0)req_args = {'Action': 'RollbackStagingConfig', 'DomainName': user_params['domain'],'FunctionName': 'edge_function'}else:parser.print_help()sys.exit(0)return req_argsif __name__ == '__main__':parser = OptionParser("%s Action=action Param1=Value1 Param2=Value2 %s\n" % (sys.argv[0], cmdlist))parser.add_option("-i", "--id", dest="accesskeyid", help="specify access key id")parser.add_option("-s", "--secret", dest="accesskeysecret", help="specify access key secret")(options, args) = parser.parse_args()if len(args) < 1:parser.print_help()sys.exit(0)if args[0] == 'help':parser.print_help()sys.exit(0)if args[0] != 'config':setup_credentials(args, options)else: # it's a configure id/secret commandconfigure_accesskeypair(args, options)sys.exit(0)user_params = {}idx = 1if sys.argv[1].lower().startswith('action='):_, value = sys.argv[1].split('=')user_params['action'] = valueidx = 2else:parser.print_help()sys.exit(0)for arg in sys.argv[idx:]:try:key, value = arg.split('=', 1)if key == 'rule': # push_test_env / push_product_envif 'rule' not in user_params:user_params['rule'] = []user_params['rule'].append(json.loads(value))else:user_params[key.strip()] = valueexcept ValueError as e:print(str(e).strip())raise SystemExit(e)req_args = parse_args(user_params)print("Request: %s" % json.dumps(req_args))make_request(req_args)
流水线配置
1. 流程配置
流水线流程配置非常简单,下载代码后1次脚本执行即可完成单个域名的部署。如果需要进行多域名部署,则重复配置“步骤”即可。
2. 脚本代码
老规矩,先发布代码。读代码前注意:
- 变量${domain}为流程需要进行变更的域名。在当前流水线中,配置在了“变量和缓存”中,作为字符串变量
- 如果发布的流水线的域名是固定的,可在发布脚本中直接配置。
# 1. 清空测试环境
python ./src/cicd/cdn_es.py action=rollback_test_env domain=${domain} --id=${CDN_AK} --secret=${CDN_SK}# 2. 发布脚本到测试环境-script1
export esName=script1_$(echo "${DATETIME}" | tr '-' '_')
export esOriFile=./src/edgeScript/script1.es
## 将要发布的脚本文件的内容复制到待发布文件中,在这一步中如果需要替换环境变量,可以使用sed命令
cat ${esOriFile} > ./cdn.es
python ./src/cicd/cdn_es.py action=push_test_env domain=${domain} 'rule={"name":"'${esName}'","pos":"head","pri":"0","rule_path":"./cdn.es","enable":"on","brk":"on","option":""}' --id=${CDN_AK} --secret=${CDN_SK}## 如果有更多脚本,可以复制上面5-10行的内容,直到所有脚本发布完毕。# 3. 将测试环境脚本发布到正式环境
python ./src/cicd/cdn_es.py action=publish_test_env domain=${domain} --id=${CDN_AK} --secret=${CDN_SK}# 4. 查询正式环境脚本(用于记录,后期方便排查日志)
python ./src/cicd/cdn_es.py action=query_product_env domain=${domain} --id=${CDN_AK} --secret=${CDN_SK}
发布脚本说明
0. 配置账户
注意,这一步可以省略,并在下面所有命令后面添加–id={ak} --secret={sk}参数
python ./src/cicd/cdn_es.py config --id={ak} --secret={sk}
1. 清空测试环境(回滚测试环境)
为避免测试环境中存在未配置的脚本,需要通过这一步骤进行清空。如果提示404是正常的(说明本来就没有)
python ./src/cicd/cdn_es.py action=rollback_test_env domain={domain} --id={ak} --secret={sk}
2. 执行脚本发布
注意,在这一步中,需要将所有脚本都进行发布。所以如果域名下有多个脚本,需要多次添加,执行所有脚本的添加步骤。
变量说明
变量名 | 说明 |
---|---|
domain | 域名 |
esName | 规则名称 |
esOriFile | 脚本原始文件名称 |
另外,命令中的JSON
需要按照实际情况进行调整。
python ./src/cicd/cdn_es.py action=push_test_env domain={domain} rule={\"name\":\"{esName}\",\"pos\":\"head\",\"pri\":\"0\",\"rule_path\":\"./cdn.es\",\"enable\":\"on\",\"brk\":\"on\",\"option\":\"\"} --id={ak} --secret={sk}
3. 发布(测试环境推送到生产环境)
执行完成所有脚本添加后,进行发布
python ./src/cicd/cdn_es.py action=publish_test_env domain={domain} --id={ak} --secret={sk}
4. 查询生产环境规则(可选)
建议在最后执行该步骤,方便日后追溯查询
python ./src/cicd/cdn_es.py action=query_product_env domain={domain} --id={ak} --secret={sk}