Building an EAI Middleware Layer
Stand Up a Gateway with nginx — Keys, Quotas, Versions
Goal
Build the principles of an API gateway with nginx — path proxy, API key authentication (401), per-consumer quota (429), version routing and Sunset, request ID propagation and logging, and the upstream timeout (504).
Why it matters
Authentication, call rate limits and versions are needed identically by every API regardless of the business. If you implement them separately in each API server, they drift apart slightly, one consumer's runaway slows everyone, and nobody knows who is using the old version. The gateway controls these in one place, without business logic.
Steps
- Start two upstreams:
nohup python3 /opt/lab/fixtures/eaimw/gateway/api.py --port 9601 --version v1 > /root/eaimw/gw/v1.out 2>&1 &,… --port 9602 --version v2 …. Call each once withcurland save the two lines of response JSON to/root/eaimw/gw/up.txt. - Write
/root/eaimw/gw/nginx.conf(a complete configuration file:pid /root/eaimw/gw/nginx.pid;, logs in/root/eaimw/gw/logs/,listen 8090;, upstreams127.0.0.1:9601and127.0.0.1:9602). Pass/v1/to v1 and/v2/to v2 with the path as it is. Start it withnginx -p /root/eaimw/gw -c /root/eaimw/gw/nginx.conf. - API key authentication: in
/root/eaimw/gw/keys.map, put two lines,key-channel-7f3a channel;andkey-partner-19c2 partner;, and include them withmap $http_x_api_key $consumer. If there is no consumer (no key, wrong key), return 401 and a JSON body. To the upstream, passX-Consumer: <소비자>(the consumer) and do not passX-API-Key. - Quota:
limit_req_zone $consumer … rate=5r/s,limit_req … burst=5 nodelayandlimit_req_status 429. If one consumer sends in a burst it gets 429, and other consumers must be unaffected. - Version: pass
/api/to v2 ifX-API-Version: 2, and to v1 if absent (map $http_x_api_version). To responses going to v1 (the/v1/and the v1 of/api/), attach the headerSunset: Thu, 31 Dec 2026 23:59:59 GMT, and do not attach it to v2. - Request ID: if an incoming
X-Request-IDexists, pass it as is, and if not, pass$request_idto the upstream asX-Request-ID. Leave the access log in the format'<요청ID> <소비자> <상태> "<요청줄>"'(request ID, consumer, status, request line; for examplelog_format gw '$rid $consumer $status "$request"';) in/root/eaimw/gw/logs/access.log. - With
proxy_read_timeout 2s, if the upstream is slow (/v1/slowtakes 5 seconds), return 504 in about 2 seconds.
Notes
- After changing the configuration:
nginx -t -p /root/eaimw/gw -c /root/eaimw/gw/nginx.conf && nginx -s reload -p /root/eaimw/gw -c /root/eaimw/gw/nginx.conf - Checking:
curl -s -H 'X-API-Key: key-channel-7f3a' localhost:8090/v1/hello | jq .headers - The grader copies the configuration file to a temporary directory, replaces
8090,9601,9602and the/root/eaimw/gw/paths with its own values, and starts it afresh. Write these numbers and paths literally in the configuration. - A
mapvalue can use variables ("" $request_id;).add_headeris by default attached only to 2xx and 3xx responses. - Common mistakes: keying the quota by
$binary_remote_addr(consumers cannot be told apart), leaving outlimit_req_statusso that 503 goes out, and forgettingproxy_set_header X-API-Key ""so that the key leaks to the upstream.
Start the two upstream versions
Start v1 (9601) and v2 (9602) with api.py and save the two lines of response JSON to /root/eaimw/gw/up.txt.
api.py mirrors the request it received as JSON. Call the two ports with curl -s and collect them into one file with >>.
Split versions by path and pass through
With /root/eaimw/gw/nginx.conf, on 8090 pass /v1/→9601 and /v2/→9602 with the path as it is.
Two upstream blocks and two locations. If you do not attach a URI part to proxy_pass, the request path is passed through as it is.
Identify the consumer by API key
Decide the consumer with a map that includes /root/eaimw/gw/keys.map, and if there is none, 401 JSON; if there is, pass only X-Consumer and strip the key.
After map $http_x_api_key $consumer { default ""; include …; }, use if ($consumer = "") { return 401 '…'; } in the server. To strip a header, give proxy_set_header an empty value.
Apply a quota per consumer
Apply a per-consumer quota with limit_req_zone $consumer rate=5r/s, burst=5 nodelay and limit_req_status 429.
What the zone key is is everything. The default status code for exceeding the limit is 503, so you must change it to 429 for the caller to know "I sent too much."
Choose the version by header and announce the version to be retired
/api/ goes to v2 if X-API-Version: 2, and to v1 if absent. Attach a Sunset header to v1 responses.
Choose the upstream name with map $http_x_api_version $api_ver and pass it on with proxy_pass http://$api_ver; . If you also use a map for Sunset to give a value only for v1, an add_header with an empty value is not attached.
Connect the request ID and leave it in the log
Pass X-Request-ID as is if present and $request_id if not, and leave an access log in the format ' ""'.
You can use a variable in a map value, like map $http_x_request_id $rid { "" $request_id; default $http_x_request_id; }. Define log_format and attach that name to access_log.
Cut off a slow upstream quickly
With proxy_read_timeout 2s, cut /v1/slow (5 seconds) off with a 504 in about 2 seconds.
The default is 60 seconds. For an API where the caller gives up first, there is no reason for the gateway to hold on longer. Put it in every location in common.