# Ziua în care CloudFormation ne-a șters serviciile App Runner: un post-mortem și cele patru plase de siguranță pe care le-am adăugat

Pe 30 aprilie 2026, un singur `cdk deploy` rulat de pe un laptop a șters toate serviciile App Runner din mediul nostru de dev. Nu s-au pierdut date, nimeni din afara echipei n-a observat, iar serviciile erau înapoi într-o oră. A fost totuși cel mai instructiv incident al anului, pentru că unealta a făcut exact ce i s-a spus, iar persoana care a rulat comanda n-a făcut nimic care să pară greșit.

Acesta e post-mortem-ul, scris pentru ingineri care folosesc AWS CDK și CloudFormation și n-au văzut niciodată acest mod de eșec. Platforma e un sistem de client pe care îl construim și îl operăm: trei aplicații Next.js pe App Runner, GraphQL cu Lambda în spate, DynamoDB, Aurora, toate într-un singur stack CDK per mediu. Nu numim clientul; mecanica e ce contează.

## Ce s-a întâmplat, pas cu pas

Stack-ul nostru CDK avea un flag de context, `hostingReady`, introdus în timpul ridicării inițiale. Primul deploy al unui mediu nou trebuie să creeze repository-urile ECR *înainte* ca vreun serviciu App Runner să poată referenția o imagine din ele. Așa că stack-ul era scris astfel: dacă `hostingReady` e false, sintetizează totul în afară de serviciile App Runner; dacă e true, include-le.

Workflow-ul de GitHub Actions transmitea întotdeauna `-c hostingReady=true`. Flag-ul avea valoarea implicită **false**, care era valoarea „sigură" pentru un prim deploy și valoarea greșită pentru fiecare deploy de după.

Un inginer a rulat local `cdk deploy PlatformStack-dev` ca să împingă o schimbare mică. Fără flag de context. CDK a sintetizat un template fără cele trei servicii App Runner. CloudFormation a comparat noul template cu stack-ul deployat, a văzut trei resurse care nu mai erau declarate și a făcut ce trebuie să facă o unealtă declarativă: le-a șters.

<div class="article-figure">
<svg viewBox="0 0 900 260" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Lanțul de evenimente: cdk deploy local fără flag-ul de context, template sintetizat fără serviciile App Runner, CloudFormation vede trei resurse eliminate, serviciile sunt șterse">
<defs><marker id="arr2" 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="#ff6b8a"/></marker></defs>
<g font-family="Inter,system-ui,sans-serif">
<rect x="20" y="70" width="190" height="110" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="115" y="100" text-anchor="middle" fill="#f1f3ff" font-size="14" font-weight="700">1. Laptop</text>
<text x="115" y="125" text-anchor="middle" fill="#9aa3c7" font-size="12">cdk deploy PlatformStack-dev</text>
<text x="115" y="145" text-anchor="middle" fill="#ff6b8a" font-size="12">fără -c hostingReady=true</text>
<text x="115" y="165" text-anchor="middle" fill="#9aa3c7" font-size="12">flag-ul e implicit false</text>
<line x1="210" y1="125" x2="245" y2="125" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arr2)"/>
<rect x="250" y="70" width="190" height="110" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="345" y="100" text-anchor="middle" fill="#f1f3ff" font-size="14" font-weight="700">2. Synth</text>
<text x="345" y="125" text-anchor="middle" fill="#9aa3c7" font-size="12">template-ul e valid</text>
<text x="345" y="145" text-anchor="middle" fill="#ff6b8a" font-size="12">lipsesc 3 servicii App Runner</text>
<text x="345" y="165" text-anchor="middle" fill="#9aa3c7" font-size="12">nicio eroare, niciun avertisment</text>
<line x1="440" y1="125" x2="475" y2="125" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arr2)"/>
<rect x="480" y="70" width="190" height="110" rx="12" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="575" y="100" text-anchor="middle" fill="#f1f3ff" font-size="14" font-weight="700">3. CloudFormation</text>
<text x="575" y="125" text-anchor="middle" fill="#9aa3c7" font-size="12">diff: 3 resurse eliminate</text>
<text x="575" y="145" text-anchor="middle" fill="#9aa3c7" font-size="12">DeletionPolicy: Delete</text>
<text x="575" y="165" text-anchor="middle" fill="#9aa3c7" font-size="12">fără termination protection</text>
<line x1="670" y1="125" x2="705" y2="125" stroke="#ff6b8a" stroke-width="1.5" marker-end="url(#arr2)"/>
<rect x="710" y="70" width="170" height="110" rx="12" fill="#2a1520" stroke="#ff6b8a" stroke-width="1.5"/>
<text x="795" y="100" text-anchor="middle" fill="#f1f3ff" font-size="14" font-weight="700">4. Rezultat</text>
<text x="795" y="125" text-anchor="middle" fill="#ff6b8a" font-size="12">servicii șterse</text>
<text x="795" y="145" text-anchor="middle" fill="#9aa3c7" font-size="12">aplicații offline ~1 oră</text>
<text x="795" y="165" text-anchor="middle" fill="#9aa3c7" font-size="12">imagini + date intacte</text>
<text x="450" y="230" text-anchor="middle" fill="#9aa3c7" font-size="13">Fiecare pas s-a comportat corect. Designul era greșit.</text>
</g>
</svg>
</div>

Recuperarea a fost simplă: re-rulăm deploy-ul cu flag-ul, așteptăm ca App Runner să tragă imaginile și să treacă health check-urile. Imaginile de container erau încă în ECR, bazele de date neatinse, Cognito neatins. Raza de impact a fost „aplicațiile au fost jos cam o oră în dev".

## De ce e un eșec de design, nu o eroare umană

Concluzia tentantă e „inginerul ar fi trebuit să transmită flag-ul". Am respins-o, din trei motive.

În primul rând, CDK nu îi spune aplicației tale ce comandă rulează. Când se execută TypeScript-ul tău, `cdk synth`, `cdk diff` și `cdk deploy` arată identic. Nu poți scrie „dacă e deploy, refuză". Orice apărare trebuie să funcționeze la nivel de template sau la nivel de CloudFormation.

În al doilea rând, valoarea implicită a unui flag e o decizie de design. Un flag cu valoarea implicită distructivă e un pistol încărcat lăsat pe masă. Cazul de ridicare inițială, care se întâmplă o singură dată, ar fi trebuit să fie opt-in-ul, nu cazul de zi cu zi.

În al treilea rând, comportamentul de ștergere al CloudFormation e corect și nu se va schimba. Infrastructură declarativă înseamnă „template-ul e adevărul". Dacă din template lipsește o resursă, resursa dispare. Singura întrebare e dacă i-ai spus lui CloudFormation că anumite resurse sunt prea importante ca să le șteargă fără luptă.

## Cele patru plase de siguranță

Le-am livrat pe toate patru într-o săptămână. Sunt straturi independente; oricare dintre ele singur ar fi prevenit incidentul, iar împreună acoperă cazuri pe care celelalte le ratează.

<div class="article-figure">
<svg viewBox="0 0 900 330" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Patru straturi independente de apărare în jurul resurselor critice: flag-uri de context sigure implicit, termination protection pe stack, un aspect RETAIN pe tipurile critice de resurse și deploy-uri exclusiv din CI cu un banner de avertizare pentru rulările locale.">
<g font-family="Inter,system-ui,sans-serif">
<rect x="10" y="10" width="880" height="310" rx="16" fill="#151b2e" stroke="#7b8cff" stroke-width="1.5"/>
<text x="26" y="36" fill="#7b8cff" font-size="14" font-weight="700">4. Deploy doar din CI + banner în afara CI</text>
<text x="874" y="36" text-anchor="end" fill="#9aa3c7" font-size="12">procedural · oprește laptopul greșit să deployeze</text>
<rect x="55" y="55" width="790" height="220" rx="16" fill="#181d33" stroke="#ffd166" stroke-width="1.5"/>
<text x="71" y="81" fill="#ffd166" font-size="14" font-weight="700">1. Flag-uri sigure implicit</text>
<text x="829" y="81" text-anchor="end" fill="#9aa3c7" font-size="12">un flag uitat nu elimină niciodată o resursă</text>
<rect x="100" y="100" width="700" height="130" rx="16" fill="#1a2038" stroke="#4fffb0" stroke-width="1.5"/>
<text x="116" y="126" fill="#4fffb0" font-size="14" font-weight="700">2. Termination protection</text>
<text x="784" y="126" text-anchor="end" fill="#9aa3c7" font-size="12">blochează cdk destroy și Delete stack</text>
<rect x="145" y="145" width="610" height="40" rx="16" fill="#1d233d" stroke="#ff6b8a" stroke-width="1.5"/>
<text x="161" y="171" fill="#ff6b8a" font-size="14" font-weight="700">3. Aspect RETAIN</text>
<text x="739" y="171" text-anchor="end" fill="#9aa3c7" font-size="12">o resursă scoasă din template continuă să ruleze</text>
<rect x="200" y="200" width="500" height="70" rx="12" fill="#2a1520" stroke="#ff6b8a" stroke-width="1.5"/>
<text x="450" y="242" text-anchor="middle" fill="#f1f3ff" font-size="14" font-weight="700">App Runner · Cognito · DynamoDB · RDS · S3 · SQS · Secrets</text>
</g>
</svg>
</div>

### 1. Flag-uri sigure implicit

`hostingReady` are acum valoarea implicită **true**. Cazul de ridicare inițială, în care trebuie să creezi repository-urile ECR înainte de servicii, e `-c skipHosting=true`, pe care nimeni nu-l tastează din greșeală. Fiecare flag de context din stack a fost revizuit cu aceeași întrebare: „dacă cineva uită de el, în ce direcție merge greșeala?" Un flag uitat nu trebuie să elimine niciodată o resursă.

### 2. Termination protection pe stack

```ts
new PlatformStack(app, `PlatformStack-${config.stage}`, {
  config,
  env: { account: config.account, region: config.region },
  terminationProtection: true,
});
```

Asta oprește `cdk destroy` și butonul „Delete stack" din consolă până când un operator îl dezactivează explicit. N-ar fi prevenit singur incidentul din aprilie, pentru că stack-ul a fost actualizat, nu șters, dar închide ușa de alături. Nu costă nimic.

### 3. Un aspect RETAIN pe tipurile critice de resurse

Acesta e cel care adresează direct incidentul. Un Aspect CDK parcurge fiecare construct din arbore după sinteză și fixează `DeletionPolicy` din CloudFormation pe `Retain` pentru tipurile de resurse pe care le considerăm critice:

```ts
const PROTECTED_TYPES = new Set([
  'AWS::AppRunner::Service',
  'AWS::Cognito::UserPool',
  'AWS::DynamoDB::Table',
  'AWS::RDS::DBCluster',
  'AWS::SQS::Queue',
  'AWS::S3::Bucket',
  'AWS::SecretsManager::Secret',
]);

export class ProtectCriticalResources implements IAspect {
  visit(node: IConstruct): void {
    if (!CfnResource.isCfnResource(node)) return;
    if (!PROTECTED_TYPES.has(node.cfnResourceType)) return;
    const current = node.cfnOptions.deletionPolicy;
    if (current && current !== CfnDeletionPolicy.RETAIN) return; // respect explicit choices
    node.applyRemovalPolicy(RemovalPolicy.RETAIN);
  }
}

Aspects.of(stack).add(new ProtectCriticalResources());
```

Cu `Retain`, când un template încetează să declare o resursă, CloudFormation o scoate din evidența stack-ului, dar lasă resursa AWS reală să ruleze. În scenariul din aprilie, serviciile ar fi continuat să servească trafic, iar următorul deploy corect ar fi avut nevoie de un import în loc de o creare. E o neplăcere, nu o cădere.

Două alegeri de design merită menționate. Aspectul respectă deciziile explicite: dacă un construct a setat intenționat `DESTROY` (un bucket de dev cu `autoDeleteObjects`, de exemplu), aspectul îl lasă în pace, pentru că suprascrierea fie ar strica sinteza, fie ar anula în tăcere o alegere deliberată. Și aspectul se aplică în fiecare stage, inclusiv dev, pentru că riscul de incident e același oriunde trăiesc utilizatori reali sau date reale de test.

Compromisul: un `cdk destroy` deliberat lasă acum în urmă resurse orfane. Curățenia e o ștergere manuală per resursă. Acceptăm asta; e prețul explicit al siguranței.

### 4. Deploy-urile se fac din CI, iar CLI-ul îți spune asta

Ultimul strat e procedural. Toate cele trei medii deployează din GitHub Actions prin roluri asumate cu OIDC, fiecare declanșat de propriul branch. Un `cdk synth` sau `cdk diff` local e în regulă și încurajat. Un `cdk deploy` local nu e, și pentru că CDK nu-l poate bloca, aplicația afișează un banner ori de câte ori rulează în afara CI:

```
⚠️  CDK is running outside of CI.
   `cdk synth` and `cdk diff` are safe to run locally.
   `cdk deploy` from a laptop is the cause of the 2026-04-30 hosting
   incident — push to the dev branch and let GitHub Actions deploy.
```

Banner-ul e suprimat de `CI=true` sau de o variabilă explicită de confirmare. Nu e un control tehnic și nu pretindem că ar fi. E un memento în exact momentul în care un memento e util, și referă incidentul după dată, ca nimeni să nu trebuiască să întrebe de ce.

## Ce ți-am spune să verifici azi

Nu trebuie să treci prin incidentul ăsta ca să beneficiezi de el. Trei întrebări de pus despre propriile stack-uri CDK săptămâna asta:

1. **Care dintre flag-urile tale de context sau variabilele de mediu, dacă sunt uitate, elimină o resursă?** Inversează-le valorile implicite.
2. **Ce `DeletionPolicy` au bazele tale de date, pool-urile de utilizatori și cozile?** Rulează `cdk synth` și caută în template. Dacă răspunsul e `Delete` sau lipsește, ești la un aspect de o linie distanță de rezolvare.
3. **Poate un laptop să deployeze în producție?** Dacă da, ce oprește un `AWS_PROFILE` greșit? Fixarea contului în `env` și o cale de deploy exclusiv prin CI sunt amândouă ieftine.

Încă o lecție, dintr-un al doilea incident mai mic, din august: alias-urile CloudFront și un certificat atașate manual au fost șterse de următorul deploy CDK, pentru că CloudFormation înlocuiește toată configurația distribuției. Aceeași cauză în alt costum: **ce nu e în template nu există**. Politicile Retain protejează resurse; nu protejează proprietăți. Singura apărare pentru proprietăți e să le pui în cod.

Dacă vrei o a doua pereche de ochi pe setup-ul tău CDK înainte să te învețe singur lecția asta, [scrie-ne](/contact).
