DEV Community

Cover image for Building a Custom NGINX Module with Ansible: A Dev-Friendly Guide
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

Building a Custom NGINX Module with Ansible: A Dev-Friendly Guide

Hi there! I'm Maneshwar. Right now, I’m building LiveAPI, a first-of-its-kind tool that helps you automatically index API endpoints across all your repositories. LiveAPI makes it easier to discover, understand, and interact with APIs in large infrastructures.


NGINX is a powerful and versatile web server, and its extensibility through modules is one of its greatest strengths.

Sometimes, however, you need functionality that isn't available out-of-the-box or as a pre-compiled dynamic module.

This is where custom NGINX modules come in.

In this blog post, we'll walk through a real-world scenario: building NGINX with the ngx_http_consul_backend_module and the ngx_devel_kit (NDK) module.

We'll leverage Ansible to automate the entire process, making it repeatable, reliable, and dev-friendly.

Why Ansible for NGINX Builds?

Manually compiling NGINX with custom modules can be a tedious and error-prone process.

Dependencies, compilation flags, and directory structures all need to be precisely managed.

Ansible simplifies this significantly by:

  • Idempotency: Tasks can be run multiple times without causing unintended side effects.
  • Automation: Automate repetitive tasks, saving time and reducing human error.
  • Version Control: Store your build process in version control, enabling easy collaboration and rollbacks.
  • Consistency: Ensure consistent builds across different environments.

Our Goal: NGINX with Consul Backend Module

We'll be building NGINX 1.23.2 and integrating two specific modules:

  1. ngx_devel_kit (NDK): A collection of utilities and APIs that simplify the development of NGINX modules. Many custom modules rely on NDK.
  2. ngx_http_consul_backend_module: A module that allows NGINX to discover backend services from HashiCorp Consul. This is particularly useful in dynamic, microservices-based environments.

Project Structure

Our Ansible project is organized as a role named nginx-with-consul-module.

This structure promotes reusability and maintainability.

ansible ├─ README.md ├─ ansible.cfg ├─ hosts.ini ├─ install_ansible.sh ├─ nginx-build-playbook.yml └─ roles └─ nginx-with-consul-module ├─ tasks │ ├─ build_consul_backend_module.yml │ ├─ build_nginx.yml │ ├─ configure_build.yml │ ├─ download_sources.yml │ ├─ install_dependencies.yml │ ├─ main.yml │ ├─ purge_deps.yml │ └─ systemd.yml └─ templates └─ nginx.service.j2 
Enter fullscreen mode Exit fullscreen mode

The main.yml in the tasks directory orchestrates the entire build process by importing other task files:

--- - import_tasks: purge_deps.yml - import_tasks: install_dependencies.yml - import_tasks: download_sources.yml - import_tasks: build_consul_backend_module.yml - import_tasks: configure_build.yml - import_tasks: build_nginx.yml - import_tasks: systemd.yml 
Enter fullscreen mode Exit fullscreen mode

Let's break down each step.

Step 1: Install Dependencies (install_dependencies.yml)

Building NGINX and its modules requires several development tools and libraries.

This task ensures all necessary packages are present on the target system.

--- - name: Install required packages apt: name: - build-essential # For gcc, g++ and make - libpcre3 - libpcre3-dev # For PCRE regular expressions support - zlib1g - zlib1g-dev # For gzip compression support - libssl-dev # For OpenSSL (HTTPS) support - git # To clone the Consul backend module - wget # To download source archives - curl # General utility state: present update_cache: true 
Enter fullscreen mode Exit fullscreen mode

Step 2: Download Sources (download_sources.yml)

Before we can compile anything, we need the source code for NGINX, NDK, and the Consul backend module.

We'll download these to a temporary directory.

--- - name: Create build directory file: path: /tmp state: directory - name: Download nginx 1.23.2 source get_url: url: https://nginx.org/download/nginx-1.23.2.tar.gz dest: /tmp/nginx.tgz - name: Extract nginx source unarchive: src: /tmp/nginx.tgz dest: /tmp/ remote_src: yes - name: Download NDK module get_url: url: https://github.com/simpl/ngx_devel_kit/archive/v0.3.0.tar.gz dest: /tmp/ngx_devel_kit-0.3.0.tgz - name: Extract NDK module unarchive: src: /tmp/ngx_devel_kit-0.3.0.tgz dest: /tmp/ remote_src: yes - name: Clone Consul backend module git: repo: https://github.com/hashicorp/ngx_http_consul_backend_module.git dest: /go/src/github.com/hashicorp/ngx_http_consul_backend_module 
Enter fullscreen mode Exit fullscreen mode

A crucial step here for the Consul backend module is a small patch.

The original code's strlen call with backend might cause a warning or error with newer compilers due to type mismatch.

We're explicitly casting backend to (const char*) to resolve this.

- name: Replace backend string length calculation replace: path: /go/src/github.com/hashicorp/ngx_http_consul_backend_module/src/ngx_http_consul_backend_module.c regexp: 'ngx_str_t ngx_backend = { strlen\(backend\), backend };' replace: "ngx_str_t ngx_backend = { strlen((const char*)backend), backend };" 
Enter fullscreen mode Exit fullscreen mode

Step 3: Build Consul Backend Module (build_consul_backend_module.yml)

The ngx_http_consul_backend_module is written in Go and needs to be compiled as a C shared library. This involves several steps:

--- - name: Ensure nginx ext directory exists file: path: /usr/local/nginx/ext/ state: directory owner: "{{ ansible_user | default('root') }}" group: "{{ ansible_user | default('root') }}" mode: "0755" - name: Change ownership of Consul backend module directory file: path: /go/src/github.com/hashicorp/ngx_http_consul_backend_module state: directory owner: "{{ ansible_user | default('root') }}" group: "{{ ansible_user | default('root') }}" recurse: yes - name: Check Go version shell: export PATH=/usr/local/go/bin:$PATH && /usr/local/go/bin/go version register: go_version_result changed_when: false failed_when: false - name: Initialize Go modules command: /usr/local/go/bin/go mod init github.com/hashicorp/ngx_http_consul_backend_module args: chdir: /go/src/github.com/hashicorp/ngx_http_consul_backend_module register: go_mod_init_result changed_when: go_mod_init_result.rc == 0 failed_when: go_mod_init_result.rc != 0 and "go.mod already exists" not in go_mod_init_result.stderr ignore_errors: true # Ignore if go.mod already exists - name: Tidy Go modules command: /usr/local/go/bin/go mod tidy args: chdir: /go/src/github.com/hashicorp/ngx_http_consul_backend_module - name: Print Go version debug: msg: "{{ go_version_result.stdout }}" - name: Build Go shared library for Consul backend module shell: | export PATH=/usr/local/go/bin:$PATH CGO_CFLAGS="-I /tmp/ngx_devel_kit-0.3.0/src" \ /usr/local/go/bin/go build -buildmode=c-shared -o /usr/local/nginx/ext/ngx_http_consul_backend_module.so ./src/ngx_http_consul_backend_module.go args: chdir: /go/src/github.com/hashicorp/ngx_http_consul_backend_module 
Enter fullscreen mode Exit fullscreen mode

Key points:

  • We create a dedicated directory /usr/local/nginx/ext/ to store our compiled dynamic module.
  • We ensure correct ownership for the cloned Consul module directory.
  • We initialize and tidy Go modules to manage dependencies.
  • The go build -buildmode=c-shared command is crucial. It compiles the Go module into a C shared library (.so file) that NGINX can load dynamically.
  • CGO_CFLAGS="-I /tmp/ngx_devel_kit-0.3.0/src" is important because the Consul module depends on headers from the NDK, so we need to tell the Go compiler where to find them.

Step 4: Configure NGINX Build (configure_build.yml)

This step involves running the ./configure script for NGINX. This script generates the Makefile based on the desired modules and features.

# Simplified for brevity, but this is where you'd run ./configure # with all your desired flags and --add-module directives. # The original configure command from the user's prompt (commented out) would be used here. # Example of how it would look if it were a separate task: - name: Configure NGINX build command: > ./configure --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module --with-mail_ssl_module --with-stream_ssl_module --with-debug --add-module=/tmp/ngx_devel_kit-0.3.0 --add-module=/go/src/github.com/hashicorp/ngx_http_consul_backend_module args: chdir: /tmp/nginx-1.23.2 
Enter fullscreen mode Exit fullscreen mode

Important configure flags:

  • --prefix=/etc/nginx: Sets the installation prefix for NGINX.
  • --add-module=/path/to/module: This is critical for including our custom modules. We point to the extracted NDK and the cloned Consul backend module source directories.
  • Other --with-* flags enable various built-in NGINX modules like SSL, HTTP/2, etc.

Step 5: Build and Install NGINX (build_nginx.yml)

Once configure has generated the Makefile, we can proceed with compilation and installation.

--- - name: Compile nginx command: make args: chdir: /tmp/nginx-1.23.2 - name: Install nginx command: make install args: chdir: /tmp/nginx-1.23.2 - name: Install apache2-utils # Useful for htpasswd, etc. apt: name: apache2-utils state: present 
Enter fullscreen mode Exit fullscreen mode
  • make: Compiles the NGINX source code along with the added modules.
  • make install: Installs NGINX and its components to the paths specified during the configure step.

Step 6: Systemd Integration (systemd.yml)

For a production-ready setup, we need NGINX to run as a system service.

This task typically involves creating a systemd service file.

# Example content for systemd.yml, assuming you have nginx.service.j2 --- - name: Copy nginx systemd service file template: src: nginx.service.j2 dest: /etc/systemd/system/nginx.service owner: root group: root mode: '0644' notify: Reload systemd - name: Enable nginx service systemd: name: nginx enabled: true daemon_reload: true # Ensures systemd picks up the new service file state: started # Handler for 'Reload systemd' # handlers/main.yml # --- # - name: Reload systemd # systemd: # daemon_reload: true 
Enter fullscreen mode Exit fullscreen mode

The nginx.service.j2 template would define how NGINX starts, stops, and reloads as a systemd service.

Step 7: Cleanup (Optional but Recommended - purge_deps.yml)

After a successful build, you might want to remove temporary build files and even some build dependencies to free up space, especially in a containerized environment.

# Example content for purge_deps.yml --- - name: Clean up temporary build files file: path: "{{ item }}" state: absent loop: - /tmp/nginx.tgz - /tmp/nginx-1.23.2 - /tmp/ngx_devel_kit-0.3.0.tgz - /tmp/ngx_devel_kit-0.3.0 - /go/src/github.com/hashicorp/ngx_http_consul_backend_module 
Enter fullscreen mode Exit fullscreen mode

Running the Ansible Playbook

To execute this entire process, you would have a playbook like nginx-build-playbook.yml:

--- - name: Build NGINX with Consul module hosts: your_nginx_servers become: yes # Run tasks with sudo/root privileges roles: - nginx-with-consul-module 
Enter fullscreen mode Exit fullscreen mode

And run it with:

ansible-playbook nginx-build-playbook.yml -i hosts.ini 
Enter fullscreen mode Exit fullscreen mode

Conclusion

Building NGINX with custom modules can seem daunting, but by breaking down the process into logical Ansible tasks, we can create a robust, automated, and easily repeatable solution.

This approach is invaluable for consistent deployments across development, testing, and production environments.

You now have a solid foundation for integrating any custom NGINX module into your infrastructure using the power of Ansible!


LiveAPI helps you get all your backend APIs documented in a few minutes.

With LiveAPI, you can generate interactive API docs that allow users to search and execute endpoints directly from the browser.

LiveAPI Demo

If you're tired of updating Swagger manually or syncing Postman collections, give it a shot.

Top comments (4)

Collapse
 
dotallio profile image
Dotallio

Love how you broke the process into clear Ansible roles, really makes custom NGINX builds less of a headache. How do you usually handle upgrades for these modules later on - have you automated that part as well?

Collapse
 
lovestaco profile image
Athreya aka Maneshwar

Thanks

Collapse
 
parag_nandy_roy profile image
Parag Nandy Roy

This is gold for DevOps beginners and pros alike ...

Collapse
 
lovestaco profile image
Athreya aka Maneshwar

Haha so true