Integrating NGINX Controller to CICD Pipeline
Introduction
Assume application development team delivers an application. An application consists of two components: frontend and API. Both of them have to be exposed to the world. If frontend part is fairly static and has just a few URIs then API URIs layout changes often. NGINX Controller provides a rich tool set to simplify application delivery whether it is API or generic app. This article covers a way to integrate NGINX Controller to CICD pipeline to speed up application publishing ever further.
Architecture and Prerequisites
Architecture is pretty typical for controller deployment. NGINX Controller manages two NGINX Plus gateways. They in turn publish an application to the world.
As a demo application I use Httpbin. This app consists of two components: frontend and an API. URI structure is flat. One pager frontend avaliable right behind "/". API endpoints base path is "/" as well, so endpoints are ("/ip", "/uuid", "/get", so on).
The way this app is deployed is not important for this article. It is deployed and only reachable from gateways.
Everything you see on the picture above is a prerequisite. NGINX Controller, NGINX Plus and application are deployed. Gateways are registered on a Controller. Controller user for a gitlab has admin permissions.
NGINX Controller Configuration Abstractions
Controller represents gateway configuration as set of abstractions. Picture below displays abstraction dependencies.
- Environment is a logical group of configuration entities which publish an app together. Entities from different environments can not be mixed.
- Application is a logical representation of a real application. Each application can include multiple components. E.g. Httpbin application consists of frontend and api components.
- Component contains lists of URIs, list of backend servers (workload groups) and routes defining where to forward request to particular URI.
- Gateway represents a virtual server which matches requests with certain hostnames.
A published application ties together a gateway to listen for requests and one or more components to define routes to backend servers.
Controller Integration to CICD Pipeline
NGINX Controller provides an REST API which allows to integrate it with any kind of CICD platform. Integration aims to automatically publish both frontend and API application components. Following pipeline implements application publishing in three stages. First stage "create-env" creates common configuration abstractions for both components. Other two "publish-frontend" and "publish-api" publish frontend and api components respectively.
Note: Each stage has "only" directive. It allows to reduce pipeline execution time by executing only stages with changed configuration.
A script for each stage has only one command. It executes Ansible playbook which contains all steps to reach desired state.
Note: Playbooks use official Ansible collection for to communicate with controller. This approach provides much more clear code than raw curl use.
image: alpine:3.12.1 variables: CONTROLLER_FQDN: ctr.f5-demo.com stages: - create-env - deploy-frontend - deploy-api default: before_script: - apk add ansible~=2.9.14 - ansible-galaxy collection install nginxinc.nginx_controller create-env: stage: create-env script: - ansible-playbook playbooks/common.yml only: changes: - playbooks/common.yml - .gitlab-ci.yml deploy-frontend: stage: deploy-frontend script: - ansible-playbook playbooks/frontend.yml only: changes: - playbooks/frontend.yml - .gitlab-ci.yml deploy-api: stage: deploy-api script: - ansible-playbook playbooks/api.yml only: changes: - playbooks/api.yml - .gitlab-ci.yml
Playbook from "create-env" stage creates common configuration entities (see "controller configuration abstractions" section): environment, application and a gateway. Playbook code is self explanatory. "nginx_controller_generate_token" role obtains a login credentials from controller. Other three roles create actual configuration entities.
- hosts: localhost gather_facts: no collections: - nginxinc.nginx_controller vars: env_name: prod app_name: httpbin roles: - role: nginx_controller_generate_token vars: nginx_controller_user_email: "{{ lookup('env', 'CONTROLLER_USER') }}" nginx_controller_user_password: "{{ lookup('env', 'CONTROLLER_PASSWORD') }}" nginx_controller_fqdn: "{{ lookup('env', 'CONTROLLER_FQDN') }}" nginx_controller_validate_certs: false - role: nginx_controller_environment vars: nginx_controller_environment: metadata: name: "{{ env_name }}" - role: nginx_controller_gateway vars: nginx_controller_environmentName: "{{ env_name }}" nginx_controller_gateway: metadata: name: "{{ app_name }}" desiredState: ingress: uris: "http://nplus.httpbin.f5-demo.com": {} placement: instanceRefs: - ref: "/infrastructure/locations/unspecified/instances/ip-10-4-96-225.us-west-2.compute.internal" - ref: "/infrastructure/locations/unspecified/instances/ip-10-4-96-90.us-west-2.compute.internal" - role: nginx_controller_application vars: nginx_controller_environmentName: "{{ env_name }}" nginx_controller_app: metadata: name: "{{ app_name }}"
Playbook from "publish-frontend" stage publishes a frontend component. Structure is similar to previous one. First role logs in to controller. "nginx_controller_component" role creates an application component which represents a gateway configuration to publish an application frontend.
- hosts: localhost gather_facts: no collections: - nginxinc.nginx_controller vars: env_name: prod app_name: httpbin roles: - role: nginx_controller_generate_token vars: nginx_controller_user_email: "{{ lookup('env', 'CONTROLLER_USER') }}" nginx_controller_user_password: "{{ lookup('env', 'CONTROLLER_PASSWORD') }}" nginx_controller_fqdn: "{{ lookup('env', 'CONTROLLER_FQDN') }}" nginx_controller_validate_certs: false - role: nginx_controller_component vars: nginx_controller_environmentName: "{{ env_name }}" nginx_controller_appName: "{{ app_name }}" nginx_controller_component: metadata: name: frontend displayName: "Frontend" description: "Frontend for {{ app_name }} API" desiredState: ingress: uris: "/": {} gatewayRefs: - ref: "/services/environments/{{ env_name }}/gateways/{{ app_name }}" backend: workloadGroups: group1: uris: "http://10.4.113.213:30445": {} monitoring: response: status: range: startCode: 200 endCode: 201 match: true
Playbook from "publish-api" stage publishes an api application component. Unlike to frontend which has only one static URI to publish (forward), API component should handle number of API URIs "endpoints". To avoid manual input and in sake of single source of truth controller can import all URIs from an OpenAPI file. Role "nginx_controller_api_definition_import" reads openAPI file and imports all endpoints as a new API version (v1). Following roles create a published API out of a version and reference it form a component.
- hosts: localhost gather_facts: no collections: - nginxinc.nginx_controller env_name: prod app_name: httpbin roles: - role: nginx_controller_generate_token vars: nginx_controller_user_email: "{{ lookup('env', 'CONTROLLER_USER') }}" nginx_controller_user_password: "{{ lookup('env', 'CONTROLLER_PASSWORD') }}" nginx_controller_fqdn: "{{ lookup('env', 'CONTROLLER_FQDN') }}" nginx_controller_validate_certs: false - role: nginx_controller_api_definition_import vars: nginx_controller_api_definition_version: v1 nginx_controller_api_definition_name: "{{ app_name }}" nginx_controller_api_definition: "{{ lookup('file', '../httpbin.openapi.json') }}" - role: nginx_controller_publish_api vars: nginx_controller_environment: "{{ env_name }}" nginx_controller_application: "{{ app_name }}" nginx_controller_publish_api: metadata: name: "v1" displayName: "v1" desiredState: basePath: "/api" stripWorkloadBasePath: true apiDefinitionVersionRef: ref: "/services/api-definitions/httpbin/versions/v1" gatewayRefs: - ref: "/services/environments/{{ env_name }}/gateways/{{ app_name }}" - role: nginx_controller_component vars: nginx_controller_environmentName: "{{ env_name }}" nginx_controller_appName: "{{ app_name }}" nginx_controller_component: metadata: name: api displayName: "API" description: "{{ app_name }} API" desiredState: ingress: uris: "/ip": matchMethod: EXACT gatewayRefs: - ref: "/services/environments/{{ env_name}}/gateways/{{ app_name}}" backend: workloadGroups: group1: uris: "http://10.4.113.213:30445": {} monitoring: response: status: range: startCode: 200 endCode: 201 match: true publishedApiRefs: - ref: "/services/environments/{{ env_name }}/apps/{{ app_name}}/published-apis/v1"
Once all stages end controller transforms all directives to NGINX Plus configuration and pushes it down to gateways. Therefore publishing both frontend and API application components to the world.
Hope it is helpful. Feel free to reach me with questions and concerns.
Repository: https://gitlab.com/464d41/deploy-httpbin-via-nginx-controller