NodeJS 实战系列:DevOps 尚未解决的问题
本文将通过展示 NodeJS 应用里环境变量的提取过程,来一窥 DevOps 技术是如何应用在现在云平台上的运维工作中的。同时我也想让大家在这里看到 DevOps 的另外一面,即它并非全能,从本地开发到持续部署再到实际运行,有一些运维鸿沟依然还未被填平。“人工操作”依然是工作中的最大风险。
实战系列来自于个人开发以及运维 site2share 网站过程中的经验
不完美的环境变量抽取
有一件事我想我们都会同意,那就是不应该在代码中硬编码(hard code)环境变量,比如生产环境数据库的用户名和密码。之所以称之为环境变量,是因为这些变量是依据你代码部署的环境而定的,例如生产环境和本地环境数据库用户名和密码就多半不会相同。下面就是一段使用 sequelize 连接数据库的代码的反例,其中硬编码了用户名和密码:
const connectionCfg = {
username: "admin",
password: "!password",
database: "order_db",
dialect: "mysql",
}
new Sequelize(connectionCfg);
将环境变量硬编码在代码中会产生几个问题:
- 安全:将用户名密码暴露给所有的开发人员并不安全,删库跑路的风险大大增加
- 耦合:运维同学如果需要对环境进行变更(比如迁移、新增),需要联系“无辜的”开发同学重现编辑和部署代码
所有我们应该利用 process.env 或者是 dotenv 工具,来将环境变量独立管理,让代码与环境解耦。
思路我们有了,本地开发的时候如果你使用了 dotnev,它默认会读取 .env 里的内容作为环境变量。但是当考虑到下一步我们把应用部署到生产环节时,你要面临的问题就是如何在你的云服务商环境上管理你的环境变量了。
这取决于你选择云服务解决方案。以 site2share 为例,因为我选择将它部署在 Azure App Service(App Service 简单来说是一个托管性质的服务部署平台,我可以快速部署代码而不用去维护背后的运行环境,并且它还提供了诸如监控、灰度部署等常用功能) 上,所以我可以直接使用 App Service 为我准备的环境变量解决方案。我只需要在我环境的 UI 界面上填写我的环境变量即可:

基本上目前主流云服务厂商提供的完全托管服务(full managed service)都提供类似的机制,下面是 Digital Ocean 的 App Platform 方案提供的环境变量管理界面:

到这里看似完美了——这样会带来一个风险:它并非是 IaC(Infrastructure as Code) 的。
什么是 IaC 我在这里举个例子,例如每次代码提交之后都会触发流水线运行测试,这里我把“需要运行测试”这件事记录在一个 yaml 文件里并且提交:
- script: |
npm run test
displayName: 'npm test'
这样将来无论是我自己回忆,还是新同学加入,都可以通过阅读这个文件判断流水线干了什么。如果有一天测试不小心被删掉了,也能追溯为什么被删以及被谁删掉的
非 IaC 的风险在于,如果你将来本地开发时新增了环境变量 REDIS_HOST,你需要保证云平台上的每个环境也需要新增 REDIS_HOST 变量(反过来说不用的环境变量也应该删除来避免将来给维护人员带来疑惑),然而这个步骤目前看来完全是依赖人工来完成的,因为云平台无法自动识别到你新增了变量
这里我们遇到了第一道鸿沟:开发环境与云平台的环境变量同步问题

问题才刚刚开始
DevOps 没有解决的问题
“人” 是DevOps 想要解决的问题之一,如果每次上线的成功都依赖自资深工程师的经验,那出错不过是早晚的事情而已。 把难的事情频繁做就是 CI(持续集成)/CD(持续交付) 的背后思想。
例如我们常常需要对数据库做变更,新增表或者列。把这类操作交给程序员手工去每个环境手工执行并非明智选择,我们应该把它交给持续交付的流水线去完成,这样会推动我们部署环境的标准化和自动化,减少风险。这一步骤我们将它命名为数据迁移( migration),我们看看 site2share 的数据迁移是如何做的。
因为我在代码中选择了 sequelize 作为数据库框架,所以理想情况是我只需要创建好数据迁移文件之后执行 sequelize 提供的命令工具即可:sequelize-cli db:migrate
第一个问题是:它怎么知道应该连接哪个数据库以及如何连接数据库?
sequelize 允许我们在目录添加一个名为 .sequelizerc 的配置文件来对 sequelize 进行配置,我们可以在其中设定数据库的连接字符串和数据迁移文件目录,比如
const path = require('path');
const migrationDir = path.join(__dirname, "migration");
require('dotenv').config();
module.exports = {
'config': path.join(migrationDir, 'config.js'),
'migrations-path': path.join(migrationDir, 'migrations'),
'seeders-path': path.join(migrationDir, 'seeders'),
};
其中 config.js 文件存储的就是数据库的连接配置,如用户名和密码:
第二个问题是:我们如何将用户名和密码等环境变量如第一小节所示抽取出来?config.js 允许我们访问 process.env.MYSQL_USERNAME,但 MYSQL_USERNAME 究竟应该如何让流水线知道呢?
site2share 使用 Azure DevOps 作为 CI/CD 工具。很遗憾 Azure DevOps 并没有如 App Service 那样提供一个最优解,一种解决思路是我可以使用 Azure KeyVault 服务用作密钥存储,在构建的时候读取;而我选择的是一种更为简单粗暴的方式:
- 将环境变量存储到名为 .env.ci 的文件中
- 把文件上传到 Azure DevOps 的 Secure Files 中,Secure Files 可以用于存储证书类型文件,上传后的文件不可以被预览,不可以被下载
- 在 CD 过程中将 .env.ci 文件从 secure files 里下载到代码目录里,然后使用 dotenv 读取载入作为环境变量
- 执行 sequelize-cli db:migrate 命令

和在界面上编辑环境变量的行为类似,这种方法看上去同样不具有可持续性的。它依赖人的手工操作
所以在这里我们看到另一道鸿沟:环境变量从开发环境到流水线的同步问题

DevOps 是进步不是银弹
DevOps 话题很大我们这里暂且只看技术层面。我这里不是对 DevOps 批评,更不是对 Azure 的批评。这类问题在不同的平台都会存在,例如你如果使用阿里云效,他们甚至不允许你以 yaml 代码去编辑和保存流水线(IaC),但 IaC 也不是万能的,当你的 yaml 文件长达800行时基本等同于不可读时,它起到的效果等同于不存在。云平台本身的 vendor-lock 的问题也是原罪
在这里我们可以想象没有它们运维工作会比现在难上不知道多少倍——不能因为美中不足就否认它们给我们带来的帮助。
我们要接纳这样的一个现实:并不是所有的问题都可以用工具完美解决,“手工”暂且无法被彻底消灭。承认不完美的存在,尽所能把风险降到最小就好。
你可能也会喜欢:
- NodeJS 实战系列:DevOps 尚未解决的问题
- NodeJS 实战系列:如何设计 try catch
- 做一个能对标阿里云的前端APM工具(下)
- 做一个能对标阿里云的前端APM工具(上)
- 小心 Serverless
- SQL Server 查询语句优化入门
- 利用Node.js+express框实现图片上传
- 一篇来自前端同学对后端接口的吐槽
- 关于Node.js后端架构的一点后知后觉
- 在Node.js中搭建缓存管理模块'
NodeJS 实战系列:DevOps 尚未解决的问题的更多相关文章
- NodeJS 实战系列:如何设计 try catch
本文将通过一个 NodeJS 程序里无效的错误捕获示例,来讲解错误捕获里常见的陷阱.错误捕获不是凭感觉添加 try catch 语句,它的首要目的是提供有效的错误排查信息,只有精心设计的错误捕获才有可 ...
- Nodejs实战系列:数据加密与crypto模块
博客地址:<NodeJS模块研究 - crypto> Github :https://github.com/dongyuanxin/blog nodejs 中的 crypto 模块提供了各 ...
- CODING DevOps 代码质量实战系列最后一课,周四发车
随着 ToB(企业服务)的兴起和 ToC(消费互联网)产品进入成熟期,线上故障带来的损失越来越大,代码质量越来越重要,而「质量内建」正是 DevOps 核心理念之一. <DevOps 代码质量实 ...
- CODING DevOps 微服务项目实战系列第一课,明天等你
CODING DevOps 微服务项目实战系列第一课<DevOps 微服务项目实战:DevOps 初体验>将由 CODING DevOps 开发工程师 王宽老师 向大家介绍 DevOps ...
- CODING DevOps 微服务项目实战系列第二课来啦!
近年来,工程项目的结构越来越复杂,需要接入合适的持续集成流水线形式,才能满足更多变的需求,那么如何优雅地使用 CI 能力提升生产效率呢?CODING DevOps 微服务项目实战系列第二课 <D ...
- CODING DevOps 微服务项目实战系列最后一课,周四开讲!
随着软件工程越来越复杂化,如何在 Kubernetes 集群进行灰度发布成为了生产部署的"必修课",而如何实现安全可控.自动化的灰度发布也成为了持续部署重点关注的问题.CODING ...
- Spark入门实战系列--7.Spark Streaming(上)--实时流计算Spark Streaming原理介绍
[注]该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取 .Spark Streaming简介 1.1 概述 Spark Streaming 是Spa ...
- .NET Core加解密实战系列之——消息摘要与数字签名算法
目录 简介 功能依赖 消息摘要算法 MD算法 家族发展史 应用场景 代码实现 MD5 示例代码 SHA算法 应用场景 代码实现 SHA1 SHA256 示例代码 MAC算法 HMAC算法的典型应用 H ...
- Spark入门实战系列--1.Spark及其生态圈简介
[注]该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取 .简介 1.1 Spark简介 年6月进入Apache成为孵化项目,8个月后成为Apache ...
- Spark入门实战系列--2.Spark编译与部署(上)--基础环境搭建
[注] 1.该系列文章以及使用到安装包/测试数据 可以在<倾情大奉送--Spark入门实战系列>获取: 2.Spark编译与部署将以CentOS 64位操作系统为基础,主要是考虑到实际应用 ...
随机推荐
- nsenter命令简介
nsenter命令是一个可以在指定进程的命令空间下运行指定程序的命令.它位于util-linux包中. 用途 一个最典型的用途就是进入容器的网络命令空间.相当多的容器为了轻量级,是不包含较为基础的命令 ...
- 带你从入门到精通学习WireShark
个人名片: 因为云计算成为了监控工程师 个人博客:念舒_C.ying CSDN主页️:念舒_C.ying 带你从入门到精通学习WireShark 一.什么是WireShark? 二.WireShar ...
- 如何在kali Linux上安装VMware Tools
作用: 1.让虚拟机和本地上的文件可以互传,直接拖动就可以实现转接 2.可在虚拟机上执行本地脚本 3.本地时钟与虚拟机同步 4........... 方法: 1.运行虚拟机 2.在上方菜单栏中点击安装 ...
- redux原理分享
概述 一个状态管理工具 Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个 Store. State:包含所有数据,如果想得到某个时点的数据,就要对 Store 生成快照,这种时点 ...
- java 分别获取当前时间的年月日以及当前时间所在周的周一周末日期
以前也经常用date去截取,但是病史所有场景都适合,或者说效率满足不了,或者说拼接格外麻烦.能用java本省的的方法去实现其实更爽.因为中西方的文化的差异有时候在简单的方法上我们不得不去加一些其他的去 ...
- 【软考-中级-数据库相关知识】03、SQL语言
SQL语言 查询 允许出现聚集函数的是:select子句和having子句 权限管理 语法:GRANT XXX ON TABLE TO USER WITH GRANT OPTION 存储过程和函数 函 ...
- 1分钟理清楚C++类模板和模板类区别
1.定义区别 类模板和模板类主要关注点是后一个单词. 类模板:主要描述的是模板,这个模板是类的模板.可以理解为一个通用的类,这个类中的数据成员,成员函数的形参类型以及成员函数的返回值类型不用具体的指定 ...
- 错误:org.springframework.beans.factory.BeanDefinitionStoreException:
在练习尚硅谷雷丰阳老师的SSM-CRUD整合的时候,因为使用的Thymeleaf,而不是jsp,跟着老师操作所有会出现一些错误,现在我把这些错误都整理一下,希望能帮助到有用的朋友. org.sprin ...
- 常用模块二——hashlib加密模块,subprocess模块,logging日志模块
一.hashlib加密模块 1.何为加密 将明文数据处理成密文数据 让人无法看懂 2.为什么加密 保证数据的安全 3.如何判断数据是否是加密的 一串没有规律的字符串(数字.字母.符号) 4.密文的长短 ...
- JS 过滤数组中 对象的某个属性
var newArray = [{name: "aaa",value: 0, height: 1},{name: "bbb",value: 1, height: ...