当前位置: 首页 > news >正文

阿里云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工具代码改造而来。优化点如下:

  1. 修改原始代码为python3.x语法。
  2. 支持命令行直接传递 --id=${AK} --secret=${SK},无需先执行config命令,再执行部署命令。
  3. 修复原代码中的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. 脚本代码

老规矩,先发布代码。读代码前注意:

  1. 变量${domain}为流程需要进行变更的域名。在当前流水线中,配置在了“变量和缓存”中,作为字符串变量
  2. 如果发布的流水线的域名是固定的,可在发布脚本中直接配置。
# 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}

相关文章:

  • MTK Android12 SystemUI 手势导航 隐藏导航栏底部布局
  • Tomcat 使用和配置文件(详解)
  • Spring Boot - 通过ServletRequestHandledEvent事件实现接口请求的性能监控
  • <数据集>停车场空位识别数据集<目标检测>
  • LabVIEW位移检测系统
  • 【CPP】slt-list由认识到简化模拟实现深度理解~
  • 储能集装箱动环监控系统,动环监控在集装箱的应用方案@卓振思众
  • 安科瑞Home EMS:引领家庭光储新纪元,让每一度电都尽在掌握
  • 旋转图像
  • Linux驱动开发—平台总线模型详解
  • 【多线程-从零开始-捌】阻塞队列,消费者生产者模型
  • Unity Android端截图保存并获取展示
  • Milvus向量数据库的简介以及用途
  • 怎么判断张量的维度(形状(shape)),即如何定义行数、列数和深度的?
  • ARM 架构硬件新趋势:嵌入式领域的未来
  • avalon2.2的VM生成过程
  • Date型的使用
  • HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP
  • JavaWeb(学习笔记二)
  • Java超时控制的实现
  • JS 面试题总结
  • js中forEach回调同异步问题
  • 浮动相关
  • 爬虫模拟登陆 SegmentFault
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 一天一个设计模式之JS实现——适配器模式
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • 用Python写一份独特的元宵节祝福
  • 智能网联汽车信息安全
  • mysql 慢查询分析工具:pt-query-digest 在mac 上的安装使用 ...
  • 关于Android全面屏虚拟导航栏的适配总结
  • ​【经验分享】微机原理、指令判断、判断指令是否正确判断指令是否正确​
  • ​LeetCode解法汇总307. 区域和检索 - 数组可修改
  • ​用户画像从0到100的构建思路
  • ( 用例图)定义了系统的功能需求,它是从系统的外部看系统功能,并不描述系统内部对功能的具体实现
  • (55)MOS管专题--->(10)MOS管的封装
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (第30天)二叉树阶段总结
  • (第8天)保姆级 PL/SQL Developer 安装与配置
  • (九)信息融合方式简介
  • (考研湖科大教书匠计算机网络)第一章概述-第五节1:计算机网络体系结构之分层思想和举例
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (删)Java线程同步实现一:synchronzied和wait()/notify()
  • (四)Tiki-taka算法(TTA)求解无人机三维路径规划研究(MATLAB)
  • (算法)硬币问题
  • (循环依赖问题)学习spring的第九天
  • (转)【Hibernate总结系列】使用举例
  • (转载)hibernate缓存
  • .net 4.0发布后不能正常显示图片问题
  • .NET CLR基本术语
  • .Net Core webapi RestFul 统一接口数据返回格式
  • .net core使用EPPlus设置Excel的页眉和页脚
  • .NET 反射 Reflect
  • .NET 中创建支持集合初始化器的类型
  • .NET8 动态添加定时任务(CRON Expression, Whatever)