# Cum construim cloud și DevOps: trei conturi, un singur codebase CDK și niciun cluster

Operăm AWS pentru produsele noastre și pentru o platformă de client cu utilizatori reali și bani reali care trec prin ea. Între martie și septembrie anul acesta am făcut majoritatea greșelilor pe care le poate face o echipă mică pe AWS și le-am scris într-o serie de 29 de articole. Aceasta e versiunea condensată: configurația cu care pornim acum fiecare proiect, la ce servește fiecare bucată și unde sunt îngropate cadavrele.

Versiunea scurtă: trei conturi sub o organizație, fiecare resursă în CDK, containere fără cluster, deploy-uri prin GitHub fără chei permanente, observabilitate care nu aparține unui singur furnizor și o revizuire lunară a costurilor. Nimic din toate astea nu e ingenios. Totul e scris.

## Mai întâi conturile: dev, staging, producție, din prima zi

Primul lucru pe care îl creăm nu e un server. E o AWS Organization cu trei conturi membre: dev, staging și producție. Același cod CDK, parametri diferiți, granițe IAM dure între ele. O greșeală în dev nu poate atinge datele de producție pentru că nici măcar nu le poate vedea.

Am moștenit prea multe platforme unde „staging” era a doua bază de date de pe serverul de producție, iar dev era laptopul cuiva. Să repari asta după lansare e o migrare; să o faci înainte de lansare e o după-amiază. Am argumentat în [dev, staging, prod din prima zi](/ro/blog/dev-staging-prod-from-day-one) și am arătat structura codului în [trei conturi AWS, un singur codebase CDK](/ro/blog/aws-three-accounts-one-cdk-codebase).

Deploy-urile ajung în acele conturi din GitHub Actions prin federare OIDC: un rol per cont, cu încredere doar pentru repository-ul și branch-ul potrivit, fără chei de acces stocate nicăieri. Când pleacă un inginer, nimic nu trebuie rotit. Configurația are o sută de linii de CDK și am publicat-o în [roluri de deploy GitHub OIDC per cont AWS](/ro/blog/github-oidc-deploy-roles-per-aws-account).

<div class="article-figure">
<svg viewBox="0 0 900 290" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Fluxul de deploy: un pull request construiește doar ce s-a schimbat, GitHub Actions își asumă un rol OIDC per cont, iar același cod CDK face deploy în conturile dev, staging și producție">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="40" y="34" fill="#f1f3ff" font-size="15" font-weight="700">Un codebase, trei conturi, zero chei stocate</text>
<rect x="40" y="110" width="140" height="50" rx="8" fill="#151b2e" stroke="#9aa3c7"/>
<text x="110" y="131" text-anchor="middle" fill="#f1f3ff">Pull request</text>
<text x="110" y="148" text-anchor="middle" fill="#9aa3c7">doar ce s-a schimbat</text>
<rect x="220" y="110" width="150" height="50" rx="8" fill="#151b2e" stroke="#7b8cff"/>
<text x="295" y="131" text-anchor="middle" fill="#f1f3ff">GitHub Actions</text>
<text x="295" y="148" text-anchor="middle" fill="#9aa3c7">OIDC → rol per cont</text>
<rect x="410" y="110" width="130" height="50" rx="8" fill="#151b2e" stroke="#4fffb0"/>
<text x="475" y="131" text-anchor="middle" fill="#f1f3ff">cdk deploy</text>
<text x="475" y="148" text-anchor="middle" fill="#9aa3c7">aceleași stack-uri</text>
<rect x="600" y="50" width="120" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="660" y="77" text-anchor="middle" fill="#f1f3ff">cont dev</text>
<rect x="600" y="113" width="120" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="660" y="140" text-anchor="middle" fill="#f1f3ff">cont staging</text>
<rect x="600" y="176" width="120" height="44" rx="8" fill="#151b2e" stroke="#ff6b8a"/>
<text x="660" y="203" text-anchor="middle" fill="#f1f3ff">cont prod</text>
<g stroke="#2a3150" stroke-width="2" fill="none">
<line x1="180" y1="135" x2="220" y2="135"/><line x1="370" y1="135" x2="410" y2="135"/>
<line x1="540" y1="135" x2="600" y2="72"/><line x1="540" y1="135" x2="600" y2="135"/><line x1="540" y1="135" x2="600" y2="198"/>
</g>
<text x="780" y="77" fill="#9aa3c7">la merge</text>
<text x="780" y="133" fill="#9aa3c7">la merge,</text><text x="780" y="149" fill="#9aa3c7">smoke tests</text>
<text x="780" y="196" fill="#9aa3c7">aprobare manuală,</text><text x="780" y="212" fill="#9aa3c7">aceeași imagine</text>
<text x="450" y="260" text-anchor="middle" fill="#9aa3c7">Tag-ul imaginii e SHA-ul de git. Producția primește exact imaginea verificată pe staging, niciodată un rebuild.</text>
</g>
</svg>
</div>

## Totul în CDK, și ce te costă asta

Fiecare resursă e TypeScript într-o aplicație CDK: conturi, rețea, baze de date, servicii, DNS, alarme, IAM. Același limbaj ca aplicația, revizuit în aceleași pull request-uri, testat cu același runner. Nimeni nu dă click în consolă; dacă s-a dat click pe ceva, e drift și se importă sau se șterge. Suntem relaxați în privința uneltei și stricți în privința principiului, cum am explicat în [infrastructura în cod nu înseamnă Terraform](/ro/blog/infrastructure-as-code-does-not-mean-terraform).

CDK are muchii ascuțite și ne-am tăiat în majoritatea. Stack-urile CloudFormation au o limită de cinci sute de resurse, iar o platformă cu trei aplicații, cozi, funcții și alarme o atinge mai repede decât te aștepți; am ajuns la 493 și a trebuit să împărțim stack-urile fără să recreăm nimic, ceea ce e o procedură în sine ([limita de 500 de resurse din CloudFormation: împarte un stack](/ro/blog/cloudformation-500-resource-limit-split-a-stack)). Un refactor care a schimbat un ID logic a șters și recreat odată două servicii App Runner în producție; post-mortem-ul e în [CloudFormation ne-a șters serviciile App Runner](/ro/blog/cloudformation-deleted-our-app-runner-services), iar regula de atunci e că fiecare diff care atinge o resursă cu stare sau expusă public e citit de doi oameni înainte de deploy.

Secretele nu stau niciodată în template. Parolele de baze de date, cheile de API și secretele de webhook stau în Secrets Manager sau SSM și sunt injectate la pornirea containerului; rolul containerului poate citi exact acele ARN-uri și nimic altceva. Compromisurile sunt în [secrete în CDK: Secrets Manager, SSM sau nimic în template](/ro/blog/secrets-in-cdk-secrets-manager-ssm-or-nothing-in-the-template).

## Containere fără taxa de cluster

Nu ai nevoie de Kubernetes. Pentru produse sub câteva sute de cereri pe secundă, adică majoritatea produselor, ECS pe Fargate sau App Runner oferă aceeași fiabilitate cu o fracțiune din suprafață. Am explicat raționamentul în [nu ai nevoie de Kubernetes](/ro/blog/you-do-not-need-kubernetes) și nu am regretat nici măcar o dată.

App Runner e implicitul nostru pentru aplicații web: împingi o imagine, primești HTTPS și auto-scaling, îl administrezi din CDK ca pe orice altă resursă. Are limite reale, printre ele un timeout de gateway de 120 de secunde și o instanță rece de un minut, și știm exact unde sunt pentru că ne-am lovit de fiecare. Recenzia onestă e [App Runner după șase luni în producție](/ro/blog/aws-app-runner-review-six-months). ECS pe Fargate e pasul următor când un serviciu are nevoie de o cale internă în VPC, de sidecar-uri sau de scalare fină.

Imaginile sunt distroless și multi-stage, ceea ce a înjumătățit dimensiunea imaginii și a eliminat o întreagă categorie de zgomot CVE din scaner ([imagini Docker pentru Node în 2026](/ro/blog/docker-images-for-node-in-2026-distroless-multi-stage)). Health check-urile verifică conexiunea la baza de date și singura dependență downstream care contează, nu doar că portul e deschis, pentru că [health check-urile care mint](/ro/blog/health-checks-that-lie) sunt mai rele decât niciunul.

## Date: gestionate, salvate și chiar restaurate

PostgreSQL pe RDS sau Aurora, criptat, în subrețele private, cu backup-uri automate și o restaurare care a fost repetată. Ultima parte e cea pe care majoritatea echipelor o sare. Restaurăm un snapshot de producție în contul de staging în fiecare lună, rulăm smoke test-urile pe el și cronometrăm totul, pentru că [backup-urile pe care nu le-ai restaurat niciodată nu sunt backup-uri](/ro/blog/backups-you-never-restored-are-not-backups).

Pentru sarcini mici cu trafic în rafale ne place Aurora Serverless v2; știm și că pragul minim de jumătate de ACU înseamnă că nu e niciodată gratuit, și am calculat prețul onest în [recenzie Aurora Serverless v2](/ro/blog/aurora-serverless-v2-review). Schimbările de schemă sunt expand-and-contract, niciodată un tabel blocat în timpul unui deploy ([zero downtime e o disciplină de migrare a schemei](/ro/blog/zero-downtime-is-a-schema-migration-discipline)), iar parola bazei de date se rotește după program fără să observe cineva ([rotirea parolei bazei de date fără downtime](/ro/blog/rotating-the-database-password-without-downtime)).

Cozile sunt SQS și EventBridge, nu Kafka, până există un motiv ([cozi fără Kafka](/ro/blog/queues-without-kafka-sqs-eventbridge-streaming)). Workerii sunt containere sau funcții Lambda în funcție de durată, iar fiecare job asincron e idempotent, pentru că va rula de două ori.

<div class="article-figure">
<svg viewBox="0 0 900 300" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Topologia de producție: CloudFront și WAF în față, servicii App Runner sau ECS, RDS PostgreSQL în subrețele private, workeri SQS și Lambda, și un pipeline de observabilitate cu OpenTelemetry care exportă loguri, trace-uri și alerte">
<g font-family="Inter,system-ui,sans-serif" font-size="12">
<text x="40" y="34" fill="#f1f3ff" font-size="15" font-weight="700">Contul de producție, pe un singur slide</text>
<rect x="40" y="100" width="130" height="50" rx="8" fill="#151b2e" stroke="#7b8cff"/>
<text x="105" y="121" text-anchor="middle" fill="#f1f3ff">CloudFront + WAF</text>
<text x="105" y="138" text-anchor="middle" fill="#9aa3c7">reguli gestionate</text>
<rect x="210" y="70" width="140" height="44" rx="8" fill="#151b2e" stroke="#4fffb0"/>
<text x="280" y="97" text-anchor="middle" fill="#f1f3ff">App Runner: web</text>
<rect x="210" y="130" width="140" height="44" rx="8" fill="#151b2e" stroke="#4fffb0"/>
<text x="280" y="157" text-anchor="middle" fill="#f1f3ff">ECS Fargate: API</text>
<rect x="390" y="70" width="130" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="455" y="90" text-anchor="middle" fill="#f1f3ff">RDS PostgreSQL</text>
<text x="455" y="105" text-anchor="middle" fill="#9aa3c7">subrețele private</text>
<rect x="390" y="130" width="130" height="44" rx="8" fill="#151b2e" stroke="#ffd166"/>
<text x="455" y="150" text-anchor="middle" fill="#f1f3ff">SQS → Lambda</text>
<text x="455" y="165" text-anchor="middle" fill="#9aa3c7">workeri idempotenți</text>
<rect x="560" y="70" width="140" height="104" rx="8" fill="#151b2e" stroke="#ff6b8a"/>
<text x="630" y="95" text-anchor="middle" fill="#f1f3ff">OpenTelemetry</text>
<text x="630" y="115" text-anchor="middle" fill="#9aa3c7" textLength="130" lengthAdjust="spacingAndGlyphs">loguri · trace-uri · metrici</text>
<text x="630" y="135" text-anchor="middle" fill="#9aa3c7">exportate, fără lock-in</text>
<text x="630" y="155" text-anchor="middle" fill="#9aa3c7">alerte → runbook</text>
<rect x="740" y="70" width="120" height="104" rx="8" fill="#151b2e" stroke="#9aa3c7"/>
<text x="800" y="95" text-anchor="middle" fill="#f1f3ff">Secrete</text>
<text x="800" y="115" text-anchor="middle" fill="#9aa3c7">Secrets Manager</text>
<text x="800" y="135" text-anchor="middle" fill="#9aa3c7">injectate la pornire</text>
<text x="800" y="155" text-anchor="middle" fill="#9aa3c7">rotite după program</text>
<g stroke="#2a3150" stroke-width="2" fill="none">
<line x1="170" y1="125" x2="210" y2="92"/><line x1="170" y1="125" x2="210" y2="152"/>
<line x1="350" y1="92" x2="390" y2="92"/><line x1="350" y1="152" x2="390" y2="152"/>
<line x1="520" y1="92" x2="560" y2="110"/><line x1="520" y1="152" x2="560" y2="140"/>
</g>
<rect x="40" y="210" width="820" height="40" rx="8" fill="#151b2e" stroke="#2a3150"/>
<text x="450" y="234" text-anchor="middle" fill="#9aa3c7">Niciun NAT gateway dacă un serviciu privat nu trebuie să iasă. Niciun Kubernetes. Fiecare săgeată de mai sus e o linie de CDK.</text>
<text x="450" y="280" text-anchor="middle" fill="#9aa3c7">Lunar: restaurăm un snapshot de prod în staging, citim factura linie cu linie, rotim ce e scadent.</text>
</g>
</svg>
</div>

## Observabilitate care rămâne a ta

Logurile, trace-urile și metricile trec prin OpenTelemetry către backend-ul pe care clientul îl plătește deja sau către un stack self-hosted când nu vrea să plătească unul. Aplicația nu știe care; emite semnale standard, iar exporter-ul decide. Când clientul a schimbat furnizorul de observabilitate anul acesta, schimbarea în aplicație a fost zero linii. Argumentul e în [logurile tale nu ar trebui să știe în ce cloud sunt](/ro/blog/your-logs-should-not-know-which-cloud) și [trace-urile tale nu ar trebui să știe în ce cloud sunt](/ro/blog/your-traces-should-not-know-which-cloud), iar realitatea prețurilor în [costul real al observabilității gestionate la 100 GB pe zi](/ro/blog/real-cost-of-managed-observability-100gb-a-day).

Alertele sunt puține și fiecare corespunde unei pagini din runbook. Runbook-ul e scris pentru o echipă care nu are on-call, pentru că majoritatea clienților noștri nu au: ce verifici, pe ce dai click, pe cine suni, în ordinea asta ([runbook de incidente pentru o echipă fără on-call](/ro/blog/incident-runbook-for-a-team-without-on-call)). Alertele pe care nu acționează nimeni se șterg; [alertele care nu știu în ce cloud sunt](/ro/blog/alerts-that-do-not-know-which-cloud) explică rutarea.

## Factura, revizuită lunar

Costul e un input de design, nu o surpriză la sfârșit de lună. Cea mai scumpă linie de pe o factură AWS mică e de obicei cea pe care nimeni nu-și amintește că a creat-o: un NAT gateway care servește o subrețea privată care ar fi putut folosi un VPC endpoint ([NAT gateway: cea mai scumpă linie pe care nu o vezi](/ro/blog/nat-gateway-the-most-expensive-line-you-do-not-see)). Apoi baze de date supradimensionate, loguri păstrate la nesfârșit și compute pe care nimeni nu l-a angajat într-un savings plan.

În fiecare lună trecem prin factură linie cu linie împreună cu clientul. Pentru propria noastră companie de trei oameni exercițiul e public, în [factura AWS a unui startup de trei persoane](/ro/blog/aws-bill-of-a-three-person-startup), iar raționamentul despre angajamente în [rezervări, savings plans sau nimic la scară mică](/ro/blog/reserved-savings-plans-or-nothing-at-small-scale). Politicile de protecție a datelor din CloudWatch țin datele personale în afara logurilor înainte să devină o problemă de conformitate ([politici de protecție a datelor CloudWatch pentru PII](/ro/blog/cloudwatch-data-protection-policies-pii)).

## Cum arată o colaborare

Mai întâi o săptămână de evaluare: acces read-only la conturi, la pipeline-uri și la ultimele trei facturi, și un raport scris cu priorități. Apoi fundația: conturi, rețea, schelet CDK, deploy-uri OIDC, primul serviciu care rulează în noul mod. Apoi migrarea, mediu cu mediu, ținând calea veche activă până când cea nouă e verificată, niciodată un weekend big-bang. Apoi fie o predare curată cu instruire, fie rămânem pentru revizuirea lunară și suport la cerere.

Dacă infrastructura ta a fost construită de mână sau deploy-urile sunt făcute de singura persoană care nu poate pleca în concediu, [spune-ne despre asta](/contact). Săptămâna de evaluare are scop fix, iar raportul e al tău, îl poți duce oriunde.
