DevOps with Kong, Deck and Azure Pipelines

Akshay
The Startup
Published in
7 min readDec 6, 2020

Kong

Since you are here, you might be already aware of what Kong is. But just for a quick heads-up, Kong is an open-source, lightweight API gateway i.e it will act as a proxy for all of your services. Hence, all of the services can be consumed from a single centralized location. It is more suitable for a microservices based application.

There are few ways to manage Kong which are -

1. using cURL commands with Admin API and saving it in a script

2. using Postman collections

3. decK i.e declarative Kong which is a tool to manage Kong’s configuration in a declarative fashion

We are going to see the 3rd approach as it allows us to maintain and manage these configurations within our source control and pipelines.

Deck

Deck uses a yaml file maintained by us to do its operation. It provides certain commands which you can execute to query kong.

Its recommended that
- If you are using deck to manage configurations, any other way of interaction with kong should be avoided.

- Deck should not be running on 2 environments simultaneously. This will avoid any conflict among different stages of the pipeline

More about deck here: https://docs.konghq.com/deck/overview/

https://docs.konghq.com/deck/guides/best-practices/

Workflow

What we are going to do?

  • We will manage our kong configurations using deck and CI pipelines to avoid manual intervention/running manual scripts
  • We will do this for each of the different environments we might be having (dev, test, master etc). For this demo, I have assumed test and master environment
  1. Pipelines

We will create 2 pipelines, Deck Diff and Deck Sync

Pipelines

Deck Diff

pr:
- master
resources:
- repo: self
stages:
- stage: build_and_push_docker_image
displayName: Build and push deck docker image
jobs:
- job: build_docker_image
displayName: Build and push docker
steps:
- task: Docker@2
displayName: Build and Push docker image
inputs:
containerRegistry: 'MyDockerHub'
repository: 'pjanicked/test-deck'
command: 'buildAndPush'
Dockerfile: '**/Dockerfile'
tags: latest
- stage: deck_test
displayName: Deck Test Environment
condition: and(or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'), eq(variables['Build.Reason'], 'Manual')), succeeded())
dependsOn: build_and_push_docker_image
jobs:
- template: /deck_commands.yaml
parameters:
kong_url: https://pjanicked-kong-test.herokuapp.com/kong-admin
kong_file: test.yaml

- stage: deck_master
displayName: Deck Master Environment
condition: and(or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'), eq(variables['Build.Reason'], 'Manual')), succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
dependsOn: deck_test
jobs:
- template: /deck_commands.yaml
parameters:
kong_url: https://pjanicked-kong-master.herokuapp.com/kong-admin
kong_file: master.yaml

Deck Sync

trigger:
- master
resources:
- repo: self
stages:
- stage: build_and_push_docker_image
displayName: Build and push deck docker image
jobs:
- job: build_docker_image
displayName: Build and push docker
steps:
- task: Docker@2
displayName: Build and Push docker image
inputs:
containerRegistry: 'MyDockerHub'
repository: 'pjanicked/test-deck'
command: 'buildAndPush'
Dockerfile: '**/Dockerfile'
tags: latest
- stage: deck_test
displayName: Deploy Test Environment
condition: and(or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'), eq(variables['Build.Reason'], 'Manual')), succeeded())
dependsOn: build_and_push_docker_image
jobs:
- template: /deck_commands.yaml
parameters:
kong_url: https://pjanicked-kong-test.herokuapp.com/kong-admin
to_sync: true
kong_file: test.yaml

- stage: deck_master
displayName: Deck Master Environment
condition: and(or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'BatchedCI'), eq(variables['Build.Reason'], 'Manual')), succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
dependsOn: deck_test
jobs:
- template: /deck_commands.yaml
parameters:
kong_url: https://pjanicked-kong-master.herokuapp.com/kong-admin
to_sync: true
kong_file: master.yaml

Deck Diff pipeline runs deck ping, validate and diff commands while Deck Sync pipeline runs deck ping, validate and sync commands

Barring parameter differences, both pipelines have same pipeline stages:

  • Building deck docker image: this stage builds a custom docker image of deck which essentially runs a shell script file (more on that later) that contains execution of deck commands
  • Running deck commands for each environment: After the image has been built, we run the deck commands for each of the available environment(test, master) one by one
  • We will have a separate configuration yaml file for each of the environment (test.yaml, master.yaml) in kong folder where one can specify the services, routes, plugins (configurations)
Pipeline stages

2. Template files

We have 2 template files for executing deck commands via stages.

Deck_commands.yaml

parameters:
- name: kong_url
type: string
default: https://pjanicked-kong-test.herokuapp.com/kong-admin
- name: to_sync
type: boolean
default: false
- name: kong_file
type: string
default: test.yaml
jobs:
- job: Deck_Ping
displayName: Deck Ping for Kong
steps:
- template: /action.yaml
parameters:
command: ping
options: kong-addr ${{ parameters.kong_url }}

- job: Deck_Validate
displayName: Deck Validate for Kong
dependsOn: Deck_Ping
steps:
- template: /action.yaml
parameters:
command: validate
options: kong-addr ${{ parameters.kong_url }}
kong_workspaces: kong
kong_file: ${{ parameters.kong_file }}
- ${{ if eq(parameters.to_sync, false) }}:
- job: Deck_Diff
displayName: Deck Diff for Kong
dependsOn:
- Deck_Ping
- Deck_Validate
steps:
- template: /action.yaml
parameters:
command: diff
options: kong-addr ${{ parameters.kong_url }}
kong_workspaces: kong
kong_file: ${{ parameters.kong_file }}
- ${{ if eq(parameters.to_sync, true) }}:
- job: Deck_Sync
displayName: Deck Sync for Kong
dependsOn:
- Deck_Ping
- Deck_Validate
steps:
- template: /action.yaml
parameters:
command: sync
options: kong-addr ${{ parameters.kong_url }}
kong_workspaces: kong
kong_file: ${{ parameters.kong_file }}

We execute each of the deck commands as job and pass paameters such as

  • command: the deck command to execute.
  • options: options/flags passed to the deck command. Here, we pass the kong address as kong-addr flag.
  • kong_workspaces: we mention directory of our config files here.
  • kong_file: the actual kong config file name.

Worth noting that, we execute deck diff job for Deck Diff pipeline and not for Deck Sync pipeline and vice versa, as we have added if condition using azure pipeline parameters

Actions.yaml

parameters:
- name: command
type: string
default: ping
- name: kong_workspaces
type: string
default: kong
- name: options
type: string
- name: kong_file
type: string
default: test.yaml

steps:
- task: Docker@2
displayName: Docker Run deck
- script: |
docker run pjanicked/test-deck ${{ parameters.command }} ${{ parameters.kong_workspaces }} ${{ parameters.options }} ${{ parameters.kong_file }}

This is the task steps template which actually executes the deck commands using the docker image we built. It passes the commands to a shell script file and the commands are executed.

3. Dockerfile and shellscript

Dockerfile

FROM hbagdi/deck

COPY entrypoint.sh /entrypoint.sh
COPY kong /kongUSER rootRUN ["chmod", "+x", "/entrypoint.sh"]ENTRYPOINT [ "/entrypoint.sh" ]

A self explanatory docker file if you are well-versed with docker I guess. We copy some files and make the shell script executable here.

ShellScript (entrypoint.sh)

#!/bin/sh -l
set -e -o pipefail
main (){
cmd=$1
dir=$2
ops=$3
opsvalue=$4
kongfile=$5
if [ ! -e ${dir} ]; then
echo "${dir}: No such file or directoy exists";
exit 1;
fi
echo "Executing: deck $cmd --$ops $opsvalue -s $dir/$kongfile"
deck $cmd --$ops $opsvalue -s $dir/$kongfile
}
case $1 in
"ping") deck $1 --$3 $4;;
"validate"|"diff"|"sync") main $1 $2 "$3" "$4" "$5";;
* ) echo "deck $1 is not supported." && exit 1 ;;
esac

As seen above, the control flows via the case statements.

For the main( ) method, we execute the deck commands directly.

4. Example pipeline run

  • I have used kong heroku (currently, archived) to deploy a kong instance. (Note: This instance comes preconfigured with certain services and plugins. Since I couldnt have a test machine, I decided to use heroku. The docker-compose file provided below works fine though when locally tested)
  • Test Env Kong Url: https://pjanicked-kong-test.herokuapp.com/
  • Master Env Kong Url: https://pjanicked-kong-master.herokuapp.com/
  • We add a new service (mock_service) and a new route (/mock) in our test.yaml and master.yaml
  • The other service (kong-admin) and consumer (heroku-admin) added is to adhere to the preconfigured setup used by kong heroku

Sample test.yaml file defining our services, routes and consumers.

_format_version: "1.1"
services:
- connect_timeout: 60000
host: mockbin.org
name: mock_service
port: 80
protocol: http
read_timeout: 60000
retries: 8
write_timeout: 60000
routes:
- name: mock_test
paths:
- /mock
preserve_host: false
protocols:
- http
- https
regex_priority: 0
strip_path: true
- connect_timeout: 60000
host: localhost
name: kong-admin
port: 8001
protocol: http
read_timeout: 60000
retries: 5
write_timeout: 60000
routes:
- name: admin_route
paths:
- /kong-admin
preserve_host: false
protocols:
- http
- https
regex_priority: 0
strip_path: true
consumers:
- username: heroku-admin

5. Result

Deck Diff Pipeline

  • Deck Ping
  • Deck Validate
  • Deck Diff

Deck Sync Pipeline

Deck Ping and Validate produces same result as Deck Diff pipeline

  • Deck Sync

After the successful run, we can now access our created service by accessing http://pjanicked-kong-test.herokuapp.com/mock/request

Voila, we used CI to successfully manage Kong.

Some notes

  1. When working with Azure pipelines and Azure Repos Git, make sure you give the template path right.
  2. If you are using a specific build agent, then make sure you add
pool:
Linux Build (whatever your build agent is)

3. In action.yaml, make sure to give the image name as ‘registryurl/image-name’ to successfully locate image

4. As mentioned earlier, make sure no 2 deck processes/jobs are running together at one moment (very imp!)

5. Example docker-compose file for kong

version: "3.7"volumes:
kong_data: {}

networks:
kong-net:
services:
kong-database:
image: postgres:9.6
container_name: kong-postgres
restart: on-failure
networks:
- kong-net
volumes:
- kong_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: kong
POSTGRES_PASSWORD: ${KONG_PG_PASSWORD:-kong}
POSTGRES_DB: kong
ports:
- "5432:5432"
healthcheck:
test: ["CMD", "pg_isready", "-U", "kong"]
interval: 30s
timeout: 30s
retries: 3
kong-migration:
image: ${KONG_DOCKER_TAG:-kong:latest}
command: kong migrations bootstrap
networks:
- kong-net
restart: on-failure
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-database
KONG_PG_DATABASE: kong
KONG_PG_USER: kong
KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-kong}
depends_on:
- kong-database
kong:
image: ${KONG_DOCKER_TAG:-kong:latest}
restart: on-failure
networks:
- kong-net
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-database
KONG_PG_DATABASE: kong
KONG_PG_USER: kong
KONG_PG_PASSWORD: ${KONG_PG_PASSWORD:-kong}
KONG_PROXY_LISTEN: 0.0.0.0:8000
KONG_PROXY_LISTEN_SSL: 0.0.0.0:8443
KONG_ADMIN_LISTEN: 0.0.0.0:8001
depends_on:
- kong-database
healthcheck:
test: ["CMD", "kong", "health"]
interval: 10s
timeout: 10s
retries: 10
ports:
- "8000:8000"
- "8001:8001"
- "8443:8443"
- "8444:8444"

Special thanks to this article https://konghq.com/blog/gitops-for-kong-managing-kong-declaratively-with-deck-and-github-actions/ and it’s author Takafumi Ikeda for showing the path.

Here’s the source code on Github

I hope this helps you!

--

--