cdk deploy a picat cu un mesaj pe care nu-l mai văzuserăm: Template format error: Number of resources, 503, is greater than maximum allowed, 500. Nimic din diff nu era mare. Adăugaserăm o funcție Lambda cu un log group și un rol, și atât a fost suficient ca să împingem un stack pe care îl construiam de opt luni peste un plafon de care nu știam.
Limita e reală, e per stack și nu e negociabilă printr-o cerere de mărire a cotei. Articolul ăsta e despre cum am aflat ce era în stack, unde am tăiat și cum am mutat o bază de date, trei tabele DynamoDB și o cheie KMS într-un stack nou fără să ștergem niciuna.
Cum ajungi la 500 fără să observi
Codul nostru CDK declara poate 60 de lucruri: un VPC, un cluster Aurora, trei servicii App Runner, o duzină de Lambda-uri, câteva tabele DynamoDB, o distribuție CloudFront, un WAF, niște secrete. CloudFormation vedea 503. Diferența e tot ce generează CDK pentru tine:
npx cdk synth platform-prod --quiet
grep -h '"Type": "AWS::' cdk.out/platform-prod.template.json | sort | uniq -c | sort -rn | head
| Tip de resursă | Număr | De unde vine |
|---|---|---|
AWS::IAM::Policy |
71 | fiecare apel grant*(), o politică per rol per grup de permisiuni |
AWS::IAM::Role |
48 | unul per Lambda, per serviciu App Runner, per custom resource |
AWS::Lambda::Function |
31 | cele 12 ale noastre, plus provider-ii de custom resource ai CDK pentru retenția logurilor, deployment în bucket, rotația secretelor clusterului |
AWS::Logs::LogGroup |
29 | unul per funcție, explicit, pentru că setăm retenția |
AWS::EC2::* |
58 | VPC-ul: subneturi, tabele de rutare, rute, asocieri, NAT, endpoint-uri, security group-uri, reguli de ingress |
AWS::Lambda::Permission |
24 | fiecare sursă de evenimente și fiecare rută de API Gateway |
Custom::* |
19 | retenția logurilor, deployment-uri S3, rotația parolei clusterului |
| Restul | 223 |
Un VPC cu trei availability zones e singur peste cincizeci de resurse. Fiecare Lambda e o funcție, un rol, una până la trei politici, un log group, un custom resource pentru retenție și o permisiune: șapte resurse per funcție pe care o credeai una. IAM singur era un sfert din stack. Nimic din asta nu e risipă; e cantitatea corectă de infrastructură. Doar că e mult peste numărătoarea din cap.
Unde tai
Constrângerea care decide împărțirea nu e numărul, ci raza exploziei și frecvența schimbării. Lucrurile care se schimbă la fiecare deploy (Lambda-uri, tag-uri de imagine App Runner) n-ar trebui să împartă un stack cu lucrurile care nu trebuie atinse niciodată din greșeală (baza de date). Scriseserăm asta ca principiu când am configurat cele trei conturi și apoi am pus oricum totul într-un singur stack, pentru că un singur stack e mai ușor până când nu mai e.
Granița pe care am tras-o, în ordinea de deploy:
| Stack | Conținut | Resurse | Se schimbă |
|---|---|---|---|
network |
VPC, subneturi, NAT, endpoint-uri, security group-uri de bază | ~70 | aproape niciodată |
data |
cluster Aurora, tabele DynamoDB, chei KMS, secrete, backup vault | ~60 | rar, și cu grijă |
app |
Lambda-uri, servicii App Runner, cozi, reguli de evenimente, roluri | ~300 | la fiecare deploy |
edge |
CloudFront, WAF, certificate, înregistrări Route 53 | ~50 | lunar |
Patru stack-uri în loc de unul, cel mai mare la 300, cu loc de creștere. Stack-ul data e cel care primește termination protection și rolul de deploy mai strict; stack-ul app e cel pe care CI îl atinge zilnic.
Am luat în calcul nested stacks și le-am respins. Un NestedStack din CDK ridică limita de 500 (fiecare nested stack o are pe a lui), dar un nested stack e desfășurat ca parte din părinte, deci raza exploziei nu se micșorează deloc: o schimbare proastă la o Lambda tot rulează un change set care include baza de date. Stack-urile separate erau tocmai ideea.
Mutarea resurselor fără stare: pur și simplu le muți
Lambda-uri, roluri, reguli de evenimente, servicii App Runner fără stare: tai construct-ul dintr-un fișier, îl lipești în celălalt, faci deploy la ambele. CloudFormation șterge resursa din stack-ul vechi și o creează în cel nou. Două lucruri de verificat înainte:
- Numele fizice. O Lambda cu
functionNameexplicit nu poate exista de două ori, deci ștergerea trebuie să se întâmple înaintea creării, adică faci deploy întâi la stack-ul vechi. Resursele fără nume explicit primesc un nume generat nou și pot coexista pe scurt. Am renunțat la numele explicite pe tot ce nu avea nevoie de ele, adică pe mai tot. - Lucrurile care arată spre resursă prin ARN din exterior. Serviciul App Runner avea un domeniu custom, deci recrearea lui ar fi însemnat un hostname
*.awsapprunner.comnou, o schimbare de DNS și o validare de certificat. Am lăsat toate cele trei servicii App Runner în stack-ulapp, pe loc, unde oricum le era locul.
Lambda-urile au primit ARN-uri noi. Nimic din afara stack-ului nu se referea la ele prin ARN, în afară de o regulă EventBridge din același stack, deci nimic n-a observat.
Mutarea resurselor cu stare: importul în patru pași
Baza de date, tabelele și cheia KMS nu pot fi recreate. Trebuiau să schimbe stack-ul rămânând exact unde erau. CloudFormation suportă asta prin resource import, iar CDK îl împachetează ca cdk import. Secvența, per resursă:
Pasul unu e cel pe care îl sare lumea. Fără RemovalPolicy.RETAIN desfășurat întâi, ștergerea construct-ului la pasul doi șterge resursa, iar pentru un cluster Aurora asta înseamnă, în cel mai bun caz, un snapshot final. Trecuserăm deja prin un incident de ștergere în anul ăla și nu ne interesa un al doilea, așa că am făcut pasul unu, deploy, și apoi am verificat în consolă că DeletionPolicy: Retain era pe resursă în template înainte de orice altceva.
Pasul trei cere ca construct-ul din stack-ul nou să producă exact proprietățile resursei existente. Pentru DynamoDB, asta înseamnă numele tabelei, schema de chei și modul de facturare; pentru Aurora, identificatorul clusterului, engine-ul și încă vreo câteva; pentru KMS, ID-ul cheii. cdk import cere identificatorii pe care nu-i poate deduce, apoi rulează un change set de import. Dacă o proprietate nu se potrivește, importul pică curat și nimic nu e schimbat, adică felul bun de eșec.
Pasul patru e dovada. cdk diff pe stack-ul nou după import ar trebui să fie gol. Al nostru n-a fost, prima dată, pentru clusterul Aurora: declaraserăm deletionProtection: true în stack-ul nou, iar clusterul real o avea oprită, pentru că stack-ul vechi n-o setase niciodată. Diff-ul a arătat-o, am desfășurat-o, și clusterul a ajuns mai bine protejat decât înainte.
Referințele între stack-uri și capcana din ele
Odată ce resursele trăiesc în stack-uri diferite, stack-ul app are nevoie de VPC-ul din network și de numele tabelelor din data. Implicit, CDK pasează obiectul și generează o pereche export/import CloudFormation. Merge, și apoi te blochează: o valoare exportată nu se poate schimba cât timp alt stack o importă, deci o schimbare la VPC care modifică un ID de subnet exportat pică până scoți fiecare consumator. Am dat de asta în ziua a doua.
Am trecut pe parametri SSM pentru tot ce traversează o graniță de stack:
// stack-ul data
new ssm.StringParameter(this, 'OrdersTableName', {
parameterName: `/platform/${env}/orders-table-name`,
stringValue: ordersTable.tableName,
});
// stack-ul app
const ordersTableName = ssm.StringParameter.valueForStringParameter(this, `/platform/${env}/orders-table-name`);
const ordersTable = dynamodb.Table.fromTableName(this, 'OrdersTable', ordersTableName);
Consumatorul rezolvă parametrul la deploy; nu există export, deci nimic nu e blocat. Costul e că CDK nu mai știe dependența, deci faci deploy la stack-uri în ordine singur. Scriptul nostru de deploy le listează: network data app edge. Pentru VPC, Vpc.fromLookup după tag face aceeași treabă, cu lookup-ul cache-uit în cdk.context.json.
Toată mutarea, cronometrată
| Pas | Timp | Downtime |
|---|---|---|
| Inventar și trasarea graniței | 2 ore | niciunul |
| Codul împărțit în patru stack-uri | 3 ore | niciunul |
| Resursele fără stare mutate (Lambda-uri, reguli, roluri) | 20 de minute de deploy-uri | ~1 minut pentru funcțiile pe evenimente |
| RETAIN desfășurat pe 5 resurse cu stare | 5 minute | niciunul |
| Orfan și import, 5 resurse | 40 de minute | niciunul |
| Referințele între stack-uri mutate pe SSM | 1 oră | niciunul |
| Verificarea diff-urilor goale pe toate cele patru stack-uri | 15 minute | niciunul |
O după-amiază și o dimineață, făcute întâi în staging și apoi în producție a doua zi, cu un runbook. Minutul de downtime a fost pentru Lambda-urile care consumă cozi: între ștergere și creare, mesajele au așteptat. Au fost procesate când au apărut funcțiile noi.
Ce le-am spune nouă din trecut
Împarte înainte de 300, nu la 500. Numără resursele cu grep după fiecare schimbare semnificativă și pune numărul în CI ca avertisment la 350. Ține lucrurile cu stare într-un stack care se schimbă cât mai rar, de la primul deploy. Și nu scoate niciodată un construct cu stare dintr-un stack fără să vezi întâi Retain în template-ul desfășurat.
Dacă te uiți chiar acum la eroarea de 500 și stack-ul are o bază de date în el, hai să vorbim înainte să rulezi următorul deploy.