Skip to content
Trei conturi AWS, un singur cod CDK: cum ținem dev, staging și prod departe unul de altul
← ← Înapoi la Idei Cloud

Trei conturi AWS, un singur cod CDK: cum ținem dev, staging și prod departe unul de altul

Majoritatea articolelor despre AWS de pe acest blog au fost, până acum, ghiduri: ce face un serviciu, cum îl configurezi, cât costă. Acesta e diferit. Documentează un setup real pe care l-am construit și îl operăm pentru un client în 2026: o platformă cu mai multe aplicații (trei aplicații Next.js, patru funcții Lambda, un API GraphQL, DynamoDB, Aurora Serverless v2, S3, Cognito) care rulează în trei conturi AWS separate, câte unul pentru fiecare mediu, toate deployate dintr-un singur cod CDK.

Nu numim intenționat clientul, produsul sau vreun ID de cont. Ce împărtășim e structura, deciziile și greșelile care le-au modelat. Dacă ești o echipă mică care a depășit stadiul „un cont cu sufixul -dev pe toate resursele", acesta e setup-ul pe care l-am recomanda.

De ce trei conturi și nu un cont cu trei stack-uri

Platforma a pornit în februarie 2026 într-un singur cont administrat manual. Până în aprilie, un audit intern de securitate a produs o listă de constatări care se reduceau toate la aceeași cauză: resursele de dev, staging și prod împărțeau roluri IAM, chei KMS, permisiuni cu wildcard (politici pe tabele de tipul platform-*) și nu exista nicio modalitate de a spune „această credențială poate atinge doar dev".

Un cont AWS e singura graniță de securitate dură pe care ți-o dă AWS. Tot restul — tag-uri, convenții de nume, condiții IAM — sunt granițe moi pe care un singur wildcard greșit le anulează. Așa că decizia a fost:

  • dev — un cont nou, complet administrat prin CDK, cu teardown ieftin permis.
  • staging — contul vechi, re-provizionat din același cod CDK după o migrare controlată (mai 2026).
  • prod — al treilea cont, dedicat, ridicat de la zero în iunie și lansat la sfârșitul lui iulie.

Aceeași regiune în toate trei. CIDR-uri de VPC care nu se suprapun (10.50, 10.51, 10.52), ca peering-ul să rămână posibil mai târziu. Fiecare cont are propriile chei KMS, propriul pool Cognito, propriile secrete, propriul CloudTrail.

infra/ (CDK, TypeScript) config.ts = singura sursă de adevăr -c stage=dev -c stage=staging -c stage=prod Cont: dev branch dev · DESTROY · fără MFA · fără PITR Cont: staging branch staging · RETAIN · IDV sandbox Cont: prod branch master · RETAIN · Object Lock 7 ani · Config Per cont, niciodată partajat • chei KMS • pool Cognito • secrete în Secrets Manager • VPC (10.50 / 10.51 / 10.52) • CloudTrail + GuardDuty • rol OIDC pentru GitHub • bootstrap CDK • tag-uri de alocare a costurilor

Un singur cod, un singur config.ts

Tot rostul celor trei conturi e că codul e identic și doar configurația diferă. Impunem asta printr-un singur fișier, infra/lib/config.ts, singurul loc în care mediile au voie să difere. Fiecare construct primește un obiect StageConfig și nu întreabă niciodată direct „sunt în prod?".

Punctul de intrare arată cam așa:

const stage = app.node.tryGetContext('stage') ?? 'dev';
const config = getStageConfig(stage);  // aruncă eroare pentru stage necunoscut

new PlatformStack(app, `PlatformStack-${config.stage}`, {
  config,
  env: { account: config.account, region: config.region },
  terminationProtection: true,
  tags: { Project: 'platform', Stage: config.stage, ManagedBy: 'cdk' },
});

Două detalii contează aici. Primul: numele stack-ului conține stage-ul, deci PlatformStack-dev și PlatformStack-prod nu pot fi confundate. Al doilea: contul e fixat în env. CDK refuză să deployeze dacă credențialele pe care le ai în mână se rezolvă la alt cont decât cel așteptat de stage. Nu poți deploya prod cu credențiale de dev din greșeală și nu poți deploya dev în contul de prod din greșeală.

Matricea de hardening

Asta e partea pe care am pune-o pe perete. În loc să împrăștiem if (stage === 'prod') prin tot codul, fiecare stage returnează un bloc de config tipizat, iar diferențele se citesc ca un tabel:

Setare dev staging prod
Removal policy DESTROY RETAIN RETAIN
DynamoDB point-in-time recovery oprit pornit pornit
DynamoDB cu cheie KMS proprie pornit pornit pornit
DynamoDB streams pe tabelele sensibile oprit pornit pornit
Lifecycle pe seiful de documente (S3) 30 de zile 30 de zile 7 ani + Object Lock
Cognito advanced security audit audit audit (enforce e o decizie de lansare)
AppSync logging complet al request-urilor pornit pornit oprit
X-Ray tracing pornit pornit pornit
WAF pe CloudFront + AppSync pornit pornit pornit
AWS Config oprit pornit pornit
CloudTrail + GuardDuty pornit pornit pornit
Capacitate Aurora Serverless v2 0,5–4 ACU 0,5–4 ACU 2–16 ACU
Retenție backup Aurora 7 zile 7 zile 30 de zile
Reserved concurrency pe Lambda de redirect 10 50 200
Verificare de identitate (terț) oprit (mock) sandbox producție

Citind tabelul îți dai seama imediat ce înseamnă „dev" pentru noi: ieftin, de unică folosință, dar cu aceeași formă ca prod. Criptarea KMS e pornită peste tot, pentru că a o porni mai târziu înseamnă să recreezi tabelele. WAF-ul e pornit peste tot, pentru că o regulă WAF care există doar în prod e o regulă pe care n-a testat-o nimeni.

Blocul de dev mai are o linie cu care ne mândrim: o listă de adrese de email permise, setată la o singură adresă imposibil de potrivit. Dev conține copii ale unor înregistrări reale de utilizatori, așa că transportul de email aruncă fiecare mesaj de ieșire. E o garanție de produs exprimată în configurația infrastructurii, nu un comentariu într-un README.

Branch către cont, nu laptop către cont

Fiecare cont e alimentat de exact un branch Git: dev → contul de dev, staging → contul de staging, master → contul de prod. Promovarea în prod e un merge fast-forward, niciodată un rebase, așa că SHA-ul de commit testat pe dev e același SHA care ajunge în prod.

Partea de CI/CD folosește provider-ul OIDC al GitHub în loc de chei de acces cu viață lungă. Fiecare cont are două roluri IAM:

  • github-actions-cdk-diff — doar citire, poate fi asumat doar din rulările de pull request. Rulează cdk diff ca reviewerii să vadă schimbările de infrastructură înainte de merge.
  • github-actions-cdk-deploy — poate fi asumat doar din push-uri pe branch-ul contului respectiv. Nu deține el însuși puteri IAM largi; delegă către rolurile de bootstrap ale CDK. Lărgirea a ceea ce poate deploya CDK trece prin cdk bootstrap, nu prin editarea unei politici.

Politica de trust fixează repository-ul și ref-ul exact, așa că un fork sau un feature branch nu poate asuma rolul de deploy.

Am mai învățat, scump, că cdk deploy de pe laptop e un bug, nu o funcționalitate. Un deploy local fără flag-urile de context corecte a sintetizat odată un template din care lipseau serviciile App Runner, iar CloudFormation le-a șters conștiincios. Incidentul are propriul articol; versiunea scurtă e că termination protection, un aspect RETAIN pe tipurile critice de resurse și un banner zgomotos când CDK rulează în afara CI fac acum parte din cod.

Pull request GitHub OIDC github-actions-cdk-diff doar citire · cdk diff push dev GitHub OIDC github-actions-cdk-deploy doar push · delegă la bootstrap CDK cdk deploy Cont: dev push staging GitHub OIDC github-actions-cdk-deploy doar push · delegă la bootstrap CDK cdk deploy Cont: staging push master GitHub OIDC github-actions-cdk-deploy doar push · delegă la bootstrap CDK cdk deploy Cont: prod Politica de trust = acest repo + acest ref exact. Zero chei de acces cu viață lungă.

Tag-urile sunt felul în care afli cât costă

Fiecare resursă din fiecare cont poartă trei tag-uri: Project, Stage, ManagedBy. Cu tag-urile de alocare a costurilor activate în Billing, factura lunară se împarte curat pe medii. Cifrele pe care le putem împărtăși fără să încălcăm confidențialitatea:

  • Menținerea Aurora Serverless v2 la un minim de 0,5 ACU în loc de auto-pause costă aproximativ 30 de dolari pe lună per mediu. Plătim asta și pe dev, pentru că auto-pause punea un cold start de 15–30 de secunde pe traseul de înregistrare.
  • Contul de dev e de departe cel mai ieftin dintre cele trei, în principal datorită politicilor DESTROY, lipsei AWS Config și instanțelor App Runner de dimensiune minimă.
  • Costul fix al prod-ului e dominat de două instanțe App Runner ținute calde pentru aplicația publică, de minimul Aurora și de WAF.

Fără tag-uri, „cât ne costă staging-ul" e o întrebare care ia o după-amiază. Cu ele, e un filtru în Cost Explorer.

Ce a mers prost pe drum

O relatare sinceră trebuie să includă și vânătăile.

Limita de 500 de resurse din CloudFormation. Un stack per cont a fost designul inițial. Până în august, stack-ul de prod sintetiza exact 500 de resurse, iar dev 499. Adăugarea unei singure Lambda strica cdk synth. Am mutat observabilitatea (politici de protecție a datelor în loguri, metric filters, alarme, SNS) într-un stack propriu, iar mai târziu o aplicație întreagă în altul. Lecția: proiectează pentru mai multe stack-uri per cont din prima zi, cu export-uri explicite, și nu lăsa un singur stack să crească peste vreo 350 de resurse.

Drift-ul moare la următorul deploy. Alias-urile CloudFront și un certificat ACM au fost atașate manual unei distribuții în iulie. Deploy-ul CDK din august a înlocuit tot DistributionConfig și le-a eliminat în tăcere. Ce nu e în cod nu există. Acum ținem alias-urile și ARN-urile certificatelor în configurația de stage.

Mediul de build nu e mediul de runtime. Next.js încorporează variabilele NEXT_PUBLIC_* la build. Variabilele de runtime de pe container nu le pot schimba. Scriptul nostru de deploy citește acum configurația de build din template-ul sintetizat și o transmite către CodeBuild la fiecare build, ca o variabilă publică schimbată să ajungă în browser de la primul deploy, nu de la al doilea.

Rate limit-ul care a blocat toată echipa. Regula de rate per IP din WAF era setată la 10.000 de request-uri per 5 minute. Echipa de QA a clientului stă în spatele unei singure adrese NAT de birou. O sesiune intensă de testare a declanșat regula și fiecare request din birou a primit 403 timp de cinci minute. Acum e 30.000 și stă în config.ts, nu în consolă.

Am face-o din nou?

Da, și mai devreme. Costul celor trei conturi e real, dar mic: trei bootstrap-uri CDK, trei roluri OIDC, trei seturi de secrete de populat și un fișier de config care trebuie să rămână onest. Costul de a nu le avea e lista de constatări din audit cu care am început, plus grija permanentă că un script de dev cu un wildcard ar putea atinge date de producție.

Dacă faci asta pentru prima dată, ordinea pe care am recomanda-o e: scrie config.ts cu matricea de hardening mai întâi, chiar dacă jumătate din setări nu sunt încă implementate; fixează conturile în env și pornește termination protection înainte de primul deploy; conectează OIDC înainte să dai cuiva credențiale de deploy; și împarte în mai multe stack-uri înainte să ai nevoie.

Vrei ajutor să-ți structurezi propriile conturi AWS și codul CDK în felul acesta? Hai să vorbim — noi am făcut-o o dată pe calea grea, ca tu să nu fii nevoit.