diff --git a/README.md b/README.md index 1ed976e..740d6fd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Built with an **AI-first development approach** — the codebase includes struct - **Shell completions** — Bash, Zsh, Fish, PowerShell (`a6 completion`) - **Self-update** — Update the CLI binary to the latest version (`a6 self-update`) - **Export** — Export configurations to Kubernetes, Helm, Terraform, or Standalone YAML (`a6 export`) -- **AI agent skills** — 35+ built-in [SKILL.md files](skills/) for AI coding agents to work effectively with APISIX +- **AI agent skills** — 40 built-in [SKILL.md files](skills/) for AI coding agents to work effectively with APISIX ## Quick Start @@ -76,6 +76,8 @@ The `skills/` directory contains structured knowledge files (`SKILL.md`) that en | **Observability** | 6 | prometheus, skywalking, zipkin, http-logger, kafka-logger, datadog | | **Advanced Plugins** | 5 | serverless, ext-plugin, fault-injection, consumer-restriction, wolf-rbac | | **Operational Recipes** | 5 | blue-green, canary, circuit-breaker, health-check, mTLS | +| **Advanced Recipes** | 3 | multi-tenant, api-versioning, graphql-proxy | +| **Personas** | 2 | operator, developer | See [docs/skills.md](docs/skills.md) for the full skill format specification, taxonomy, and authoring guide. diff --git a/skills/a6-persona-developer/SKILL.md b/skills/a6-persona-developer/SKILL.md new file mode 100644 index 0000000..00edc0d --- /dev/null +++ b/skills/a6-persona-developer/SKILL.md @@ -0,0 +1,389 @@ +--- +name: a6-persona-developer +description: >- + Persona skill for API developers building and testing APIs on APISIX using + the a6 CLI. Provides decision frameworks for API design, route configuration, + plugin selection, testing workflows, local development setup, and CI/CD + integration patterns. +version: "1.0.0" +author: Apache APISIX Contributors +license: Apache-2.0 +metadata: + category: persona + apisix_version: ">=3.0.0" + a6_commands: + - a6 route create + - a6 route update + - a6 route get + - a6 upstream create + - a6 service create + - a6 consumer create + - a6 plugin list + - a6 plugin get + - a6 config sync + - a6 config dump + - a6 config validate + - a6 debug trace +--- + +# a6-persona-developer + +## Who This Is For + +You are an **API developer** responsible for: +- Designing and configuring API routes on APISIX +- Choosing and configuring plugins for auth, rate limiting, transformation +- Testing APIs locally against a development APISIX instance +- Writing declarative configs for CI/CD pipelines +- Debugging request flow through the gateway + +## Getting Started + +### 1. Install and configure + +```bash +# Install a6 +go install github.com/api7/a6/cmd/a6@latest + +# Connect to your dev APISIX instance +a6 context create dev --server http://localhost:9180 --api-key edd1c9f034335f136f87ad84b625c8f1 + +# Verify connection +a6 health +``` + +### 2. Explore available plugins + +```bash +# List all available plugins +a6 plugin list + +# Get the schema for a specific plugin +a6 plugin get key-auth --output json +a6 plugin get limit-count --output json +``` + +## Building Your First API + +### Step 1: Create an upstream (your backend) + +```bash +a6 upstream create -f - <<'EOF' +{ + "id": "my-api-backend", + "type": "roundrobin", + "nodes": { + "localhost:3000": 1 + } +} +EOF +``` + +### Step 2: Create a route + +```bash +a6 route create -f - <<'EOF' +{ + "id": "my-api", + "uri": "/api/*", + "methods": ["GET", "POST", "PUT", "DELETE"], + "upstream_id": "my-api-backend" +} +EOF +``` + +### Step 3: Test it + +```bash +curl http://localhost:9080/api/hello +``` + +### Step 4: Add authentication + +```bash +# Create a consumer with key-auth +a6 consumer create -f - <<'EOF' +{ + "username": "dev-user", + "plugins": { + "key-auth": { "key": "my-dev-key" } + } +} +EOF + +# Enable key-auth on the route +a6 route update my-api -f - <<'EOF' +{ + "plugins": { + "key-auth": {} + } +} +EOF + +# Test with the key +curl -H "apikey: my-dev-key" http://localhost:9080/api/hello +``` + +## Plugin Selection Guide + +Use this decision tree to choose the right plugins for your API. + +### Authentication — "Who is calling?" + +| Need | Plugin | Key Feature | +|------|--------|-------------| +| Simple API key | `key-auth` | Header/query param key lookup | +| JWT tokens | `jwt-auth` | RS256/HS256, token in header/query/cookie | +| Username/password | `basic-auth` | HTTP Basic authentication | +| HMAC signatures | `hmac-auth` | Request body signing, replay prevention | +| OAuth2/OIDC | `openid-connect` | Auth0, Okta, Keycloak integration | + +### Rate Limiting — "How much can they call?" + +| Need | Plugin | Key Feature | +|------|--------|-------------| +| Fixed window counter | `limit-count` | N requests per time window, Redis cluster support | +| Leaky bucket | `limit-req` | Smooth rate limiting, burst allowance | + +### Transformation — "Change request/response" + +| Need | Plugin | Key Feature | +|------|--------|-------------| +| Rewrite URI/headers | `proxy-rewrite` | Strip prefixes, add headers, change host | +| Modify response | `response-rewrite` | Change status code, body, headers | +| A/B testing, canary | `traffic-split` | Weighted routing, conditional matching | +| URL redirect | `redirect` | HTTP 301/302/307 redirects | + +### Security — "Block bad traffic" + +| Need | Plugin | Key Feature | +|------|--------|-------------| +| IP whitelist/blacklist | `ip-restriction` | CIDR support, allow/deny lists | +| CORS headers | `cors` | Cross-origin resource sharing | +| Access control | `consumer-restriction` | Restrict by consumer, group, or route | + +### Observability — "What's happening?" + +| Need | Plugin | Key Feature | +|------|--------|-------------| +| Metrics | `prometheus` | Latency, status codes, bandwidth | +| Distributed tracing | `zipkin` or `skywalking` | Request trace correlation | +| Access logs | `http-logger` or `kafka-logger` | Structured log export | + +## Common Patterns + +### API with auth + rate limiting + +```bash +a6 route create -f - <<'EOF' +{ + "uri": "/api/*", + "upstream_id": "my-api-backend", + "plugins": { + "key-auth": {}, + "limit-count": { + "count": 1000, + "time_window": 3600, + "key_type": "var", + "key": "consumer_name", + "rejected_code": 429 + } + } +} +EOF +``` + +### Strip version prefix + +```bash +a6 route create -f - <<'EOF' +{ + "uri": "/v1/*", + "upstream_id": "my-api-backend", + "plugins": { + "proxy-rewrite": { + "regex_uri": ["^/v1/(.*)", "/$1"] + } + } +} +EOF +``` + +### Add CORS for frontend apps + +```bash +a6 route update my-api -f - <<'EOF' +{ + "plugins": { + "cors": { + "allow_origins": "http://localhost:3001", + "allow_methods": "GET,POST,PUT,DELETE,OPTIONS", + "allow_headers": "Authorization,Content-Type", + "allow_credential": true, + "max_age": 3600 + } + } +} +EOF +``` + +### Use a Service for shared config + +When multiple routes share the same upstream and plugins, use a Service: + +```bash +# Create service with shared config +a6 service create -f - <<'EOF' +{ + "id": "my-api-service", + "upstream_id": "my-api-backend", + "plugins": { + "key-auth": {}, + "cors": { "allow_origins": "*" } + } +} +EOF + +# Routes inherit service config +a6 route create -f - <<'EOF' +{ "uri": "/users/*", "service_id": "my-api-service" } +EOF + +a6 route create -f - <<'EOF' +{ "uri": "/orders/*", "service_id": "my-api-service" } +EOF +``` + +## Local Development Setup + +### Start APISIX locally with Docker + +```bash +# If using the a6 repo's docker-compose +make docker-up + +# Or manually +docker run -d --name etcd \ + -p 2379:2379 \ + -e ALLOW_NONE_AUTHENTICATION=yes \ + bitnami/etcd:3.5 + +docker run -d --name apisix \ + -p 9080:9080 -p 9180:9180 \ + -v $(pwd)/apisix-config.yaml:/usr/local/apisix/conf/config.yaml \ + apache/apisix:3.11.0-debian +``` + +### Seed development data + +```bash +# Create your dev config file +cat > dev-config.yaml <<'EOF' +upstreams: + - id: local-backend + type: roundrobin + nodes: + "host.docker.internal:3000": 1 + +consumers: + - username: dev + plugins: + key-auth: + key: dev-key + +routes: + - id: api + uri: "/api/*" + upstream_id: local-backend + plugins: + key-auth: {} +EOF + +# Apply it +a6 config sync -f dev-config.yaml +``` + +## Debugging + +### Trace a request + +```bash +# See how APISIX routes a specific request +a6 debug trace --uri /api/users --method GET --header "apikey: dev-key" +``` + +### Stream logs + +```bash +# Watch APISIX error logs in real-time +a6 debug logs --follow + +# Filter by log level +a6 debug logs --follow --level error +``` + +### Inspect a route's full config + +```bash +# See the merged config (route + service + plugins) +a6 route get my-api --output json | jq . +``` + +## CI/CD Integration + +### Validate in CI + +```yaml +# .github/workflows/apisix.yml +- name: Validate APISIX config + run: a6 config validate -f apisix-config.yaml +``` + +### Deploy with config sync + +```yaml +- name: Deploy to staging + run: | + a6 context create staging --server ${{ secrets.STAGING_URL }} --api-key ${{ secrets.STAGING_KEY }} + a6 context use staging + a6 config diff -f apisix-config.yaml + a6 config sync -f apisix-config.yaml +``` + +### Export for other tools + +```bash +# Export to Kubernetes-friendly format +a6 export --format kubernetes > k8s-apisix.yaml + +# Export to standalone YAML +a6 export --format standalone > apisix-standalone.yaml +``` + +## Decision Framework + +| Situation | Action | +|-----------|--------| +| New API endpoint | Create upstream → create route → add plugins → test | +| Add auth to existing API | Create consumer → update route with auth plugin → test | +| Multiple routes, same config | Create a Service → reference via `service_id` | +| Need rate limiting | Choose `limit-count` (fixed) or `limit-req` (smooth) → add to route | +| Backend URL changed | `a6 upstream update ` with new nodes | +| Debug 502 errors | `a6 debug trace` → `a6 upstream health` → check backend | +| Prepare for production | `a6 config dump` → commit to git → `a6 config validate` in CI | +| Test a new plugin | `a6 plugin get ` for schema → add to a test route → verify | + +## Best Practices + +1. **Use declarative configs** — store `apisix-config.yaml` in your repo, use + `a6 config sync` for deployments instead of imperative commands +2. **One service per API** — group related routes under a Service for shared config +3. **Auth on every route** — never expose unauthenticated routes in production +4. **Rate limit by consumer** — use `key_type: "var"` with `key: "consumer_name"` + for per-user limits +5. **Test locally first** — always test against a dev APISIX instance before deploying +6. **Inspect plugin schemas** — run `a6 plugin get ` to see required/optional + fields before configuring +7. **Use `--output json`** — pipe JSON output to `jq` for scripting and automation +8. **Keep routes focused** — one route per endpoint pattern; avoid overly broad URI + matchers like `/*` in production diff --git a/skills/a6-persona-operator/SKILL.md b/skills/a6-persona-operator/SKILL.md new file mode 100644 index 0000000..50fdfe2 --- /dev/null +++ b/skills/a6-persona-operator/SKILL.md @@ -0,0 +1,298 @@ +--- +name: a6-persona-operator +description: >- + Persona skill for platform operators and DevOps engineers managing APISIX + instances using the a6 CLI. Provides decision frameworks for day-to-day + operations including deployment, monitoring, troubleshooting, scaling, + security hardening, and disaster recovery workflows. +version: "1.0.0" +author: Apache APISIX Contributors +license: Apache-2.0 +metadata: + category: persona + apisix_version: ">=3.0.0" + a6_commands: + - a6 route list + - a6 upstream list + - a6 upstream health + - a6 config sync + - a6 config dump + - a6 config diff + - a6 config validate + - a6 debug logs + - a6 debug trace + - a6 health + - a6 ssl create + - a6 global-rule create +--- + +# a6-persona-operator + +## Who This Is For + +You are a **platform operator or DevOps engineer** responsible for: +- Managing one or more APISIX gateway instances +- Ensuring API availability and performance +- Deploying and rolling back configuration changes +- Monitoring health, diagnosing issues, and responding to incidents +- Enforcing security policies across all APIs + +## Context Management + +Operators typically manage multiple environments. Use contexts to switch +between them without re-entering connection details. + +```bash +# Set up contexts for each environment +a6 context create dev --server http://apisix-dev:9180 --api-key dev-key-123 +a6 context create staging --server http://apisix-staging:9180 --api-key staging-key-456 +a6 context create prod --server http://apisix-prod:9180 --api-key prod-key-789 + +# Switch to production +a6 context use prod + +# Check current context +a6 context current + +# List all contexts +a6 context list +``` + +Always verify the active context before running destructive operations. + +## Daily Operations Checklist + +### 1. Health check + +```bash +# Verify APISIX is reachable and get version +a6 health + +# Check all upstream health status +a6 upstream list --output json | jq '.[] | {id: .id, name: .name}' +a6 upstream health +``` + +### 2. Configuration audit + +```bash +# Dump current state +a6 config dump > current-state.yaml + +# Compare with expected state +a6 config diff -f expected-state.yaml + +# Validate a config file before applying +a6 config validate -f new-config.yaml +``` + +### 3. Certificate management + +```bash +# List SSL certificates and check expiry +a6 ssl list + +# Upload a new certificate +a6 ssl create -f - <<'EOF' +{ + "cert": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----", + "key": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----", + "snis": ["api.example.com", "*.example.com"] +} +EOF +``` + +## Deployment Workflow + +### Safe deployment pattern + +```bash +# 1. Validate the config locally +a6 config validate -f new-config.yaml + +# 2. Preview what will change +a6 config diff -f new-config.yaml + +# 3. Apply to staging first +a6 --context staging config sync -f new-config.yaml + +# 4. Verify staging +a6 --context staging health +a6 --context staging route list + +# 5. Apply to production +a6 --context prod config sync -f new-config.yaml + +# 6. Verify production +a6 --context prod health +``` + +### Rollback + +```bash +# Keep a backup before every deployment +a6 config dump > backup-$(date +%Y%m%d-%H%M%S).yaml + +# Rollback by syncing the backup +a6 config sync -f backup-20260308-143000.yaml +``` + +## Troubleshooting + +### Request not reaching upstream + +```bash +# 1. Check if the route exists +a6 route list +a6 route get --output json + +# 2. Trace the request path +a6 debug trace --uri /api/v1/users --method GET + +# 3. Stream error logs in real-time +a6 debug logs --follow + +# 4. Check upstream health +a6 upstream health +``` + +### 502 Bad Gateway + +```bash +# Check upstream node health +a6 upstream get --output json + +# Verify backend is reachable from APISIX +a6 debug trace --uri /failing-endpoint + +# Check error logs for connection refused / timeout +a6 debug logs --follow --level error +``` + +### Authentication failures (401/403) + +```bash +# Verify consumer exists and has correct credentials +a6 consumer list +a6 consumer get --output json + +# Check the route's auth plugin configuration +a6 route get --output json | jq '.plugins' + +# Check global rules that might override +a6 global-rule list --output json +``` + +## Security Hardening + +### Global rate limiting + +```bash +a6 global-rule create -f - <<'EOF' +{ + "id": "global-rate-limit", + "plugins": { + "limit-count": { + "count": 10000, + "time_window": 60, + "key_type": "var", + "key": "remote_addr", + "rejected_code": 429 + } + } +} +EOF +``` + +### Global IP restriction + +```bash +a6 global-rule create -f - <<'EOF' +{ + "id": "global-ip-block", + "plugins": { + "ip-restriction": { + "blacklist": ["10.0.0.0/8", "192.168.0.0/16"] + } + } +} +EOF +``` + +### Enforce CORS globally + +```bash +a6 global-rule create -f - <<'EOF' +{ + "id": "global-cors", + "plugins": { + "cors": { + "allow_origins": "https://app.example.com", + "allow_methods": "GET,POST,PUT,DELETE,OPTIONS", + "allow_headers": "Authorization,Content-Type", + "max_age": 3600 + } + } +} +EOF +``` + +## Monitoring Setup + +### Enable Prometheus metrics + +```bash +# Global rule to expose metrics for all routes +a6 global-rule create -f - <<'EOF' +{ + "id": "prometheus-metrics", + "plugins": { + "prometheus": {} + } +} +EOF +``` + +Scrape metrics at `http://apisix:9091/apisix/prometheus/metrics`. + +### Add HTTP logging + +```bash +a6 global-rule create -f - <<'EOF' +{ + "id": "http-logging", + "plugins": { + "http-logger": { + "uri": "http://log-collector:9200/_bulk", + "batch_max_size": 1000, + "inactive_timeout": 5 + } + } +} +EOF +``` + +## Decision Framework + +| Situation | Action | +|-----------|--------| +| New deployment | `config validate` → `config diff` → `config sync` (staging) → verify → `config sync` (prod) | +| Incident — route broken | `debug trace` → `debug logs` → fix → `config sync` | +| Incident — upstream down | `upstream health` → check backends → update nodes or enable health checks | +| Certificate expiring | `ssl list` → `ssl create` with new cert → `ssl delete` old | +| Performance issue | `debug logs` to find slow routes → add rate limiting or caching | +| Security audit | `config dump` → review global rules, auth plugins, IP restrictions | +| Rollback needed | `config sync -f backup.yaml` | +| New environment | `context create` → `config sync -f base-config.yaml` | + +## Best Practices + +1. **Always dump before sync** — `a6 config dump > backup.yaml` before every deployment +2. **Validate before apply** — `a6 config validate -f config.yaml` catches errors early +3. **Diff before sync** — `a6 config diff -f config.yaml` shows exactly what will change +4. **Stage before prod** — always apply to staging first, verify, then promote to production +5. **Use global rules sparingly** — they apply to ALL routes; prefer per-route plugins +6. **Monitor upstream health** — enable active health checks on critical upstreams +7. **Keep contexts organized** — name contexts clearly (prod, staging, dev) and verify + the current context before destructive operations +8. **Version control configs** — store YAML configs in git for audit trail and rollback diff --git a/skills/a6-recipe-api-versioning/SKILL.md b/skills/a6-recipe-api-versioning/SKILL.md new file mode 100644 index 0000000..3457697 --- /dev/null +++ b/skills/a6-recipe-api-versioning/SKILL.md @@ -0,0 +1,351 @@ +--- +name: a6-recipe-api-versioning +description: >- + Recipe skill for implementing API versioning strategies using the a6 CLI. + Covers URI path versioning with proxy-rewrite, header-based versioning with + traffic-split, query parameter versioning, gradual version migration with + weighted traffic splitting, and version deprecation with redirect. +version: "1.0.0" +author: Apache APISIX Contributors +license: Apache-2.0 +metadata: + category: recipe + apisix_version: ">=3.0.0" + a6_commands: + - a6 route create + - a6 route update + - a6 config sync + - a6 config diff +--- + +# a6-recipe-api-versioning + +## Overview + +API versioning allows you to evolve your API without breaking existing clients. +APISIX supports multiple versioning strategies through routing rules, header +matching, and traffic splitting — all configurable via the a6 CLI. + +Strategies covered: +1. **URI path versioning** — `/v1/users`, `/v2/users` +2. **Header-based versioning** — `Accept: application/vnd.api.v2+json` +3. **Query parameter versioning** — `?version=2` +4. **Gradual migration** — weighted traffic split between versions +5. **Version deprecation** — redirect old versions to new + +## When to Use + +- Introducing breaking changes to an existing API +- Running multiple API versions simultaneously +- Gradually migrating clients from v1 to v2 +- Deprecating old API versions with user-friendly redirects + +## Approach A: URI Path Versioning + +The most common pattern. Each version has its own URI prefix, and +`proxy-rewrite` strips the version prefix before forwarding to the backend. + +### 1. Create versioned upstreams + +```bash +a6 upstream create -f - <<'EOF' +{ + "id": "api-v1", + "type": "roundrobin", + "nodes": { "api-v1-backend:8080": 1 } +} +EOF + +a6 upstream create -f - <<'EOF' +{ + "id": "api-v2", + "type": "roundrobin", + "nodes": { "api-v2-backend:8080": 1 } +} +EOF +``` + +### 2. Create versioned routes with URI rewriting + +```bash +# v1: /v1/users/123 → /users/123 on api-v1 backend +a6 route create -f - <<'EOF' +{ + "id": "route-v1", + "uri": "/v1/*", + "upstream_id": "api-v1", + "plugins": { + "proxy-rewrite": { + "regex_uri": ["^/v1/(.*)", "/$1"] + } + } +} +EOF + +# v2: /v2/users/123 → /users/123 on api-v2 backend +a6 route create -f - <<'EOF' +{ + "id": "route-v2", + "uri": "/v2/*", + "upstream_id": "api-v2", + "plugins": { + "proxy-rewrite": { + "regex_uri": ["^/v2/(.*)", "/$1"] + } + } +} +EOF +``` + +Clients call `/v1/users` or `/v2/users`, and the backend always sees `/users`. + +## Approach B: Header-Based Versioning + +Route based on the `Accept` header using `traffic-split` with `vars` matching. +A single URI serves multiple versions. + +```bash +a6 route create -f - <<'EOF' +{ + "uri": "/api/*", + "plugins": { + "traffic-split": { + "rules": [ + { + "match": [ + { "vars": [["http_accept", "~~", "application/vnd\\.api\\.v2\\+json"]] } + ], + "weighted_upstreams": [ + { + "upstream": { + "type": "roundrobin", + "nodes": { "api-v2-backend:8080": 1 } + }, + "weight": 1 + } + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "api-v1-backend:8080": 1 } + } +} +EOF +``` + +- `Accept: application/vnd.api.v2+json` → v2 backend +- Any other `Accept` value → v1 backend (default upstream) +- `~~` is the regex match operator in APISIX vars expressions + +## Approach C: Query Parameter Versioning + +Route based on `?version=2` query parameter. + +```bash +a6 route create -f - <<'EOF' +{ + "uri": "/api/*", + "plugins": { + "traffic-split": { + "rules": [ + { + "match": [ + { "vars": [["arg_version", "==", "2"]] } + ], + "weighted_upstreams": [ + { + "upstream": { + "type": "roundrobin", + "nodes": { "api-v2-backend:8080": 1 } + }, + "weight": 1 + } + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "api-v1-backend:8080": 1 } + } +} +EOF +``` + +- `/api/users?version=2` → v2 backend +- `/api/users` or `/api/users?version=1` → v1 backend + +## Gradual Version Migration + +Use weighted traffic splitting to gradually shift traffic from v1 to v2. + +### Start: 90% v1, 10% v2 + +```bash +a6 route create -f - <<'EOF' +{ + "id": "api-migration", + "uri": "/api/*", + "plugins": { + "traffic-split": { + "rules": [ + { + "weighted_upstreams": [ + { + "upstream": { + "type": "roundrobin", + "nodes": { "api-v2-backend:8080": 1 } + }, + "weight": 1 + }, + { "weight": 9 } + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "api-v1-backend:8080": 1 } + } +} +EOF +``` + +### Shift to 50/50 + +```bash +a6 route update api-migration -f - <<'EOF' +{ + "plugins": { + "traffic-split": { + "rules": [ + { + "weighted_upstreams": [ + { + "upstream": { + "type": "roundrobin", + "nodes": { "api-v2-backend:8080": 1 } + }, + "weight": 1 + }, + { "weight": 1 } + ] + } + ] + } + } +} +EOF +``` + +### Complete: 100% v2 + +```bash +a6 route update api-migration -f - <<'EOF' +{ + "upstream": { + "type": "roundrobin", + "nodes": { "api-v2-backend:8080": 1 } + }, + "plugins": {} +} +EOF +``` + +## Version Deprecation with Redirect + +When sunsetting v1, redirect clients to v2 with a `301 Moved Permanently`. + +```bash +a6 route update route-v1 -f - <<'EOF' +{ + "uri": "/v1/*", + "plugins": { + "redirect": { + "regex_uri": ["^/v1/(.*)", "/v2/$1"], + "ret_code": 301 + } + } +} +EOF +``` + +Clients calling `/v1/users` receive: +``` +HTTP/1.1 301 Moved Permanently +Location: /v2/users +``` + +## Declarative Versioning Config + +```yaml +# apisix-versioning.yaml +upstreams: + - id: api-v1 + type: roundrobin + nodes: + "api-v1-backend:8080": 1 + - id: api-v2 + type: roundrobin + nodes: + "api-v2-backend:8080": 1 + +routes: + - id: route-v1 + uri: "/v1/*" + upstream_id: api-v1 + plugins: + proxy-rewrite: + regex_uri: ["^/v1/(.*)", "/$1"] + - id: route-v2 + uri: "/v2/*" + upstream_id: api-v2 + plugins: + proxy-rewrite: + regex_uri: ["^/v2/(.*)", "/$1"] +``` + +```bash +a6 config diff -f apisix-versioning.yaml +a6 config sync -f apisix-versioning.yaml +``` + +## Gotchas + +- **`regex_uri` is an array of two strings** — `["pattern", "replacement"]`, not + an object. The pattern is a Lua regex (PCRE-compatible). +- **traffic-split weight semantics** — a `weighted_upstreams` entry without an + `upstream` field means "use the route's default upstream". Weight `9` + weight + `1` = 90%/10%. +- **`~~` operator** — regex match in vars expressions. Must double-escape backslashes + in JSON: `"application/vnd\\\\.api\\\\.v2\\\\+json"`. +- **Order matters** — traffic-split rules are evaluated top-down. First matching + rule wins. +- **URI rewrite happens before upstream** — `proxy-rewrite` changes the URI that + the backend sees, not the URI used for route matching. +- **redirect plugin is terminal** — when redirect is active, the request never + reaches an upstream. Remove the upstream_id to avoid confusion. + +## Verification + +```bash +# Test URI path versioning +curl http://localhost:9080/v1/users # → v1 backend +curl http://localhost:9080/v2/users # → v2 backend + +# Test header-based versioning +curl -H "Accept: application/vnd.api.v2+json" http://localhost:9080/api/users # → v2 +curl http://localhost:9080/api/users # → v1 (default) + +# Test query parameter versioning +curl "http://localhost:9080/api/users?version=2" # → v2 +curl http://localhost:9080/api/users # → v1 + +# Test redirect (deprecation) +curl -v http://localhost:9080/v1/users # → 301 to /v2/users +``` diff --git a/skills/a6-recipe-graphql-proxy/SKILL.md b/skills/a6-recipe-graphql-proxy/SKILL.md new file mode 100644 index 0000000..d84e6dc --- /dev/null +++ b/skills/a6-recipe-graphql-proxy/SKILL.md @@ -0,0 +1,329 @@ +--- +name: a6-recipe-graphql-proxy +description: >- + Recipe skill for implementing GraphQL proxying patterns using the a6 CLI. + Covers operation-based routing with built-in GraphQL variables, per-operation + rate limiting, REST-to-GraphQL conversion with the degraphql plugin, and + security patterns for GraphQL APIs. +version: "1.0.0" +author: Apache APISIX Contributors +license: Apache-2.0 +metadata: + category: recipe + apisix_version: ">=3.0.0" + a6_commands: + - a6 route create + - a6 route update + - a6 config sync + - a6 config diff +--- + +# a6-recipe-graphql-proxy + +## Overview + +APISIX provides built-in GraphQL support through three variables that let you +route and apply policies based on GraphQL query content — without parsing +GraphQL yourself: + +| Variable | Description | Example Value | +|----------|-------------|---------------| +| `graphql_name` | Operation name from the query | `"getUser"` | +| `graphql_operation` | Operation type | `"query"`, `"mutation"` | +| `graphql_root_fields` | Top-level fields requested | `["user", "orders"]` | + +These variables are extracted automatically from POST requests with +`Content-Type: application/json` or `application/graphql`, and from GET +requests with a `query` parameter. + +## When to Use + +- Routing different GraphQL operations to different backends +- Applying rate limits per operation type (queries vs mutations) +- Restricting which operations specific consumers can execute +- Converting REST endpoints to GraphQL queries (degraphql) +- Adding security layers (auth, rate limiting) to a GraphQL API + +## Approach A: Operation-Based Routing + +Route GraphQL queries and mutations to different backends using the +`graphql_operation` variable. + +### Route queries to read replicas, mutations to primary + +```bash +# Queries → read replica +a6 route create -f - <<'EOF' +{ + "id": "graphql-queries", + "uri": "/graphql", + "vars": [["graphql_operation", "==", "query"]], + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-read-replica:4000": 1 } + } +} +EOF + +# Mutations → primary database +a6 route create -f - <<'EOF' +{ + "id": "graphql-mutations", + "uri": "/graphql", + "vars": [["graphql_operation", "==", "mutation"]], + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-primary:4000": 1 } + } +} +EOF +``` + +### Route by operation name + +```bash +# Route the expensive "analytics" query to a dedicated backend +a6 route create -f - <<'EOF' +{ + "id": "graphql-analytics", + "uri": "/graphql", + "vars": [["graphql_name", "==", "getAnalytics"]], + "priority": 10, + "upstream": { + "type": "roundrobin", + "nodes": { "analytics-backend:4000": 1 } + } +} +EOF +``` + +The `priority` field ensures this route is matched before a generic `/graphql` route. + +## Approach B: Per-Operation Rate Limiting + +Apply different rate limits to queries vs mutations. + +```bash +# Queries: 1000 req/min +a6 route create -f - <<'EOF' +{ + "id": "graphql-query-limited", + "uri": "/graphql", + "vars": [["graphql_operation", "==", "query"]], + "plugins": { + "key-auth": {}, + "limit-count": { + "count": 1000, + "time_window": 60, + "key_type": "var", + "key": "consumer_name", + "rejected_code": 429 + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-backend:4000": 1 } + } +} +EOF + +# Mutations: 100 req/min (more restrictive) +a6 route create -f - <<'EOF' +{ + "id": "graphql-mutation-limited", + "uri": "/graphql", + "vars": [["graphql_operation", "==", "mutation"]], + "plugins": { + "key-auth": {}, + "limit-count": { + "count": 100, + "time_window": 60, + "key_type": "var", + "key": "consumer_name", + "rejected_code": 429, + "rejected_msg": "Mutation rate limit exceeded" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-backend:4000": 1 } + } +} +EOF +``` + +## Approach C: Restrict Operations by Consumer + +Use `consumer-restriction` to allow only specific consumers to execute +mutations. + +```bash +a6 route create -f - <<'EOF' +{ + "id": "graphql-mutations-restricted", + "uri": "/graphql", + "vars": [["graphql_operation", "==", "mutation"]], + "plugins": { + "key-auth": {}, + "consumer-restriction": { + "whitelist": ["admin-user", "service-account"], + "rejected_code": 403, + "rejected_msg": "Mutations not allowed for your account" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-backend:4000": 1 } + } +} +EOF +``` + +## Approach D: REST-to-GraphQL with degraphql + +The `degraphql` plugin converts RESTful endpoints into GraphQL queries, +allowing REST clients to consume a GraphQL backend. + +### 1. Enable degraphql on a route + +```bash +a6 route create -f - <<'EOF' +{ + "id": "rest-to-graphql-users", + "uri": "/users/:id", + "methods": ["GET"], + "plugins": { + "degraphql": { + "query": "query getUser($id: ID!) { user(id: $id) { id name email } }", + "variables": ["id"] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-backend:4000": 1 } + } +} +EOF +``` + +REST clients call `GET /users/123` and receive the GraphQL response +for `user(id: "123")`. + +### 2. Static query (no variables) + +```bash +a6 route create -f - <<'EOF' +{ + "id": "rest-to-graphql-stats", + "uri": "/stats", + "methods": ["GET"], + "plugins": { + "degraphql": { + "query": "{ systemStats { cpu memory uptime } }" + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "graphql-backend:4000": 1 } + } +} +EOF +``` + +## Declarative GraphQL Config + +```yaml +# apisix-graphql.yaml +routes: + - id: graphql-queries + uri: "/graphql" + vars: [["graphql_operation", "==", "query"]] + plugins: + key-auth: {} + limit-count: + count: 1000 + time_window: 60 + key_type: var + key: consumer_name + upstream: + type: roundrobin + nodes: + "graphql-read-replica:4000": 1 + + - id: graphql-mutations + uri: "/graphql" + vars: [["graphql_operation", "==", "mutation"]] + plugins: + key-auth: {} + limit-count: + count: 100 + time_window: 60 + key_type: var + key: consumer_name + consumer-restriction: + whitelist: ["admin-user", "service-account"] + upstream: + type: roundrobin + nodes: + "graphql-primary:4000": 1 +``` + +```bash +a6 config diff -f apisix-graphql.yaml +a6 config sync -f apisix-graphql.yaml +``` + +## Gotchas + +- **Body size limit** — APISIX parses GraphQL from the request body. Default max + body size is 1 MiB (configurable via `client_max_body_size` in APISIX config). + Large queries may be rejected. +- **Single operation only** — APISIX extracts variables from the **first** operation + in the request. Batched GraphQL queries (multiple operations) are not supported + for routing purposes. +- **No WebSocket subscriptions** — GraphQL subscriptions over WebSocket are not + supported by the built-in GraphQL parsing. You can still proxy WebSocket + connections, but without operation-based routing. +- **POST content types** — GraphQL parsing works with `application/json` (standard) + and `application/graphql` (query in body as text). Other content types are not + parsed. +- **GET requests** — GraphQL variables are read from the `query` URL parameter + (URL-encoded GraphQL query string). +- **`vars` matching** — the `vars` field on a route accepts an array of conditions. + Each condition is `["variable", "operator", "value"]`. Multiple conditions are + AND-ed together. +- **degraphql limitations** — the plugin sends a POST with `application/json` to + the upstream, regardless of the original request method. The `variables` field + maps URI path parameters to GraphQL variables by name. +- **Priority for overlapping routes** — when multiple routes match `/graphql` with + different `vars`, use the `priority` field to control matching order. Higher + priority = matched first. + +## Verification + +```bash +# Test query routing +curl -X POST http://localhost:9080/graphql \ + -H "Content-Type: application/json" \ + -H "apikey: my-key" \ + -d '{"query": "query getUser { user(id: 1) { name } }"}' + +# Test mutation routing +curl -X POST http://localhost:9080/graphql \ + -H "Content-Type: application/json" \ + -H "apikey: my-key" \ + -d '{"query": "mutation createUser { createUser(name: \"test\") { id } }"}' + +# Test rate limiting (should 429 after exceeding limit) +for i in $(seq 1 1001); do + curl -s -o /dev/null -w "%{http_code}\n" \ + -X POST http://localhost:9080/graphql \ + -H "Content-Type: application/json" \ + -H "apikey: my-key" \ + -d '{"query": "{ users { id } }"}' +done + +# Test REST-to-GraphQL +curl http://localhost:9080/users/123 +# Returns GraphQL response for user(id: "123") +``` diff --git a/skills/a6-recipe-multi-tenant/SKILL.md b/skills/a6-recipe-multi-tenant/SKILL.md new file mode 100644 index 0000000..cef9a84 --- /dev/null +++ b/skills/a6-recipe-multi-tenant/SKILL.md @@ -0,0 +1,336 @@ +--- +name: a6-recipe-multi-tenant +description: >- + Recipe skill for implementing multi-tenant API gateway patterns using the a6 + CLI. Covers tenant isolation via Consumer Groups, host/path/header-based + routing, per-tenant rate limiting, context forwarding with proxy-rewrite, + and declarative config sync workflows for multi-tenant management. +version: "1.0.0" +author: Apache APISIX Contributors +license: Apache-2.0 +metadata: + category: recipe + apisix_version: ">=3.0.0" + a6_commands: + - a6 consumer create + - a6 consumer-group create + - a6 route create + - a6 route update + - a6 config sync + - a6 config dump +--- + +# a6-recipe-multi-tenant + +## Overview + +Multi-tenancy in an API gateway means serving multiple isolated tenants (customers, +teams, or business units) through the same gateway instance, each with their own +rate limits, authentication, and routing rules. + +APISIX achieves multi-tenancy through: +1. **Consumer Groups** — group consumers into tenants with shared plugin configs +2. **Host/path/header-based routing** — route requests to tenant-specific upstreams +3. **Per-tenant rate limiting** — enforce quotas per consumer group +4. **Proxy-rewrite** — forward tenant context to backends via headers + +## When to Use + +- Multiple customers sharing a single API gateway +- Internal platform serving different teams with isolated quotas +- SaaS application requiring per-tenant rate limits and auth +- Need to forward tenant identity to backend services + +## Approach A: Consumer Groups for Tenant Isolation + +Group consumers by tenant. Each tenant gets shared plugin configuration +(rate limits, transformations) applied via the consumer group. + +### 1. Create consumer groups (one per tenant) + +```bash +# Free tier — 100 requests/day +a6 consumer-group create -f - <<'EOF' +{ + "id": "tenant-free", + "desc": "Free tier tenant", + "plugins": { + "limit-count": { + "count": 100, + "time_window": 86400, + "key_type": "var", + "key": "consumer_name", + "rejected_code": 429, + "rejected_msg": "Free tier quota exceeded" + } + } +} +EOF + +# Pro tier — 10000 requests/day +a6 consumer-group create -f - <<'EOF' +{ + "id": "tenant-pro", + "desc": "Pro tier tenant", + "plugins": { + "limit-count": { + "count": 10000, + "time_window": 86400, + "key_type": "var", + "key": "consumer_name", + "rejected_code": 429, + "rejected_msg": "Pro tier quota exceeded" + } + } +} +EOF +``` + +### 2. Create consumers assigned to groups + +```bash +a6 consumer create -f - <<'EOF' +{ + "username": "acme-corp", + "group_id": "tenant-pro", + "plugins": { + "key-auth": { "key": "acme-secret-key" } + } +} +EOF + +a6 consumer create -f - <<'EOF' +{ + "username": "startup-xyz", + "group_id": "tenant-free", + "plugins": { + "key-auth": { "key": "startup-xyz-key" } + } +} +EOF +``` + +### 3. Create a shared route with auth + +```bash +a6 route create -f - <<'EOF' +{ + "id": "api-v1", + "uri": "/api/v1/*", + "upstream": { + "type": "roundrobin", + "nodes": { "api-backend:8080": 1 } + }, + "plugins": { + "key-auth": {} + } +} +EOF +``` + +Now `acme-corp` gets 10,000 req/day and `startup-xyz` gets 100 req/day, +both through the same route. + +## Approach B: Host-Based Tenant Routing + +Route each tenant to their own backend based on the `Host` header. + +### 1. Create per-tenant upstreams + +```bash +a6 upstream create -f - <<'EOF' +{ + "id": "upstream-tenant-a", + "type": "roundrobin", + "nodes": { "tenant-a-backend:8080": 1 } +} +EOF + +a6 upstream create -f - <<'EOF' +{ + "id": "upstream-tenant-b", + "type": "roundrobin", + "nodes": { "tenant-b-backend:8080": 1 } +} +EOF +``` + +### 2. Create host-based routes + +```bash +a6 route create -f - <<'EOF' +{ + "id": "tenant-a-route", + "host": "tenant-a.example.com", + "uri": "/*", + "upstream_id": "upstream-tenant-a", + "plugins": { "key-auth": {} } +} +EOF + +a6 route create -f - <<'EOF' +{ + "id": "tenant-b-route", + "host": "tenant-b.example.com", + "uri": "/*", + "upstream_id": "upstream-tenant-b", + "plugins": { "key-auth": {} } +} +EOF +``` + +## Approach C: Header-Based Tenant Routing + +Use a custom header (e.g., `X-Tenant-ID`) to route to different upstreams +via `traffic-split`. + +```bash +a6 route create -f - <<'EOF' +{ + "uri": "/api/*", + "plugins": { + "key-auth": {}, + "traffic-split": { + "rules": [ + { + "match": [{ "vars": [["http_x_tenant_id", "==", "tenant-a"]] }], + "weighted_upstreams": [ + { "upstream": { "type": "roundrobin", "nodes": { "tenant-a-backend:8080": 1 } }, "weight": 1 } + ] + }, + { + "match": [{ "vars": [["http_x_tenant_id", "==", "tenant-b"]] }], + "weighted_upstreams": [ + { "upstream": { "type": "roundrobin", "nodes": { "tenant-b-backend:8080": 1 } }, "weight": 1 } + ] + } + ] + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { "default-backend:8080": 1 } + } +} +EOF +``` + +## Forwarding Tenant Context to Backends + +Use `proxy-rewrite` to inject tenant identity as headers so backends +know which tenant the request belongs to. + +```bash +a6 route update api-v1 -f - <<'EOF' +{ + "plugins": { + "key-auth": {}, + "proxy-rewrite": { + "headers": { + "set": { + "X-Consumer-Name": "$consumer_name", + "X-Consumer-Group": "$consumer_group_id" + } + } + } + } +} +EOF +``` + +Backend receives `X-Consumer-Name: acme-corp` and `X-Consumer-Group: tenant-pro`. + +## Declarative Multi-Tenant Config + +Manage all tenants declaratively with `a6 config sync`: + +```yaml +# apisix-tenants.yaml +consumer_groups: + - id: tenant-free + desc: "Free tier" + plugins: + limit-count: + count: 100 + time_window: 86400 + key_type: var + key: consumer_name + - id: tenant-pro + desc: "Pro tier" + plugins: + limit-count: + count: 10000 + time_window: 86400 + key_type: var + key: consumer_name + +consumers: + - username: acme-corp + group_id: tenant-pro + plugins: + key-auth: + key: acme-secret-key + - username: startup-xyz + group_id: tenant-free + plugins: + key-auth: + key: startup-xyz-key + +routes: + - id: api-v1 + uri: "/api/v1/*" + upstream: + type: roundrobin + nodes: + "api-backend:8080": 1 + plugins: + key-auth: {} + proxy-rewrite: + headers: + set: + X-Consumer-Name: "$consumer_name" + X-Consumer-Group: "$consumer_group_id" +``` + +```bash +# Preview changes +a6 config diff -f apisix-tenants.yaml + +# Apply +a6 config sync -f apisix-tenants.yaml +``` + +## Gotchas + +- **Consumer group plugins merge** — plugins set on the consumer group are merged + with plugins on the individual consumer. The consumer's plugin config takes + precedence if both define the same plugin. +- **`group_id` is a string** — must match an existing consumer group ID exactly. +- **Rate limit key** — use `key_type: "var"` with `key: "consumer_name"` to + enforce per-consumer limits within a group. Without this, the limit applies + globally across all consumers in the group. +- **Variable names in proxy-rewrite** — `$consumer_name` and `$consumer_group_id` + are APISIX built-in variables, available only after authentication runs. + Ensure the auth plugin (key-auth, jwt-auth, etc.) has higher priority than + proxy-rewrite. + +## Verification + +```bash +# List consumer groups +a6 consumer-group list + +# Verify consumer assignment +a6 consumer get acme-corp --output json | grep group_id + +# Test rate limiting for free tier +for i in $(seq 1 101); do + curl -s -o /dev/null -w "%{http_code}\n" \ + -H "apikey: startup-xyz-key" http://localhost:9080/api/v1/hello +done +# Request 101 should return 429 + +# Verify tenant headers reach backend +curl -H "apikey: acme-secret-key" http://localhost:9080/api/v1/headers +# Response should show X-Consumer-Name and X-Consumer-Group headers +```