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

Published Dec 08, 2020
Version 1.0
No CommentsBe the first to comment