Skip to main content

NGINX Ingress with Nomad

TST, Hong Kong

NGINX Ingress with Nomad

Running several web applications on a single server usually requires a web proxy to handle to have a single ingress to your server that handles TLS encryption. I already prepared a NGINX Ingress template that can be used to spin up an Ingress inside a Docker container:

Github Repository

Let's migrate this to Nomad.

Preparation

Certbot

First I need to create an TLS certificate with certbot:

apt install certbot

Since NGINX will be running inside a docker container we cannot use the --nginx flag to auto-configure NGINX to use the certificate. Instead we have to use:

certbot certonly --standalone

Before running this command make sure that you have a domain pointing towards your server, e.g. my.server.com and you will have to have opened port 80, e.g. ufw allow 80/tcp to have certbot verify your domain!

The command is going to create your certificates in /etc/letsencrypt/live/my.server.com which we will have to provide as a volume to the Nomad process - check out the Hashicorp Nomad Client Configuration for how to configure the client on your server that should run the NGNIX ingress.

Nomad Job File

There are two ways on how to provide the NGINX configuration to your Docker container. The first one is to source your configuration in a Git repository and add it to your Nomad job using the Artifact stanza inside the Task directive:

artifact {
source = "git::git@my.gitlab.com:nginx_ingress.git"
destination = "local/nginx"
options {
sshkey = "${base64encode(file(pathexpand("/etc/nomad.d/.ssh/id_rsa")))}"
depth = 1
}
}

Create an SSH key /etc/nomad.d/.ssh/id_rsa and add it to your Git repository to allow Nomad to clone it. Now you can mount the local/nginx directory as volumes:

config {
network_mode = "host"
image = "nginx:alpine"
ports = ["http","https"]
volumes = [
"local/nginx/configuration/conf.d:/etc/nginx/conf.d",
"local/nginx/configuration/ssl:/etc/nginx/ssl",
"local/nginx/configuration/nginx.conf:/etc/nginx/nginx.conf"
]
}

But I don't want to create another repo and just use the Template stanza to generate the entire configuration from my job file:

nginx_ingress.nomad

job "nginx_ingress" {
datacenters = ["nginxNTS"]

group "nginx" {
count = 1

network {
mode = "host"
port "http" {
static = "80"
}
port "https" {
static = "443"
}
}

service {
name = "nginx-ingress"
port = "http"

check {
name = "HTTP Health"
path = "/"
type = "http"
protocol = "http"
interval = "10s"
timeout = "2s"
}
}

volume "letsencrypt" {
type = "host"
read_only = true
source = "letsencrypt"
}

task "ingress_container" {
driver = "docker"

volume_mount {
volume = "letsencrypt"
destination = "/opt/letsencrypt" #in the container
read_only = false
}

config {
network_mode = "host"
image = "nginx:alpine"
ports = ["http","https"]
volumes = [
"local/nginx/nginx.conf:/etc/nginx/nginx.conf",
"local/nginx/dhparam.pem:/etc/nginx/ssl/dhparam.pem",
"local/nginx/ssl-params.conf:/etc/nginx/ssl/ssl-params.conf",
"local/nginx/default.conf:/etc/nginx/conf.d/default.conf",
"local/nginx/buffers.conf:/etc/nginx/conf.d/buffers.conf",
"local/nginx/timeouts.conf:/etc/nginx/conf.d/timeouts.conf",
"local/nginx/header.conf:/etc/nginx/conf.d/header.conf",
"local/nginx/cache.conf:/etc/nginx/conf.d/cache.conf",
"local/nginx/gzip.conf:/etc/nginx/conf.d/gzip.conf",
"local/nginx/index.html:/usr/share/nginx/html/index.html"
]
}

template {
data = <<EOH
user nginx;
worker_processes auto;
worker_rlimit_nofile 15000;
pid /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;


events {
worker_connections 2048;
multi_accept on;
use epoll;
}


http {
default_type application/octet-stream;
# access_log /var/log/nginx/access.log;
# activate the server access log only when needed
access_log off;
error_log /var/log/nginx/error.log;
# don't display server version on error pages
server_tokens off;
server_names_hash_bucket_size 64;
include /etc/nginx/mime.types;
sendfile on;
tcp_nopush on;
tcp_nodelay on;

charset utf-8;
source_charset utf-8;
charset_types text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml;

include /etc/nginx/conf.d/default.conf;
include /etc/nginx/conf.d/buffers.conf;
include /etc/nginx/conf.d/timeouts.conf;
include /etc/nginx/conf.d/cache.conf;
include /etc/nginx/conf.d/gzip.conf;
}
EOH

destination = "local/nginx/nginx.conf"
}

template {
data = <<EOH
server {
listen 80;
listen [::]:80;

server_name my.server.com;

return 301 https://$server_name$request_uri;
}


server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl;
ssl_certificate /opt/letsencrypt/live/my.server.com/fullchain.pem;
ssl_certificate_key /opt/letsencrypt/live/my.server.com/privkey.pem;
include ssl/ssl-params.conf;
include /etc/nginx/conf.d/header.conf;

server_name my.server.com;

#access_log /var/log/nginx/host.access.log main;

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

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
EOH

destination = "local/nginx/default.conf"
}

template {
data = <<EOH
<!DOCTYPE html>
<html>
<head>
<title>Hello World!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
EOH

destination = "local/nginx/index.html"
}

template {
data = <<EOH
-----BEGIN DH PARAMETERS-----
MIICCAKCAgEAt59...
...dfszADFDGV346dfzxg9AC=
-----END DH PARAMETERS-----
EOH

destination = "local/nginx/dhparam.pem"
}

template {
data = <<EOH
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM;
ssl_conf_command Options PrioritizeChaCha;
ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256;
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
EOH

destination = "local/nginx/ssl-params.conf"
}

template {
data = <<EOH
client_body_buffer_size 10k;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 2 1k;
# Directive needs to be increased for certain site types to prevent ERROR 400
# large_client_header_buffers 4 32k;
EOH

destination = "local/nginx/buffers.conf"
}

template {
data = <<EOH
add_header Cache-Control "public, must-revalidate, proxy-revalidate, max-age=0";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto http;
proxy_hide_header X-Frame-Options;
proxy_set_header Accept-Encoding "";
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_max_temp_file_size 0;
proxy_redirect off;
proxy_read_timeout 240s;
EOH

destination = "local/nginx/header.conf"
}

template {
data = <<EOH
open_file_cache max=1500 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 5;
open_file_cache_errors off;
EOH

destination = "local/nginx/cache.conf"
}

template {
data = <<EOH
client_header_timeout 3m;
client_body_timeout 3m;
keepalive_timeout 100;
keepalive_requests 1000;
send_timeout 3m;
EOH

destination = "local/nginx/timeouts.conf"
}

template {
data = <<EOH
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_min_length 256;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript
image/svg+xml application/xhtml+xml application/atom+xml;
EOH

destination = "local/nginx/gzip.conf"
}

}
}
}