Manage Gateway Configuration with GitOps
Keep API7 Gateway configuration in Git and apply it from CI with ADC, including the traps that silently delete resources on the first run.
This guide sets up a declarative workflow: gateway configuration lives in Git, changes are reviewed as code, and CI applies them with ADC. It covers promotion between environments, adopting GitOps on a gateway that already has hand-made configuration, and the two behaviours most likely to cause an outage on your first pipeline run.
Read Declare every global rule before you run adc sync against anything you care about. A configuration file that omits global_rules deletes the rules on the gateway — including the prometheus rule that every deployment starts with. Scoping the sync with --label-selector does not protect them.
Before you begin
- A gateway group to target, and a data plane attached to it.
- ADC on the machine that will run the sync. Download the release for your platform from the ADC releases page.
Tell ADC how to trust the control plane
ADC verifies the control plane's TLS certificate. The Dashboard's default self-signed certificate carries no Subject Alternative Names, which modern TLS clients require, so a default installation fails verification:
Unable to connect to the "api7ee" backend. Error: self-signed certificateYou have three options, in increasing order of how much you should want them in production:
# 1. Production: a certificate from your own or a public CA. Nothing extra to set.
# 2. A private CA: point ADC at it.
export ADC_CA_CERT_FILE=/path/to/ca.crt
# 3. Evaluation only: skip verification.
export ADC_TLS_SKIP_VERIFY=true # or pass --tls-skip-verify per commandOption 3 works against a stock installation and is the quickest way to try ADC, but it disables certificate verification on the channel that carries your gateway configuration and a token with write access. Do not leave it on in a pipeline.
To serve the Dashboard a proper certificate, set server.tls.cert_file and server.tls.key_file in
dashboard_conf/conf.yaml, with SANs covering the address ADC will use.
ADC needs a token from a non-root user
The initial administrator account cannot create tokens — POST /api/tokens returns the current API is not accessible to root user. Create a separate user for the pipeline, give it only the roles it needs, and create the token as that user. See Obtain a Token from the Dashboard and Create a Custom Role.
export ADC_BACKEND=api7ee
export ADC_SERVER=https://api7-dashboard.example.com:7443
export ADC_TOKEN=a7ee-xxxxxxxxxxxx
export ADC_GATEWAY_GROUP=default
# plus one of ADC_CA_CERT_FILE or ADC_TLS_SKIP_VERIFY, from the step aboveConfirm the connection before anything else:
adc pingConnected to the "api7ee" backend successfully!Step 1: Capture what is already running
Start from reality rather than from a blank file:
adc dump -o gateway.yamlconsumers: []
global_rules:
prometheus:
_meta:
disable: false
prefer_name: false
plugin_metadata: {}
services: []
ssls: []Commit this as your baseline. Note that the dump already contains global_rules.prometheus — keep it there.
adc dump --with-id includes backend IDs, which is what you want when you intend to keep managing the same objects rather than recreating them.
Step 2: Declare every global rule
This is the trap. ADC treats your file as the desired state of everything in scope, so a rule that is not in the file is a rule you have asked it to delete.
Syncing a file with no global_rules key against a stock gateway produces:
✔ success Create service: "echo"
✔ success Create route: "echo"
✔ success Delete global_rule: "prometheus"The consequence is specific, and worse than the metrics simply disappearing. Three families depend on that rule — apisix_http_status, apisix_http_latency and apisix_bandwidth — and what happens to them depends on whether the data plane has served traffic since it started:
- On a running gateway, the series stay and freeze. Verified: with the rule in place ten requests moved
apisix_http_statusfrom 61 to 71; with the rule deleted, ten more requests left it at 71. The exporter keeps publishing the series it has already registered, at their last value. - After a data-plane restart with the rule still absent, the three families disappear, leaving nine.
The frozen case is the one a running deployment hits, and it is the more misleading of the two. A gap in a graph is visible; a flat line reads as a quiet period. rate() over a frozen counter returns zero, so traffic appears to have stopped rather than to have gone unmeasured, and an error-ratio alert computing errors / total gets 0 / 0 and never fires.
There are two ways to avoid this. Exclude the type, so the sync never considers global rules:
adc sync -f gateway.yaml --exclude-resource-type global_ruleOr keep the block in the file, and the sync updates it in place instead:
global_rules:
prometheus:
_meta:
disable: false
prefer_name: false✔ success Update global_rule: "prometheus"Step 3: Scope the sync while you adopt it
On a gateway that already carries hand-made configuration, an unscoped sync deletes everything absent from your file:
✔ success Delete service: "manual-not-gitops"--label-selector limits which services, routes and consumers the sync considers, so unlabelled resources are left alone. Pair it with --exclude-resource-type global_rule, which keeps the sync away from global rules entirely:
adc sync -f gateway.yaml \
--label-selector team=payments \
--exclude-resource-type global_ruleLabel the resources in your file to match:
services:
- name: echo
labels:
team: paymentsTwo things to know about labels:
managed-byis ADC's own key by default. ADC stampsmanaged-by: adcon everything it writes and overwrites whatever you put there, unless you pass--no-managed-by-label. Either way, use your own key —team,app,domain— for selection; your keys are preserved alongside ADC's.- The selector alone does not cover global rules. A sync scoped only by
--label-selectorstill deletes every global rule missing from the file. Either declare them (Step 2) or exclude the type, as above. Both were verified; the exclusion is the better fit for a team-owned pipeline, because the team's file then does not have to carry platform-owned global rules at all.
Step 4: Review before applying
adc lint -f gateway.yaml
adc validate -f gateway.yaml
adc diff -f gateway.yamllint checks the file against the schema locally. validate asks the control plane to check it. Run both in CI on every pull request — lint catches the common mistake below without needing credentials.
The file format is not the Admin API format. A route's paths are uris in an ADC file and paths in the Admin API. Copying a body out of an Admin API example into a declarative file produces:
✖ Unrecognized key: "paths"
✖ Invalid input: expected array, received undefined → at services[0].routes[0].urisIn ADC 0.30.5, adc diff writes its detail to a file and prints no operation summary to the terminal, so an empty-looking run is not evidence that nothing will change. To see the operation list before touching production, run adc sync against a non-production gateway group first — the log lines it prints are the operations it performed.
Step 5: Promote between environments
One file, one gateway group per environment. --gateway-group takes the group name, not its ID — passing an ID fails with Gateway group "..." does not exist.
adc sync -f gateway.yaml --gateway-group staging
adc sync -f gateway.yaml --gateway-group defaultGroups are isolated: syncing to staging leaves default untouched. Create the staging group with environment: non_production so its data-plane cores count against the non-production quota rather than your production entitlement — see License Management.
Keep environment differences out of the file itself. The same configuration should promote unchanged, with per-environment values supplied through variables rather than by maintaining divergent copies.
Step 6: Run it from CI
name: gateway-config
on:
pull_request:
paths: ['gateway.yaml']
push:
branches: [main]
paths: ['gateway.yaml']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install ADC
run: |
curl -fsSL -o adc.tar.gz \
https://github.com/api7/adc/releases/download/v0.30.5/adc_0.30.5_linux_amd64.tar.gz
tar xzf adc.tar.gz && sudo mv adc /usr/local/bin/
- run: adc lint -f gateway.yaml
promote-staging:
needs: validate
if: github.event_name == 'push'
runs-on: ubuntu-latest
env:
ADC_BACKEND: api7ee
ADC_SERVER: ${{ vars.API7_SERVER }}
ADC_TOKEN: ${{ secrets.API7_TOKEN }}
steps:
- uses: actions/checkout@v5
- run: adc sync -f gateway.yaml --gateway-group stagingPin the ADC version rather than tracking the latest release, so a pipeline run cannot change behaviour without a commit.
Promotion to production belongs behind whatever approval your change process requires — a protected environment, a manual gate, or a separate workflow. The mechanics are identical; only the gateway group changes.
Validate
After a sync, confirm the gateway is serving what you declared and that metrics survived:
adc dump -o current.yaml
diff <(grep -A3 global_rules gateway.yaml) <(grep -A3 global_rules current.yaml)Then check that request metrics are still being produced — see Monitor Metrics. If apisix_http_status has stopped while the gateway is plainly serving traffic, the global rule was deleted.
Roll back
ADC has no rollback command. Roll back by syncing the previous commit:
git checkout HEAD~1 -- gateway.yaml
adc sync -f gateway.yamlThis is why the baseline dump in Step 1 matters: it is the state you can always return to. It does not restore anything a sync deleted that was never in the file — recover those from the Dashboard or the Admin API.
Related
- API Declarative CLI (ADC)
- Manage Gateway Groups
- Run API7 Gateway in Production
- Configure Secret Management — keeping credentials out of the file you commit