iHealth基于Docker的DevOps CI/CD实践

2020 年 4 月 12 日

iHealth基于Docker的DevOps CI/CD实践

本文由1月31日晚iHealth运维技术负责人郭拓在Rancher官方技术交流群内所做分享的内容整理而成,分享了iHealth从最初的服务器端直接部署,到现在实现全自动CI/CD的实践经验。文末添加Rancher小助手为好友加入技术交流群,可实时参加下一次技术分享~


前言


相信我,一切事情的发生都是赶鸭子上架,没有例外。人类所有伟大的变革都是迫不得已,可又是那么顺其自然。比如容器(docker)技术的诞生,比如箭在弦上的创业,比如野心勃勃的 kubernetes,比如如今已作为左膀右臂的 rancher,比如这篇文章。


iHealth 致力于用全新的移动互联体验整合传统的个人健康管理方式,公司业务范围包含移动医疗、慢病护理以及健康与医疗硬件研发。不同于郑兄的 CI/CD 实践《如何利用Docker构建基于DevOps的全自动CI》,我们结合自身状况,构建了一套我们自己的 DevOps CI/CD 流程,更轻更小,更适合 Startup。


合适的才是最好的(Node.js & Docker)


如果世界只有 FLAG、BAT,那就太无趣了。iHealth 是一家初创型公司,我所在的部门有大概 10 名研发人员,担负着三端研发工作的同时,所有围绕服务的交付和运维工作也都是我们来做。


技术的选型上,服务端、Web 端和移动端(Android、iOS)都要上,但人少。所以招人的时候并没有以貌取人,部门对外的 Title 都是全栈。能一门语言通吃三端,群众基础广泛,恐怕没有比 Javascript/Typescript(Node.js)更合适的了。


服务端有 Express、Koa、Feather、Nest、Meteor 等各有其长的框架,前端大而火的 Reactjs、Vuejs 和 Angular,不管是 Server Render 还是前后端分离,都可以得心应手。因为公司的健康设备(血糖仪、血压计、体温计、血氧、体脂秤等等)会有专门的部门研发设计以及提供 SDK,所以移动端的研发工作更多是在设计实现和性能优化上,React Native 是一枚大杀器。虽然现在公司并没有桌面端的需求,但不能否认的是 Electron 是一个很有趣的项目,也为“全栈”这个词增加了更多背书。



另外,选择使用 Node/Js/Ts 作为全栈的基础会附带有 RPC 的好处。无需集成传统意义上的 RPC 框架(如 gRPC),只需在编写远程(微)服务方法时,编写相应的 npm package,也可以达到相同的目的,且成本更小,更易理解。


运维环境的选型上,所有的业务都运行在云端,省去了机房维护和服务器运维的成本。其实在盘古开荒时,我们也是编写了 Node 程序后,使用 PM2 部署在服务器上,并没有使用 Docker。当然也存在没有使用 Docker 所带来的一切问题:三端不同步、环境无法隔离……而 Docker 带给我最大的惊喜除了超强的可移植性,更在于研发人员可以非常容易对程序的顶级架构进行推理。


事实上,我们直接使用 docker-compose 做容器编排着实有一段时间,在一次大规模的服务器迁移中,发现需要重新思考越来越多的 container 管理和更完善的编排方案。Rancher(Cattle)就是在这时被应用到技术栈中。


一切从 Github 开始


在运维环境一波三折的同时,DevOps 的征程也是亦步亦趋,步步惊心。幸运的是,我们知道自己缺乏什么,想要什么,所以能比较容易的做到“哪里不会点哪里”。如同上一章节所述,合适的才是最好的。持续集成(CI)与持续交付(CD)的迭代过程,从最初的代码拷贝,到结合 docker-compose 与 rsync 命令,到使用 CI/CD 工具,做到相对意义上的自动化……迄今为止,我们摸索出一套相对好用并且好玩的流程:



故事大致是这样的,当一只代码猴提交代码之后,他需要去接一杯咖啡。在猫屎氤氲的雾气里 45°角仰望天花板,手机微信提醒这次构建成功(或失败,并附带污言秽语)。这时他可以开始往工位走,坐下时,微信又会提醒本次部署到 Rancher 成功(或失败)。


这一切开始的地方是 github。当开发者写完 BUG 功能之后,需要有地方保存这些宝贵的资料。之所以没有使用 Gitlab 或 Bitbucket 搭建私有的 Git 服务器,是因为我们认为代码是最直接的价值体现。服务如骨架,终端如皮肤,UE 如衣服,三者组成让人赏心悦目的风景,代码是这背后的基础。我们认为在团队精力无法更分散、人口规模尚小时,购买 Github 的商业版是稳妥且必要的,毕竟那帮人修复一次故障就像把网线拔下来再插上那样简单。


Drone CI


Drone 这个单词在翻译中译作雄蜂、无人机。我特意咨询了一位精通一千零二十四国语言的英国朋友,说这个词的意思是 autonomous,works by itself。白话就是有活它自己干,而且是自主的。不过这个解释对于 Drone 来说名副其实。这个在 Github 上拥有 13,000+ Stars 的开源项目,使用 Golang 编写,相比 Jenkins 的大而全,Drone 是为 Docker 而生的 CI 软件。如果有使用过 Gitlab CI 的小伙伴,相信对 Drone 的使用方式不会感到陌生,他们都是使用 Yaml 风格文件来定义 pipeline:


pipeline:  build:    image: node:latest    commands:      - npm install      - npm run lint      - npm run test  publish:    image: plugins/npm    when:      branch: master
复制代码


Drone 的安装方式如同 Rancher 一样简单,一行 docker 命令即可。当然,大家也可以看 Drone 的官方文档,在这里,只讲一下使用 Rancher catalog 安装 Drone 的方式:




查看大图大家可以看到 Drone 使用 Rancher catalog 安装的方法(with github),在 Github 的 Settings 中创建 Drone 的 OAuth App 时,Home Page Url 务必要写你能访问 Drone 的 IP 地址或域名,例如:


http://drone.company.com


而 OAuth App 的 Authorization callback URL 应该对应上面的写法:http://drone.company.com/authorize


小功告成:



登录进 Drone 之后,在 Repositories 中找到你想要开启 CI 的 Git Repo,用 switch 按钮打开它:



这表示已经打开了 Drone 对于这个 Repo 的 webhook,当有代码提交时,Drone 会检测这个 Repo 的根目录中是否包含.drone.yml 文件,如存在,则根据 yaml 文件定义的 pipeline 执行 CI 流程。


Drone 与 Rancher、Harbor、企业微信的集成


在决定使用 Drone 之前,需要知道的是,Drone 是一个高度依赖社区的项目。其文档诸多不完善(完善过,版本迭代,文档跟不上了),plugins 质量良莠。但对于擅长 Github issue、Google、Stackoverflow 的朋友来说,这并不是特别困难的事情。Drone 也有付费版本,无需自己提供服务器,而是像 Github 那样作为服务使用。


如果你决定开始使用 Drone,截止到上面的步骤,我们打开了 Drone 对于 Github Repo 的监听,再次提醒,需要在代码 repo 的根目录包含.drone.yml 文件,才会真正触发 Drone 的 pipeline。


那么,如果想重现上面故事中的场景,应该如何进行集成呢?


我司在构建 CI/CD 的过程中,现使用 Harbor 作为私有镜像仓库,从提交代码到自动部署到 Rancher,其实应当经历如下步骤:


  • 提交代码,触发Github Webhook

  • Drone使用docker插件,根据Dockerfile构建镜像,并推送到Harbor中

  • Drone使用rancher插件,根据stack/service,部署上面构建好的image

  • Drone使用企业微信插件,报告部署结果


在这里节选公司项目中的一段 yaml 代码,描述了上述步骤:


# .drone.yamlpipeline:  # 使用plugins/docker插件,构建镜像,推送到harbor  build_step:    image: plugins/docker    username: harbor_username    password: harbor_password    registry: harbor.company.com    repo: harbor.company.com/registry/test    mirror: 'https://registry.docker-cn.com'    tag:      - dev    dockerfile: Dockerfile    when:      branch: develop      event: push
# 使用rancher插件,自动更新实例 rancher: image: peloton/drone-rancher url: 'http://rancher.company.com/v2-beta/projects/1a870' access_key: rancher access key secret_key: rancher secret key service: rancher_stack/rancher_service docker_image:'harbor.company.com/registry/test:dev' batch_size: 1 timeout: 600 confirm: true when: branch: develop event: push
# 使用clem109/drone-wechat插件,报告到企业微信 report-deploy: image: clem109/drone-wechat secrets: - plugin_corp_secret - plugin_corpid - plugin_agent_id title: '${DRONE_REPO_NAME}' description: | 构建序列: ${DRONE_BUILD_NUMBER} 部署成功,干得好${DRONE_COMMIT_AUTHOR} ! 更新内容: ${DRONE_COMMIT_MESSAGE} msg_url: 'http://project.company.com' btn_txt: 点击前往 when: branch: develop status: - success
复制代码


对接企业微信之前,需要在企业微信中新建自定义应用,比如我们的应用名字叫 Drone CI/CD。当然,您也可以给每一个项目创建一个企业微信 App,这样虽然麻烦,但是可以让需要关注该项目的人关注到构建信息。


下面是企业微信测试的截图:



企业微信与微信客户端是连通的,可玩性还不错:



在这里我认为 有必要提醒一下,使用 Drone 的企业微信插件时,不要使用 Drone Plugins 列表里的企业微信。翻阅其源码可以发现,其中一个函数会将企业的敏感信息发送至私人服务器。不管作者本身是出于 BaaS 的好意,还是其它想法,我认为 都是不妥的



代码地址:https://github.com/lizheming/drone-wechat/blob/master/index.js


在此 Drone Plugins 里的企业微信插件出现很久之前,我的好友 Clément 克雷蒙同学写了一个企业微信插件,至今仍在使用。欢迎检查源代码,提 issue 提 bug,为了不让克雷蒙同学骄傲,我并不打算号召大家给他 star:clem109/drone-wechat


而在构建完成后,可以看到 Drone 控制面板里小伙伴们战斗过的痕迹:



ELK 与 Rancher 的集成


ELK 是 ElasticSearch、Logstash 与 Kibana 的集合,是一套非常强大的分布式日志方案。ELK 的使用更多在于其本身的优化以及 Kibana 面向业务时的使用,这本身是一个很大的话题,只 ElasticSearch 就有许多奇技淫巧。因为人力资源的原因,我们使用了兄弟部门搭建的 ELK,等同于使用已有的 ELK 服务。所以在此也不再赘述 ELK 的搭建,网上有许多资源可供参考。


在这里要做的事情,就是把 rancher 中的日志归集到已有的 ELK 中。


在 Rancher 的 catalog 中找到 logspout,这是一个 logstash 的 adapter,为 docker 而生:



在配置中设置 LOGSPOUT=ignore,然后把 ROUTE_URIS 设置为已经搭建好的 logstash 地址,就可以将当前环境的日志集成到 ELK 中:



Traefik 与 Rancher 的集成


目前看来一切都很好,对吗?的确是这样。我们提交了代码,drone 自动构建镜像到 harbor,自动部署到 Rancher,自动发送构建结果,Rancher 又可以帮助自动重启死掉的 container,使用 Rancher webhook 也可以实现自动弹性计算,并且可以使用 yaml 文件定制构建流程,定制一些 report 信息,当构建或部署失败时,让企业微信自动侮辱我们的小伙伴……


可是据说微服务还讲究服务注册和服务发现,如果并不想动用 Zookeeper 这样的核武器(就像我们不想用 Kong 一样,一是有一定学习和维护成本,二是 Logo 越改越丑),那就需要找到一个轻量级,能满足需求的替代品。况且目前并没有遇到需要削峰的处理。


对于域名的解析,我们选择使用 Traefik 作为 LB,这个同样使用 Golang 编写,同样拥有将近 13,000 Stars,并且兼具简单的服务注册和服务发现功能。更值得一提的是,Rancher catalog 里的 Traefik 非常友好的集成了 Let’s Encrypt(ACME)的功能,可以做到自动申请 SSL 证书,过期自动续期。当然,不推荐在生产环境使用,SSL 免费证书的数量非常容易达到阈值而使得域名无法访问。


Traefik 内部架构图(Image from traefik.io):



如何安装 Traefik 呢?我们以 Rancher catalog 中的 Traefik 为例(不使用 ACME):



我们的目的是做域名解析,integration mode 应该设置为 metadata。Http Port 设置为 80,Https Port 设置为 443,Admin Port 可以根据自己实际情况填写,默认 8000。


此时的 Traefik 已经准备就绪,但是打开 traefik_host:8000 查看控制面板时,发现 Traefik 并没有做任何代理。原因是需要在代理的目标中,使用 rancher labels 标示出 traefik 的代理方式。


比如刚才安装的 Drone,如果我们想代理到 drone.company.com 这个域名,则需要在 drone server 的 container 中设置 lables:



  • traefik.enable=true 表示启用traefik代理

  • traefik.domain=company.com 表示traefik代理的根域名

  • traefik.port=8000 表示这个container对外暴露的端口

  • traefik.alias=drone 表示想将drone server这个container解析为drone.company.com


需要注意的是,traefik.alias 有可能导致重复解析,同时 traefik 有自己的一套默认解析规范。更详细的文档请看 GitHub 地址:


rawmind0/alpine-traefik


在设置 Rancher labels 后,可以看到 Traefik 的控制面板中,已经注册了服务地址:



利用 Traefik 的这个特性和 Rancher 对于 Container 的弹性计算,可以做到简单的服务注册和服务发现。


最后需要在域名服务商那里做 A 记录解析,解析的 IP 地址应为 Traefik 的公网地址。 因为域名解析的默认端口是 80 和 443,后面发生的事情就和 Nginx 的作用一毛一样了。域名解析到 Traefik 服务器的 80 端口(https 则是 443),Traefik 发现这个域名已经注册到服务中,于是代理到 10.xx 开头的虚拟 IP,转发请求并发送 response。与 Nginx Conf 如出一辙:



至此,我们已经完全实现从代码提交,到自动部署以及域名解析的自动化。在生产环境的 Traefik on Rancher 中开启 Https,可以把 ssl 的整个信任链以文本的形式粘贴进去,同时修改 Traefik 的 Https 选项为 true 即可:



另外,Traefik 并不是 LB/Proxy 的唯一选择,甚至不是最酷的选择,但确是目前与 Rancher 集成最好的。下面图中的程序都值得做调研(可以小小的注意一下 istio,天庭饱满,骨骼轻奇,这还只是 2017 年 7 月底的数据……):



事实上对于 Traefik 我们是又爱又恨。它能非常方便的与 Rancher 集成,功能简便强大,性能可观。但在最开始着实踩了不少坑,一度打算放弃并回归到传统的 Nginx 做反向代理的方式,甚至写了 PR 并被 merge 到 master 中。截止目前 Rancher Catalog 中最新的 1.5 版本,已经是一个真正稳定可用的版本了。


小技巧


Node.js 的项目中书写 Dockerfile 时,经常会用到 yarn 或者 npm i 来拉取依赖包。但 npm 的服务器远在世界的另一端,这时可以使用淘宝的镜像进行加速。通常我们在本地开发时执行会记得加上 npm 镜像,在服务器上跑 Dockerfile 也是一样的道理:


FROM node:alpineWORKDIR /appCOPY package.json .RUN npm i --registry https://registry.npm.taobao.orgCOPY . .CMD [ "node", "bin/www" ]
复制代码


Drone 在构建镜像并推送到镜像仓库时,需要根据 Dockerfile 的基础镜像进行构建,而 docker 服务器也远在世界的另一端,同样的可以使用 mirror 来指定镜像仓库,并尽量使用 alpine 镜像缩小体积:


pipeline:  build_step:    image: plugins/docker    username: harbor_name    password: harbor_pwd    registry: harbor.company.com    repo: harbor.company.com/repo/test    mirror: 'https://registry.docker-cn.com'
复制代码


作大死命令,不要在服务器上使用。但本地开发很好用。意思是停止所有 container,删除所有 container,删除所有 image:


docker stop $(docker ps -aq) && docker rm $(docker ps -aq) && docker rmi $(docker images -aq)
复制代码


结语,附带工具链汇总


罗马不是一天建成,万丈高楼平地起。在企业发展之初,我们在打基础的同时,也要保证项目高速迭代。短时间内无法做到 Netflix 的体量以及其对于微服务治理的精妙,在运作的细节中也有诸多需要完善的部分,例如 BDD、TDD 的实践,传统意义上的 UAT 与蓝绿灰度发布,移动时代的全链路日志,服务熔断、隔离、限流以及降级的能力,亦或是星火燎原的 Service Mesh……所以退一步讲,必须先生存,才能生活。我们可以允许服务死掉,但是要保证无感知或极短感知的情况下,服务能迅速的活过来。


在持续交付的过程中,我们也尝试使用 sonar 代码质量管理,使用 phabricator 作为 code review 环节,因为配置的变更和微服务数量的逐渐增多,配置中心(主要考虑携程的 Apollo)的引入也迫在眉睫,调用链监控以及代码重新埋点的成本(二节所述 npm package rpc 的优势又可体现)是否能抵过其带来的好处等等。但因目前尚未达到一个非常成熟的阶段,所以本次不再分享,仅表述其名来启发各位聪明的小伙伴。


除此之外,技术视野的成长也非朝夕。就像我国政府在大家买不起自行车时就开始修建高速公路,时至今日,还能说它是面子(KPI)工程吗?与社区一同进步,开阔视野的同时,保持独立思考的能力,是比上述所有更为重要的技能。


回到本文开头所写,一切都是赶鸭子上架。与其说笔者天资聪慧才貌过人风度翩翩儒雅风流,不如说这都是被逼的。同事抱怨流程繁琐不直观,若要做到代码和咖啡那样大繁若简,就需要思考 CI/CD 的目的与本质。大智若愚,真正的天才,必须能够让事情变得简单。


拓展资料:


Rancher: https://github.com/rancher/rancher


Drone: https://github.com/drone/drone


Drone 企业微信 API 插件: clem109/drone-wechat


Harbor: vmware/harbor


Traefik: containous/traefik


Phabricator: phacility/phabricator


SonarQube: SonarSource/sonarqube


Logspout: gliderlabs/logspout


配置中心(携程做的,代码写的还不错): ctripcorp/apollo


SuperSet(BI): apache/incubator-superset


Q&A


Q:traefik api 方式有没有尝试过?有没有坑分享下。可以对 rancher lb 做转发吗?


A:没有尝试过 traefik api。我尝试对 rancher lb 做转发,但是没有成功。


Q:如果 traefik 所在的主机崩溃掉了怎么办?有高可用么?npm 的那个我的做法是在物理机上做好安装然后再 copy 容器里面。


A:traefik 的安装依赖于 traefik=true 的所有主机,所以是课已做到部分高可用。另外,traefik 也本身也是 container 的形式,也可以考虑针对 container 做 HA。


Q:因为域名是解析到 traefik 的公网 IP 所以是不是可以用 vip 的形势漂移,到另外一台具有 traefik 的主机实现 ha?


A:我感觉是可以的,这是个很好的思路,回头我要试试,哈哈。


Q:请问服务器都是远程的话下载和更新不慢么?


A:原生是慢的,所以文中有使用镜像的方法。对于部署时,因为 harbor 和业务服务器在一个内网,或者做对等网络,速度也是很快的。


Q:Reactjs、Vuejs 和 Angular 贵公司选的哪个?


A:首选 Reactjs,毕竟重度使用 React Native。内部一些 admin portal,可以自由选择,但目前并没有发现有队友主动使用 angular。


Q:你们生产现在使用 Rancher 做多少节点?网络模式使用的是默认 Rancher 代理吗? Rancher 多节点的持续部署与单节点有没有什么不同?


A:我们服务器规模不大,生产环境只有三台服务器。数据库是单独的另外三台。网络模式也不复杂,traefik 代替了 nginx 的地位做反向代理。多节点持续部署这个我不太清楚,和其他非生产环境感觉上并没有什么不同。


Q:请问 Rancher 在管理大规模 docker 时底层的网络结构和存储是怎么处理的?


A:这个问题有些宽……网络结构并不复杂,traefik 接到请求后转发到不同的微服务,服务器的首选 DNS 是我们自己搭建的,直接指向服务器的内网地址。存储使用 docker volume,服务器会单独挂载一块硬盘用于 docker 的存储。


Q:微信插件关联是可以按项目分别指向不同的企业号么?是否需要多个 drone?


A:不知道您说的企业号是不是企业微信,二者不同,文中指的是企业微信。


可以使用一个 drone,也可以使用多个。但我感觉使用 1 个就可以。因为 drone 与企业微信集成时,可以根据项目的不同指向不同的企业微信 agent id,就可以做到不同 repo 分发到不同的企业微信应用中。


Q:如何跟 git flow 结合的?是每个 release 开一个分支,然后分支合并到 develop 或者 master 的时候开启 pipeline?


A:可以都开,也可以只在 master 开。这是我觉得 drone 和 gitlab ci 做的很好的地方。查看 drone 的 yaml 文件可以看到,可以指定


when  branch=*
复制代码


的时候触发不同的构建,并且支持通配符。这就可以做很多想象了。


Q:traefik 负载可以与很多种服务发现结合,请问下你们选择的是哪种?为什么这么选呢?


A:直接使用的 traefik 本身。因为人少,精力少,业务上的并发并不惊人。


Q:选择 Reactjs 在苹果上架的时候有没有什么审核的限制?最近使用 h5 做苹果应用, 发现 H5 与原生代码的比例过高, 苹果上架审核不能通过, Reactjs 有没有这种问题?


A:据我所知并没有这个问题。我们大部分被驳回的理由都是文案、截图,或者点击提交按钮前没有默念佛祖保佑。


Q:traefik 的性能和 nginx 哪个更好,有做过压测吗?


A:traefik 官方的说法是 traefik 比 nginx 更快,但我们的测试看出不有太多优势。胜在配置和部署更方便吧。


Q:请问 sonar 服务在流水线哪个环节引入?应用哪些插件?给出哪些指标?


A:sonar 并未引入,在我们 todo 里。


Q:请问 CI/CD 对小型团队来说有必要吗,帮助有多大?


A:我们就是小型团队,如果花时间重复做几乎相同的事情,我觉得这些东西都是有必要自动化的。


Q:logspout 能再详细一点吗?


A:logspout 的使用真的是简单的过分。真的就是只需要配置那两个变量!这一点我最初也表示非常震惊,感谢 Rancher。


作者简介


郭拓,北京爱和健康科技有限公司(iHealth)。负责公司基础服务构建与研发流程定制,曾供职于乐视、21vianet,高龄攻城狮活跃在一线研发工作中,乐此不疲。


2020 年 4 月 12 日 20:39126

评论

发布
暂无评论
发现更多内容

架构师训练营学习小结(2020.9.14 - 9.20)

zjzj2017

在多架构时代,英特尔扩展高性能计算边界

intel001

最通俗易懂的——如何将机器学习模型的准确性从80%提高到90%以上

计算机与AI

学习 数据科学

LeetCode题解:641. 设计循环双端队列,使用队列,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

鸿蒙系统究竟是PPT秀还是有真材实料?鸿蒙HarmonyOS开发环境搭建与运行Demo

软测小生

华为 鸿蒙 HarmonyOS

架构师训练营营第 1 期之框架设计02

天行健

架构师训练营第二周作业

zjzj2017

收款神器!解读聚合收款码背后的原理

楼下小黑哥

学习思路

hasWhere

栈与队列简介

Java旅途

数据结构 队列

数字货币合约交易所开发源码,永续合约开发app

WX13823153201

数字货币合约交易所开

架构师训练营学习小结(第二周2020.9.21 - 9.27)

zjzj2017

c++ 杂谈3

菜鸟小sailor 🐕

格式化报文输出

hasWhere

高难度对话读书笔记—求助的勇气

wo是一棵草

TensorFlow 篇 | TensorFlow 2.x 基于 HParams 的超参数调优

Alex

tensorflow keras hparams tensorboard 超参数调优

java安全编码指南之:敏感类的拷贝

程序那些事

Java java安全编码 java安全 java安全编码指南

基于数组的有界阻塞队列 —— ArrayBlockingQueue

程序员小航

Java 源码 队列 源码阅读 JUC

《我在你床下》观后感

徐说科技

Redis 缓存性能实践及总结

vivo互联网技术

redis redis集群 redis监控

如何避免option请求

hasWhere

一文了解Zookeeper

Java旅途

kafka zookeeper 分布式

onblur调用alert导致的死循环

hasWhere

逼着面试官问了我ArrayList和LinkedList的区别,他对我彻底服了

沉默王二

Java ArrayList linkedlist

学习路线

hasWhere

数据提交

hasWhere

ARChatRoom功能介绍手册

anyRTC开发者

音视频 WebRTC 语音 RTC 安卓

CICD实战——服务自动构建与部署

TARS基金会

DevOps 后端 jenkins CI/CD TARS

架构师训练营第 1 期 第 2 周作业

李循律(祥龙)

极客大学架构师训练营

Http自定义请求头接收不正确

hasWhere

form表单提交get请求

hasWhere

iHealth基于Docker的DevOps CI/CD实践-InfoQ