Skip to main content

Deploy API gateway configuration from your CI/CD pipeline

How platform teams automate API gateway CI/CD configuration deployment from GitHub Actions, with a full audit trail covering every pipeline-initiated change.

  • ci-cd
  • platform-engineering
  • api-management
  • management-mcp
  • automation
  • infrastructure-as-code
Zerq team

Every service team knows the feeling. The application deploy succeeds, the tests pass, the container starts cleanly. Then someone checks the gateway and the new route is missing. The API does not exist yet. A manual step is waiting: log into the admin console, find the right collection, add the proxy, set the upstream URL, publish. The release is technically done but not actually reachable.

That gap creates real problems. The config change happens outside the pull request, outside the deploy pipeline, and outside version control. If something breaks after the release, reconstructing what changed is hard. The application change is in Git, the container image is tagged, but the API gateway CI/CD configuration step is somewhere in the console, possibly different from what anyone remembers, with no clear history of who changed it or when.

The right answer is to manage gateway configuration as code, using the same pipeline that deploys your services. Zerq's Management MCP provides a programmatic interface to the full Zerq control plane. It authenticates with the same OIDC identity model as your admin console, and records every operation in the audit trail alongside manual changes.

Why existing approaches fall short

The typical workaround is a bash script that calls the gateway's management REST API directly. It runs in the pipeline, creates the route, and moves on. This pattern works until it does not.

The first problem is credentials. A script calling a management API needs a token with admin-level access. That token lives in CI secrets, has no expiry enforcement, no rotation schedule, and no attached identity in the audit log. It is a shared secret with root-level access to your API platform. When someone leaves the team, the token stays. When the pipeline runs, the audit record shows a generic service account identifier with no context about which pipeline job, which commit, or which engineer triggered the run.

Kong's decK CLI follows a similar pattern. It is a well-designed tool, but it requires learning Kong's proprietary YAML schema: a different data model from your application code, a different toolchain to install and version in CI, and a different operator model to maintain. Apigee requires gcloud apigee commands and ties you to GCP authentication even when your services run elsewhere. AWS API Gateway requires CloudFormation change sets, which are slow, require their own permission model, and produce a separate change history from your application deployments.

The second problem is the audit gap. Even when scripts work correctly, they produce changes that appear differently in the audit log from human-initiated changes, or do not appear at all. For regulated environments, this creates a gap in the evidence record: the compliance team can see what a human admin changed, but the automated changes are either missing or unattributed.

How Zerq handles CI/CD configuration deployment

Zerq's Management MCP exposes the full control plane as MCP tools over JSON-RPC. A pipeline authenticates with an OIDC token from your identity provider, initializes a session, and calls tools to read or write platform configuration. Every call lands in the audit log under the pipeline's own identity, with the same schema as a change made by an admin in the console.

The tools available to an automation pipeline include:

  • list_collections, get_collection, create_collection, update_collection, toggle_collection_status (manage API collections)
  • list_proxies, get_proxy, create_proxy, update_proxy (manage proxies within collections)
  • get_proxy_workflow, update_proxy_workflow, validate (manage workflow configuration)

The auth model uses OIDC throughout. You create a machine identity in your identity provider, assign it the modifier role in Zerq's RBAC, and configure OIDC token exchange in your pipeline. No shared API tokens. No separate credential path. The pipeline identity appears in the audit log with its OIDC subject claim, filterable by any team member with audit access.

Step 1: create a service account with the right role

In your identity provider (Okta, Auth0, Azure AD, Keycloak), create a machine identity for your CI pipeline. In Zerq's RBAC, assign it the modifier role. The modifier role allows create and update operations but excludes destructive operations (DELETE) and audit log access, which require admin and auditor roles respectively.

Keep read automation and write automation on separate identities. A separate viewer-role service account handles inventory sync and dashboard reads. A modifier-role account handles create and update operations. This limits the blast radius: a compromised read-only pipeline credential cannot modify platform configuration.

Step 2: configure OIDC token exchange in GitHub Actions

# .github/workflows/deploy-api-config.yml
name: Deploy API configuration

on:
  push:
    branches: [main]
    paths:
      - 'api-config/**'

permissions:
  id-token: write   # Required for OIDC token exchange
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Exchange OIDC token
        id: oidc
        run: |
          TOKEN=$(curl -s -X POST \
            "${{ vars.OIDC_TOKEN_URL }}" \
            -d "grant_type=client_credentials" \
            -d "client_id=${{ secrets.MCP_CLIENT_ID }}" \
            -d "client_secret=${{ secrets.MCP_CLIENT_SECRET }}" \
            -d "scope=openid" \
            | jq -r '.access_token')
          echo "token=$TOKEN" >> $GITHUB_OUTPUT

      - name: Deploy API config
        env:
          MCP_TOKEN: ${{ steps.oidc.outputs.token }}
          MCP_URL: ${{ vars.MCP_MANAGEMENT_URL }}
        run: ./scripts/deploy-api-config.sh

Store MCP_CLIENT_ID and MCP_CLIENT_SECRET as GitHub Actions secrets. MCP_MANAGEMENT_URL is the Management MCP endpoint, typically https://api.example.com/api/v1/mcp, stored as a repository variable so it is visible without being sensitive. Gate production deployments behind a required manual approval step in the workflow so the approval record and the audit log entry are correlated evidence.

Step 3: initialize a Management MCP session

Every Management MCP interaction starts with an initialize call. The response includes an Mcp-Session-Id header that all subsequent requests must include.

#!/bin/bash
# scripts/deploy-api-config.sh
set -euo pipefail

MCP_URL="${MCP_URL:-https://api.example.com/api/v1/mcp}"

INIT_RESPONSE=$(curl -s -i "$MCP_URL" \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "init-1",
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": { "name": "github-actions", "version": "1.0" }
    }
  }')

SESSION_ID=$(echo "$INIT_RESPONSE" | grep -i "Mcp-Session-Id" | awk '{print $2}' | tr -d '\r')
echo "Session initialized: $SESSION_ID"

Pass a meaningful clientInfo.name in the initialize payload. This value appears in the user_agent field of every audit log entry for this session, making it easy to filter audit events by pipeline name.

Step 4: read current state before writing

Always read before writing. This makes the pipeline idempotent: if the collection already exists, update it in place rather than failing on a duplicate create.

# Read existing collections to check if target already exists
EXISTING=$(curl -s "$MCP_URL" \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": "read-1",
    "method": "tools/call",
    "params": { "name": "list_collections", "arguments": {} }
  }' | jq '.result.content[0].text | fromjson')

echo "Existing collections: $(echo $EXISTING | jq length)"

# Check if target collection exists by name
TARGET_ID=$(echo $EXISTING | jq -r '.[] | select(.name == "payments-api-v2") | .id // empty')

If TARGET_ID is non-empty, call update_collection on the existing resource. If it is empty, call create_collection. The same pattern applies to proxies within a collection. This read-before-write approach is the core safety gate for CI/CD automation.

Step 5: create or update the collection and proxy

if [ -z "$TARGET_ID" ]; then
  # Collection does not exist, create it
  COLLECTION=$(curl -s "$MCP_URL" \
    -H "Authorization: Bearer $MCP_TOKEN" \
    -H "Mcp-Session-Id: $SESSION_ID" \
    -H "Content-Type: application/json" \
    -d '{
      "jsonrpc": "2.0",
      "id": "create-col-1",
      "method": "tools/call",
      "params": {
        "name": "create_collection",
        "arguments": {
          "name": "payments-api-v2",
          "basePath": "/payments/v2",
          "description": "Deployed by CI pipeline"
        }
      }
    }' | jq '.result.content[0].text | fromjson')

  COLLECTION_ID=$(echo $COLLECTION | jq -r '.id')
else
  COLLECTION_ID="$TARGET_ID"
fi

# Add a proxy to the collection
curl -s "$MCP_URL" \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d "{
    \"jsonrpc\": \"2.0\",
    \"id\": \"create-proxy-1\",
    \"method\": \"tools/call\",
    \"params\": {
      \"name\": \"create_proxy\",
      \"arguments\": {
        \"collectionId\": \"$COLLECTION_ID\",
        \"name\": \"Process payment\",
        \"path\": \"/process\",
        \"method\": \"POST\",
        \"targetUrl\": \"https://payments.internal/v2/process\"
      }
    }
  }"

Step 6: validate and publish

Before the collection receives traffic, call validate to check the configuration for structural errors. If validation passes, toggle the collection to active.

# Validate configuration
VALIDATION=$(curl -s "$MCP_URL" \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d "{
    \"jsonrpc\": \"2.0\",
    \"id\": \"validate-1\",
    \"method\": \"tools/call\",
    \"params\": {
      \"name\": \"validate\",
      \"arguments\": { \"collectionId\": \"$COLLECTION_ID\" }
    }
  }" | jq '.result.content[0].text | fromjson')

STATUS=$(echo $VALIDATION | jq -r '.status')

if [ "$STATUS" != "valid" ]; then
  echo "Validation failed: $(echo $VALIDATION | jq -r '.message')"
  exit 1
fi

# Publish: toggle collection to active
curl -s "$MCP_URL" \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Mcp-Session-Id: $SESSION_ID" \
  -H "Content-Type: application/json" \
  -d "{
    \"jsonrpc\": \"2.0\",
    \"id\": \"publish-1\",
    \"method\": \"tools/call\",
    \"params\": {
      \"name\": \"toggle_collection_status\",
      \"arguments\": { \"collectionId\": \"$COLLECTION_ID\", \"active\": true }
    }
  }"

echo "Collection published: $COLLECTION_ID"

Step 7: smoke test through the gateway

After publishing, send a test request through the actual gateway endpoint to confirm the proxy is live and routing correctly. Use a dedicated smoke test client with its own profile credentials. The CI service account authenticates to the Management MCP; the smoke test client authenticates to the API gateway as a regular consumer.

HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  https://gateway.example.com/payments/v2/process \
  -X POST \
  -H "Authorization: Bearer $SMOKE_TEST_TOKEN" \
  -H "X-Client-ID: $SMOKE_TEST_CLIENT_ID" \
  -H "X-Profile-ID: $SMOKE_TEST_PROFILE_ID" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1, "currency": "USD"}')

if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "201" ]; then
  echo "Smoke test failed: HTTP $HTTP_STATUS"
  exit 1
fi

echo "Smoke test passed: $HTTP_STATUS"

If the smoke test fails, stop the pipeline and flag it for operator review. Do not attempt an automatic rollback on the first failed run. Let the last validated state remain and alert the on-call engineer. The audit log records both the publish action and the time of failure, giving the operator a clear starting point for investigation.

What the audit trail shows

Every Management MCP tool call appears in Zerq's audit log with the same schema as a manual admin action:

{
  "timestamp": "2026-04-30T14:23:11Z",
  "actor_id": "[email protected]",
  "actor_type": "service",
  "action": "CREATE",
  "resource_type": "collection",
  "resource_id": "coll_abc123",
  "http_method": "POST",
  "url": "/api/v1/mcp",
  "ip_address": "10.42.0.88",
  "user_agent": "github-actions/1.0",
  "request_id": "req_xyz789",
  "response_status": 200
}

actor_id is the OIDC sub claim from the service account token. actor_type is service, determined from the token's issuer claims. user_agent carries the clientInfo.name from the MCP initialize call, in this case github-actions/1.0. Filter the audit log by actor_id or user_agent to see every change the pipeline has made across any time window.

The security model applies uniformly. Denied operations, such as a modifier-role service account attempting a DELETE, appear in the audit log as failed attempts with 403 response status. The pipeline cannot hide what it tried to do and cannot succeed at operations outside its assigned role.

Users with the Auditor role can see this complete record without having any ability to modify platform configuration. The compliance team's weekly change review covers both human and pipeline changes in one view, without needing to correlate a separate CI log.

What this looks like in practice

A fintech team releasing payments-api v2 alongside a new backend service. The backend is deployed by one pipeline job. The API gateway configuration, covering the new collection, new proxies, and updated rate limit policy assignment, is deployed by a second job that runs immediately after the container health check passes.

Before this setup, the gateway step was manual. It required a specific engineer with admin access to log in and make the changes. Releases happened on fixed days when that engineer was available. Hotfixes were bottlenecked by availability.

After: the gateway config lives in api-config/ in the same repository as the service. Any engineer can open a pull request against that directory. The pipeline job runs in under two minutes. The audit log shows actor_id: [email protected] alongside the human changes made during code review week, all in one filterable view.

The same pattern works across environments with separate service accounts per environment. The dev environment uses a viewer-role read-only account for state inspection. Staging uses a modifier-role account for full create and update. Production adds a required manual approval gate in the GitHub Actions workflow before the pipeline calls the Management MCP. The approval is recorded in GitHub's event log and the resulting configuration change is recorded in Zerq's audit log: two correlated evidence points for every production release, from two independent systems.

What you get that the workaround approach cannot give you

Traditional management scripts give you automation but not identity. The change happens, but the audit record is either missing or attributed to a shared credential with no context about which human or which commit triggered it.

Zerq's Management MCP gives you automation and identity together. The same RBAC model that governs your admin team governs your pipelines. Role enforcement, denied-operation logging, and IP tracking apply equally to service accounts and to humans. The audit trail is one table, one schema, one set of filter tools.

For regulated environments, this means a single evidence package covers all platform changes, human and automated, for a given review window. No reconciliation between the gateway audit log and a separate CI change log. No gaps where the pipeline made changes that do not appear in the compliance record.

Gateway configuration as code, with a complete audit trail, using your existing OIDC identity provider and your existing CI platform. No new toolchain required.


Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.