阿里云CDN-边缘脚本EdgeScript的CI/CD实践

阿里云CDN-ES脚本CI/CD实践

  • 背景
  • 环境
  • 项目代码结构及发布脚本代码
    • [1. 项目结构](#1. 项目结构)
    • [2. 发布工具代码](#2. 发布工具代码)
  • 流水线配置
    • [1. 流程配置](#1. 流程配置)
    • [2. 脚本代码](#2. 脚本代码)
      • 发布脚本说明
        • [0. 配置账户](#0. 配置账户)
        • [1. 清空测试环境(回滚测试环境)](#1. 清空测试环境(回滚测试环境))
        • [2. 执行脚本发布](#2. 执行脚本发布)
        • [3. 发布(测试环境推送到生产环境)](#3. 发布(测试环境推送到生产环境))
        • [4. 查询生产环境规则(可选)](#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。

代码如下:

python3 复制代码
#!/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 traceback

access_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 res


def 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 signature


def 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'] = signature
    url = cdn_server_address + "/?" + urllib.parse.urlencode(parameters)
    return url


def 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_id
    global access_key_secret
    if options.accesskeyid is None or options.accesskeysecret is None:
        # 在这条分支下,命令中没有ak和sk
        try:
            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和sk
        access_key_id = options.accesskeyid
        access_key_secret = options.accesskeysecret


def 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'] = code
                    rule_cfg['functionArgs'].append(arg_cfg)
                else:
                    arg_cfg['argName'] = k
                    arg_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_args


if __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 command
        configure_accesskeypair(args, options)
        sys.exit(0)

    user_params = {}
    idx = 1
    if sys.argv[1].lower().startswith('action='):
        _, value = sys.argv[1].split('=')
        user_params['action'] = value
        idx = 2
    else:
        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_env
                if 'rule' not in user_params:
                    user_params['rule'] = []
                user_params['rule'].append(json.loads(value))
            else:
                user_params[key.strip()] = value
        except 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. 如果发布的流水线的域名是固定的,可在发布脚本中直接配置。
bash 复制代码
# 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}
相关推荐
长征coder2 分钟前
AWS MySQL 读写分离配置指南
mysql·云计算·aws
Johny_Zhao2 小时前
Docker 一键安装部署 JumpServer 堡垒机
linux·网络安全·信息安全·云计算·shell·jumpserver·ldap·yum源·系统运维
热爱生活的猴子8 小时前
阿里云服务器正确配置 Docker 国内镜像的方法
服务器·阿里云·docker
艾伦_耶格宇11 小时前
【ACP】阿里云云计算高级运维工程师--ACP
运维·阿里云·云计算
Johny_Zhao14 小时前
Ubuntu系统安装部署Pandawiki智能知识库
linux·mysql·网络安全·信息安全·云计算·shell·yum源·系统运维·itsm·pandawiki
裁二尺秋风16 小时前
CI/CD — Pipeline的使用以及Blue Ocean多分支流水线的使用方法
ci/cd·gitlab·jenkins
云资源服务商16 小时前
阿里云Flink:开启大数据实时处理新时代
大数据·阿里云·云计算
数据与人工智能律师18 小时前
数字资产革命中的信任之锚:RWA法律架构的隐形密码
大数据·网络·人工智能·云计算·区块链
真实的菜19 小时前
Jenkins 插件深度应用:让你的CI/CD流水线如虎添翼 [特殊字符]
servlet·ci/cd·jenkins
快快网络-三七1 天前
云计算环境下的成本控制策略:按需计费与资源弹性扩展详解
服务器·开发语言·数据库·安全·云计算·perl