# 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:

```bash
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.

<div class="article-figure">
<svg viewBox="0 0 900 250" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Grafic cu bare orizontale al celor 503 resurse din stack-ul unic, pe tipuri: politici IAM 71, rețelistică EC2 58, roluri IAM 48, funcții Lambda 31, log group-uri 29, permisiuni Lambda 24, custom resources 19, restul 223.">
<g font-family="Inter,system-ui,sans-serif" font-size="13">
<text x="20" y="24" fill="#f1f3ff" font-size="14" font-weight="700">503 resurse într-un stack, pe tipuri</text>
<g fill="#9aa3c7">
<text x="20" y="58">Politici IAM</text><rect x="190" y="46" width="213" height="16" rx="3" fill="#7b8cff"/><text x="412" y="58" fill="#f1f3ff">71</text>
<text x="20" y="84">Rețelistică EC2</text><rect x="190" y="72" width="174" height="16" rx="3" fill="#7b8cff"/><text x="373" y="84" fill="#f1f3ff">58</text>
<text x="20" y="110">Roluri IAM</text><rect x="190" y="98" width="144" height="16" rx="3" fill="#7b8cff"/><text x="343" y="110" fill="#f1f3ff">48</text>
<text x="20" y="136">Funcții Lambda</text><rect x="190" y="124" width="93" height="16" rx="3" fill="#4fffb0"/><text x="292" y="136" fill="#f1f3ff">31 · doar 12 sunt ale noastre</text>
<text x="20" y="162">Log group-uri</text><rect x="190" y="150" width="87" height="16" rx="3" fill="#7b8cff"/><text x="286" y="162" fill="#f1f3ff">29</text>
<text x="20" y="188">Permisiuni Lambda</text><rect x="190" y="176" width="72" height="16" rx="3" fill="#7b8cff"/><text x="271" y="188" fill="#f1f3ff">24</text>
<text x="20" y="214">Custom resources</text><rect x="190" y="202" width="57" height="16" rx="3" fill="#ffd166"/><text x="256" y="214" fill="#f1f3ff">19 · ajutoare CDK</text>
<text x="20" y="240">Restul</text><rect x="190" y="228" width="669" height="16" rx="3" fill="#2a3150"/><text x="700" y="240" fill="#f1f3ff">223</text>
</g>
</g>
</svg>
</div>

## 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](/ro/blog/aws-three-accounts-one-cdk-codebase) ș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ă:

<div class="article-figure">
<svg viewBox="0 0 900 230" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Patru pași pentru a muta o resursă cu stare între stack-uri. Pasul 1: setezi RemovalPolicy RETAIN în stack-ul vechi și faci deploy. Pasul 2: ștergi construct-ul din stack-ul vechi și faci deploy; CloudFormation uită resursa, dar nu o șterge. Pasul 3: adaugi construct-ul în stack-ul nou cu același nume fizic și rulezi cdk import; CloudFormation adoptă resursa existentă. Pasul 4: faci deploy normal la stack-ul nou; diff-ul trebuie să fie gol.">
<defs><marker id="arrI" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#4fffb0"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<rect x="15" y="40" width="200" height="90" rx="10" fill="#151b2e" stroke="#ffd166" stroke-width="1.5"/><text x="115" y="64" text-anchor="middle" fill="#ffd166" font-weight="700">1 · RETAIN</text><text x="115" y="84" text-anchor="middle" fill="#f1f3ff">stack vechi: removalPolicy</text><text x="115" y="100" text-anchor="middle" fill="#f1f3ff">= RETAIN, deploy</text><text x="115" y="120" text-anchor="middle" fill="#9aa3c7">nimic nu se schimbă încă</text>
<line x1="217" y1="85" x2="240" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrI)"/>
<rect x="243" y="40" width="200" height="90" rx="10" fill="#151b2e" stroke="#ff6b8a" stroke-width="1.5"/><text x="343" y="64" text-anchor="middle" fill="#ff6b8a" font-weight="700">2 · orfan</text><text x="343" y="84" text-anchor="middle" fill="#f1f3ff">ștergi construct-ul,</text><text x="343" y="100" text-anchor="middle" fill="#f1f3ff">deploy la stack-ul vechi</text><text x="343" y="120" text-anchor="middle" fill="#9aa3c7">resursa rămâne vie, negestionată</text>
<line x1="445" y1="85" x2="468" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrI)"/>
<rect x="471" y="40" width="200" height="90" rx="10" fill="#151b2e" stroke="#4fffb0" stroke-width="1.5"/><text x="571" y="64" text-anchor="middle" fill="#4fffb0" font-weight="700">3 · cdk import</text><text x="571" y="84" text-anchor="middle" fill="#f1f3ff">același construct, același</text><text x="571" y="100" text-anchor="middle" fill="#f1f3ff">nume fizic, stack nou</text><text x="571" y="120" text-anchor="middle" fill="#9aa3c7">CloudFormation o adoptă</text>
<line x1="673" y1="85" x2="696" y2="85" stroke="#4fffb0" stroke-width="1.5" marker-end="url(#arrI)"/>
<rect x="699" y="40" width="185" height="90" rx="10" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/><text x="791" y="64" text-anchor="middle" fill="#7b8cff" font-weight="700">4 · verifici</text><text x="791" y="84" text-anchor="middle" fill="#f1f3ff">cdk diff trebuie să fie</text><text x="791" y="100" text-anchor="middle" fill="#f1f3ff">gol, apoi deploy</text><text x="791" y="120" text-anchor="middle" fill="#9aa3c7">gestionată din nou</text>
<text x="450" y="170" text-anchor="middle" fill="#9aa3c7">Î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.</text>
<text x="450" y="196" text-anchor="middle" fill="#ff6b8a">Dacă sari pasul 1, pasul 2 șterge baza de date. Ăsta e tot motivul pentru care există pasul 1.</text>
</g>
</svg>
</div>

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](/ro/blog/cloudformation-deleted-our-app-runner-services) î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:

```ts
// 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](/contact) înainte să rulezi următorul deploy.
