如何在环境中存储配置

关于「在环境中存储配置」,是 The Twelve-Factor App 倡导的方法论之一。通常,应用的配置在不同环境(预发布、生产环境、开发环境等等)间会有很大差异,比如说数据库的用户名密码等等配置,通过把配置和代码分离,我们可以保证部署在不同环境的代码完全一致,如何把配置和代码分离呢?最佳实战是把配置存储到环境变量中,它可以非常方便地在不同的部署间做修改,却不动一行代码;与配置文件不同,不小心把它们签入代码库的概率微乎其微;此外环境变量与语言和系统无关。

在实际应用中,现在比较流行的解决方案是 dotenv(Ruby dotenvPHP dotenv):首先创建一个 .env 文件,然后把配置信息都保存在里面,接着把这些信息加载的环境变量里,最后直接使用环境变量。

通过使用此方案,我们可以给不同的环境设置不同的 .env 文件,在一定程度上实现了配置和代码分离,可惜还有一些明显的缺点,比如:

  • 如果有很多台服务器需要同步配置,那么是一件很痛苦的事情。
  • 如果忘了把 .env 加入到 .gitignore,那么很有可能泄露敏感信息。
  • 如果部署不当,那么很可能泄露敏感信息,比如这里

通过引入服务发现机制可以解决多台服务器同步配置的问题,主流方案如下:

它们的实现机制类似,都是把配置保存在服务发现的存储里,一旦发生变化,可以自动通过模板技术静态化保存成本地文件,从而解决多台服务器同步配置的问题。

不过这些方案归根到底还是要需要静态化保存成本地文件的,有没有直接使用环境变量保存配置的解决方案呢?答案就是 envconsul,其工作原理如下:在 consul 中保存配置,然后 envconsul 启动后会加载配置,并通过环境变量的方式传递给子进程,此外 envconsul 还会通过 consul 的 http 接口以 long polling 的方式监听,一旦发现配置出现了变动,就会发送信号给子进程,从而完成配置的更新。

如果你已经安装好了 consul 和 envconsul,那么让我们来试一试(未考虑权限控制):

shell> consul kv put app/db/username root
shell> consul kv put app/db/password 123456

shell> envconsul \
    -pristine \
    -sanitize \
    -upcase \
    -prefix app \
    env

DB_USERNAME=root
DB_PASSWORD=123456

如上,我使用 env 命令作为 envconsul 的子进程来显示环境变量,实际使用中,你可以把 ruby,php 之类的应用作为 envconsul 的子进程,下面我用一个 shell 脚本来展示配置发生变化的时候 envconsul 是如何应对的,shell 脚本名为 test.sh,内容如下:

#! /bin/bash

signals=(HUP INT QUIT TERM USR1 USR2)

for signal in "${signals[@]}"
do
    trap "echo $signal; exit" "$signal"
done

for i in {1..1000}
do
    echo $i: PASSWORD: $DB_PASSWORD
    sleep 1
done

其作用就是监听信号,并且显示 DB_PASSWORD 环境变量,这次我们开启两个命令行窗口,一个运行 envconsul,另一个运行 consul kv put app/db/password … 来修改配置:

shell> envconsul \
    -pristine \
    -sanitize \
    -upcase \
    -prefix app \
    /path/to/test.sh

1: PASSWORD: <OLD VALUE>
2: PASSWORD: <OLD VALUE>
INT
1: PASSWORD: <NEW VALUE>
2: PASSWORD: <NEW VALUE>

我们能看到,当 envconsul 发现配置改变了之后,缺省情况下会发送 INT 信号(可配置)给子进程,使子进程完成重启,从而加载到新的配置。

此外还有一些细节问题需要考虑,比如:假设有一百台应用服务器,都是通过 envconsul 运行的,那么当配置发生变化的时候,如果这一百台应用服务器同时重启进程的话,无疑是一场灾难,实际上 envconsul 已经考虑到了此类情况,你可以通过配置 splay 选项把重启的时间随机化,避免「Thundering herd problem」;再假设配置发生变化的时候,如果子进程一直没有完成重启怎么办,envconsul 有一个 kill_timeout 选项,重启超时的话被直接强杀子进程。其它更多配置参见文档说明,篇幅所限,恕不赘述。

结尾再推荐一篇不同的声音:Why you shouldn’t use ENV variables for secret data,其以安全性为由,不建议使用环境变量,而是推荐使用 docker swarm 的密钥机制来管理敏感信息(相关教程),这很酷,如果你使用 docker,不妨一试。

回到 envconsul,环境变量仅针对子进程有效,虽然在一定程度上降低了风险,但是确实有可能泄露敏感信息,比如在 PHP 里,如果能运行 phpinfo 函数的话,那么可以打印出所有的环境变量,但我觉得不能因噎废食,以 PHP 为例,在生产环境中,类似 phpinfo,eval 之类的危险函数,原本就应该通过 disable_functions 禁用,而且数据库密码之类的信息,一般有 ip 访问限制,即便泄露了也影响有限,但这并不意味着可以不假思索的把任何信息都往环境变量里塞,比如银行卡密码,比特币密钥之类高度敏感的信息,如果泄露了就全完了,此时还是用 Vault 比较好,当然,envconsul 也支持 Vault。

1 thought on “如何在环境中存储配置

发表评论

电子邮件地址不会被公开。 必填项已用*标注