ЁЯПл The SchoolтА║ЁЯЫОя╕П API GatewayтА║ЁЯОн рдзрдбрд╛ 05 тАФ Stages рдЖрдгрд┐ deployments: рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо рдЖрдгрд┐ рдЦрд░рд╛ рдкреНрд░рдпреЛрдЧ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯОн рдзрдбрд╛ 05 тАФ Stages рдЖрдгрд┐ deployments: рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо рдЖрдгрд┐ рдЦрд░рд╛ рдкреНрд░рдпреЛрдЧ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 05 ┬╖ рдорд╛рдЧреЗ: lesson-04-mapping-validation ┬╖ рдкреБрдвреЗ: lesson-06-auth


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ04, рдЖрдгрд┐ stages (dev, prod), deployments (API рдЪреЗ snapshots), stage variables (рддреЛрдЪ route marks-dev рдХрд┐рдВрд╡рд╛ marks-prod рдордзреНрдпреЗ рд▓рд┐рд╣рд┐рддреЛ), рдЖрдгрд┐ canary releases. apigw/demo.py рдордзрд▓реЗ stages() рддреАрдЪ request рддреАрди stages рдирд╛ рдкрд╛рдард╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдЪреНрдпрд╛ рдирд╛рдЯрдХрд╛рдЪреА рдПрдХ рд░рдВрдЧреАрдд рддрд╛рд▓реАрдо ЁЯОн рдЕрд╕рддреЗ рдЖрдгрд┐ рдПрдХ рдЦрд░рд╛ рдкреНрд░рдпреЛрдЧ ЁЯОЯя╕П. рддреАрдЪ рд╕рдВрд╣рд┐рддрд╛, рддреЗрдЪ рдХрд▓рд╛рдХрд╛рд░ тАФ рдкрдг рддрд╛рд▓рдореАрдд рдкреБрдареНрдареНрдпрд╛рдЪреЗ рд╕рд╛рд╣рд┐рддреНрдп рдЖрдгрд┐ рд▓рд╣рд╛рди рд╕рднрд╛рдЧреГрд╣ рд╡рд╛рдкрд░рддрд╛рдд, рдЖрдгрд┐ рдкреНрд░рдпреЛрдЧрд╛рдд рдЦрд░рд╛ рд░рдВрдЧрдордВрдЪ рдЖрдгрд┐ рдЦрд░реЗ рдкреНрд░реЗрдХреНрд╖рдХ рдЕрд╕рддрд╛рдд.

рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рд╕рдВрд╣рд┐рддрд╛ рдмрджрд▓рддреЗ рддреЗрд╡реНрд╣рд╛ рддреЛ рдмрджрд▓ рдЬрд╛рджреВрдиреЗ рдЖрдЬрдЪреНрдпрд╛ рд░рд╛рддреНрд░реАрдЪреНрдпрд╛ рдкреНрд░рдпреЛрдЧрд╛рдд рдпреЗрдд рдирд╛рд╣реА. рдХреЛрдгреАрддрд░реА рдкреНрд░рдпреЛрдЧрд╛рд▓рд╛ рдирд╡реА рд╕рдВрд╣рд┐рддрд╛ рджреНрдпрд╛рд╡реА рд▓рд╛рдЧрддреЗ тАФ рддреЗрдЪ рдореНрд╣рдгрдЬреЗ deployment. рддреЛрдкрд░реНрдпрдВрдд рдкреНрд░рдпреЛрдЧ рдЬреБрдиреАрдЪ рд╕рдВрд╣рд┐рддрд╛ рдЪрд╛рд▓рд╡рдд рд░рд╛рд╣рддреЛ.

рдкреНрд░рддреНрдпреЗрдХ stage рдЪреНрдпрд╛ рднрд┐рдВрддреАрд╡рд░ рд╕реНрд╡рддрдГрдЪреНрдпрд╛ рдЪрд┐рдареНрдареНрдпрд╛ рдЕрд╕рддрд╛рдд: рддрд╛рд▓рдореАрд╕рд╛рдареА "рд╕рд╛рд╣рд┐рддреНрдп рдЦреЛрд▓реА 12 рдордзреНрдпреЗ рдЖрд╣реЗ", рдкреНрд░рдпреЛрдЧрд╛рд╕рд╛рдареА "рд╕рд╛рд╣рд┐рддреНрдп рдореБрдЦреНрдп рднрд╛рдВрдбрд╛рд░рд╛рдд рдЖрд╣реЗ". рддреНрдпрд╛ рдЪрд┐рдареНрдареНрдпрд╛ рдореНрд╣рдгрдЬреЗ stage variables.

рдЖрдгрд┐ рдХрд╛рд│рдЬреА рдШреЗрдгрд╛рд░реА рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рд╕рдЧрд│реНрдпрд╛рдВрдЪреНрдпрд╛ рдЖрдзреА рдкреНрд░реЗрдХреНрд╖рдХрд╛рдВрдЪреНрдпрд╛ рдПрдХрд╛ рд░рд╛рдВрдЧреЗрд▓рд╛ рдирд╡рд╛ рдкреНрд░рд╡реЗрд╢ рджрд╛рдЦрд╡реВ рд╢рдХрддреЗ тАФ рддреЛ рдореНрд╣рдгрдЬреЗ canary.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    edit["тЬПя╕П you change routes"] --> dep["ЁЯУ╕ deployment<br/>a snapshot of the API"]
    dep --> dev["ЁЯОн stage dev<br/>table = marks-dev ┬╖ rate 5 ┬╖ no cache"]
    dep --> prod["ЁЯОЯя╕П stage prod<br/>table = marks-prod ┬╖ rate 10,000 ┬╖ cache 300 s"]
    prod -.->|"canary 10%"| new["ЁЯРд new deployment"]
    x["stage test"] -->|"no such stage"| no["ЁЯЪл 403 Forbidden"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрд╡реГрддреНрддреА + рдПрдХ lab: https://school-edh.pages.dev/apigateway/lesson-diagrams.html#l05

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг "рдореА console рдордзреНрдпреЗ рдмрджрд▓рд▓реЗ" рдореНрд╣рдгрдЬреЗ release рдирд╡реНрд╣реЗ. Snapshots рдореБрд│реЗ рдХрд╛рдп live рдЖрд╣реЗ рдпрд╛рдЪреА рдиреЛрдВрдж рдорд┐рд│рддреЗ, рдкрд░рдд рдЬрд╛рдгреНрдпрд╛рдЪрд╛ рдорд╛рд░реНрдЧ рдорд┐рд│рддреЛ (stage рдЖрдзреАрдЪреНрдпрд╛ deployment рдХрдбреЗ рд╡рд│рд╡рд╛), рдЖрдгрд┐ рдЦрд░реЗ users (prod) рдкрд╛рд╣рдгреНрдпрд╛рдЖрдзреА рдмрджрд▓ рдХрд░реВрди рдкрд╛рд╣рдгреНрдпрд╛рд╕рд╛рдареА рд╕реБрд░рдХреНрд╖рд┐рдд рдЬрд╛рдЧрд╛ (dev) рдорд┐рд│рддреЗ. Stage variables рдореБрд│реЗ рдкреНрд░рддреНрдпреЗрдХ environment рд╕рд╛рдареА рдПрдХрдЪ API definition рд░рд╛рд╣рддреЗ, рд╣рд│реВрд╣рд│реВ рд╡реЗрдЧрд│реНрдпрд╛ рд╣реЛрдд рдЬрд╛рдгрд╛рд▒реНрдпрд╛ рдкреНрд░рддреА рдирд╛рд╣реАрдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

apigw/demo.py рдордзрд▓реЗ office() рд╣реЗ Gateway рд▓рд╛ рджреЛрди stages рджреЗрддреЗ: dev (vars={"table": "marks-dev"}, rate 5, burst 5, cache_ttl=0) рдЖрдгрд┐ prod (marks-prod, 10,000 / 5,000, cache_ttl=300). Gateway._handle() рдЕрд╕реНрддрд┐рддреНрд╡рд╛рдд рдирд╕рд▓реЗрд▓реНрдпрд╛ stage рд▓рд╛ 403 рдЙрддреНрддрд░ рджреЗрддреЗ, рдкреНрд░рддреНрдпреЗрдХ stage рд▓рд╛ рд╕реНрд╡рддрдГрдЪрд╛ token bucket рджреЗрддреЗ, рдЖрдгрд┐ stage рдЪреЗ vars integration рдЪреНрдпрд╛ event["stageVariables"] рдордзреНрдпреЗ рдареЗрд╡рддреЗ; add_mark() table рдЪреЗ рдирд╛рд╡ рддрд┐рдереВрдирдЪ рд╡рд╛рдЪрддреЗ. Lab рдордзреНрдпреЗ рд╡реЗрдЧрд│рд╛ deployment object рдирд╛рд╣реА: рдмрджрд▓рд▓реЗрд▓реНрдпрд╛ settings рд╕рд╣ рдирд╡реЗ office(...) рдмрдирд╡рдгреЗ рд╣реЗрдЪ "рдирд╡реЗ deployment" рдЪреА рднреВрдорд┐рдХрд╛ рдмрдЬрд╛рд╡рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 apigw/demo.py stages
python3 - <<'EOF'
import sys; sys.path.insert(0, "apigw"); from demo import office
gw, c = office()
for stage in ("dev", "prod"):
    print(stage, gw.stages[stage]["vars"], "rate", gw.stages[stage]["rate"], "burst", gw.stages[stage]["burst"], "cache_ttl", gw.stages[stage]["cache_ttl"])
    print("   ", [gw.handle("GET", "/students/7", stage=stage)[1].get("X-Cache") for _ in range(2)])
print("beta:", gw.handle("GET", "/students/7", stage="beta")[0::2])
gw2, _ = office(cache_ttl=0, vars={"table": "marks-prod-v2"})
print("prod after a new 'deployment':", gw2.stages["prod"]["vars"], [gw2.handle("GET", "/students/7")[1].get("X-Cache") for _ in range(2)])
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

stages рд╣реЗ stage dev тЖТ 201 {'saved': {'student': '7', 'subject': 'maths', 'mark': 91}, 'table': 'marks-dev'}, 'table': 'marks-prod' рд╕рд╣ prod рд╕рд╛рдареА рддрд╕реЗрдЪ, рдЖрдгрд┐ stage test тЖТ 403 {'message': 'Forbidden'} print рдХрд░рддреЗ.

рддреБрдордЪрд╛ snippet [None, None] рд╕рд╣ dev {'table': 'marks-dev'} rate 5 burst 5 cache_ttl 0 (dev рд╡рд░ cache рдирд╛рд╣реА тАФ X-Cache header рдЕрдЬрд┐рдмрд╛рдд рдирд╛рд╣реА), ['Miss', 'Hit'] рд╕рд╣ prod {'table': 'marks-prod'} rate 10000 burst 5000 cache_ttl 300, рдордЧ beta: (403, {'message': 'Forbidden'}), рдЖрдгрд┐ prod after a new 'deployment': {'table': 'marks-prod-v2'} [None, None] print рдХрд░рддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдПрдХ API, рдЕрдиреЗрдХ stages: рдкреНрд░рддреНрдпреЗрдХ stage рд╡рд░ рддреНрдпрд╛рдЪ route рдиреЗ рд╡реЗрдЧрд│реНрдпрд╛ table рдордзреНрдпреЗ рд▓рд┐рд╣рд┐рд▓реЗ, рддреНрдпрд╛рд▓рд╛ рд╡реЗрдЧрд│реНрдпрд╛ limits рдЖрдгрд┐ рд╡реЗрдЧрд│рд╛ cache рд╣реЛрддрд╛, рдЖрдгрд┐ рдХрдзреАрдЪ deploy рди рдЭрд╛рд▓реЗрд▓реА stage рдЕрд╕реНрддрд┐рддреНрд╡рд╛рддрдЪ рдирд╕рддреЗ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

On a real account тАФ REST API prod рд╡рд░ deploy рдХрд░рд╛, рдордЧ рдкреБрдврдЪреЗ deployment 10% traffic рд╡рд░ рдХрд░реВрди рдкрд╛рд╣рд╛, рдордЧ рддреЗ promote рдХрд░рд╛:

aws apigateway create-deployment --rest-api-id abc123 --stage-name prod \
    --description "marks v1"
aws apigateway update-stage --rest-api-id abc123 --stage-name prod \
    --patch-operations op=replace,path=/variables/table,value=marks-prod
aws apigateway create-deployment --rest-api-id abc123 --stage-name prod \
    --canary-settings percentTraffic=10.0 --description "marks v2 canary"
# promote: the canary's deployment becomes the stage's deployment, canary back to 0%
aws apigateway update-stage --rest-api-id abc123 --stage-name prod --patch-operations \
    '[{"op":"replace","path":"/canarySettings/percentTraffic","value":"0.0"},
      {"op":"copy","from":"/canarySettings/deploymentId","path":"/deploymentId"}]'

Terraform тАФ API definition рдмрджрд▓рд▓реА рдХреА рдкреНрд░рддреНрдпреЗрдХ рд╡реЗрд│реА рдирд╡реЗ deployment:

resource "aws_api_gateway_deployment" "school" {
  rest_api_id = aws_api_gateway_rest_api.school.id
  triggers    = { redeploy = sha1(jsonencode(aws_api_gateway_rest_api.school.body)) }
  lifecycle   { create_before_destroy = true }
}
resource "aws_api_gateway_stage" "prod" {
  rest_api_id   = aws_api_gateway_rest_api.school.id
  deployment_id = aws_api_gateway_deployment.school.id
  stage_name    = "prod"
  variables     = { table = "marks-prod" }
}

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: releases pipeline рдордзреВрди рдЬрд╛рддрд╛рдд тАФ dev рд╡рд░ deploy, test, prod рд╡рд░ deploy (рдмрджрд▓ рдзреЛрдХрд╛рджрд╛рдпрдХ рдЕрд╕реЗрд▓ рддрд░ canary рд╕рд╣) тАФ рдЖрдгрд┐ рдЖрдзреАрдЪрд╛ deployment id рд▓рд┐рд╣реВрди рдареЗрд╡рд▓реЗрд▓рд╛ рдЕрд╕рддреЛ, рдореНрд╣рдгрдЬреЗ rollback рдлрдХреНрдд рдПрдХрд╛ command рдЪреЗ рдХрд╛рдо рдард░рддреЗ.

тПня╕П рдкреБрдвреЗ

рдХреЛрдгреАрд╣реА рджрд╛рд░ рдареЛрдард╛рд╡реВ рд╢рдХрддреЛ. рдЖрддрд╛ badge рдЪреА рддрдкрд╛рд╕рдгреА: рдЖрдд рдХреЛрдг рдпреЗрдК рд╢рдХрддреЛ тАФ IAM, JWT рдЖрдгрд┐ Lambda authorizers.

git checkout lesson-06-auth

ЁЯОн Lesson 05 тАФ Stages & deployments: the rehearsal and the real show

ЁЯУН You are here: Lesson 05 of 12 ┬╖ Previous: lesson-04-mapping-validation ┬╖ Next: lesson-06-auth


ЁЯУж What's in this branch

Lessons 01тАУ04, plus stages (dev, prod), deployments (snapshots of the API), stage variables (the same route writes to marks-dev or marks-prod), and canary releases. stages() in apigw/demo.py sends the same request to three stages.

ЁЯзТ Explain like I'm 5

The school play has a rehearsal ЁЯОн and a real show ЁЯОЯя╕П. Same script, same actors тАФ but the rehearsal uses cardboard props and a small hall, and the show uses the real stage and the real audience.

When the teacher changes the script, the change is not in tonight's show by magic. Someone must hand the new script to the show тАФ that is a deployment. Until then, the show keeps playing the old script.

Each stage also has its own notes on the wall: "props are in room 12" for the rehearsal, "props are in the main store" for the show. Those notes are stage variables.

And a careful teacher can let one row of the audience see the new scene first тАФ a canary тАФ before everyone does.

ЁЯЧ║я╕П Diagram

flowchart LR
    edit["тЬПя╕П you change routes"] --> dep["ЁЯУ╕ deployment<br/>a snapshot of the API"]
    dep --> dev["ЁЯОн stage dev<br/>table = marks-dev ┬╖ rate 5 ┬╖ no cache"]
    dep --> prod["ЁЯОЯя╕П stage prod<br/>table = marks-prod ┬╖ rate 10,000 ┬╖ cache 300 s"]
    prod -.->|"canary 10%"| new["ЁЯРд new deployment"]
    x["stage test"] -->|"no such stage"| no["ЁЯЪл 403 Forbidden"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/apigateway/lesson-diagrams.html#l05

тЭУ What

ЁЯдФ Why

Because "I changed it in the console" is not a release. Snapshots give you a record of what is live, a way back (point the stage at the previous deployment), and a safe place to try changes (dev) before real users see them (prod). Stage variables keep one API definition for every environment instead of copies that drift.

ЁЯФз How (in this repo)

office() in apigw/demo.py passes two stages to Gateway: dev (vars={"table": "marks-dev"}, rate 5, burst 5, cache_ttl=0) and prod (marks-prod, 10,000 / 5,000, cache_ttl=300). Gateway._handle() answers 403 for a stage that does not exist, gives each stage its own token bucket, and puts the stage's vars into the integration's event["stageVariables"]; add_mark() reads the table name from there. The lab has no separate deployment object: building a new office(...) with changed settings plays the part of "a new deployment".

ЁЯзк Try it

python3 apigw/demo.py stages
python3 - <<'EOF'
import sys; sys.path.insert(0, "apigw"); from demo import office
gw, c = office()
for stage in ("dev", "prod"):
    print(stage, gw.stages[stage]["vars"], "rate", gw.stages[stage]["rate"], "burst", gw.stages[stage]["burst"], "cache_ttl", gw.stages[stage]["cache_ttl"])
    print("   ", [gw.handle("GET", "/students/7", stage=stage)[1].get("X-Cache") for _ in range(2)])
print("beta:", gw.handle("GET", "/students/7", stage="beta")[0::2])
gw2, _ = office(cache_ttl=0, vars={"table": "marks-prod-v2"})
print("prod after a new 'deployment':", gw2.stages["prod"]["vars"], [gw2.handle("GET", "/students/7")[1].get("X-Cache") for _ in range(2)])
EOF

тЬЕ Verify тАФ what you should see

stages prints stage dev тЖТ 201 {'saved': {'student': '7', 'subject': 'maths', 'mark': 91}, 'table': 'marks-dev'}, the same for prod with 'table': 'marks-prod', and stage test тЖТ 403 {'message': 'Forbidden'}.

Your snippet prints dev {'table': 'marks-dev'} rate 5 burst 5 cache_ttl 0 with [None, None] (no cache on dev тАФ no X-Cache header at all), prod {'table': 'marks-prod'} rate 10000 burst 5000 cache_ttl 300 with ['Miss', 'Hit'], then beta: (403, {'message': 'Forbidden'}), and prod after a new 'deployment': {'table': 'marks-prod-v2'} [None, None].

ЁЯПБ What you just proved

One API, many stages: the same route wrote to a different table, had different limits and a different cache per stage, and a stage that was never deployed does not exist.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ deploy a REST API to prod, then try the next deployment on 10% of traffic, then promote it:

aws apigateway create-deployment --rest-api-id abc123 --stage-name prod \
    --description "marks v1"
aws apigateway update-stage --rest-api-id abc123 --stage-name prod \
    --patch-operations op=replace,path=/variables/table,value=marks-prod
aws apigateway create-deployment --rest-api-id abc123 --stage-name prod \
    --canary-settings percentTraffic=10.0 --description "marks v2 canary"
# promote: the canary's deployment becomes the stage's deployment, canary back to 0%
aws apigateway update-stage --rest-api-id abc123 --stage-name prod --patch-operations \
    '[{"op":"replace","path":"/canarySettings/percentTraffic","value":"0.0"},
      {"op":"copy","from":"/canarySettings/deploymentId","path":"/deploymentId"}]'

Terraform тАФ a new deployment whenever the API definition changes:

resource "aws_api_gateway_deployment" "school" {
  rest_api_id = aws_api_gateway_rest_api.school.id
  triggers    = { redeploy = sha1(jsonencode(aws_api_gateway_rest_api.school.body)) }
  lifecycle   { create_before_destroy = true }
}
resource "aws_api_gateway_stage" "prod" {
  rest_api_id   = aws_api_gateway_rest_api.school.id
  deployment_id = aws_api_gateway_deployment.school.id
  stage_name    = "prod"
  variables     = { table = "marks-prod" }
}

ЁЯПн Why this matters in production: releases through a pipeline тАФ deploy to dev, test, deploy to prod (with a canary when the change is risky) тАФ and the previous deployment id written down, so rolling back is one command.

тПня╕П Next

Anyone can knock. Now the badge check: who may come in тАФ IAM, JWT and Lambda authorizers.

git checkout lesson-06-auth
тЖР Previousmapping validationNext тЖТauth

This page is the lesson's README from the lesson-05-stages-deployments branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.