利用环境变量注入修改Docker中的Nginx配置

Docker官方的Nginx镜像,是不支持环境变量修改配置的。 当然,这是因为Nginx本身就不支持。 通过环境变量修改软件运行方式,本身就是一件很糟糕的事,远不如配置文件直观可靠——在Docker以前的时代。 在Docker时代,配置的修改,远不如环境变量来得方便;尤其是集群化之后,更是使得挂载的方式失效,必须再加一层镜像。 虽然Kubernetes的ConfigMap能勉强解决问题,但环境变量显得更加通用。

但Docker官方的文档,也给出了解决方案,虽然是个歪招——envsubst

envsubst

Shell中,是可以通过以下两种形式来拼接、生成字符串的。

$ echo "I am $USER."
I am yanqd0.

这是最常见的用变量拼接字符串,有时也用更严谨的${USER}。 这种设计虽然古老,但异常好用。 在很多新生语言,如Groovy,乃至古老语言的新版本,如Python的3.6+等,都借用了它。

docker rmi `docker images -q`

这句是删除所有Docker镜像。 (危险操作,慎用!) 在``中, 或$()中的表达式,将执行后输出的stdout替换到外部表达式执行。 这就是一种简单而强大的元编程。 (虽然Pipeline也能实现上句中的相同功能。)

这么方便的字符串功能,能不能用在写配置文件上? envsubst就是为此而设计的。

官方样例

官方样例非常简单,能明了地展示用envsubst实现配置生成的思路。

web:
  image: nginx
  volumes:
   - ./mysite.template:/etc/nginx/conf.d/mysite.template
  ports:
   - "8080:80"
  environment:
   - NGINX_HOST=foobar.com
   - NGINX_PORT=80
  command: /bin/bash -c "envsubst < /etc/nginx/conf.d/mysite.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"

利用mysite.template中的${NGINX_PORT}${NGINX_HOST},生成了default.conf。 官方的Nginx配置的样例未给出(谜之消失),以下给出一个参考文件。

server {
    listen      ${NGINX_PORT};
    listen      [::]:${NGINX_PORT};
    server_name ${NGINX_HOST};

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

这个例子虽然看似能解决问题,其实也只是样例。 首先,配置既然是挂载进去的,而环境变量也是写死的,那不如写死在一起,省事。 然后……应该已经不需要『然后』了。 当然,官方文档只是为了展示这种技术可能性。

真实案例

以下展示一个更复杂的例子。

Nginx配置

首先看看配置文件default.template

# vim: set filetype=nginx:

server {
    listen      80 default_server;
    listen      [::]:80;
    server_name _;
    client_max_body_size 0;
    underscores_in_headers on;

    location ^~ /api/ {
        proxy_pass ${BACKEND_URL};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_connect_timeout 1s;
    }

    location / {
        proxy_pass http://frontend:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

在这个配置文件中,$host$remote_addr等,是与Nginx的约定,并非需要动态修改的变量。 然而,在官方的使用方法下,它们都惨遭修改、面目全非。

如果避免修改$host,精准地只修改${BACKEND_URL}呢?

docker-compose

这是一个NodeJS项目调试用的docker-compose.yaml文件。 前端就是当前项目,而后端则不确定在哪,以环境变量BACKEND_URL的方式配置。 这个环境变量,也是npm run serve运行时使用的。

version: "3"

services:
  frontend:
    build: .
    image: your/frontend:latest
    expose:
      - 80

  nginx:
    image: nginx:1.14.2
    entrypoint: /opt/entry.bash
    ports:
      - 80:80
    volumes:
      - ./nginx/entry.bash:/opt/entry.bash:ro
      - ./nginx/conf.d/default.template:/etc/nginx/conf.d/default.template:ro
    environment:
      - BACKEND_URL
    depends_on:
      - frontend

可以看到,与官方样例有两点不同。 一是环境变量BACKEND_URL并非写死的,而是将更外层运行时的环境变量转手注入容器中; 二是并非通过command,而是通过entrypoint实现envsubst

entry.bash

首先,我们假设环境变量是这样的东西:

export BACKEND_URL=http://domain-or-ip:5000/api/v1/

通过端口号,应该很容易猜到这是什么框架开发的后端。 但这不是重点。 重点是,前端在运行时,竟然注入的是个带/api/v1/的东西。

以下是entry.bash

#!/bin/bash

BACKEND_URL=${BACKEND_URL/?api*/}
envsubst '$BACKEND_URL' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf
cat /etc/nginx/conf.d/default.conf

if [ $# = 0 ]
then
    exec nginx -g 'daemon off;'
else
    exec "$@"
fi

首先,利用Bash的字符串切分操作,把/api/v1/切去。 这一步是使用entrypoint的原因,它无法在bash -c中完成。 其次,利用envsubst限定变量的功能,只对$BACKEND_URL做替换,忽略其它。 最后,启动Nginx。

知道上面最重要的是哪一行吗? 血泪会告诉你,是cat那一行。

总结

同样,这个复杂案例也不是没有其它办法实现。 /api/v1/的问题,可以通过Nginx自身的配置调整来解决。 (但你会吗?)

这里的案例是为了开发阶段的类生产环境调试。 如果是生产环境,volumesentrypoint配置可以省去,直接build新镜像来使用,会简洁、灵活许多。

参考


相关笔记