Modern applications are complex. Complex frontends must interact with complex backends. These interactions are handled through APIs that the backends expose. One of the most common forms of these APIs are restful APIs that leverage common HTTP verbs sent to meaningful URIs.
There are multiple moving parts here and it's not uncommon to have multiple teams working on these parts. There might be one team building the frontend that consumes these APIs and another that builds the APIs. Or it could be one team with multiple people working on the frontend and backend in parallel. In either case, you have multiple people involved in the development of the overall application.
How do you keep them aligned through the development? One of the best ways is by exercising strong design first principles with the creation of specifications that define the contract of the APIs that are going to be consumed. OpenAPI provides a well adopted language for doing so. It standardizes how you represent APIs.
By having a standardized representation of the APIs, all parties have a strong starting point for the development. But more is needed to keep everyone aligned. As a project evolves, changes happen and drift can occur. Drift is costly and should be avoid.
But why is drift costly? It's because it can have a major negative impact to the artifacts that are created by all parties involved. Mocks, code, and deployments get out of sync with the API specification. Therefore, it is important to keep the people working on the project and the artifacts associated with it aligned.
For example, you are building an application with one set of people building the frontend and one set building the backend. You start off in agreement and settle on a contract, but over time one side or the other evolves the contract. One group is working with the new contract and the other is working with the old contract. Now when it is time to integrate, they two sides no longer line up and some amount of rework is needed.
This is where Ambassador Blackbird can help out.
Introducing Ambassador Blackbird
Ambassador Blackbird provides a set of capabilities that allow you to store your OpenAPI spec, generate mocks and code, and deploy the implemented code in a hosted environment. Today, I'm going to walk through the first part of this to show how Blackbird can be used in a CI/CD environment to keep a running mock up to date with changes that are happening to your API spec.
Our OpenAPI spec
Let's start off by looking at a very simple spec for a user service. We will be using this spec as a starting point and make a few changes to it in order to test out the CI/CD process.
{
"openapi": "3.0.0",
"info": {
"title": "Simple Users Service",
"description": "This is a simple user service for demo purposes.",
"version": "0.0.1"
},
"paths": {
"/users": {
"get": {
"operationId": "listUsers",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paged array of Users",
"headers": {
"x-next": {
"description": "A link to the next page of responses",
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Users"
}
}
}
},
"default": {
"description": "Unexpected Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"description": "Creates a new user",
"operationId": "createUser",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Created user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"required": [
"username"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"username": {
"type": "string"
}
}
},
"Users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"Error": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
}
Getting started with Ambassador Blackbird
Next, let's get things going with Ambassador Blackbird. First, we need to visit Blackbird to create an account and upload the spec. Once the spec is uploaded, let's get some details on the command line. For more information, checkout out the Blackbird documentation. What we are really interested in is the download information for the Blackbird CLI. These details can be found in the quickstart.
Go ahead and make a note of the URL for the Linux AMD64 binary. At the time of this writing, that URL is https://blackbird.a8r.io/api/download/cli/v1.7.0/linux/amd64
. This will be important when we setup the CI/CD job.
Before moving on, I'd like to talk a little about mocking. Mocking provides a mechanism for work to begin before dependent APIs are implemented, allowing development in isolation. Doing so allows you to be able to develop more quickly by allowing you to develop in parallel. It can also give you more control in testing your solutions and being able to try various edge cases. Blackbird enables you to quickly spin up mocks from your API spec and keep them in sync.
Now we're going to be using Blackbird to create a mock for us of the uploaded API spec. In the Blackbird UI, let's go ahead and create a mock from our spec. Doing so is very easy. We just navigate to the APIs and click Create Mock
on the card for our uploaded API spec. Will give it a simple name like simple-users-service
. Once the mock has been created, make a note of the URL for use later.
Setting up CI/CD
Now that the API spec has been uploaded and a mock is running, let's get things hooked up into a CI/CD pipeline so that the mock is updated anytime there is a change to the OpenAPI spec. For this post, we are going to be working with GitHub, but any combination of tools would work as long as changes can trigger a step that runs a command.
Here's the plan that we are going to follow:
- Add the OpenAPI spec to a git repository in GitHub
- Download the Blackbird CLI locally and login in
- Setup secrets in GitHub
- Create the GitHub Action workflow
Creating the repo and adding our OpenAPI spec
First, let's create the repo and push up our initial commit. For this, I am creating the simple-users-service-spec repo in GitHub. From there, just create a local directory, add the spec above, commit it, and push it to your repo.
Download the Blackbird CLI and login
For step two, find the appropriate download instructions for your device in the Blackbird quickstart. Once you have it download, login with the CLI by running blackbird login
in your terminal. This step is going to create a configuration file that we need to store in GitLab. Better automation will be added in the future to simplify this process.
Setup secrets in GitHub
To get GitHub ready, find the configuration file that was created at ~/.blackbird/config
and copy the contents. Go into your GitHub repository and create a new secret under Settings -> Secrets and variables -> Actions
named BLACKBIRD_CONFIG
. Copy the contents of the config file into that secret.
Create the GitHub Action
Now, let's setup the GitHub Action. The goal of this job will be to update the API spec and the mock in Blackbird whenever changes are merged to master. We want to make note of a couple of items. These items are the slug names of the API and the Mock within Blackbird. They can be found by running blackbird api list
and blackbird mock list
respectively. In my case, the slug name for the API is simple-users-service
and the slug name for the mock is also simple-users-service
.
Here is the GitHub Action that we are going to add to our repo. We are saving this under .github/workflows/update-api.yml
.
name: Update API Job
run-name: Update API
on:
push:
branches:
- main
jobs:
Update-API-Mock:
if: contains( github.ref, 'main')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- shell: bash
env:
BLACKBIRD_CONFIG: ${{ secrets.BLACKBIRD_CONFIG }}
run: |
mkdir ~/.blackbird
printf "$BLACKBIRD_CONFIG" > ~/.blackbird/config
curl -fL 'https://blackbird.a8r.io/api/download/cli/v1.7.0/linux/amd64' -o ./blackbird
chmod +x ./blackbird
./blackbird login
./blackbird api update --spec-path=simple-users-service.json simple-users-service
Pulling everything together
We now have our user service OpenAPI spec in our repo, the repo setup with secrets, and out GitHub Action defined. Let's pull it all together now and see it all working together.
To get started, commit your GitHub action and push it up. Now everything should be in sync. Let's do some testing.
Testing the initial spec
First, we're going to test hitting the existing /users
endpoint and the users/{userId}
endpoint that doesn't exist and see what we get.
The first request we are making is to the /users
endpoint.
curl <Mock URL from earlier>/users
The response we get back is something like this:
[
{
"username": "dolore cupidatat aute in esse",
"id": "46021b4a-602e-b53c-17c0-77fa5a0d7865"
},
{
"username": "reprehenderit laboris sed",
"id": "833c47a2-6c27-7861-21cc-6db880030ed8"
}
]
Now we're going to hit the non-existent /users/{userId}
endpoint.
curl <Mock URL from earlier>/users/32073770-8180-4fd5-be48-c4c8bd2b9998
The response we get back is something like this:
{
"type": "https://www.getambassador.io/docs/blackbird/latest/reference/mock-server-errors#no_path_matched_error",
"title": "Route not resolved, no path matched.",
"status": 404,
"detail": "The route /users/32073770-8180-4fd5-be48-c4c8bd2b9998 hasn't been found in the specification file."
}
Updating our spec
Now that we've established the baseline, let's go ahead and update our spec to add in the get user by ID endpoint. Feel free to create a branch, update the spec, and follow your normal process. The GitHub Action will trigger when the changes are merged or pushed to main
.
For the change, I am updating the spec file so that it looks like the below.
{
"openapi": "3.0.0",
"info": {
"title": "Simple Users Service",
"description": "This is a simple user service for demo purposes.",
"version": "0.0.1"
},
"paths": {
"/users": {
"get": {
"operationId": "listUsers",
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "A paged array of Users",
"headers": {
"x-next": {
"description": "A link to the next page of responses",
"schema": {
"type": "string"
}
}
},
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Users"
}
}
}
},
"default": {
"description": "Unexpected Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"description": "Creates a new user",
"operationId": "createUser",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"username": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Created user",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/users/{userId}": {
"get": {
"parameters": [
{
"in": "path",
"name": "userId",
"schema": {
"type": "string",
"format": "uuid"
},
"required": true,
"description": "String UUID ID of the user to get",
"example": "32073770-8180-4fd5-be48-c4c8bd2b9998"
}
],
"responses": {
"200": {
"description": "A User object",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/User"
}
}
}
},
"default": {
"description": "Unexpected error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"User": {
"type": "object",
"required": [
"username"
],
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"username": {
"type": "string"
}
}
},
"Users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"Error": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
}
Now commit the changes and push them up! Once the change hits main
, the GitHub Action job will trigger and update your API and mock running in Blackbird. You can confirm these changes by checking out your API and Mock in the Blackbird UI.
Testing the new get user endpoint
Once everything is updated, we can try out the new endpoint. Blackbird has updated the mock in-place, so we are able to use our previous URL for testing.
We're now going to test /users/{userId}
now that we've added that endpoint to the spec.
curl <Mock URL from earlier>/users/32073770-8180-4fd5-be48-c4c8bd2b9998
The response we get back is something like this:
{
"id": "e33e9d81-4398-a6a2-f334-3c7cd9c04258",
"username": "voluptate"
}
And there you have it. We've quickly and easily setup a working CI/CD pipeline to help you keep the contract aligned as you iterate in your development! But this example is only scratching the surface of how Blackbird can help you. For more information on Blackbird, check out the Ambassador Blackbird website.
Top comments (0)