[docker] [nginx] Make a simple ALB with nginx

I wanted to launch multiple apps for development and testing, so I made a simple ALB tool. (Environment is AWS) I decided to make a tool because of the following restrictions.

Constraints: --Any port cannot be published outside the server (80/443 only) --ALB launch prohibited (normally this)

As an approach, since we are using docker, we will consider using the default container of nginx. Originally, I want to run multiple apps in parallel on the host, so I will allocate different ports for each and start them up, and aim for a form that sorts by host name with nginx.

simple-alb.png

Since the distribution destination is another container running on the same host, from the viewpoint of nginx, it will connect to the host from inside the container.

simple-alb-network.png

Since it is a simplified version as a tool, we will proceed as long as we do not create a new docker image or add a plugin.

I put the tool I created on GitHub. https://github.com/batatch/simple-alb

Overview

Although it is a simplified version, the setting contents are similar to those of AWS ALB, prepare a definition file, generate nginx settings from there → aim for a configuration like docker execution.

procedure:

$ make build    #Generate nginx configuration file from definition file
$ make up       # docker-Allocate a config file with compose and start an nginx container
$ make down     # docker-Stop nginx container with compose

Definition file:

alb.yml


---
http:
  listen: 80                       #Listener, this time HTTP only
  rules:
    - if:                          #IF condition of ALB
        host: app01.example.com    #Hostname matching
        pathes: [ "/" ]            #Path matching
      then:                        #ALB THEN statement
        forward:                   #Transfer settings
          name: tg-app01
          targets:                 #Forwarding destination(Multiple), Image like target group
            - target: http://docker0:21080
              weight: 30
            - target: http://docker0:22080
          stickiness: true
    :

The procedure is summarized in a Makefile. I like it, but I think this is the easiest and easiest to understand. Then, based on the above definition file, create the following nginx configuration file.

nginx/conf.d/default.conf


upstream target1 {
    server http://docker0:21080;
    server http://docker0:22080;
}
server {
    listen 80;
    server_name app01.example.com;
       :
    location / {
        proxy_pass http://target1;
    }
}

The framework looks like this.

Template engine

Since the configuration file is automatically generated, if you want some kind of template engine, try using Jinja2 of Python used in Ansible etc.

I made it possible to get the conversion result from the template file and the YAML file of the configuration file with a simple Python script like the following.

j2.py


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])

The command line looks like this.

$ python j2.pl template.conf.j2 param.yml > output.conf

Communication from docker container to host

This is quite troublesome, and in the Docker environment of Windows or Mac, it seems that you can connect from inside the container to the host with host.docker.internal, but Linux does not have such a method.

In Linux, it seems that the host / container is connected by an interface called docker0, so I got the IP address assigned to docker0 on the host side, made it an environment variable, and passed it when docker started.

$ env DOCKER0_ADDRESS=$( ip route | awk '/docker0/ {print $9}' ) \
  docker-compose up -d

docker-compose.yml


version: '3'
services:
  alb:
    image: nginx:stable
      :
    extra_hosts:
      - "docker0:${DOCKER0_ADDRESS}"

If you write the mapping in extra_hosts in docker-compose.yml, the mapping of the host name and IP address will be added to / etc / hosts in the container when the container is started, so it seems that you can refer to the name in the nginx settings.

/etc/hosts
----
172.17.0.1      docker0

Load balancer with nginx

To set up a load balancer with nginx, define a group of targets in http / upstream and specify the upstream name in proxy_pass in http / server / location. .. I expected it, but it doesn't start with an error.

Apparently, the free version of nginx doesn't allow DNS resolver for host names written in upstream. (I learned for the first time that nginx has a paid / free version.)

This is an article summarizing this matter. The Qiita article below shows how to use UNIX sockets. I chose this one because it met the restrictions of not using plugins or creating docker images. (Although the configuration file will be longer)

Summary of Nginx name resolution https://ktrysmt.github.io/blog/name-specification-of-nginx/ Dynamic DNS resolution in nginx upstream context without paid resolve option https://qiita.com/minamijoyo/items/183e51a28a3a9d79182f

nginx/conf.d/default.conf


upstream tg-app01 {
    server unix:/var/run/nginx_tg-app01_1;  # (2-1) tg-The first target of app01
    server unix:/var/run/nginx_tg-app01_2;  # (2-2) tg-Second target for app01
}
server {
    listen 80;
    server_name app01.example.com;
        :
    location / {
        proxy_pass http://tg-app01;  # (1) upstream tg-See app01
    }
}
server {
    listen unix:/var/run/nginx_tg-app01_1;  # (2-1)Reference for the first target
    server_name app01.example.com;
        :
    location / {
        proxy_pass http://docker0:21080;
    }
}
server {
    listen unix:/var/run/nginx_tg-app01_2;  # (2-2)Second target reference
    server_name app01.example.com;
        :
    location / {
        proxy_pass http://docker0:22080;
    }
}

Websocket communication

Since it was necessary to pass through Websocket this time, make the following settings. Seems to be needed for each server block.

nginx/conf.d/default.conf


#With this
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
    :
server {
    listen 80;
    server_name app02.example.com;

    #from here
    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;
    #So far

    location / {
        proxy_pass http://tg-app02;
    }
}

nginx config file template

Based on the contents so far, the template definition is as follows. It's fine, but + α also includes the default pattern specification and fixed response setting.

src/default.conf.j2


## 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 %}

The docker-compose settings are as follows. Mount the nginx settings folder on an external volume for the settings to take effect.

docker-compose.yml


version: '3'
services:
  alb:
    image: nginx:stable
    ports:
      - "80:80"
    volumes:
      - ./conf.d:/etc/nginx/conf.d
    extra_hosts:
      - "docker0:${DOCKER0_ADDRESS}"
    restart: always

The operation check is as follows.

$ make build    #Configuration file conversion
$ make up       #nginx container start

$ curl http://localhost:80 -i -H "Host:app01.example.com"  #Access by setting the host name
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>
    :

Summary

I think it will be easier to manage because you don't have to write AWS ALB-like components in the YAML definition and write complicated nginx settings. You don't need to build or install any plugins, so it's easy to feel like ALB as long as you get the nginx image from DockerHub.

I also learned how to set up nginx. (I also learned about the free version / paid version.)

If you investigate further and do your best, you may be able to realize the setting pattern of the ALB head family. (This time I wanted to make it as easy to use as possible, so I did not make it)

Recommended Posts

[docker] [nginx] Make a simple ALB with nginx
A simple CRUD app made with Nuxt / Laravel (Docker)
Practice making a simple chat app with Docker + Sinatra
[Beginner] Try to make a simple RPG game with Java ①
Make a simple CRUD with SpringBoot + JPA + Thymeleaf ① ~ Hello World ~
Let's make a simple API with EC2 + RDS + Spring boot ①
Make a simple CRUD with SpringBoot + JPA + Thymeleaf ⑤ ~ Template standardization ~
Create a Vue3 environment with Docker!
Deploy a Docker application with Greengrass
Make a language! (Making a simple calculator ②)
Try to make a simple callback
Make a slideshow tool with JavaFX
Make a Christmas tree with swift
Make a garbage reminder with line-bot-sdk-java
Make a list map with LazyMap
Operate a honeypot (Dionaea) with Docker
Make a language! (Making a simple calculator ①)
Make JupyterLab run anywhere with docker
Make a typing game with ruby
Build Metabase with Docker on Lightsail and make it https with nginx
Build a SPA for Laravel 6.2 / Vue.js / Nginx / Mysql / Redis with Docker
Prepare a transcendentally simple PHP & Apache environment on Mac with Docker
Make a C compiler to use with Rust x CLion with Docker
Build a development environment for Django + MySQL + nginx with Docker Compose
Make a family todo list with Sinatra
Create a simple web application with Dropwizard
Build a PureScript development environment with Docker
Create a simple on-demand batch with Spring Batch
Environment construction with Docker (Ubuntu20.04) + Laravel + nginx
[Rails withdrawal] Create a simple withdrawal function with rails
Create a MySQL environment with Docker from 0-> 1
Create a simple bar chart with MPAndroidChart
Make a family todo list with Sinatra
Let's make a smart home with Ruby!
Make a login function with Rails anyway
Build a Wordpress development environment with Docker
Make a site template easily with Rails
Implement simple CRUD with Go + MySQL + Docker
Build a simple Docker + Django development environment
Make a daily build of the TOPPERS kernel with Gitlab and Docker
[Memo] Create a CentOS 8 environment easily with Docker
I tried to make a machine learning application with Dash (+ Docker) part3 ~ Practice ~
Create a simple search app with Spring Boot
Make SpringBoot1.5 + Gradle4.4 + Java8 + Docker environment compatible with Java11
Create a simple bulletin board with Java + MySQL
Build a Laravel / Docker environment with VSCode devcontainer
Build a WordPress development environment quickly with Docker
Simple installation of nginx and Docker using ansible
I tried to make a simple game with Javafx ① "Let's find happiness game" (unfinished)
Build a simple Docker Compose + Django development environment
Let's make a search function with Rails (ransack)
Quick docker / nginx
Prepare a scraping environment with Docker and Java
Make Volume faster when using Docker with vscode.
Show a simple Hello World with SpringBoot + IntelliJ
React + Django + Nginx + MySQL environment construction with Docker
Make System.out a Mock with Spock Test Framework
A simple rock-paper-scissors game with JavaFX and SceneBuilder
Create a Spring Boot development environment with docker
[Rails 6] Dockerize existing Rails apps [Docker]
Make JupyterLab run anywhere with docker
docker
Migrate existing Rails 6 apps to Docker environment
Scraping with puppeteer in Nuxt on Docker.
Make Docker in Docker Toolbox accessible as localhost
[docker] [nginx] Make a simple ALB with nginx
I tried to make a simple game with Javafx ① "Let's find happiness game" (unfinished version ②)