云原生 02:ROS 资源编排,用 Custom Container 做 Serverless,Flow 云效流水线

系列专栏声明:比较流水,主要是写一些踩坑的点,和实践中与文档差距较大的地方的思考。这个专栏的典型特征可能是 次佳实践,争取能在大量的最佳实践中生存。

TL;DR

  1. 本期基于阿里云,主要介绍用阿里云资源编排服务 ROS,Resource Orchestration ServiceCustom ContainerServerless 的踩坑;不包括 Terraform,不包括 AWS 等其它厂商。
  2. 以 Spring Boot 类比,一个执行持续交付的企业,应该会选择 1 Service + 1 Function = 1 原 Method 的方案;一个执行双周迭代的企业,应该会选择 1 Service + 1 Function = 1 原 Controller 的方案
  3. 发新版的方式是点修改,然后把 FunctionCustomContainerConfigImage 的版本号 +1。

最近突然发现原来 FC,Function Compute 是支持用自定义容器部署的,翻了一下这个功能是 2020-09 上线的,突然就觉得去年一年白折腾了;包括 ECI,Elastic Container InstanceMidway / Egg-Layer 部署模式,都不需要了,Custom Container 就是我一直在找的那个解决方案。又仔细翻了一下产品日志,云效是 2021-03 支持构建镜像并推送到私有仓库的,尝试甩锅是因为云效跟进得太慢导致我没有在内容农场中找到这个宝藏。

一、部署目标

云原生 02:ROS 资源编排,用 Custom Container 做 Serverless,Flow 云效流水线_第1张图片

最终的目标肯定是 IaC,但在这个演进过程中得有取舍,哪些是不得不优先自动化的,哪些是可以暂时用 ClickOps 来过渡的。如果一味的高举 IaC 的口号,那么最先自动化的一定是那些 最容易 被自动化的,而不是 最应该 被自动化的。

二、利用 云效 Flow 拉取代码,并构建镜像发布到 镜像仓库 ACR

关于怎么开通 云效 以及 CodeUpFlow官方文档吧。

Flow 对标的是 Jenkins / Pipeline / CICD,看文档旧版是有配置文件所以可以 IaC 的,新版没有找到这个功能。新版的 ClickOps 够用了,而且我的场景几乎是一次性配置的,不会经常变更,所以够用了。

第一个坑点是,代码源是支持 GitHub 的,但实际并不支持。首先在墙外拉不动代码,对比镜像仓库 ACR 自带的构建功能就可以勾选用海外服务器构建,Flow 没有这个选项。其次就算连上了,速度也会很慢,而流水线是按耗时而不是次数收费的,所以代码源,包括三方库的源实际上都必须在阿里云上面。以及构建镜像时的 FROM,官方文档也建议把基础镜像提前上传到自己的 ACR 私有仓库里,以提高拉取时的效率。跨生态是不存在的,vender lockin 了。

因此就自然引出了第二个坑点,云效的自建三方库只有 Maven 和 NPM,所以这个生态里只能用 Java 和 JavaScript。虽然理论上可以使用自建的 Nexus,但我上云效的目的不就是不想自建么。关于怎么本地开发怎么配置 .m2/settings.xml 才能使用云效提供的 Maven,以及上传一些私有包,参考需要登录云效才能看的文档,能搜到的常规文档里是旧版的,还没更新。Flow 里的 settings.xml 都是配置好的,使用系统推荐不要修改就可以了。

第三个需要注意的点是 工作目录 的概念。代码是可以有多个来源的,理论上可以将来自不同上游的代码分别拉取,然后混在一起,微信小程序的分包似乎就是这个逻辑?从体验上说 monorepo 会更直觉一点,但对多源的支持也许落地时 对企业内部混乱的组织架构支持更好,更容易帮助甲方转型一点。可以将源 A 的工作目录指定为 /path/to/app-a,源 B 的工作目录指定为 /oath/to/app-b,然后使用一些诸如 cd app-b 的命令来操作。

以上,虽然云效是 ClickOps 但刻意没有附云效的截图和代码片段,一是因为没有 IaC 不想贴脚本,等找到了再来更新这个部分;二是交互做得意外的不错吧,按提示到处点点上手很快的。

三、部署在 函数计算 FC 里的最终架构应该是什么样子

以 Spring Boot 类比,直觉的想法是,Service 对应原微服务的 Application,Function 对应的是 Controller 的一个 Method,实践下来并不是这样。

首先 FC 的很多重要配置都是在 Service 这个粒度的,比如 VpcConfigLogConfigTraceConfigNasConfig,所以 Service 这个粒度是最重要的需要仔细思考架构的地方,可以假想对应为一个拆得比较干净的微服务中台 原子 Application。但是现实中的微服务大多拆得不干净,如果是做现存应用的无痛迁移的话,实践上 Application 会对应多个 Service。其次,Method 的粒度又太细了,不好管理,所以实践上 Function 对应的是 ControllerEnvironmentVariables 是配置在 Function 上的,所以如果有一些参数不便共享,也可以使用 Function 来隔离。

接下来直觉的想法肯定是,一个 Service 对应多个 Function,实践下来这样还是不行的,因为现存应用的迁移并没有那么无痛,甚至是很痛,业务方期待的其实还真的是 Method 粒度的渐进式迁移,通过网关,依次将每个接口流量打到新的架构上去观察。所以一个执行持续交付的企业,应该会选择 1 Service + 1 Function = 1 原 Method 的方式做迁移;一个执行双周迭代的企业,应该会选择 1 Service + 1 Function = 1 原 Controller 的方式做迁移。至于迁移全部完成后怎么去做合并,那是另一个故事了。

最后,由于我的项目中只存在 Http 类的接口需求,所以只需要 Http 类型的 Trigger。这里有一个限制,其它类型的 Trigger 可以在 Function 启动之后增删,但 Http 类型的 Trigger 必须在创建 Function 时一并创建,所以我最终的选型是 1 Service + 1 Function + 1 Http Trigger = 1 原 Controller

四、ROS 的最小可用够用集

先吐一个坑点,说是支持 JSON 和 YAML,但文档和示例只有 JSON,而且交互的部分明显只支持了 JSON,比如某个参数是 Map 类型的,即使在 YAML 版里也要传 JSON 进去。总之认为实际只支持 JSON 就对了。

ROS 模板文档。首先想象一下,最终应用是对着一个 YAML 启动的,这个 YAML 就是该应用的 Declarative IaC,通过编排系统的 Reconcile 能力,保证应用的状态和 YAML 的期望是一致的。然后,对于一组架构一致或类似的应用 YAML,把其中的关键参数扣掉改成占位符,等着用户去输入,这就是模板。ROS 把应用的 YAML 叫做 资源栈 Resource Stack,把模板叫做 Template。Template 是有专门的交互去管理的,真的是 YAML 格式,既可以在线编辑,也可以上传下载,也就意味着你可以 GitOps。Resource Stack 没有这个能力,你不能从一个手工写好的填好参数的 YAML 直接启动,必须要先抽象出模板,第一步使用模板,第二步在交互中填入参数。这些参数,比如 VpcId,ROS 是不会帮你管理的,所以我猜实际上企业还是需要自建 CMDB?

{
    "ROSTemplateFormatVersion" : "2015-09-01", // 非常神奇,原来这么早就开始搞了
    "Description" : "",
    "Resources" : {
        "ServiceA": {
            "Type": "ALIYUN::FC::Service",
            "Properties": {
                "ServiceName": { // 这里的命名要按接口文档来
                    "Ref": "ServiceName" // 这里 ref 的是 Parameter 里面定义的「占位符」处由用户输入的值,这里的命名自己定,比如不同的 Type 有同名的,就可以自定义命名成 ServiceNameForServiceA
                },
                ...
            }
        },
        "FunctionA": { "Type": "ALIYUN::FC::Function", "DependsOn": "Service", "Properties": {} },
        "TriggerA": { "Type": "ALIYUN::FC::Trigger", "DependsOn": "Function", "Properties": {} },
    },
    "Parameters" : {}, // 参数,每种资源都有自己独立的文档详解需要哪些参数
    "Metadata" : {}, // 例如存放用于可视化的布局信息
    "Outputs" : {}// 用于输出一些资源属性等有用信息。可以通过API或控制台获取输出的内容
}

关于 Resource,这里是 Declarative 的部分,就是你对最终希望启动的资源达到的状态的描述。ServiceA变量命名,不需要带有 Service 字样,"Type": "ALIYUN::FC::Service" 才是指定资源类型。如果需要一组启动多个 Service,那命名就应该形如 ServiceA,ServiceB;Function,Trigger 及其它也一样。这个 变量命名 是用来在配置文件内 编程 的,好奇怪的说法,我也没研究过 Terraform,不知道这个理解是否正确。比如在 Output 里,由于 Service 是系统拉起来的,所以 ServiceId 是实时生成的,那么如果后面需要使用到这个值,就可以用 "Fn::GetAttr": [ "ServiceA", "ServiceId" ] 来获取这个值。

Type 指定了资源类型,每种 Type 都有专门的 文档 描述该类型有哪些参数必填或选填,从这里面挑你需要的参数放到 Properties 里面,在从模板启动的时候会要求用户提供这些参数,作为输入来启动资源。

"Parameters": {
    "TriggerName": {
      "Type": "String",
      "Default": "http-index"
    },
    ...
}

Parameters 是用来做低代码自动生成交互的,Type: String 会被渲染成 ,Type: Boolean 会被渲染成 等等。另外有一个 AssociationProperty 的概念,看上去可以渲染成对接阿里云资源接口的