
Alibabacloud Nginx Ingress To Api Gateway
- 150 installs
- 208 repo stars
- Updated August 4, 2026
- aliyun/alibabacloud-aiops-skills
Migrate Kubernetes NGINX Ingress routes to Alibaba Cloud API Gateway for unified auth, rate limiting, TLS termination, and production traffic cutover at launch.
About
Ship-phase skill for migrating NGINX Ingress configurations to Alibaba Cloud API Gateway: translate routes and backends, align TLS and auth policies, validate parity, and execute controlled production cutover for Kubernetes-hosted APIs.
- NGINX Ingress to API Gateway mapping
- Route, host, and path rule migration
- TLS and domain binding at the edge
- Auth and throttling policy transfer
- Zero-downtime traffic cutover planning
Alibabacloud Nginx Ingress To Api Gateway by the numbers
- 150 all-time installs (skills.sh)
- Ranked #454 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aliyun/alibabacloud-aiops-skills --skill alibabacloud-nginx-ingress-to-api-gatewayAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 150 |
|---|---|
| repo stars | ★ 208 |
| Last updated | August 4, 2026 |
| Repository | aliyun/alibabacloud-aiops-skills ↗ |
What it does
Migrate Kubernetes NGINX Ingress routes to Alibaba Cloud API Gateway for unified auth, rate limiting, TLS termination, and production traffic cutover at launch.
Files
Nginx Ingress to APIG Migration
Scenario Description
Migrate Kubernetes nginx Ingress resources to Alibaba Cloud API Gateway (APIG). APIG is an Envoy-based gateway (Higress) that uses ingressClassName: apig. This skill classifies every nginx.ingress.kubernetes.io/* annotation into Compatible / Ignorable / Unsupported, resolves unsupported annotations via a four-level decision tree (Higress native → safe-to-drop → built-in plugin → custom WasmPlugin), generates migrated Ingress YAML, and produces a deployment-ready migration report.
Architecture: nginx Ingress Controller → APIG (Envoy/Higress) + optional WasmPlugin (Go, proxy-wasm-go-sdk)
The core analysis workflow operates entirely offline on user-provided YAML — no cluster access, CLI tools, or cloud credentials required.
Installation
This skill operates entirely offline on user-provided YAML. No CLI tools, SDKs, or cloud credentials are required.
On-demand tools (only when the workflow reaches a step that needs them):
| Tool | When needed | Check command | Minimum version |
|---|---|---|---|
| jq | Script-based offline analysis | jq --version | >= 1.6 |
| python3 + PyYAML | YAML parsing (alternative to yq) | python3 -c "import yaml; print(yaml.__version__)" | python3 >= 3.8, PyYAML >= 5.0 |
| yq | YAML parsing (alternative to python3+PyYAML) | yq --version | >= 4.0 |
| Go | Step 3 determines a custom WasmPlugin is needed | go version | >= 1.24 |
| Docker | Custom WasmPlugin needs to be built as OCI image | docker version | — |
Do NOT pre-check or prompt installation of any tool during analysis.
Environment Variables
No environment variables required. This skill does not invoke any cloud APIs or CLI tools.
Authentication
Not applicable. This skill does not invoke Alibaba Cloud APIs or CLI. No credentials are needed.
RAM Policy
Not applicable. This skill operates entirely on local YAML files and does not call any cloud APIs.
Parameter Confirmation
This skill only performs analysis and code generation — it does NOT execute any deployment or cluster write operations.
>
When the user provides Ingress YAML, proceed immediately with the full workflow (Step 1→5) and output the complete result. Do NOT ask for RegionId, OCI registry, or any other parameter. Use<REGION>and<YOUR_REGISTRY>placeholders in the output.
>
请避免以下交互模式,直接执行对应操作即可:
- "是否需要我分析这个 Ingress?" — 建议直接分析
- "是否需要生成迁移后的 YAML?" — 建议直接生成
- "是否需要创建迁移配置文件/检查清单?" — 建议直接创建
- "是否需要开发 WasmPlugin?" — 如果决策树判定需要,建议直接开发
- "请确认 RegionId / OCI 地址" — 建议使用占位符
| Parameter Name | Required/Optional | Description | Default Value |
|---|---|---|---|
| Ingress YAML | Required | nginx Ingress YAML to migrate (paste, file, or directory) | — |
When Ingress YAML is not provided: If the user asks about migration but does not provide YAML,
respond with: "请提供需要迁移的 nginx Ingress YAML(可以直接粘贴、提供文件路径或目录路径)。"
Do NOT abort the conversation — guide the user to provide the required input.
Core Workflow
建议:收到 YAML 后一次性完成全部分析步骤
>
当用户提供 Ingress YAML 时,建议立即执行全部步骤(Step 1→5)并在一次响应中输出完整结果。
- 对于未指定的参数(如 RegionId、OCI registry),使用 <REGION> 等占位符- 收到 YAML 后直接进入分析流程,无需额外确认
- 各步骤之间连续执行,无需中途暂停询问用户
- 迁移配置文件和检查清单作为标准输出的一部分自动生成
- 整个工作流是确定性的:YAML 输入 → 完整迁移报告输出,无需中间确认
- 唯一必需的输入是 Ingress YAML 本身
Step 1: Parse Ingress YAML
Accept YAML from any of the following input formats:
- Direct paste in conversation (with or without markdown code fences)
- File path (e.g.,
ingress.yaml,./k8s/ingress.yaml) - Directory path (scan all
.yaml/.ymlfiles for Ingress resources) - Multi-document YAML (separated by
---) - Partial YAML (missing
apiVersion/kind— infer as Ingress ifannotationswithnginx.ingress.kubernetes.io/*are present)
For each Ingress found, extract all nginx.ingress.kubernetes.io/* annotations.
If the user's message mentions migration/analysis but does NOT include any YAML, respond with:
"请提供需要迁移的 nginx Ingress YAML(可以直接粘贴、提供文件路径或目录路径)。"
Do NOT abort or error out — guide the user to provide input.
Step 2: Classify Annotations
Classify each annotation into exactly one of three categories. See references/annotation-mapping.md for the complete 117-annotation lookup table.
| Category | Count | Action | Example |
|---|---|---|---|
| Compatible | 50 | Keep in migrated YAML | rewrite-target, enable-cors, canary-weight, ssl-redirect |
| Ignorable | 16 | Strip (Envoy handles natively) | proxy-connect-timeout, proxy-buffering, proxy-body-size |
| Unsupported | 51 | Strip → resolve via decision tree | auth-url, server-snippet, limit-rps |
Inline Quick Lookup — High-Frequency Annotations:
| Annotation | Category | Action |
|---|---|---|
rewrite-target | ✅ Compatible | Keep |
enable-cors | ✅ Compatible | Keep |
cors-allow-origin | ✅ Compatible | Keep |
ssl-redirect | ✅ Compatible | Keep |
canary / canary-weight / canary-by-header | ✅ Compatible | Keep |
whitelist-source-range | ✅ Compatible | Keep |
backend-protocol | ✅ Compatible | Keep |
use-regex | ✅ Compatible | Keep |
upstream-vhost | ✅ Compatible | Keep |
proxy-connect-timeout | ⚪ Ignorable | Strip |
proxy-read-timeout | ⚪ Ignorable | Strip |
proxy-send-timeout | ⚪ Ignorable | Strip |
proxy-body-size | ⚪ Ignorable | Strip |
proxy-buffering | ⚪ Ignorable | Strip |
client-body-buffer-size | ⚪ Ignorable | Strip |
auth-url | ❌ Unsupported | WasmPlugin (HTTP callout) |
server-snippet | ❌ Unsupported | WasmPlugin (directive conversion) |
configuration-snippet | ❌ Unsupported | WasmPlugin (directive conversion) |
limit-rps | ❌ Unsupported | Built-in key-rate-limit plugin |
limit-connections | ❌ Unsupported | Built-in key-rate-limit plugin |
enable-modsecurity | ❌ Unsupported | Built-in waf plugin |
denylist-source-range | ❌ Unsupported | Higress native higress.io/blacklist-source-range |
service-upstream | ❌ Unsupported | Safe to drop (Envoy default behavior) |
ssl-ciphers | ❌ Unsupported | Rename to ssl-cipher (compatible) |
If an annotation is NOT in the above table, look it up in references/annotation-mapping.md. If still not found, classify as Unsupported and resolve via the decision tree in Step 3.Special value changes (compatible but value must change):
load-balance: ewma→round_robin(APIG does not support EWMA)ssl-ciphers→ rename tossl-cipher(singular form)affinity-mode: persistent→balanced(APIG only supports balanced)
Step 3: Resolve Unsupported Annotations
For each unsupported annotation, follow this decision tree in order:
1. Higress native annotation? → Use native equivalent (no WasmPlugin needed)
2. Safe to drop? → Remove without replacement
3. Built-in platform plugin? → Use built-in OCI image via higress.io/wasmplugin annotation
4. None of the above? → Develop custom WasmPluginSee references/migration-patterns.md for the complete decision tree, and references/builtin-plugins.md for the built-in plugin catalog.
Higress native mappings:
| nginx annotation | Higress equivalent |
|---|---|
denylist-source-range | higress.io/blacklist-source-range |
mirror-target | higress.io/mirror-target-service + higress.io/mirror-percentage |
Safe-to-drop: service-upstream, enable-access-log, proxy-request-buffering: off, connection-proxy-header
Built-in plugins: limit-rps/limit-connections → key-rate-limit, enable-modsecurity → waf. See references/builtin-plugins.md.
Custom WasmPlugin (last resort): auth-url, server-snippet, configuration-snippet, etc. See references/wasm-plugin-sdk.md for SDK reference, references/snippet-patterns.md for conversion patterns.
Step 4: Generate Migrated Ingress YAML
For each input Ingress, generate a migrated copy:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: <original-name>-apig
namespace: <original-namespace>
annotations:
# Compatible annotations preserved
# Unsupported annotations replaced with higress.io/wasmplugin if needed
spec:
ingressClassName: apig # MUST be hardcoded to apig
rules: ... # Preserved from original
tls: ... # Preserved from originalStep 5: Output Migration Report
所有输出建议使用中文(中文)。包括分析表、迁移总结、后续操作指南及所有说明性文字。代码块(YAML、Go、bash)保持原始语法。
>
以下所有内容均为标准输出项,建议在一次响应中完整输出,无需逐项询问用户。
Output ALL of the following for each Ingress:
1. 兼容性分析表 — annotation, value, category (兼容/可忽略/不支持), action 2. 迁移后的 Ingress YAML — ready for user to apply 3. 自定义 WasmPlugin 源码 — if Step 3 determined custom plugins are needed (skip only if no custom plugin is needed) 4. 迁移总结 — what changed, value changes, plugins needed 5. 后续操作指南 — 根据兼容性分析结果,分场景告知用户完整的迁移操作路径:
- 完全兼容(无不兼容注解):所有注解均为兼容或可忽略类型,用户可直接参考 Nginx Ingress 迁移到云原生 API 网关 完成迁移。
- 不完全兼容(存在不兼容注解):按以下顺序操作:
1. 构建并推送自定义 WasmPlugin OCI 镜像 2. 将迁移后 Ingress YAML 中的 OCI URL 占位符替换为真实的 WasmPlugin 镜像地址 3. 将替换后的 Ingress YAML 部署到集群中 4. 参考 Nginx Ingress 迁移到云原生 API 网关 继续后续操作,在步骤一「指定 IngressClass」处需指定为 apig 5. 网关版本要求:使用 WasmPlugin 需确保云原生 API 网关版本在 2.1.16 及以上,否则需要升级版本或创建新网关
See references/deployment-guide-template.md for the guide template.
Scope boundary: This skill generates all artifacts and instructions. It does NOT executekubectl apply,docker push, or any cluster/registry write operations. Those are left to the user.
No confirmation needed: Every item above is always generated. Never ask "是否需要生成迁移文件/检查清单/部署指南?"
Success Verification Method
See references/verification-method.md for verification steps to include in the migration report.
The migration report should instruct the user to verify with:
# Validate migrated YAML syntax (user runs this)
kubectl apply --dry-run=client -f <migrated-ingress>.yaml
# Confirm ingressClassName is apig
grep "ingressClassName: apig" <migrated-ingress>.yamlThis skill outputs verification instructions for the user. It does NOT execute these commands.
Cleanup
Not applicable. This skill only generates text output (YAML, Go source code, migration report). No cloud resources or cluster objects are created by this skill.
API and Command Tables
This skill does not execute any CLI commands or API calls. All output is text-based (YAML, Go source code, migration report with instructions for the user).
Best Practices
1. Always classify ALL annotations before generating migrated YAML — never skip annotations 2. Use placeholders (<REGION>, <YOUR_REGISTRY>) for unspecified parameters; never hardcode user-specific values 3. Preserve original rules, tls, and namespace in migrated YAML 4. Add -apig suffix to migrated Ingress name for easy identification 5. Prefer built-in plugins over custom WasmPlugin — check references/builtin-plugins.md first 6. For custom WasmPlugin, use github.com/higress-group/wasm-go/pkg/wrapper SDK exclusively 7. Track annotation value changes (e.g., ewma → round_robin) explicitly in the report 8. For server-snippet/configuration-snippet, enumerate every directive and verify 1:1 conversion completeness 9. Never execute cluster write operations (kubectl apply, docker push, etc.) — only output instructions for the user
Reference Links
| Reference | Contents |
|---|---|
references/annotation-mapping.md | Complete 117-annotation compatibility lookup table |
references/migration-patterns.md | Decision tree, Higress native mappings, safe-to-drop list, special handling |
references/builtin-plugins.md | APIG built-in platform plugins catalog with OCI URLs |
references/platform-oci-registry.md | Region-specific OCI registry addresses for built-in plugins |
references/snippet-patterns.md | server-snippet / configuration-snippet → WasmPlugin conversion patterns |
references/wasm-plugin-sdk.md | Higress WASM Go Plugin SDK reference (core API) |
references/wasm-http-client.md | WasmPlugin HTTP client patterns (external auth, callouts) |
references/wasm-redis-client.md | WasmPlugin Redis client patterns (rate limiting, session) |
references/wasm-advanced-patterns.md | Advanced WasmPlugin patterns (streaming, tick, leader election) |
references/wasm-local-testing.md | Local WasmPlugin testing with Docker Compose |
references/plugin-deployment.md | WasmPlugin build, OCI push, and Ingress annotation binding |
references/deployment-guide-template.md | Migration report deployment guide template |
references/acceptance-criteria.md | Testing acceptance criteria with correct/incorrect patterns |
references/verification-method.md | Success verification steps and commands |
references/security-review-policy.md | 定期安全复审策略与检查项 |
references/security-impact-assessment.md | 安全影响评估与数据处理流程 |
references/ram-policies.md | RAM 权限声明(本 Skill 无需任何权限) |
Acceptance Criteria: alibabacloud-nginx-ingress-to-api-gateway
Scenario: Nginx Ingress to APIG Migration Purpose: Skill testing acceptance criteria
---
Correct Annotation Classification Patterns
1. Compatible Annotations — Must be kept in migrated YAML
✅ CORRECT
# These annotations should be preserved in migrated Ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"❌ INCORRECT
# These annotations should NOT be kept — they are ignorable
annotations:
nginx.ingress.kubernetes.io/proxy-connect-timeout: "30" # Ignorable
nginx.ingress.kubernetes.io/proxy-read-timeout: "60" # Ignorable
nginx.ingress.kubernetes.io/proxy-body-size: "10m" # Ignorable2. Special Value Handling — Must change values
✅ CORRECT
# load-balance: ewma must be changed
nginx.ingress.kubernetes.io/load-balance: round_robin
# ssl-ciphers must be renamed to ssl-cipher (singular)
nginx.ingress.kubernetes.io/ssl-cipher: "ECDHE-RSA-AES128-GCM-SHA256"❌ INCORRECT
# EWMA is not supported by APIG
nginx.ingress.kubernetes.io/load-balance: ewma
# Plural form is not supported
nginx.ingress.kubernetes.io/ssl-ciphers: "ECDHE-RSA-AES128-GCM-SHA256"3. Migrated Ingress YAML — Must have correct structure
✅ CORRECT
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress-apig # -apig suffix added
namespace: production
labels:
migration.higress.io/source: nginx # migration label
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # compatible, kept
spec:
ingressClassName: apig # changed from nginx to apig❌ INCORRECT
metadata:
name: my-app-ingress # Missing -apig suffix
spec:
ingressClassName: nginx # Not changed to apig4. WasmPlugin — Must use correct SDK patterns
✅ CORRECT
import "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
func (ctx *MyPlugin) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action {
return types.ActionContinue
}❌ INCORRECT
// types.BodyContinue does not exist in proxy-wasm-go-sdk
return types.BodyContinue
// Never call ResumeHttpRequest after SendHttpResponse
proxywasm.SendHttpResponse(403, nil, nil, -1)
proxywasm.ResumeHttpRequest() // WRONG — auto-resumes internally5. Migration Step Guidance — Must match analysis result
✅ CORRECT — No unsupported annotations: direct migration reference
迁移步骤指引:
所有注解均为兼容或可忽略类型,无需额外插件开发。
请直接参考阿里云官方文档完成迁移:
https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/user-guide/migrating-from-nginx-ingress-to-cloud-native-api-gateway✅ CORRECT — Has unsupported annotations: deploy new Ingress YAML + IngressClass apig + version requirement
迁移步骤指引:
存在不兼容注解,需要将新生成的 Ingress YAML 部署到网关中。
请参考阿里云官方文档操作:
https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/user-guide/migrating-from-nginx-ingress-to-cloud-native-api-gateway
注意:在步骤一「指定 IngressClass」处需指定为 apig。
网关版本要求:必须确保云原生 API 网关版本在 2.1.16 及以上,否则需要升级网关版本或创建新网关。❌ INCORRECT — Missing version requirement when unsupported annotations exist
# Wrong: has unsupported annotations but does not mention gateway version 2.1.16 requirement
迁移步骤指引:
存在不兼容注解,请参考文档操作。❌ INCORRECT — Missing migration doc link
# Wrong: no reference to the official migration document
迁移步骤指引:
所有注解均兼容,可以直接迁移。6. Higress Native Mapping — Must use correct annotation names
✅ CORRECT
# denylist-source-range maps to higress.io/blacklist-source-range
higress.io/blacklist-source-range: "192.168.1.0/24,10.0.0.5"❌ INCORRECT
# Wrong: keeping nginx annotation for unsupported feature
nginx.ingress.kubernetes.io/denylist-source-range: "192.168.1.0/24"Nginx Ingress Annotation → APIG Compatibility
Table of Contents
- Classification Rule
- 1. Compatible Annotations (50)
- 2. Ignorable Annotations (16)
- 3. Unsupported Annotations (51)
- Migration Processing Summary
- Quick Reference: Annotation → Category Lookup
- Analysis Script
Authority source:annotations/compatible_annotations.go(CompatibleAnnotations/IgnoreAnnotations)
Cross-referenced with: Nginx Ingress Annotations and APIG Supported Annotations
Classification Rule
Every nginx.ingress.kubernetes.io/* annotation falls into exactly one of three categories:
| Category | Source | Count | Migration Action |
|---|---|---|---|
| Compatible | CompatibleAnnotations | 50 | Keep annotation in new Ingress |
| Ignorable | IgnoreAnnotations | 16 | Strip annotation (no replacement needed) |
| Unsupported | Not in either set | 51 | Strip annotation → replace with higress.io/wasmplugin annotation |
---
1. Compatible Annotations (50)
Source: CompatibleAnnotations set in annotations/compatible_annotations.go
These annotations are natively supported by APIG. Keep them as-is in the migrated Ingress.
Canary / Grayscale (7)
| # | Annotation | Notes |
|---|---|---|
| 1 | canary | Enable/disable canary |
| 2 | canary-by-header | Traffic split by header key |
| 3 | canary-by-header-value | Traffic split by header value (exact) |
| 4 | canary-by-header-pattern | Traffic split by header value (regex) |
| 5 | canary-by-cookie | Traffic split by cookie key |
| 6 | canary-weight | Weight-based traffic split |
| 7 | canary-weight-total | Weight total |
CORS (7)
| # | Annotation | Notes |
|---|---|---|
| 8 | enable-cors | Enable/disable CORS |
| 9 | cors-allow-origin | Allowed origins |
| 10 | cors-allow-methods | Allowed methods |
| 11 | cors-allow-headers | Allowed headers |
| 12 | cors-expose-headers | Exposed headers |
| 13 | cors-allow-credentials | Allow credentials |
| 14 | cors-max-age | Preflight cache duration |
Redirect (6)
| # | Annotation | Notes |
|---|---|---|
| 15 | app-root | Redirect / to specified path |
| 16 | temporal-redirect | Temporary redirect (302) |
| 17 | permanent-redirect | Permanent redirect (301) |
| 18 | permanent-redirect-code | Custom permanent redirect code |
| 19 | ssl-redirect | HTTP → HTTPS |
| 20 | force-ssl-redirect | Force HTTP → HTTPS |
Rewrite (3)
| # | Annotation | Notes |
|---|---|---|
| 21 | rewrite-target | Path rewrite, supports group capture |
| 22 | use-regex | Enable regex path matching (RE2) |
| 23 | upstream-vhost | Override Host header to upstream |
Retry (3)
| # | Annotation | Notes |
|---|---|---|
| 24 | proxy-next-upstream-tries | Max retry attempts (default: 3) |
| 25 | proxy-next-upstream-timeout | Retry timeout in seconds |
| 26 | proxy-next-upstream | Retry conditions |
Fallback (2)
| # | Annotation | Notes |
|---|---|---|
| 27 | default-backend | Fallback service when primary has no endpoints |
| 28 | custom-http-errors | Forward to default-backend on specified HTTP codes |
Downstream TLS (2)
| # | Annotation | Notes |
|---|---|---|
| 29 | auth-tls-secret | CA cert for client mTLS (format: {domain-cert-secret}-cacert) |
| 30 | ssl-cipher | TLS cipher suites. ⚠️ Nginx uses ssl-ciphers (with 's'); APIG uses ssl-cipher (without 's') |
Upstream TLS (5)
| # | Annotation | Notes |
|---|---|---|
| 31 | backend-protocol | HTTP/HTTP2/HTTPS/gRPC/gRPCS (⚠️ no AJP/FCGI) |
| 32 | proxy-ssl-secret | Client certificate for upstream mTLS |
| 33 | proxy-ssl-verify | Enable/disable upstream cert verification |
| 34 | proxy-ssl-name | SNI for upstream TLS |
| 35 | proxy-ssl-server-name | Enable/disable SNI |
Load Balancing & Session Affinity (9)
| # | Annotation | Notes |
|---|---|---|
| 36 | load-balance | round_robin/least_conn/random (⚠️ no EWMA). If the original Ingress uses ewma, change to round_robin or least_conn in the migrated copy |
| 37 | upstream-hash-by | Consistent hash key (⚠️ no variable combinations) |
| 38 | affinity | Affinity type (cookie only) |
| 39 | affinity-mode | ⚠️ Balanced only (persistent not supported) |
| 40 | affinity-canary-behavior | sticky/legacy for canary affinity |
| 41 | session-cookie-name | Cookie name as hash key |
| 42 | session-cookie-path | Cookie path (default: /) |
| 43 | session-cookie-max-age | Cookie max age in seconds |
| 44 | session-cookie-expires | Cookie expiry in seconds |
IP Access Control (1)
| # | Annotation | Notes |
|---|---|---|
| 45 | whitelist-source-range | IP whitelist (IP/CIDR) |
Authentication (4)
| # | Annotation | Notes |
|---|---|---|
| 46 | auth-type | ⚠️ Basic only (digest not supported) |
| 47 | auth-realm | Protection realm |
| 48 | auth-secret | Secret name (namespace/name format) |
| 49 | auth-secret-type | auth-file or auth-map |
Domain Alias (1)
| # | Annotation | Notes |
|---|---|---|
| 50 | server-alias | ⚠️ Exact/wildcard only (gateway ≥1.2.30) |
---
2. Ignorable Annotations (16)
Source: IgnoreAnnotations set in annotations/compatible_annotations.go
These annotations have no meaningful effect in Envoy-based APIG. During migration, strip them from the new Ingress — no replacement needed.
| # | Annotation | Why ignored |
|---|---|---|
| 1 | client-body-buffer-size | Envoy has own buffer management |
| 2 | proxy-buffering | Envoy has own buffer management |
| 3 | proxy-buffers-number | Envoy has own buffer management |
| 4 | proxy-buffer-size | Envoy has own buffer management |
| 5 | proxy-max-temp-file-size | Envoy has own buffer management |
| 6 | proxy-read-timeout | Envoy uses unified route timeout (higress.io/timeout) |
| 7 | proxy-send-timeout | Same as above |
| 8 | proxy-connect-timeout | Same as above |
| 9 | proxy-http-version | Envoy auto-manages upstream HTTP version |
| 10 | ssl-prefer-server-ciphers | Envoy has own cipher preference |
| 11 | proxy-ssl-protocols | Envoy has own TLS protocol management |
| 12 | preserve-trailing-slash | Envoy preserves trailing slashes by default |
| 13 | http2-push-preload | HTTP/2 Push deprecated by major browsers |
| 14 | proxy-ssl-ciphers | Envoy has own upstream cipher management |
| 15 | enable-rewrite-log | Nginx-specific rewrite debug logging |
| 16 | proxy-body-size | APIG uses chunked streaming; no preset body size limit |
---
3. Unsupported Annotations (51)
These annotations exist in Nginx Ingress but are NOT in CompatibleAnnotations or IgnoreAnnotations. During migration, strip the unsupported annotation from the new Ingress, then add a `higress.io/wasmplugin` annotation to the same Ingress to replicate the logic via a WasmPlugin (built-in or custom). The Ingress itself is always migrated.
Snippets (5)
These inject raw Nginx/Lua/ModSecurity code and require full WasmPlugin conversion:
| # | Annotation | Nginx Functionality | WasmPlugin Approach |
|---|---|---|---|
| 1 | configuration-snippet | Location-level Nginx config injection | Parse directives → implement equivalent logic in Go WASM |
| 2 | server-snippet | Server-level Nginx config injection | Same as above |
| 3 | stream-snippet | TCP/UDP stream config | Envoy TCP filter via WASM if applicable |
| 4 | modsecurity-snippet | Custom ModSecurity rules | Use built-in waf plugin or custom WAF WASM |
| 5 | auth-snippet | Custom auth config block | Implement auth logic in WASM |
External Authentication (10)
These implement external auth (subrequest-based) — requires a single WasmPlugin:
| # | Annotation | Nginx Functionality |
|---|---|---|
| 6 | auth-url | URL for external auth service |
| 7 | auth-cache-key | Cache key for auth responses |
| 8 | auth-cache-duration | Cache TTL for auth responses |
| 9 | auth-keepalive | Max keepalive connections to auth service |
| 10 | auth-keepalive-share-vars | Share Nginx vars with auth request |
| 11 | auth-keepalive-requests | Max requests per keepalive connection |
| 12 | auth-keepalive-timeout | Keepalive timeout to auth service |
| 13 | auth-proxy-set-headers | ConfigMap of headers to send to auth service |
| 14 | enable-global-auth | Toggle global external auth |
WasmPlugin approach: Implement HTTP callout to external auth service using proxy_http_call in proxy-wasm-go SDK.Client Certificate / mTLS Extended (5)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 15 | auth-tls-verify-depth | Client cert chain verification depth |
| 16 | auth-tls-verify-client | Client cert verification mode (on/off/optional) |
| 17 | auth-tls-error-page | Redirect URL on cert auth failure |
| 18 | auth-tls-pass-certificate-to-upstream | Pass client cert to upstream via header |
| 19 | auth-tls-match-cn | Match CN of client cert (regex) |
WasmPlugin approach: Read client cert from connection properties, validate CN, set headers.
Rate Limiting (2)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 20 | limit-connections | Max concurrent connections per IP |
| 21 | limit-rps | Max requests per second per IP |
WasmPlugin approach: Use built-in key-rate-limit plugin, or implement custom counter logic in WASM.ModSecurity / WAF (3)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 22 | enable-modsecurity | Enable ModSecurity WAF |
| 23 | enable-owasp-core-rules | Enable OWASP CRS ruleset |
| 24 | modsecurity-transaction-id | Set ModSecurity transaction ID |
WasmPlugin approach: Use built-inwafplugin (oci://apiginner-registry-vpc.<REGION>.cr.aliyuncs.com/platform_wasm/waf:1.0.0— replace<REGION>with cluster region).
Traffic Mirroring (3)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 25 | mirror-target | Mirror traffic to specified URI |
| 26 | mirror-request-body | Whether to include body in mirrored request |
| 27 | mirror-host | Override Host header for mirrored request |
WasmPlugin approach: Use Higress annotation higress.io/mirror-target-service (mirrors to K8s Service instead of URI), or implement custom mirror logic in WASM.Custom Headers (1)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 28 | custom-headers | Add response headers via ConfigMap reference |
WasmPlugin approach: Generate a custom WasmPlugin to add/modify response headers, or use Higress annotations higress.io/response-header-control-add if available.Proxy Settings (6)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 29 | proxy-cookie-domain | Rewrite Set-Cookie domain attribute |
| 30 | proxy-cookie-path | Rewrite Set-Cookie path attribute |
| 31 | proxy-request-buffering | Enable/disable request body buffering |
Envoy streams request bodies by default (equivalent toproxy_request_buffering off). This annotation can usually be safely dropped. If the original value wasonand the backend requires buffered requests, this may need investigation.
| 32 | proxy-redirect-from | Rewrite Location/Refresh header (source) | | 33 | proxy-redirect-to | Rewrite Location/Refresh header (target) | | 34 | proxy-ssl-verify-depth | Upstream cert chain verification depth |
WasmPlugin approach: For cookie/redirect rewriting, implement header manipulation in WASM using on_http_response_headers.Proxy Buffer Extended (1)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 35 | proxy-busy-buffers-size | Limit busy buffer size during response streaming |
Not applicable in Envoy architecture. Can be safely removed in most cases.
Session Cookie Extended (5)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 36 | session-cookie-change-on-failure | Regenerate cookie on upstream failure |
| 37 | session-cookie-conditional-samesite-none | Browser-compat SameSite=None handling |
| 38 | session-cookie-domain | Set cookie Domain attribute |
| 39 | session-cookie-samesite | Set cookie SameSite attribute |
| 40 | session-cookie-secure | Set cookie Secure flag |
WasmPlugin approach: Implement cookie attribute manipulation in WASM using on_http_response_headers.TLS / SSL (2)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 41 | ssl-ciphers | Downstream cipher suites (⚠️ APIG uses ssl-cipher without 's') |
| 42 | ssl-passthrough | TLS passthrough to backend (layer 4) |
Note: Forssl-ciphers, the compatible annotation isssl-cipher(without 's'). Migration should rename it. Forssl-passthrough, Envoy does not natively support layer-4 TLS passthrough via Ingress.
Redirect Extended (2)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 43 | from-to-www-redirect | Redirect between www and non-www |
| 44 | temporal-redirect-code | Custom temporal redirect status code |
WasmPlugin approach: Implement redirect logic checking Host header in WASM.
IP Access Control (1)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 45 | denylist-source-range | IP blacklist (CIDR) |
Note: APIG officially supports this via higress.io/blacklist-source-range. Migration should use the Higress annotation.Observability (3)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 46 | enable-access-log | Enable/disable access logging per Ingress |
| 47 | enable-opentelemetry | Enable/disable OpenTelemetry tracing |
| 48 | opentelemetry-trust-incoming-span | Trust incoming trace spans |
Envoy has its own observability stack. These are typically configured at gateway level, not per-Ingress. enable-access-log can be safely dropped — configure access logging in the APIG console instead.Miscellaneous (3)
| # | Annotation | Nginx Functionality |
|---|---|---|
| 49 | satisfy | Auth combination logic (any/all) |
| 50 | service-upstream | Route to ClusterIP instead of Pod IPs |
| 51 | connection-proxy-header | Override Connection header (e.g., keep-alive) |
WasmPlugin approach:satisfycan be implemented as multi-auth logic in WASM.connection-proxy-headeris safe to drop as Envoy manages connection headers.
>
⚠️ `service-upstream` is safe to drop: Envoy routes via Service ClusterIP by default (equivalent toservice-upstream: "true"), so this annotation can be safely removed regardless of whether its value is"true"or"false"— no WasmPlugin replacement is needed. During Step 3 analysis, if an Ingress's only unsupported annotation isservice-upstream, it does not actually need a WasmPlugin and should be classified as "cleaned" (strip the annotation only).
Additional: x-forwarded-prefix (documented in nginx but not in annotation table)
| # | Annotation | Nginx Functionality |
|---|---|---|
| — | x-forwarded-prefix | Add X-Forwarded-Prefix header |
Envoy automatically handles X-Forwarded headers. Typically no replacement needed.
---
Migration Processing Summary
When the AI agent processes each Ingress:
1. Compatible (50) → Preserve in the new apig Ingress copy 2. Ignorable (16) → Strip annotation from new Ingress — no replacement needed 3. Unsupported (51) → Strip annotation from new Ingress → develop/select WasmPlugin → add `higress.io/wasmplugin` annotation to the same Ingress
Quick Reference: Annotation → Category Lookup
All annotations use prefix nginx.ingress.kubernetes.io/. Sorted alphabetically:
| Annotation | Category |
|---|---|
affinity | ✅ Compatible |
affinity-canary-behavior | ✅ Compatible |
affinity-mode | ✅ Compatible |
app-root | ✅ Compatible |
auth-cache-duration | ❌ Unsupported |
auth-cache-key | ❌ Unsupported |
auth-keepalive | ❌ Unsupported |
auth-keepalive-requests | ❌ Unsupported |
auth-keepalive-share-vars | ❌ Unsupported |
auth-keepalive-timeout | ❌ Unsupported |
auth-proxy-set-headers | ❌ Unsupported |
auth-realm | ✅ Compatible |
auth-secret | ✅ Compatible |
auth-secret-type | ✅ Compatible |
auth-snippet | ❌ Unsupported |
auth-tls-error-page | ❌ Unsupported |
auth-tls-match-cn | ❌ Unsupported |
auth-tls-pass-certificate-to-upstream | ❌ Unsupported |
auth-tls-secret | ✅ Compatible |
auth-tls-verify-client | ❌ Unsupported |
auth-tls-verify-depth | ❌ Unsupported |
auth-type | ✅ Compatible |
auth-url | ❌ Unsupported |
backend-protocol | ✅ Compatible |
canary | ✅ Compatible |
canary-by-cookie | ✅ Compatible |
canary-by-header | ✅ Compatible |
canary-by-header-pattern | ✅ Compatible |
canary-by-header-value | ✅ Compatible |
canary-weight | ✅ Compatible |
canary-weight-total | ✅ Compatible |
client-body-buffer-size | ⚪ Ignorable |
configuration-snippet | ❌ Unsupported |
connection-proxy-header | ❌ Unsupported |
cors-allow-credentials | ✅ Compatible |
cors-allow-headers | ✅ Compatible |
cors-allow-methods | ✅ Compatible |
cors-allow-origin | ✅ Compatible |
cors-expose-headers | ✅ Compatible |
cors-max-age | ✅ Compatible |
custom-headers | ❌ Unsupported |
custom-http-errors | ✅ Compatible |
default-backend | ✅ Compatible |
denylist-source-range | ❌ Unsupported |
enable-access-log | ❌ Unsupported |
enable-cors | ✅ Compatible |
enable-global-auth | ❌ Unsupported |
enable-modsecurity | ❌ Unsupported |
enable-opentelemetry | ❌ Unsupported |
enable-owasp-core-rules | ❌ Unsupported |
enable-rewrite-log | ⚪ Ignorable |
force-ssl-redirect | ✅ Compatible |
from-to-www-redirect | ❌ Unsupported |
http2-push-preload | ⚪ Ignorable |
limit-connections | ❌ Unsupported |
limit-rps | ❌ Unsupported |
load-balance | ✅ Compatible |
mirror-host | ❌ Unsupported |
mirror-request-body | ❌ Unsupported |
mirror-target | ❌ Unsupported |
modsecurity-snippet | ❌ Unsupported |
modsecurity-transaction-id | ❌ Unsupported |
permanent-redirect | ✅ Compatible |
permanent-redirect-code | ✅ Compatible |
preserve-trailing-slash | ⚪ Ignorable |
proxy-body-size | ⚪ Ignorable |
proxy-buffer-size | ⚪ Ignorable |
proxy-buffering | ⚪ Ignorable |
proxy-buffers-number | ⚪ Ignorable |
proxy-busy-buffers-size | ❌ Unsupported |
proxy-connect-timeout | ⚪ Ignorable |
proxy-cookie-domain | ❌ Unsupported |
proxy-cookie-path | ❌ Unsupported |
proxy-http-version | ⚪ Ignorable |
proxy-max-temp-file-size | ⚪ Ignorable |
proxy-next-upstream | ✅ Compatible |
proxy-next-upstream-timeout | ✅ Compatible |
proxy-next-upstream-tries | ✅ Compatible |
proxy-read-timeout | ⚪ Ignorable |
proxy-redirect-from | ❌ Unsupported |
proxy-redirect-to | ❌ Unsupported |
proxy-request-buffering | ❌ Unsupported |
proxy-send-timeout | ⚪ Ignorable |
proxy-ssl-ciphers | ⚪ Ignorable |
proxy-ssl-name | ✅ Compatible |
proxy-ssl-protocols | ⚪ Ignorable |
proxy-ssl-secret | ✅ Compatible |
proxy-ssl-server-name | ✅ Compatible |
proxy-ssl-verify | ✅ Compatible |
proxy-ssl-verify-depth | ❌ Unsupported |
rewrite-target | ✅ Compatible |
satisfy | ❌ Unsupported |
server-alias | ✅ Compatible |
server-snippet | ❌ Unsupported |
service-upstream | ❌ Unsupported |
session-cookie-change-on-failure | ❌ Unsupported |
session-cookie-conditional-samesite-none | ❌ Unsupported |
session-cookie-domain | ❌ Unsupported |
session-cookie-expires | ✅ Compatible |
session-cookie-max-age | ✅ Compatible |
session-cookie-name | ✅ Compatible |
session-cookie-path | ✅ Compatible |
session-cookie-samesite | ❌ Unsupported |
session-cookie-secure | ❌ Unsupported |
ssl-ciphers | ❌ Unsupported |
ssl-cipher | ✅ Compatible |
ssl-passthrough | ❌ Unsupported |
ssl-prefer-server-ciphers | ⚪ Ignorable |
ssl-redirect | ✅ Compatible |
stream-snippet | ❌ Unsupported |
temporal-redirect | ✅ Compatible |
temporal-redirect-code | ❌ Unsupported |
upstream-hash-by | ✅ Compatible |
upstream-vhost | ✅ Compatible |
use-regex | ✅ Compatible |
whitelist-source-range | ✅ Compatible |
Analysis Script
./scripts/analyze-ingress.sh [namespace]APIG Built-in Platform Plugins
Before writing custom WASM plugins, check if APIG has a built-in platform plugin that meets your needs.
Official docs: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/user-guide/platform-plug-ins/
Authentication & Authorization
| Plugin | Description | Replaces nginx feature | Docs |
|---|---|---|---|
key-auth | API Key authentication from URL params or headers | Custom auth headers | doc |
basic-auth | HTTP Basic Auth (RFC 7617) | auth_basic directive | doc |
hmac-auth | HMAC signature-based authentication | Signature validation scripts | doc |
jwt-auth | JWT validation from URL params, headers, or cookies; supports per-caller credentials | JWT Lua scripts, auth_request for JWT | doc |
oauth | OAuth 2.0 Access Token issuance based on JWT (RFC 9068) | OAuth Lua scripts | doc |
jwt-logout | JWT logout & unique-login control via Redis; supports session kick-off across devices | Custom session invalidation logic | doc |
Traffic Control
| Plugin | Description | Replaces nginx feature | Docs |
|---|---|---|---|
key-rate-limit | Rate limiting by key (URL param or header) | limit_req directive | doc |
cluster-key-rate-limit | Distributed rate limiting via Redis across gateway instances | limit_req with shared state | doc |
http-real-ip | WASM implementation of nginx ngx_http_realip_module; extracts real client IP from trusted proxies | set_real_ip_from, real_ip_header directives | doc |
hsts | Adds Strict-Transport-Security header to HTTPS responses; browser-side 307 redirect to HTTPS | add_header Strict-Transport-Security | doc |
canary-header | Adds headers by configurable weight for proportional grayscale routing without client-side changes | Custom canary routing scripts | doc |
traffic-tag | Tags/colors traffic by weight or request content via request headers | Custom headers for routing | doc |
Transmission Protocol
| Plugin | Description | Replaces nginx feature | Docs |
|---|---|---|---|
custom-response | Custom HTTP response (status code, headers, body); can be used for mocking or custom error pages | return directive, error_page | doc |
de-graphql | Maps URIs to GraphQL queries, converting GraphQL upstream to REST-like access | GraphQL handling | doc |
frontend-gray | Frontend A/B testing and grayscale release by user ID, cookie, weight, or localStorage | Frontend deployment scripts | doc |
cache-control | Adds Expires and Cache-Control headers by URL file suffix (e.g. jpg, png) | expires, add_header Cache-Control | doc |
geo-ip | Resolves client IP to geographic location; passes results via request headers and attributes | geoip module | doc |
Security Protection
| Plugin | Description | Replaces nginx feature | Docs |
|---|---|---|---|
request-block | Blocks HTTP requests by URL, header, or other patterns | if + return 403 | doc |
bot-detect | Identifies and blocks web crawlers/bots | Bot detection Lua scripts | doc |
waf | Web Application Firewall based on ModSecurity; supports OWASP CRS | ModSecurity module | doc |
Open-source Higress Plugins NOT Confirmed in APIG
The following plugins exist in open-source Higress but are NOT listed in the APIG platform plugin documentation. They may still work if you push the WASM image to a private registry, but they are not officially supported as platform plugins. The agent should not assume these are available as built-in; if equivalent functionality is needed, generate a custom WasmPlugin instead.
| Plugin | Description | Status |
|---|---|---|
transformer | Request/response header/body transformation | Not in APIG docs |
cors | CORS header injection | Not in APIG docs (CORS is handled via native annotations enable-cors etc.) |
ip-restriction | IP whitelist/blacklist | Not in APIG docs (use request-block or native annotation whitelist-source-range) |
ext-auth | External authorization service | Not in APIG docs |
oidc | OpenID Connect | Not in APIG docs |
opa | Open Policy Agent | Not in APIG docs |
request-validation | Request parameter validation | Not in APIG docs |
Using Built-in Plugins
Via Ingress Annotation (Recommended for Migration)
For APIG migration, bind built-in plugins directly to Ingress resources via the higress.io/wasmplugin annotation:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-apig
namespace: production
annotations:
higress.io/wasmplugin: |
{
"apiVersion": "extensions.istio.io/v1alpha1",
"kind": "WasmPlugin",
"metadata": {"name": "my-app-rate-limit"},
"spec": {
"phase": "UNSPECIFIED_PHASE",
"pluginConfig": {
"_rules_": [
{
"limit_by_per_ip": 10
}
]
},
"priority": 200,
"url": "oci://apiginner-registry-vpc.cn-shanghai.cr.aliyuncs.com/platform_wasm/key-rate-limit:1.0.0"
}
}
spec:
ingressClassName: apig
# ... rules ...Key points:
- Route matching is automatic —
_match_route_is auto-filled by the controller from the Ingress path rules - One Ingress = one plugin annotation — if multiple behaviors are needed, combine into one plugin or use native annotations for standard features
- Self-contained — deleting the Ingress removes the plugin binding automatically
Via Higress Console
1. Navigate to Plugins → Plugin Market 2. Find the desired plugin 3. Click Enable and configure 4. Under Scope, select specific routes/domains
OCI Image Registry
Platform built-in plugin images are hosted in a region-specific VPC registry. To construct the correct OCI URL for a built-in plugin, consult platform-oci-registry.md (loaded separately from SKILL.md) for:
- Auto-detection command (via kubectl node labels)
- Full region ID →
PLATFORM_OCI_BASElookup table - OCI URL construction formula:
oci://${PLATFORM_OCI_BASE}/<plugin-name>:<version>
Custom plugins use the user's own OCI registry (e.g. oci://registry.cn-hangzhou.aliyuncs.com/my-plugins/higress-wasm-foo:v1). The user must ensure VPC connectivity from the gateway to their registry.Plugin Configuration Reference
Each plugin has its own configuration schema. For detailed configuration, refer to the official Alibaba Cloud documentation: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/user-guide/platform-plug-ins/
Or check the open-source plugin specs: https://github.com/higress-group/higress-console/tree/main/backend/sdk/src/main/resources/plugins/<plugin-name>/spec.yaml
迁移报告模板
迁移报告 Step 5 输出模板。agent 根据实际迁移结果填充具体值。
6.1 前置检查
- 确认 IngressClass
apig存在:kubectl get ingressclass apig - 确认 APIG 网关可达
- 降低 DNS TTL
6.2 迁移操作
根据兼容性分析结果,选择对应的迁移路径:
场景一:完全兼容(无不兼容注解)
所有注解均为兼容或可忽略类型,无需额外插件开发。请直接参考阿里云官方文档完成迁移:
📖 Nginx Ingress 迁移到云原生 API 网关
按照文档步骤操作即可。
场景二:不完全兼容(存在不兼容注解)
按以下顺序操作:
第一步:构建并推送自定义 WasmPlugin 镜像
# 登录镜像仓库
docker login <your-registry>
# 为每个自定义插件打标签并推送
docker tag higress-wasm-<name>:v1 <your-registry>/higress-wasm-<name>:v1
docker push <your-registry>/higress-wasm-<name>:v1第二步:将 Ingress YAML 中的 OCI URL 占位符替换为真实的 WasmPlugin 镜像地址
# 替换自定义插件 OCI 占位符
sed -i 's|<YOUR_REGISTRY>|your-actual-registry.com/namespace|g' all-migrated-ingress.yaml
# 替换内置插件区域占位符(如需要)
sed -i 's|<REGION>|cn-hangzhou|g' all-migrated-ingress.yaml第三步:将替换后的 Ingress YAML 部署到集群中
kubectl apply -f all-migrated-ingress.yaml
kubectl get ingress -l migration.higress.io/source=nginx第四步:参考官方文档继续后续操作
📖 Nginx Ingress 迁移到云原生 API 网关
在文档步骤一「指定 IngressClass」处,需要将 IngressClass 指定为 apig。
⚠️ 网关版本要求:使用 WasmPlugin 需确保云原生 API 网关版本在 2.1.16 及以上。如果当前网关版本低于 2.1.16,需要先升级网关版本或创建新网关后再进行迁移。
6.3 验证路由
- 阶段一:路由可达性 — 验证网关能正确接收和转发流量
- 阶段二:WasmPlugin 功能验证 — 针对每种插件类型提供具体的 curl 命令:
- 认证插件:无凭证时预期 401/403,有效凭证时预期 200
- 响应头插件:检查注入的 header 是否存在
- WAF 插件:发送攻击载荷,预期 403
需根据用户 Ingress 中的实际域名和路径定制 curl 命令。
6.4 流量切换
DNS/SLB 切换表(域名 → 网关地址),所有测试通过后再执行。
6.5 迁移后监控(48 小时以上)
- APIG 控制台检查
- 5xx 错误监控
- WasmPlugin 健康状态
- DNS TTL 恢复
- nginx 缩容时间线
6.6 回滚
kubectl delete ingress -l migration.higress.io/source=nginx
# 将 DNS 恢复指向原 nginx-ingressMigration Patterns and Decision Tree
Table of Contents
- Annotation Resolution Decision Tree
- Higress Native Annotation Mappings
- Safe-to-Drop Annotations
- Special Annotation Handling
- Snippet Conversion Completeness
- Handling satisfy Annotation
- Common Plugin Patterns by Annotation Type
Annotation Resolution Decision Tree
For each Ingress with unsupported annotations, follow this order:
1. Higress native annotation? → Use native equivalent (no WasmPlugin)
2. Safe to drop? → Remove without replacement
3. Built-in platform plugin? → Use built-in OCI image
4. None of the above? → Develop custom WasmPluginIf an Ingress's only unsupported annotations are all safe-to-drop or have native equivalents, classify it as "cleaned" (no WasmPlugin needed).
Higress Native Annotation Mappings
| nginx annotation | Higress equivalent | Notes |
|---|---|---|
denylist-source-range | higress.io/blacklist-source-range | Direct mapping |
mirror-target | higress.io/mirror-target-service + higress.io/mirror-percentage | Extract service FQDN from URL; set percentage to 100 or user-specified |
mirror-request-body | (drop) | Higress mirrors the full request by default |
mirror-host | (drop) | Higress uses the target service's host; if custom Host header is needed, implement via WasmPlugin |
ssl-ciphers | ssl-cipher (compatible annotation, singular form) | Rename only — no WasmPlugin needed |
Safe-to-Drop Annotations
These unsupported annotations can be removed without any replacement:
| Annotation | Why safe to drop |
|---|---|
service-upstream | Envoy routes via Service ClusterIP by default (equivalent to service-upstream: "true"), safe regardless of value |
enable-access-log | Configure at gateway level in APIG console |
proxy-request-buffering: off | Envoy streams by default |
connection-proxy-header | Envoy manages connection headers |
proxy-busy-buffers-size | Not applicable in Envoy architecture |
auth-tls-error-page | APIG returns its own TLS error responses; if custom error pages are critical, implement redirect in WasmPlugin, but usually safe to drop |
enable-global-auth: false | Only meaningful with a global auth-url at the nginx-ingress controller level; APIG doesn't have a global external auth concept |
Special Annotation Handling
ssl-ciphers → ssl-cipher
APIG uses the singular form ssl-cipher. During migration, rename the annotation key (drop the trailing 's'). The value stays the same.
load-balance: ewma
APIG doesn't support EWMA. Change to round_robin or least_conn. Call out the old and new values explicitly in the report — the user needs to verify the change doesn't break traffic routing.
affinity-mode: persistent
APIG only supports balanced. Change the value and note it in the report.
server-snippet / configuration-snippet
Analyze each directive individually:
- Directives with APIG-native equivalents (e.g.,
gzip,limit_req,proxy_cache) → drop and note in report add_headerdirectives → use a response-headers type WasmPlugin; count alladd_headerlines and verify the same count in the plugin config- Lua blocks (
access_by_lua_block,content_by_lua_block) → convert to WasmPlugin set+ifvariable logic → convert to WasmPlugin header manipulation- If a snippet mixes multiple concerns (e.g., compression + auth + headers), split into: native features (drop) + WasmPlugin (convert)
Value Change Tracking
When a compatible annotation is kept but its value changes, the migration report must include an "Annotation Value Changes" table with: Ingress name, annotation, old value, new value, and reason.
Snippet Conversion Completeness
When converting configuration-snippet, server-snippet, or auth-snippet to a WasmPlugin, follow this process to avoid losing logic:
1. Enumerate every directive/statement in the original snippet 2. Produce a 1:1 mapping table: Original directive → WasmPlugin code location → Status 3. After implementation, verify the table has no gaps
Common Pitfalls
- Dropping `add_header` directives — e.g., a security header snippet with 6 headers but the WasmPlugin only adds 4. The missing 2 weaken the security posture
- Simplifying multi-step validation — e.g., a Lua script that performs both format validation AND structural validation. The WasmPlugin needs all checks, because skipping any one may open a security gap
- Losing error response bodies — e.g., original returns
{"error":"specific_reason"}but WasmPlugin returns a generic message. Downstream clients may depend on the error format - Confusing `more_set_headers` context — in
configuration-snippet(location block),more_set_headerssets response headers; butngx.req.set_header()in Lua sets request headers to upstream. Map each header operation to the correct WasmPlugin phase - Ignoring APIG-native directives —
gzip on/off,gzip_types,limit_req,proxy_cacheetc. should be dropped or mapped to APIG-native features, not converted to WasmPlugin code - Missing conditional branches — if the original snippet has multiple
ifblocks, the WasmPlugin must handle all branches including the implicit "else" (fall-through) case
Handling satisfy Annotation
The satisfy annotation controls how multiple auth mechanisms combine:
satisfy: all(default) — ALL auth checks must pass (AND logic)satisfy: any— ANY auth check passing is sufficient (OR logic)
Migrating satisfy: any
1. Identify all auth mechanisms on the Ingress (e.g., IP whitelist via whitelist-source-range, Basic Auth via auth-type, HMAC via auth-snippet, external auth via auth-url, mTLS via auth-tls-secret) 2. In the WasmPlugin, check each mechanism in order — if any one passes, allow immediately 3. Only reject if ALL mechanisms fail
// Generic satisfy:any pattern in onHttpRequestHeaders:
if firstAuthPasses(ctx, config) {
return types.HeaderContinue
}
if secondAuthPasses(ctx, config) {
return types.HeaderContinue
}
// All failed — reject
proxywasm.SendHttpResponse(401, headers, body, -1)
return types.HeaderStopAllIterationAndWatermarksatisfy: any with whitelist-source-range
When combined with whitelist-source-range (a compatible annotation handled natively by APIG), the IP whitelist check happens at the gateway level before the WasmPlugin runs. The WasmPlugin only needs to handle non-IP auth mechanisms. For explicit/self-contained OR logic (e.g., testing or portability), you can replicate the IP check in the plugin.
satisfy: all
Each auth mechanism should be a separate check that must all pass — this is the default behavior when multiple auth annotations are present.
Common Plugin Patterns by Annotation Type
| Nginx annotation | Plugin pattern | Key SDK APIs |
|---|---|---|
configuration-snippet / server-snippet | Parse directives → implement in Go | proxywasm.GetHttpRequestHeader, proxywasm.SendHttpResponse |
auth-url (external auth) | HTTP callout to auth service | wrapper.NewClusterClient + client.Get with async callback |
custom-headers | Add response headers | proxywasm.AddHttpResponseHeader in ProcessResponseHeaders |
proxy-cookie-domain/path | Rewrite Set-Cookie | proxywasm.GetHttpResponseHeader("set-cookie") + string replace |
modsecurity-* | Use built-in waf plugin | N/A (built-in) |
denylist-source-range | Use higress.io/blacklist-source-range or built-in request-block | N/A |
auth-snippet + satisfy: any | Multi-auth OR logic | ctx.BufferRequestBody(), proxywasm.GetHttpRequestHeader, proxywasm.SendHttpResponse |
mirror-target | Use Higress native annotations | higress.io/mirror-target-service + higress.io/mirror-percentage |
APIG Platform Plugin OCI Registry
Built-in platform plugin images are hosted in a region-specific VPC registry. This file is the authoritative source for constructing PLATFORM_OCI_BASE when built-in plugins are needed in Step 3a.
OCI URL Format
oci://apiginner-registry-vpc.<REGION>.cr.aliyuncs.com/platform_wasm/<plugin-name>:<version>Set PLATFORM_OCI_BASE to the base path for the cluster's region, then append plugin name and version:
PLATFORM_OCI_BASE=apiginner-registry-vpc.<REGION>.cr.aliyuncs.com/platform_wasm
# Full URL example:
oci://${PLATFORM_OCI_BASE}/waf:1.0.0Determine Cluster Region
Auto-detect via kubectl (preferred):
kubectl get nodes -o jsonpath='{.items[0].metadata.labels.topology\.kubernetes\.io/region}' 2>/dev/null || \
kubectl get nodes -o jsonpath='{.items[0].spec.providerID}' | grep -oP '(?<=\.)[a-z]+-[a-z]+-?\d*(?=\.)'If auto-detection fails, ask the user which region their APIG instance is in.
Region → PLATFORM_OCI_BASE Table
| Area | Region | Region ID | PLATFORM_OCI_BASE |
|---|---|---|---|
| China | Qingdao | cn-qingdao | apiginner-registry-vpc.cn-qingdao.cr.aliyuncs.com/platform_wasm |
| Beijing | cn-beijing | apiginner-registry-vpc.cn-beijing.cr.aliyuncs.com/platform_wasm | |
| Zhangjiakou | cn-zhangjiakou | apiginner-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com/platform_wasm | |
| Ulanqab | cn-wulanchabu | apiginner-registry-vpc.cn-wulanchabu.cr.aliyuncs.com/platform_wasm | |
| Hangzhou | cn-hangzhou | apiginner-registry-vpc.cn-hangzhou.cr.aliyuncs.com/platform_wasm | |
| Shanghai | cn-shanghai | apiginner-registry-vpc.cn-shanghai.cr.aliyuncs.com/platform_wasm | |
| Shenzhen | cn-shenzhen | apiginner-registry-vpc.cn-shenzhen.cr.aliyuncs.com/platform_wasm | |
| Chengdu | cn-chengdu | apiginner-registry-vpc.cn-chengdu.cr.aliyuncs.com/platform_wasm | |
| Hong Kong | cn-hongkong | apiginner-registry-vpc.cn-hongkong.cr.aliyuncs.com/platform_wasm | |
| Asia Pacific | Tokyo | ap-northeast-1 | apiginner-registry-vpc.ap-northeast-1.cr.aliyuncs.com/platform_wasm |
| Singapore | ap-southeast-1 | apiginner-registry-vpc.ap-southeast-1.cr.aliyuncs.com/platform_wasm | |
| Jakarta | ap-southeast-5 | apiginner-registry-vpc.ap-southeast-5.cr.aliyuncs.com/platform_wasm | |
| Seoul | ap-northeast-2 | apiginner-registry-vpc.ap-northeast-2.cr.aliyuncs.com/platform_wasm | |
| Kuala Lumpur | ap-southeast-3 | apiginner-registry-vpc.ap-southeast-3.cr.aliyuncs.com/platform_wasm | |
| Europe & Americas | Silicon Valley | us-west-1 | apiginner-registry-vpc.us-west-1.cr.aliyuncs.com/platform_wasm |
| Virginia | us-east-1 | apiginner-registry-vpc.us-east-1.cr.aliyuncs.com/platform_wasm | |
| Frankfurt | eu-central-1 | apiginner-registry-vpc.eu-central-1.cr.aliyuncs.com/platform_wasm | |
| Finance Cloud | Shanghai Finance | cn-shanghai-finance-1 | apiginner-registry-vpc.cn-shanghai-finance-1.cr.aliyuncs.com/platform_wasm |
Full region list: https://help.aliyun.com/zh/api-gateway/cloud-native-api-gateway/product-overview/regions
WASM Plugin Build and Deployment
Table of Contents
- Plugin Project Structure
- Build Process
- Deployment: Ingress Annotation Binding
- OCI Image Registry (Region-Specific)
- Verify Deployment
- Troubleshooting
Safety notice: Custom plugin images are only pushed to the user-specified registry path and never overwrite existing images. Always use new image names (e.g., higress-wasm-<name>:v1).Plugin Project Structure
my-plugin/
├── main.go # Plugin entry point
├── go.mod # Go module
├── go.sum # Dependencies
├── Dockerfile # OCI image build
├── build.sh # Compile script
└── push.sh # Build & push OCI imageBuild Process
1. Initialize Project
mkdir my-plugin && cd my-plugin
go mod init my-plugin
# Set proxy (only needed in China mainland due to network restrictions)
# Skip this step if you're outside China or have direct access to GitHub
go env -w GOPROXY=https://proxy.golang.com.cn,direct
# Get dependencies (pinned versions for reproducible builds)
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
go get github.com/higress-group/wasm-go@main
go get github.com/tidwall/gjson2. Write Plugin Code
See the higress-wasm-go-plugin skill for detailed API reference. Basic template:
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {}
func init() {
wrapper.SetCtx(
"my-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
)
}
type MyConfig struct {
// Config fields parsed from pluginConfig._rules_[]
}
func parseConfig(json gjson.Result, config *MyConfig) error {
// Parse YAML config (converted to JSON)
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Process request
return types.HeaderContinue
}3. Compile to WASM
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./4. Create Dockerfile
FROM scratch
COPY main.wasm /plugin.wasm5. Login and Push OCI Image
Standard docker push to ACR produces an OCI-compliant image. APIG gateway uses the oci:// protocol to pull the image and extract the WASM binary from the image layer. No special OCI tooling is needed.
# User provides registry (must be VPC-accessible from the APIG gateway)
REGISTRY=your-registry.com/higress-plugins
# Login to registry first
docker login $(echo ${REGISTRY} | cut -d'/' -f1)
# Build OCI image (FROM scratch + .wasm = minimal OCI image with only the WASM binary)
docker build -t ${REGISTRY}/my-plugin:v1 .
# Push
docker push ${REGISTRY}/my-plugin:v1Deployment: Ingress Annotation Binding
APIG supports binding WasmPlugin directly to an Ingress resource via annotation. This is the recommended approach for migration because:
- No separate WasmPlugin CRD — the plugin config is embedded in the Ingress annotation
- Automatic route matching — the controller auto-fills
_match_route_from the Ingress path rules - Self-contained — each migrated Ingress carries its own plugin config
- Easy rollback — deleting the Ingress removes the plugin binding automatically
Supported Annotation Keys
Any one of these annotation keys can be used (they are equivalent):
| Annotation Key | Notes |
|---|---|
higress.io/wasmplugin | Recommended |
higress.ingress.kubernetes.io/wasmplugin | Alternative |
mse.ingress.kubernetes.io/wasmplugin | MSE compatible |
Annotation Value Format
The annotation value is a JSON string with the following structure:
{
"apiVersion": "extensions.istio.io/v1alpha1",
"kind": "WasmPlugin",
"metadata": {
"name": "<plugin-name>"
},
"spec": {
"imagePullPolicy": "Always",
"phase": "<AUTHN|AUTHZ|STATS|UNSPECIFIED_PHASE>",
"pluginConfig": {
"_rules_": [
{
"config_key": "config_value"
}
]
},
"priority": 100,
"url": "oci://<registry>/<image>:<tag>"
}
}Field Reference
| Field | Required | Description |
|---|---|---|
metadata.name | Optional | Plugin name. Auto-generates as {ingress-name}-wasmplugin if omitted |
spec.url | Yes | OCI image URL of the WASM plugin |
spec.phase | Optional | Execution phase: AUTHN, AUTHZ, STATS, or UNSPECIFIED_PHASE |
spec.priority | Optional | Execution order within same phase (higher = earlier). Default: 0 |
spec.imagePullPolicy | Optional | Always, IfNotPresent, or Never. Default: IfNotPresent |
spec.pluginConfig._rules_ | Yes | Array of config objects for route matching |
Understanding _rules_ Structure
The _rules_ field is an array where each element is the plugin's config object. The controller auto-matches routes from the Ingress path rules — you never need to specify route matching yourself.
"pluginConfig": {
"_rules_": [
{
"key1": "value1",
"key2": "value2"
}
]
}In most migration cases, _rules_ contains a single element — the config for all routes in that Ingress. The config schema is defined by the plugin's parseConfig function: whatever fields you read via json.Get("xxx") in parseConfig, those are the fields you put in _rules_[0].
For example, if your plugin does config.AuthURL = json.Get("auth_url").String(), then the config is:
"_rules_": [{ "auth_url": "http://auth-service/verify" }]For array configs, use JSON arrays:
"_rules_": [{
"headers": [
{"name": "X-Frame-Options", "value": "DENY"},
{"name": "X-XSS-Protection", "value": "1; mode=block"}
]
}]Key Behaviors
1. `_match_route_` is auto-populated — The controller automatically fills _match_route_ based on the Ingress path rules. Do NOT manually specify it; any value you provide will be overridden.
2. One Ingress = one WasmPlugin — Each Ingress can only have one wasmplugin annotation. If multiple plugin behaviors are needed, combine them into a single plugin image.
3. Route scoping is automatic — The plugin only applies to routes defined in the Ingress that carries the annotation.
Complete Example
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-apig
namespace: production
labels:
migration.higress.io/source: nginx
migration.higress.io/original-name: my-app
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/use-regex: "true"
higress.io/wasmplugin: |
{
"apiVersion": "extensions.istio.io/v1alpha1",
"kind": "WasmPlugin",
"metadata": {
"name": "my-app-apig-wasmplugin"
},
"spec": {
"imagePullPolicy": "Always",
"phase": "UNSPECIFIED_PHASE",
"pluginConfig": {
"_rules_": [
{
"headers": [
{"name": "X-Custom-Header", "value": "custom-value"}
]
}
]
},
"priority": 100,
"url": "oci://your-registry.com/higress-wasm-custom-headers:v1"
}
}
spec:
ingressClassName: apig
rules:
- host: api.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: backend
port:
number: 8080Using Built-in Plugins via Annotation
For built-in plugins, use the official OCI registry URL directly:
annotations:
higress.io/wasmplugin: |
{
"apiVersion": "extensions.istio.io/v1alpha1",
"kind": "WasmPlugin",
"metadata": {"name": "rate-limit"},
"spec": {
"phase": "UNSPECIFIED_PHASE",
"pluginConfig": {
"_rules_": [
{
"limit_by_per_ip": 10
}
]
},
"priority": 200,
"url": "oci://apiginner-registry-vpc.cn-shanghai.cr.aliyuncs.com/platform_wasm/key-rate-limit:1.0.0"
}
}OCI Image Registry (Region-Specific)
Platform built-in plugin images are in a region-specific VPC registry. To determine the correct PLATFORM_OCI_BASE for the target cluster, see platform-oci-registry.md (loaded separately from SKILL.md).
Custom plugins use the user's own registry and must be VPC-accessible from the gateway.
Verify Deployment
# Check plugin annotation on the Ingress
kubectl get ingress <name>-apig -o jsonpath='{.metadata.annotations.higress\.io/wasmplugin}' | jq .
# Test endpoint (user must provide the gateway VPC address)
curl -v -H "Host: example.com" http://<gateway-address>/test-pathThe APIG gateway runs outside the ACK cluster. For gateway-side logs (plugin loading, errors), ask the user to check the Alibaba Cloud APIG console.
Troubleshooting
Plugin Not Loading
1. Verify the OCI image URL uses the correct region and platform_wasm path for built-in plugins 2. For custom plugins, verify VPC connectivity from the gateway to the user's OCI registry 3. Check gateway logs in the Alibaba Cloud APIG console for image pull or WASM loading errors
Plugin Errors
1. Verify the annotation JSON is well-formed: kubectl get ingress <name>-apig -o jsonpath='{.metadata.annotations.higress\.io/wasmplugin}' | jq . 2. Check the pluginConfig matches the plugin's expected schema 3. Check gateway logs in the Alibaba Cloud APIG console for runtime errors
Multiple Plugins Needed for One Ingress
Since one Ingress can only carry one wasmplugin annotation, if you need multiple plugin behaviors:
1. Combine into one plugin — create a single WASM plugin that implements all needed logic 2. Use built-in for common features — some features (CORS, rate limiting) may be handled via native annotations without a WasmPlugin 3. Split the Ingress — if the paths are independent, split into multiple Ingress resources, each with its own plugin annotation
RAM Policies
required_permissions
无。
本 Skill(alibabacloud-nginx-ingress-to-api-gateway)完全离线运行,不调用任何阿里云 OpenAPI 或云服务接口,因此不需要任何 RAM 权限。
说明
- 不涉及 AccessKey / SecretKey 等凭证
- 不访问任何云资源(ECS、OSS、ACK 等)
- 所有分析和代码生成均在本地完成
Common Nginx Snippet to WASM Plugin Patterns
Table of Contents
- Header Manipulation
- Request Validation
- Request Modification
- Lua Script Conversion
- Response Modification
- Best Practices
When migrating to APIG, incompatible nginx snippet annotations are stripped from the new Ingress and replaced with a higress.io/wasmplugin annotation pointing to a WasmPlugin that implements equivalent logic. Use the patterns below to create those replacement WasmPlugins.
Important: All custom plugins must be validated for correct behavior in a test environment before deploying to production via the operations manual. Faulty plugin logic can cause requests to be incorrectly blocked or security controls to be bypassed.
Header Manipulation
When converting server-snippet or configuration-snippet that contains multiple add_header directives, you MUST convert ALL of them — not just a subset. Count the add_header lines in the original snippet and verify the same count appears in your WasmPlugin config. Security headers are especially critical: Strict-Transport-Security (HSTS), Content-Security-Policy (CSP), X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, and Referrer-Policy are commonly used together — dropping any one of them weakens the security posture.
Add Response Header
Nginx snippet:
more_set_headers "X-Custom-Header: custom-value";
more_set_headers "X-Request-ID: $request_id";WASM plugin:
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
proxywasm.AddHttpResponseHeader("X-Custom-Header", "custom-value")
// For request ID, get from request context
if reqId, err := proxywasm.GetHttpRequestHeader("x-request-id"); err == nil {
proxywasm.AddHttpResponseHeader("X-Request-ID", reqId)
}
return types.HeaderContinue
}Deploy via Ingress annotation:
annotations:
higress.io/wasmplugin: |
{
"apiVersion": "extensions.istio.io/v1alpha1",
"kind": "WasmPlugin",
"metadata": {"name": "<ingress-name>-apig-wasmplugin"},
"spec": {
"phase": "UNSPECIFIED_PHASE",
"pluginConfig": {
"_rules_": [
{
"headers": [
{"name": "X-Custom-Header", "value": "custom-value"}
]
}
]
},
"priority": 100,
"url": "oci://<registry>/higress-wasm-custom-headers:v1"
}
}Route matching is automatic — _match_route_ is auto-filled from the Ingress path rules.
Remove Headers
Nginx snippet:
more_clear_headers "Server";
more_clear_headers "X-Powered-By";WASM plugin:
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
proxywasm.RemoveHttpResponseHeader("Server")
proxywasm.RemoveHttpResponseHeader("X-Powered-By")
return types.HeaderContinue
}Conditional Header
Nginx snippet:
if ($http_x_custom_flag = "enabled") {
more_set_headers "X-Feature: active";
}WASM plugin:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
flag, _ := proxywasm.GetHttpRequestHeader("x-custom-flag")
if flag == "enabled" {
proxywasm.AddHttpRequestHeader("X-Feature", "active")
}
return types.HeaderContinue
}Request Validation
Block by Path Pattern
Nginx snippet:
if ($request_uri ~* "(\.php|\.asp|\.aspx)$") {
return 403;
}WASM plugin:
import "regexp"
type MyConfig struct {
BlockPattern *regexp.Regexp
}
func parseConfig(json gjson.Result, config *MyConfig) error {
pattern := json.Get("blockPattern").String()
if pattern == "" {
pattern = `\.(php|asp|aspx)$`
}
config.BlockPattern = regexp.MustCompile(pattern)
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
path := ctx.Path()
if config.BlockPattern.MatchString(path) {
proxywasm.SendHttpResponse(403, nil, []byte("Forbidden"), -1)
return types.HeaderStopAllIterationAndWatermark
}
return types.HeaderContinue
}Block by User Agent
Nginx snippet:
if ($http_user_agent ~* "(bot|crawler|spider)") {
return 403;
}Built-in alternative: Use bot-detect plugin instead of custom WASM. See the built-in plugins catalog (builtin-plugins.md, loaded separately from SKILL.md).WASM plugin (if custom logic needed):
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
ua, _ := proxywasm.GetHttpRequestHeader("user-agent")
ua = strings.ToLower(ua)
blockedPatterns := []string{"bot", "crawler", "spider"}
for _, pattern := range blockedPatterns {
if strings.Contains(ua, pattern) {
proxywasm.SendHttpResponse(403, nil, []byte("Blocked"), -1)
return types.HeaderStopAllIterationAndWatermark
}
}
return types.HeaderContinue
}Request Size Validation
Nginx snippet:
if ($content_length > 10485760) {
return 413;
}WASM plugin:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
clStr, _ := proxywasm.GetHttpRequestHeader("content-length")
if cl, err := strconv.ParseInt(clStr, 10, 64); err == nil {
if cl > 10*1024*1024 { // 10MB
proxywasm.SendHttpResponse(413, nil, []byte("Request too large"), -1)
return types.HeaderStopAllIterationAndWatermark
}
}
return types.HeaderContinue
}Request Modification
URL Rewrite with Logic
Nginx snippet:
set $backend "default";
if ($http_x_version = "v2") {
set $backend "v2";
}
rewrite ^/api/(.*)$ /api/$backend/$1 break;WASM plugin:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
version, _ := proxywasm.GetHttpRequestHeader("x-version")
backend := "default"
if version == "v2" {
backend = "v2"
}
path := ctx.Path()
if strings.HasPrefix(path, "/api/") {
newPath := "/api/" + backend + path[4:]
proxywasm.ReplaceHttpRequestHeader(":path", newPath)
}
return types.HeaderContinue
}Add Query Parameter
Nginx snippet:
if ($args !~ "source=") {
set $args "${args}&source=gateway";
}WASM plugin:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
path := ctx.Path()
if !strings.Contains(path, "source=") {
separator := "?"
if strings.Contains(path, "?") {
separator = "&"
}
newPath := path + separator + "source=gateway"
proxywasm.ReplaceHttpRequestHeader(":path", newPath)
}
return types.HeaderContinue
}Lua Script Conversion
Conversion Completeness Checklist
When converting a Lua access_by_lua_block or content_by_lua_block to a WasmPlugin, follow this process to avoid losing logic:
1. Enumerate every code block in the original Lua script — list each if/else, ngx.exit(), ngx.req.set_header(), more_set_headers, and variable assignment 2. Create a mapping table with columns: Original Lua line/block → WasmPlugin Go code location → Status (done/skipped/simplified) 3. Preserve validation strictness — if the Lua checks a regex pattern (e.g., JWT format ^Bearer [A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+$), the WasmPlugin must validate with equivalent strictness. A simple strings.HasPrefix(auth, "Bearer ") is NOT equivalent to a full JWT 3-part structure check 4. Preserve error responses — if the Lua returns specific JSON error bodies (e.g., {"error":"invalid_token_format"}), the WasmPlugin must return the same or equivalent error bodies, not generic messages 5. Preserve all header injections — if the Lua sets 3 upstream headers, the WasmPlugin must set all 3, not just 2
Common Lua → Go equivalences:
ngx.var.http_xxx→proxywasm.GetHttpRequestHeader("xxx")ngx.req.set_header("X-Foo", val)→proxywasm.AddHttpRequestHeader("X-Foo", val)more_set_headers "X-Foo: bar"→proxywasm.AddHttpResponseHeader("X-Foo", "bar")(in response phase)ngx.exit(401)→proxywasm.SendHttpResponse(401, ...)+return types.HeaderStopAllIterationAndWatermarkngx.say(json)→ include inproxywasm.SendHttpResponsebody parameterstring:match("pattern")→regexp.MustCompile("pattern").MatchString(s)orstrings.Containsfor simple casestoken:gmatch("[^%.]+")(split by dot) →strings.Split(token, ".")
Simple Lua Access Check
Nginx Lua:
access_by_lua_block {
local token = ngx.var.http_authorization
if not token or token == "" then
ngx.exit(401)
end
}WASM plugin:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
token, _ := proxywasm.GetHttpRequestHeader("authorization")
if token == "" {
proxywasm.SendHttpResponse(401, [][2]string{
{"WWW-Authenticate", "Bearer"},
}, []byte("Unauthorized"), -1)
return types.HeaderStopAllIterationAndWatermark
}
return types.HeaderContinue
}Lua with Redis
Nginx Lua:
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
local ip = ngx.var.remote_addr
local count = red:incr("rate:" .. ip)
if count > 100 then
ngx.exit(429)
end
red:expire("rate:" .. ip, 60)
}Built-in alternative: Usekey-rate-limitorcluster-key-rate-limitplugin. See the built-in plugins catalog (builtin-plugins.md, loaded separately from SKILL.md).
WASM plugin (if custom logic needed):
// Redis callback uses resp.Value — import: github.com/higress-group/proxy-wasm-go-sdk/proxywasm/resp
// See references/redis-client.md in higress-wasm-go-plugin skill for full API
func parseConfig(json gjson.Result, config *MyConfig) error {
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: json.Get("redisService").String(),
Port: json.Get("redisPort").Int(),
})
return config.redis.Init("", json.Get("redisPassword").String(), 1000)
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
ip, _ := proxywasm.GetHttpRequestHeader("x-real-ip")
if ip == "" {
ip, _ = proxywasm.GetHttpRequestHeader("x-forwarded-for")
}
key := "rate:" + ip
err := config.redis.Incr(key, func(response resp.Value) {
if response.Error() != nil {
proxywasm.LogErrorf("redis error: %v", response.Error())
proxywasm.ResumeHttpRequest()
return
}
count := response.Integer()
ctx.SetContext("timeStamp", key)
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm - count))
if count == 1 {
// First request in this minute, set expiry
config.redis.Expire(key, 60, func(response resp.Value) {
if response.Error() != nil {
proxywasm.LogErrorf("expire error: %v", response.Error())
}
proxywasm.ResumeHttpRequest()
})
} else if count > config.qpm {
proxywasm.SendHttpResponse(429, [][2]string{
{"Retry-After", "60"},
}, []byte("Rate limited\n"), -1)
} else {
proxywasm.ResumeHttpRequest()
}
})
if err != nil {
return types.HeaderContinue // Fallback on Redis error
}
return types.HeaderStopAllIterationAndWatermark
}Response Modification
HMAC Signature Validation (Request Body)
Nginx Lua:
access_by_lua_block {
ngx.req.read_body()
local body = ngx.req.get_body_data() or ""
local sig = ngx.var.http_x_hub_signature_256 or ""
-- compute HMAC and compare...
if sig ~= expected then
ngx.exit(403)
end
ngx.req.set_header("X-Verified", "true")
}WASM plugin (request headers + body phases):
When a plugin needs to read the request body (e.g., HMAC validation), it must use two phases: 1. ProcessRequestHeaders — check preconditions, call ctx.BufferRequestBody() to buffer the body 2. ProcessRequestBody — receive the buffered body, perform validation
func init() {
wrapper.SetCtx(
"hmac-auth",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
wrapper.ProcessRequestBody(onHttpRequestBody),
)
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config PluginConfig) types.Action {
sig, _ := proxywasm.GetHttpRequestHeader("x-hub-signature-256")
if sig == "" {
proxywasm.SendHttpResponse(401, [][2]string{
{"Content-Type", "application/json"},
}, []byte(`{"error":"missing_signature"}`), -1)
return types.HeaderStopAllIterationAndWatermark
}
// Store signature for body phase, then buffer the body
ctx.SetContext("signature", sig)
ctx.BufferRequestBody()
return types.HeaderContinue
}
func onHttpRequestBody(ctx wrapper.HttpContext, config PluginConfig, body []byte) types.Action {
sig := ctx.GetStringContext("signature", "")
// Compute HMAC over body and compare with sig...
if !valid {
proxywasm.SendHttpResponse(403, [][2]string{
{"Content-Type", "application/json"},
}, []byte(`{"error":"invalid_signature"}`), -1)
// After SendHttpResponse in body phase, return ActionContinue
// (NOT HeaderStopAllIterationAndWatermark — that's only for header phase)
return types.ActionContinue
}
// Inject verification headers
proxywasm.AddHttpRequestHeader("X-Verified", "true")
return types.ActionContinue
}Key rules for body-phase handlers:
- Return
types.ActionContinue(nottypes.HeaderContinueortypes.HeaderStopAllIterationAndWatermark) — body phase usesActionContinueexclusively - After
proxywasm.SendHttpResponse()in body phase, still returntypes.ActionContinue— the response auto-resumes - Call
ctx.BufferRequestBody()in the header phase to ensure the body is available in the body phase
Inject Script/Content
Nginx snippet:
sub_filter '</head>' '<script src="/tracking.js"></script></head>';
sub_filter_once on;WASM plugin:
func init() {
wrapper.SetCtx(
"inject-script",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
wrapper.ProcessResponseBody(onHttpResponseBody),
)
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
contentType, _ := proxywasm.GetHttpResponseHeader("content-type")
if strings.Contains(contentType, "text/html") {
ctx.BufferResponseBody()
proxywasm.RemoveHttpResponseHeader("content-length")
}
return types.HeaderContinue
}
func onHttpResponseBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
bodyStr := string(body)
injection := `<script src="/tracking.js"></script></head>`
newBody := strings.Replace(bodyStr, "</head>", injection, 1)
proxywasm.ReplaceHttpResponseBody([]byte(newBody))
return types.ActionContinue
}Best Practices
1. Error Handling: Always handle external call failures gracefully 2. Performance: Cache regex patterns in config, avoid recompiling 3. Timeout: Set appropriate timeouts for external calls (default 500ms) 4. Logging: Use proxywasm.LogInfo/Warn/Error for debugging 5. Testing: Test locally with Docker Compose before deploying 6. Always check built-in plugins first — avoid custom WASM when a built-in plugin exists 7. Use annotation binding: Embed WasmPlugin via higress.io/wasmplugin annotation, route matching is automatic 8. Validate JSON: Always validate the annotation JSON with jq before applying
Verification Method
Step-by-Step Verification
Step 1: Parse and Archive Verification
# Verify original YAML is saved
ls -la migration-output/original-ingress.yaml
# Verify Ingress count matches expected
python3 -c "
import yaml
with open('migration-output/original-ingress.yaml', 'r') as f:
docs = list(yaml.safe_load_all(f))
ingresses = [d for d in docs if d and d.get('kind') == 'Ingress']
print(f'Total Ingress resources: {len(ingresses)}')
"Step 2: Compatibility Analysis Verification
# Verify analysis files exist
ls -la migration-output/reports/analysis.json
ls -la migration-output/reports/compatibility-analysis.txt
# Verify JSON is valid
jq '.' migration-output/reports/analysis.json
# Count Ingress by category
jq '[.[] | .classification] | group_by(.) | map({category: .[0], count: length})' migration-output/reports/analysis.jsonStep 3: Resolution Verification
For each Ingress with unsupported annotations, verify one of:
- Higress native mapping applied
- Safe-to-drop confirmed
- Built-in plugin selected
- Custom WasmPlugin developed and compiled
# Verify custom WasmPlugins compile
for plugin_dir in migration-output/plugins/*/; do
if [ -d "$plugin_dir" ]; then
echo "Checking plugin: $plugin_dir"
ls -la "${plugin_dir}main.wasm" 2>/dev/null || echo "WARNING: main.wasm not found in $plugin_dir"
fi
doneStep 4: Migrated YAML Verification
# Verify individual Ingress files exist
ls -la migration-output/ingresses/
# Verify combined YAML exists
ls -la migration-output/all-migrated-ingress.yaml
# Verify YAML is valid Kubernetes Ingress
python3 -c "
import yaml
with open('migration-output/all-migrated-ingress.yaml', 'r') as f:
docs = list(yaml.safe_load_all(f))
for doc in docs:
if doc:
assert doc.get('kind') == 'Ingress', f'Invalid kind: {doc.get(\"kind\")}'
assert doc.get('spec', {}).get('ingressClassName') == 'apig', 'ingressClassName must be apig'
print(f'All {len([d for d in docs if d])} Ingress resources are valid')
"
# Verify ingressClassName is set to apig
grep -c "ingressClassName: apig" migration-output/all-migrated-ingress.yaml
# Verify migration label is added
grep -c "migration.higress.io/source: nginx" migration-output/all-migrated-ingress.yamlStep 5: Report Verification
# Verify migration report exists
ls -la migration-output/migration-report.md
# Verify report sections
echo "Checking report sections..."
grep -q "## Overview" migration-output/migration-report.md && echo "✓ Overview"
grep -q "## Compatibility Analysis" migration-output/migration-report.md && echo "✓ Compatibility Analysis"
grep -q "## Deployment Guide" migration-output/migration-report.md && echo "✓ Deployment Guide"Docker Image Verification
# List built images
docker images | grep higress-wasm
# Verify image contents (optional)
docker run --rm higress-wasm-<plugin-name>:v1 ls -la /plugin.wasmComplete Verification Checklist
- [ ]
migration-output/original-ingress.yamlexists and contains all input Ingress - [ ]
migration-output/reports/analysis.jsonis valid JSON with classification for each Ingress - [ ]
migration-output/reports/compatibility-analysis.txtcontains human-readable report - [ ] All custom WasmPlugins have compiled
main.wasm - [ ]
migration-output/ingresses/contains individual migrated YAML files - [ ]
migration-output/all-migrated-ingress.yamlcontains all migrated Ingress - [ ] All migrated Ingress have
ingressClassName: apig - [ ] All migrated Ingress have label
migration.higress.io/source: nginx - [ ]
migration-output/migration-report.mdis complete with all sections - [ ] Docker images are built for custom plugins (if any)
Advanced Patterns
Table of Contents
- Streaming Body Processing
- Buffered Body Processing
- Route Call Pattern
- Tick Functions (Periodic Tasks)
- Leader Election
- Plugin Context Storage
- Rule-Level Config Isolation
- Memory Management
- Custom Logging
- Disable Re-routing
- Buffer Limits
Streaming Body Processing
Process body chunks as they arrive without buffering:
func init() {
wrapper.SetCtx(
"streaming-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessStreamingRequestBody(onStreamingRequestBody),
wrapper.ProcessStreamingResponseBody(onStreamingResponseBody),
)
}
func onStreamingRequestBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
// Modify chunk and return
modified := bytes.ReplaceAll(chunk, []byte("old"), []byte("new"))
return modified
}
func onStreamingResponseBody(ctx wrapper.HttpContext, config MyConfig, chunk []byte, isLastChunk bool) []byte {
// Can call external services with NeedPauseStreamingResponse()
return chunk
}Buffered Body Processing
Buffer entire body before processing:
func init() {
wrapper.SetCtx(
"buffered-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestBody(onRequestBody),
wrapper.ProcessResponseBody(onResponseBody),
)
}
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
// Full request body available
var data map[string]interface{}
json.Unmarshal(body, &data)
// Modify and replace
data["injected"] = "value"
newBody, _ := json.Marshal(data)
proxywasm.ReplaceHttpRequestBody(newBody)
return types.ActionContinue
}Route Call Pattern
Call the current route's upstream with modified request:
func onRequestBody(ctx wrapper.HttpContext, config MyConfig, body []byte) types.Action {
err := ctx.RouteCall("POST", "/modified-path", [][2]string{
{"Content-Type", "application/json"},
{"X-Custom", "header"},
}, body, func(statusCode int, headers [][2]string, body []byte) {
// Handle response from upstream
proxywasm.SendHttpResponse(statusCode, headers, body, -1)
})
if err != nil {
proxywasm.SendHttpResponse(500, nil, []byte("Route call failed"), -1)
}
return types.ActionContinue
}Tick Functions (Periodic Tasks)
Register periodic background tasks:
func parseConfig(json gjson.Result, config *MyConfig) error {
// Register tick functions during config parsing
wrapper.RegisterTickFunc(1000, func() {
// Executes every 1 second
log.Info("1s tick")
})
wrapper.RegisterTickFunc(5000, func() {
// Executes every 5 seconds
log.Info("5s tick")
})
return nil
}Leader Election
For tasks that should run on only one VM instance:
func init() {
wrapper.SetCtx(
"leader-plugin",
wrapper.PrePluginStartOrReload(onPluginStart),
wrapper.ParseConfig(parseConfig),
)
}
func onPluginStart(ctx wrapper.PluginContext) error {
ctx.DoLeaderElection()
return nil
}
func parseConfig(json gjson.Result, config *MyConfig) error {
wrapper.RegisterTickFunc(10000, func() {
if ctx.IsLeader() {
// Only leader executes this
log.Info("Leader task")
}
})
return nil
}Plugin Context Storage
Store data across requests at plugin level:
type MyConfig struct {
// Config fields
}
func init() {
wrapper.SetCtx(
"context-plugin",
wrapper.ParseConfigWithContext(parseConfigWithContext),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
)
}
func parseConfigWithContext(ctx wrapper.PluginContext, json gjson.Result, config *MyConfig) error {
// Store in plugin context (survives across requests)
ctx.SetContext("initTime", time.Now().Unix())
return nil
}Rule-Level Config Isolation
Enable graceful degradation when rule config parsing fails:
func init() {
wrapper.SetCtx(
"isolated-plugin",
wrapper.PrePluginStartOrReload(func(ctx wrapper.PluginContext) error {
ctx.EnableRuleLevelConfigIsolation()
return nil
}),
wrapper.ParseOverrideConfig(parseGlobal, parseRule),
)
}
func parseGlobal(json gjson.Result, config *MyConfig) error {
// Parse global config
return nil
}
func parseRule(json gjson.Result, global MyConfig, config *MyConfig) error {
// Parse per-rule config, inheriting from global
*config = global // Copy global defaults
// Override with rule-specific values
return nil
}Memory Management
Configure automatic VM rebuild to prevent memory leaks:
func init() {
wrapper.SetCtxWithOptions(
"memory-managed-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.WithRebuildAfterRequests(10000), // Rebuild after 10k requests
wrapper.WithRebuildMaxMemBytes(100*1024*1024), // Rebuild at 100MB
wrapper.WithMaxRequestsPerIoCycle(20), // Limit concurrent requests
)
}Custom Logging
Add structured fields to access logs:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Set custom attributes
ctx.SetUserAttribute("user_id", "12345")
ctx.SetUserAttribute("request_type", "api")
return types.HeaderContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Write to access log
ctx.WriteUserAttributeToLog()
// Or write to trace spans
ctx.WriteUserAttributeToTrace()
return types.HeaderContinue
}Disable Re-routing
Prevent Envoy from recalculating routes after header modification:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Call BEFORE modifying headers
ctx.DisableReroute()
// Now safe to modify headers without triggering re-route
proxywasm.ReplaceHttpRequestHeader(":path", "/new-path")
return types.HeaderContinue
}Buffer Limits
Set per-request buffer limits to control memory usage:
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Allow larger request bodies for this request
ctx.SetRequestBodyBufferLimit(10 * 1024 * 1024) // 10MB
return types.HeaderContinue
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
// Allow larger response bodies
ctx.SetResponseBodyBufferLimit(50 * 1024 * 1024) // 50MB
return types.HeaderContinue
}HTTP Client Reference
Cluster Types
FQDNCluster (Most Common)
For services registered in Higress with FQDN:
wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: "my-service.dns", // Service FQDN with suffix
Port: 8080,
Host: "optional-host-header", // Optional
})Common FQDN suffixes:
.dns- DNS service.static- Static IP service (port defaults to 80).nacos- Nacos service
K8sCluster
For Kubernetes services:
wrapper.NewClusterClient(wrapper.K8sCluster{
ServiceName: "my-service",
Namespace: "default",
Port: 8080,
Version: "", // Optional subset version
})
// Generates: outbound|8080||my-service.default.svc.cluster.localNacosCluster
For Nacos registry services:
wrapper.NewClusterClient(wrapper.NacosCluster{
ServiceName: "my-service",
Group: "DEFAULT-GROUP",
NamespaceID: "public",
Port: 8080,
IsExtRegistry: false, // true for EDAS/SAE
})StaticIpCluster
For static IP services:
wrapper.NewClusterClient(wrapper.StaticIpCluster{
ServiceName: "my-service",
Port: 8080,
})
// Generates: outbound|8080||my-service.staticDnsCluster
For DNS-resolved services:
wrapper.NewClusterClient(wrapper.DnsCluster{
ServiceName: "my-service",
Domain: "api.example.com",
Port: 443,
})RouteCluster
Use current route's upstream:
wrapper.NewClusterClient(wrapper.RouteCluster{
Host: "optional-host-override",
})TargetCluster
Direct cluster name specification:
wrapper.NewClusterClient(wrapper.TargetCluster{
Cluster: "outbound|8080||my-service.dns",
Host: "api.example.com",
})HTTP Methods
client.Get(path, headers, callback, timeout...)
client.Post(path, headers, body, callback, timeout...)
client.Put(path, headers, body, callback, timeout...)
client.Patch(path, headers, body, callback, timeout...)
client.Delete(path, headers, body, callback, timeout...)
client.Head(path, headers, callback, timeout...)
client.Options(path, headers, callback, timeout...)
client.Call(method, path, headers, body, callback, timeout...)Callback Signature
func(statusCode int, responseHeaders http.Header, responseBody []byte)Complete Example
type MyConfig struct {
client wrapper.HttpClient
requestPath string
tokenHeader string
}
func parseConfig(json gjson.Result, config *MyConfig) error {
config.tokenHeader = json.Get("tokenHeader").String()
if config.tokenHeader == "" {
return errors.New("missing tokenHeader")
}
config.requestPath = json.Get("requestPath").String()
if config.requestPath == "" {
return errors.New("missing requestPath")
}
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
if strings.HasSuffix(serviceName, ".static") {
servicePort = 80
}
}
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
err := config.client.Get(config.requestPath, nil,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
if statusCode != http.StatusOK {
log.Errorf("http call failed, status: %d", statusCode)
proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
[]byte("http call failed"), -1)
return
}
token := responseHeaders.Get(config.tokenHeader)
if token != "" {
proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
}
proxywasm.ResumeHttpRequest()
})
if err != nil {
log.Errorf("http call dispatch failed: %v", err)
return types.HeaderContinue
}
return types.HeaderStopAllIterationAndWatermark
}Important Notes
1. Cannot use net/http for outbound calls - Must use wrapper's HTTP client. The net/http package is imported only for the http.Header type used in callback signatures — http.Client, http.Get, etc. will not work in the WASM sandbox 2. Default timeout is 500ms - Pass explicit timeout for longer calls (3000-5000ms recommended for auth services) 3. Callback is async - Must return HeaderStopAllIterationAndWatermark and call ResumeHttpRequest() in callback 4. Error handling - If dispatch fails, return HeaderContinue to avoid blocking the request. Log the error with proxywasm.LogWarnf 5. Never call ResumeHttpRequest after SendHttpResponse - SendHttpResponse auto-resumes the filter chain. Calling Resume after it causes undefined behavior 6. Cluster connectivity - For K8s clusters, the service must be reachable from the gateway's network. APIG runs outside ACK, so use FQDN or static IP clusters for services not in the same VPC
Local Testing with Docker Compose
Prerequisites
- Docker installed
- Compiled
main.wasmfile
Setup
Create these files in your plugin directory:
docker-compose.yaml
version: '3.7'
services:
envoy:
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
entrypoint: /usr/local/bin/envoy
command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
depends_on:
- httpbin
networks:
- wasmtest
ports:
- "10000:10000"
volumes:
- ./envoy.yaml:/etc/envoy/envoy.yaml
- ./main.wasm:/etc/envoy/main.wasm
httpbin:
image: kennethreitz/httpbin:latest
networks:
- wasmtest
ports:
- "12345:80"
networks:
wasmtest: {}envoy.yaml
admin:
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 9901
static_resources:
listeners:
- name: listener_0
address:
socket_address:
protocol: TCP
address: 0.0.0.0
port_value: 10000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
scheme_header_transformation:
scheme_to_overwrite: https
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: httpbin
http_filters:
- name: wasmdemo
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
value:
config:
name: wasmdemo
vm_config:
runtime: envoy.wasm.runtime.v8
code:
local:
filename: /etc/envoy/main.wasm
configuration:
"@type": "type.googleapis.com/google.protobuf.StringValue"
value: |
{
"mockEnable": false
}
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: httpbin
connect_timeout: 30s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: httpbin
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: httpbin
port_value: 80Running
# Start
docker compose up
# Test without gateway (baseline)
curl http://127.0.0.1:12345/get
# Test with gateway (plugin applied)
curl http://127.0.0.1:10000/get
# Stop
docker compose downModifying Plugin Config
1. Edit the configuration.value section in envoy.yaml 2. Restart: docker compose restart envoy
Viewing Logs
# Follow Envoy logs
docker compose logs -f envoy
# WASM debug logs (enabled by --component-log-level wasm:debug)Adding External Services
To test external HTTP/Redis calls, add services to docker-compose.yaml:
services:
# ... existing services ...
redis:
image: redis:7-alpine
networks:
- wasmtest
ports:
- "6379:6379"
auth-service:
image: your-auth-service:latest
networks:
- wasmtestThen add clusters to envoy.yaml:
clusters:
# ... existing clusters ...
- name: outbound|6379||redis.static
connect_timeout: 5s
type: LOGICAL_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: redis
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: redis
port_value: 6379Higress WASM Go Plugin SDK Reference
Table of Contents
Consolidated from the higress-wasm-go-plugin skill. Additional reference files (HTTP client, Redis client, advanced patterns, local testing) are linked from SKILL.md Step 3b.⚠️ Safety Notice: Plugin code should be thoroughly validated in a test environment before deploying to production. Plugins run in the gateway data plane — a faulty implementation can affect all traffic passing through the gateway.
Quick Start
Project Setup
mkdir my-plugin && cd my-plugin
go mod init my-plugin
# Set proxy (China mainland — skip if you have direct GitHub access)
go env -w GOPROXY=https://proxy.golang.com.cn,direct
# Download dependencies (use pinned versions for reproducible builds)
go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
go get github.com/higress-group/wasm-go@main
go get github.com/tidwall/gjsonIfgo mod tidyfails with "unknown revision", rungo get github.com/higress-group/proxy-wasm-go-sdk@go-1.24andgo get github.com/higress-group/wasm-go@mainto resolve correct versions.
Minimal Plugin Template
package main
import (
"github.com/higress-group/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
)
func main() {}
func init() {
wrapper.SetCtx(
"my-plugin",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
)
}
type MyConfig struct {
Enabled bool
}
func parseConfig(json gjson.Result, config *MyConfig) error {
config.Enabled = json.Get("enabled").Bool()
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
if config.Enabled {
proxywasm.AddHttpRequestHeader("x-my-header", "hello")
}
return types.HeaderContinue
}Compile
go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./Core Concepts
Plugin Lifecycle
1. init() — Register plugin with wrapper.SetCtx() 2. parseConfig — Parse YAML config (auto-converted to JSON via gjson) 3. HTTP processing phases — Handle requests/responses
HTTP Processing Phases
Register only the phases you need — unused phases add overhead.
| Phase | Trigger | Handler | When to use |
|---|---|---|---|
| Request Headers | Gateway receives client request headers | ProcessRequestHeaders | Auth checks, header manipulation, routing decisions |
| Request Body | Gateway receives client request body | ProcessRequestBody | Body validation, transformation (buffers entire body) |
| Response Headers | Gateway receives backend response headers | ProcessResponseHeaders | Add/modify response headers, set cookies |
| Response Body | Gateway receives backend response body | ProcessResponseBody | Body transformation (buffers entire body) |
| Stream Done | HTTP stream completes | ProcessStreamDone | Cleanup, logging |
Action Return Values
| Action | Behavior | When to use |
|---|---|---|
types.HeaderContinue | Continue to next filter | Default — processing complete, pass through |
types.HeaderStopIteration | Stop header processing, wait for body | When you need the body but don't need async calls |
types.HeaderStopAllIterationAndWatermark | Stop all processing, buffer data | Required for async external calls — call proxywasm.ResumeHttpRequest/Response() in callback to resume |
Body Action Return Values
| Action | Behavior |
|---|---|
types.ActionContinue | Continue processing (used in body phase handlers) |
API Reference
HttpContext Methods
ctx.Scheme() // :scheme
ctx.Host() // :authority
ctx.Path() // :path
ctx.Method() // :method
ctx.HasRequestBody() // Check if request has body
ctx.HasResponseBody() // Check if response has body
ctx.DontReadRequestBody() // Skip reading request body
ctx.DontReadResponseBody() // Skip reading response body
ctx.BufferRequestBody() // Buffer instead of stream
ctx.BufferResponseBody() // Buffer instead of stream
ctx.IsWebsocket() // Check WebSocket upgrade
ctx.IsBinaryRequestBody() // Check binary content
ctx.IsBinaryResponseBody() // Check binary content
ctx.SetContext(key, value)
ctx.GetContext(key)
ctx.GetStringContext(key, defaultValue)
ctx.GetBoolContext(key, defaultValue)
ctx.SetUserAttribute(key, value)
ctx.WriteUserAttributeToLog()Header/Body Operations (proxywasm)
// Request headers
proxywasm.GetHttpRequestHeader(name)
proxywasm.AddHttpRequestHeader(name, value)
proxywasm.ReplaceHttpRequestHeader(name, value)
proxywasm.RemoveHttpRequestHeader(name)
proxywasm.GetHttpRequestHeaders()
proxywasm.ReplaceHttpRequestHeaders(headers)
// Response headers
proxywasm.GetHttpResponseHeader(name)
proxywasm.AddHttpResponseHeader(name, value)
proxywasm.ReplaceHttpResponseHeader(name, value)
proxywasm.RemoveHttpResponseHeader(name)
proxywasm.GetHttpResponseHeaders()
proxywasm.ReplaceHttpResponseHeaders(headers)
// Request body (only in body phase)
proxywasm.GetHttpRequestBody(start, size)
proxywasm.ReplaceHttpRequestBody(body)
proxywasm.AppendHttpRequestBody(data)
proxywasm.PrependHttpRequestBody(data)
// Response body (only in body phase)
proxywasm.GetHttpResponseBody(start, size)
proxywasm.ReplaceHttpResponseBody(body)
proxywasm.AppendHttpResponseBody(data)
proxywasm.PrependHttpResponseBody(data)
// Direct response (blocks request, auto-resumes — do NOT call ResumeHttpRequest after this)
proxywasm.SendHttpResponse(statusCode, headers, body, grpcStatus)
// Flow control
proxywasm.ResumeHttpRequest() // Resume paused request (after async call completes)
proxywasm.ResumeHttpResponse() // Resume paused responseLogging (proxywasm)
proxywasm.LogInfo(msg)
proxywasm.LogInfof(format, args...)
proxywasm.LogWarn(msg)
proxywasm.LogWarnf(format, args...)
proxywasm.LogError(msg)
proxywasm.LogErrorf(format, args...)
proxywasm.LogDebug(msg)
proxywasm.LogDebugf(format, args...)Common Patterns
External HTTP Call (Async)
The async call pattern is the most important pattern in WASM plugin development — pause the request, make an async HTTP call, then resume or reject in the callback.
import "net/http" // Only for http.Header type in callback — do NOT use http.Client
type MyConfig struct {
client wrapper.HttpClient
}
func parseConfig(json gjson.Result, config *MyConfig) error {
config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
FQDN: json.Get("serviceName").String(),
Port: json.Get("servicePort").Int(),
})
return nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
var headers [][2]string
if auth, err := proxywasm.GetHttpRequestHeader("authorization"); err == nil && auth != "" {
headers = append(headers, [2]string{"Authorization", auth})
}
err := config.client.Get("/api/check", headers,
func(statusCode int, responseHeaders http.Header, responseBody []byte) {
if statusCode != 200 {
proxywasm.SendHttpResponse(403, [][2]string{
{"Content-Type", "application/json"},
}, []byte(`{"error":"forbidden"}`), -1)
return
}
proxywasm.ResumeHttpRequest()
}, 3000)
if err != nil {
proxywasm.LogWarnf("http call dispatch failed: %v", err)
return types.HeaderContinue
}
return types.HeaderStopAllIterationAndWatermark
}Redis Integration
func parseConfig(json gjson.Result, config *MyConfig) error {
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: json.Get("redisService").String(),
Port: json.Get("redisPort").Int(),
})
return config.redis.Init(
json.Get("username").String(),
json.Get("password").String(),
json.Get("timeout").Int(),
)
}Phase Registration Patterns
// Auth-only plugin (most common for migration): request headers only
wrapper.SetCtx("my-auth",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
)
// Response header injection: response headers only
wrapper.SetCtx("add-headers",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
)
// Cookie/redirect rewriting: needs both response headers and body
wrapper.SetCtx("rewrite",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessResponseHeaders(onHttpResponseHeaders),
wrapper.ProcessResponseBody(onHttpResponseBody),
)
// Body validation: request headers + body
wrapper.SetCtx("validate",
wrapper.ParseConfig(parseConfig),
wrapper.ProcessRequestHeaders(onHttpRequestHeaders),
wrapper.ProcessRequestBody(onHttpRequestBody),
)Config Parsing with gjson.ForEach
// Array iteration
json.Get("headers").ForEach(func(_, item gjson.Result) bool {
name := item.Get("name").String()
value := item.Get("value").String()
if name != "" {
config.Headers = append(config.Headers, Header{Name: name, Value: value})
}
return true // return true to continue, false to stop
})
// Map/object iteration
config.InjectHeaders = make(map[string]string)
json.Get("inject_headers").ForEach(func(key, value gjson.Result) bool {
config.InjectHeaders[key.String()] = value.String()
return true
})Multi-level Config
Plugin configuration supports multiple levels in the console: global, domain-level, and route-level. The control plane automatically handles config priority and matching logic — the config received by parseConfig is the one that matched the current request.
Best Practices
1. Never call Resume after SendHttpResponse — SendHttpResponse auto-resumes the filter chain 2. Always return `HeaderStopAllIterationAndWatermark` for async calls — Using HeaderStopIteration instead will cause the request to proceed before the callback fires 3. Check HasRequestBody() before returning HeaderStopIteration — If there's no body, the body phase handler will never fire, blocking the request forever 4. Use cached ctx methods — ctx.Path(), ctx.Host(), ctx.Method() work in any phase; GetHttpRequestHeader(":path") only works in the request header phase 5. Handle external call failures gracefully — Return HeaderContinue on dispatch error to avoid blocking the request 6. Set appropriate timeouts — Default HTTP call timeout is 500ms, which is too short for most auth services. Use 3000-5000ms 7. Cannot use `net/http` for outbound calls — Use wrapper.NewClusterClient exclusively. net/http is only imported for the http.Header type in callback signatures 8. Register only needed phases — Each registered phase adds processing overhead 9. Cache regex patterns in config — Compile regexp.Regexp in parseConfig, not in request handlers 10. `GetHttpRequestHeader` returns `(string, error)` — Check both: if auth, err := proxywasm.GetHttpRequestHeader("authorization"); err == nil && auth != ""
Redis Client Reference
Table of Contents
Initialization
type MyConfig struct {
redis wrapper.RedisClient
qpm int
}
func parseConfig(json gjson.Result, config *MyConfig) error {
serviceName := json.Get("serviceName").String()
servicePort := json.Get("servicePort").Int()
if servicePort == 0 {
servicePort = 6379
}
config.redis = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
FQDN: serviceName,
Port: servicePort,
})
return config.redis.Init(
json.Get("username").String(),
json.Get("password").String(),
json.Get("timeout").Int(), // milliseconds
// Optional settings:
// wrapper.WithDataBase(1),
// wrapper.WithBufferFlushTimeout(3*time.Millisecond),
// wrapper.WithMaxBufferSizeBeforeFlush(1024),
// wrapper.WithDisableBuffer(), // For latency-sensitive scenarios
)
}Callback Signature
func(response resp.Value)
// Check for errors
if response.Error() != nil {
// Handle error
}
// Get values
response.Integer() // int
response.String() // string
response.Bool() // bool
response.Array() // []resp.Value
response.Bytes() // []byteAvailable Commands
Key Operations
redis.Del(key, callback)
redis.Exists(key, callback)
redis.Expire(key, ttlSeconds, callback)
redis.Persist(key, callback)String Operations
redis.Get(key, callback)
redis.Set(key, value, callback)
redis.SetEx(key, value, ttlSeconds, callback)
redis.SetNX(key, value, ttlSeconds, callback) // ttl=0 means no expiry
redis.MGet(keys, callback)
redis.MSet(kvMap, callback)
redis.Incr(key, callback)
redis.Decr(key, callback)
redis.IncrBy(key, delta, callback)
redis.DecrBy(key, delta, callback)List Operations
redis.LLen(key, callback)
redis.RPush(key, values, callback)
redis.RPop(key, callback)
redis.LPush(key, values, callback)
redis.LPop(key, callback)
redis.LIndex(key, index, callback)
redis.LRange(key, start, stop, callback)
redis.LRem(key, count, value, callback)
redis.LInsertBefore(key, pivot, value, callback)
redis.LInsertAfter(key, pivot, value, callback)Hash Operations
redis.HExists(key, field, callback)
redis.HDel(key, fields, callback)
redis.HLen(key, callback)
redis.HGet(key, field, callback)
redis.HSet(key, field, value, callback)
redis.HMGet(key, fields, callback)
redis.HMSet(key, kvMap, callback)
redis.HKeys(key, callback)
redis.HVals(key, callback)
redis.HGetAll(key, callback)
redis.HIncrBy(key, field, delta, callback)
redis.HIncrByFloat(key, field, delta, callback)Set Operations
redis.SCard(key, callback)
redis.SAdd(key, values, callback)
redis.SRem(key, values, callback)
redis.SIsMember(key, value, callback)
redis.SMembers(key, callback)
redis.SDiff(key1, key2, callback)
redis.SDiffStore(dest, key1, key2, callback)
redis.SInter(key1, key2, callback)
redis.SInterStore(dest, key1, key2, callback)
redis.SUnion(key1, key2, callback)
redis.SUnionStore(dest, key1, key2, callback)Sorted Set Operations
redis.ZCard(key, callback)
redis.ZAdd(key, memberScoreMap, callback)
redis.ZCount(key, min, max, callback)
redis.ZIncrBy(key, member, delta, callback)
redis.ZScore(key, member, callback)
redis.ZRank(key, member, callback)
redis.ZRevRank(key, member, callback)
redis.ZRem(key, members, callback)
redis.ZRange(key, start, stop, callback)
redis.ZRevRange(key, start, stop, callback)Lua Script
redis.Eval(script, numkeys, keys, args, callback)Raw Command
redis.Command([]interface{}{"SET", "key", "value"}, callback)Rate Limiting Example
func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
now := time.Now()
minuteAligned := now.Truncate(time.Minute)
timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
err := config.redis.Incr(timeStamp, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("redis error: %v", response.Error())
proxywasm.ResumeHttpRequest()
return
}
count := response.Integer()
ctx.SetContext("timeStamp", timeStamp)
ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm - count))
if count == 1 {
// First request in this minute, set expiry
config.redis.Expire(timeStamp, 60, func(response resp.Value) {
if response.Error() != nil {
log.Errorf("expire error: %v", response.Error())
}
proxywasm.ResumeHttpRequest()
})
} else if count > config.qpm {
proxywasm.SendHttpResponse(429, [][2]string{
{"timeStamp", timeStamp},
{"callTimeLeft", "0"},
}, []byte("Too many requests\n"), -1)
} else {
proxywasm.ResumeHttpRequest()
}
})
if err != nil {
log.Errorf("redis call failed: %v", err)
return types.HeaderContinue
}
return types.HeaderStopAllIterationAndWatermark
}
func onHttpResponseHeaders(ctx wrapper.HttpContext, config MyConfig) types.Action {
if ts := ctx.GetContext("timeStamp"); ts != nil {
proxywasm.AddHttpResponseHeader("timeStamp", ts.(string))
}
if left := ctx.GetContext("callTimeLeft"); left != nil {
proxywasm.AddHttpResponseHeader("callTimeLeft", left.(string))
}
return types.HeaderContinue
}Important Notes
1. Check Ready() - redis.Ready() returns false if init failed 2. Auto-reconnect - Client handles NOAUTH errors and re-authenticates automatically 3. Buffering - Default 3ms flush timeout and 1024 byte buffer; use WithDisableBuffer() for latency-sensitive scenarios 4. Error handling - Always check response.Error() in callbacks