Skip to content
Limita de 500 de resurse din CloudFormation și cum am spart un stack fără să pierdem date
← ← Înapoi la Idei Cloud

Limita de 500 de resurse din CloudFormation și cum am spart un stack fără să pierdem date

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.

503 resurse într-un stack, pe tipuri Politici IAM71 Rețelistică EC258 Roluri IAM48 Funcții Lambda31 · doar 12 sunt ale noastre Log group-uri29 Permisiuni Lambda24 Custom resources19 · ajutoare CDK Restul223

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 functionName explicit 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.com nou, o schimbare de DNS și o validare de certificat. Am lăsat toate cele trei servicii App Runner în stack-ul app, 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ă:

1 · RETAINstack vechi: removalPolicy= RETAIN, deploynimic nu se schimbă încă 2 · orfanștergi construct-ul,deploy la stack-ul vechiresursa rămâne vie, negestionată 3 · cdk importacelași construct, acelașinume fizic, stack nouCloudFormation o adoptă 4 · verificicdk diff trebuie să fiegol, apoi deploygestionată din nou Între pașii 2 și 3 resursa există, dar niciun stack n-o deține. Fă cei doi pași unul după altul, într-o singură ședință, cu un snapshot luat înainte. Dacă sari pasul 1, pasul 2 șterge baza de date. Ăsta e tot motivul pentru care există pasul 1.

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.