[容器][虚拟机][nginx] 使用nginx创建简易的应用负载均衡器
因为我希望在开发和测试中启动多个应用程序,所以我编写了一个简单的ALB工具。(环境为AWS)
我决定制作这个工具是因为有以下限制。
约束条件:
– 无法将任意端口公开到服务器外部(仅限80/443端口)
– 禁止启动ALB(本应允许)
作为一种方法,我们考虑使用Docker,并使用Nginx的默认容器进行实现。
原本想做的是在主机上同时运行多个应用程序,所以我们会为每个应用程序分配不同的端口进行启动,并通过Nginx根据主机名进行分发。
由于负载均衡策略,目标容器与同一主机上运行的其他容器相连,因此从nginx的角度来看,它将通过容器内部连接到主机。
因为这只是一个简易版本的工具,所以我们将在不创建新的Docker镜像或添加插件的范围内进行。
我把创建的工具放在了 GitHub 上。
https://github.com/batatch/simple-alb
总结
虽然说是简易版,但是我们会准备一个定义文件,模仿 AWS ALB 的设置内容,并从中生成 nginx 的配置文件,以便实现类似于 Docker 执行的结构。
步骤:
$ make build # 定義ファイルから nginx 設定ファイルを生成
$ make up # docker-compose で設定ファイルを割り当てて nginx のコンテナを起動
$ make down # docker-compose で nginx のコンテナを停止
定义文件:
---
http:
listen: 80 # リスナー、今回は HTTPのみ
rules:
- if: # ALB の IF 条件
host: app01.example.com # ホスト名マッチング
pathes: [ "/" ] # パスマッチング
then: # ALB の THEN 文
forward: # 転送設定
name: tg-app01
targets: # 転送先(複数)、ターゲットグループのようなイメージ
- target: http://docker0:21080
weight: 30
- target: http://docker0:22080
stickiness: true
:
将步骤总结在Makefile中。
这是个人偏好,但我认为这是最方便和易懂的方式。
然后,根据上述的定义文件,创建如下的nginx配置文件。
upstream target1 {
server http://docker0:21080;
server http://docker0:22080;
}
server {
listen 80;
server_name app01.example.com;
:
location / {
proxy_pass http://target1;
}
}
框架大致如下。
模板引擎
因为要自动生成设置文件,所以我想要一种模板引擎,考虑使用在Ansible等工具中使用的Python的Jinja2。
我用下面这段简单的Python脚本实现了从模板文件和YAML配置文件中获取转换结果。
import sys
import yaml
from jinja2 import Template, Environment, FileSystemLoader
def _j2(templateFile, configFile):
env = Environment(loader=FileSystemLoader('.', encoding='utf_8'))
tpl = env.get_template(templateFile)
with open(configFile) as f:
conf = yaml.load(f)
ret = tpl.render(conf)
print(ret)
if __name__ == '__main__':
if (len(sys.argv) <= 2):
print("Usage: j2.pl <template file> <config file>")
sys.exit(-1)
_j2(sys.argv[1], sys.argv[2])
命令行是这样的。
$ python j2.pl template.conf.j2 param.yml > output.conf
Docker容器与主机之间的通信
这个有些麻烦,在Windows和Mac的Docker环境下使用host.docker.internal可以实现容器内部连接到主机的功能,但是在Linux中并没有提供这样的方法。
在Linux上,它似乎通过一个名为docker0的接口将主机和容器连接在一起,因此我们获取了分配给docker0的IP地址,并将其转化为环境变量,在docker启动时传递。
$ env DOCKER0_ADDRESS=$( ip route | awk '/docker0/ {print $9}' ) \
docker-compose up -d
version: '3'
services:
alb:
image: nginx:stable
:
extra_hosts:
- "docker0:${DOCKER0_ADDRESS}"
如果在docker-compose.yml中的extra_hosts中进行映射,那么在容器启动时将会在容器内的/etc/hosts中追加主机名和IP地址的映射,从而使得在nginx的配置中可以进行名称解析。
/etc/hosts
----
172.17.0.1 docker0
使用nginx作为负载均衡器。
要在Nginx中设置一个负载均衡器,需要在http/upstream中定义目标组,并在http/server/location的proxy_pass中指定上游(upstream)的名称。虽然我原本是这样设想的,但在启动时却遇到了错误。
似乎 nginx 的免费版本无法使用在 upstream 中编写的主机名的 DNS解析(解析器)。
(这次我第一次知道 nginx 有付费版本/免费版本..)
這是一篇關於該問題的總結文章。
在下面的Qiita文章中介紹了使用UNIX套接字的方法。
由於符合使用插件和不創建Docker映像的限制,我們選擇了這種方法。(儘管設定文件變得很長)
Nginx名称解析汇总
https://ktrysmt.github.io/blog/name-specification-of-nginx/
在Nginx的upstream上下文中,动态地进行DNS解析而不使用有偿的resolve选项
https://qiita.com/minamijoyo/items/183e51a28a3a9d79182f
upstream tg-app01 {
server unix:/var/run/nginx_tg-app01_1; # (2-1) tg-app01の 1つめのターゲット
server unix:/var/run/nginx_tg-app01_2; # (2-2) tg-app01の 2つめのターゲット
}
server {
listen 80;
server_name app01.example.com;
:
location / {
proxy_pass http://tg-app01; # (1) upstream tg-app01 を参照
}
}
server {
listen unix:/var/run/nginx_tg-app01_1; # (2-1) 1つめのターゲットの参照先
server_name app01.example.com;
:
location / {
proxy_pass http://docker0:21080;
}
}
server {
listen unix:/var/run/nginx_tg-app01_2; # (2-2) 2つめのターゲットの参照先
server_name app01.example.com;
:
location / {
proxy_pass http://docker0:22080;
}
}
WebSocket的通信
因为这次需要通过Websocket进行通信,所以需要进行以下设置。
似乎每个服务器块都需要这个配置。
# これと
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
:
server {
listen 80;
server_name app02.example.com;
# ここから
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# ここまで
location / {
proxy_pass http://tg-app02;
}
}
nginx的配置文件模板
考虑到前面的内容,模板定义如下:
虽然有些细节,但也包括了通过+α设置默认模式以及固定响应的设置。
## http listener settings
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
{% for rule in http.rules %}
{%- if rule.then.forward %}
upstream {{ rule.then.forward.name }} {
{%- if rule.then.forward.stickiness %}
ip_hash;
{%- endif %}
{%- for tg in rule.then.forward.targets %}
server {{ 'unix:/var/run/nginx_%s_%d' % (rule.then.forward.name, loop.index) }}{{ ' weight=%d' % tg.weight if tg.weight else '' }};
{%- endfor %}
}
{%- endif %}
server {
listen {{ http.listen }}{{ ' default_server' if rule.if.default_server }};
server_name {{ rule.if.host }};
{% if rule.then.forward %}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
{% endif %}
{%- for path in rule.if.pathes %}
location {{ path }} {
{%- if rule.if.headers %}
{%- for header in rule.if.headers %}
if ($http_{{ header|replace('-','_')|lower() }} = "{{ rule.if.headers[header] }}") {
proxy_pass http://{{ rule.then.forward.name }};
break;
}
{%- endfor %}
{%- else %}
proxy_pass http://{{ rule.then.forward.name }};
{%- endif %}
}
{%- endfor %}
{%- if rule.then.response %}
location / {
{%- if rule.then.response.content_type %}
default_type {{ rule.then.response.content_type }};
{%- endif %}
return {{ rule.then.response.code }}{{ ' \'%s\'' % rule.then.response.message if rule.then.response.message }};
}
{%- endif %}
}
{%- if rule.then.forward %}
{%- for tg in rule.then.forward.targets %}
server {
listen {{ 'unix:/var/run/nginx_%s_%d' % (rule.then.forward.name, loop.index) }};
server_name {{ rule.if.host }};
{% if rule.then.forward %}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
{% endif %}
location / {
proxy_pass {{ tg.target }};
}
}
{%- endfor %}
{%- endif %}
{% endfor %}
Docker-compose 配置如下。
请将 nginx 的配置文件夹挂载到外部卷,并应用配置。
version: '3'
services:
alb:
image: nginx:stable
ports:
- "80:80"
volumes:
- ./conf.d:/etc/nginx/conf.d
extra_hosts:
- "docker0:${DOCKER0_ADDRESS}"
restart: always
动作确认如下所示。
$ make build # 設定ファイル変換
$ make up # nginx コンテナ起動
$ curl http://localhost:80 -i -H "Host:app01.example.com" # ホスト名を設定してアクセス
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sat, 29 Aug 2020 17:26:23 GMT
Content-Type: text/html
Content-Length: 1863
Connection: keep-alive
Last-Modified: Wed, 11 Mar 2020 05:22:13 GMT
ETag: "747-5a08d6b34ab40"
Accept-Ranges: bytes
<!DOCTYPE html>
<html>
:
总结
只需在YAML定义中写入类似AWS ALB的配置元素,就无需编写复杂的nginx配置,使其易于管理。不需要构建或安装插件,只需从DockerHub获取nginx镜像即可轻松实现类似ALB的效果。
学习nginx配置对我很有帮助。(我也不知道有免费版/付费版。。)
如果更深入地调查和努力,或许可以实现ALB原厂的设置模式呢。(这次为了尽可能方便使用,所以没有进行详细设计)