Skip to content
Același tag, niciun deploy: de ce cdk deploy cu o imagine neschimbată nu face nimic, și scriptul de 300 de linii pe care l-am scris în jurul lui
← ← Înapoi la Idei Cloud

Același tag, niciun deploy: de ce cdk deploy cu o imagine neschimbată nu face nimic, și scriptul de 300 de linii pe care l-am scris în jurul lui

Două povești despre același pipeline de deploy, spuse împreună pentru că a doua există din cauza primei.

Prima e un bug care nu e bug: am reconstruit o imagine de container, am împins-o sub același tag, am rulat cdk deploy, l-am văzut raportând no changes și am petrecut o oră înțelegând de ce producția rula în continuare codul vechi. A doua e ce s-a întâmplat când CI-ul nostru a fost indisponibil o vreme, din motive de facturare în afara controlului nostru, și aveam nevoie de un mod de a face deploy de pe un laptop mai sigur decât cel prin care șterseserăm cândva trei servicii de pe un laptop.

Partea întâi: CloudFormation compară string-uri, nu octeți

Serviciul App Runner e o resursă CloudFormation, iar imaginea pe care o rulează e o proprietate string: ImageIdentifier: 123456.dkr.ecr.us-east-1.amazonaws.com/api:abc1234. Când rulezi cdk deploy, CloudFormation compară noul template cu ultimul pe care l-a aplicat. Dacă string-ul e identic, nu e diff, nu e update, iar App Runner continuă să servească ce a tras ultima dată.

E un comportament corect și mușcă într-o singură situație: tag-ul e același, dar imaginea din spatele lui nu e. Am dat de asta când am reconstruit același commit cu un argument de build schimbat. Același SHA git, același tag api:abc1234, alți octeți în ECR, iar CloudFormation, corect, n-a văzut nimic de făcut. Tag-ul din registry arăta acum spre un digest nou; serviciul rula în continuare pe cel vechi; nimic nicăieri nu era greșit, în afară de așteptarea noastră.

fixare pe tag tag ECR api:abc1234sha256:1111 → rebuild → sha256:2222 templateImageIdentifier: …/api:abc1234 niciun diffacelași string ca data trecută App Runnerrulează tot sha256:1111 fixare pe digest scriptul de deployrezolvă tag → sha256:2222 templateImageIdentifier: …/api@sha256:2222 diffdigest-ul s-a schimbat App Runnerdesfășoară sha256:2222

Există două rezolvări oneste. Prima e o regulă: fiecare deploy mută SHA-ul. Dacă vrei o altă imagine, comite ceva. E simplă și e ce am făcut o vreme, și pică în momentul în care cineva trebuie să reconstruiască o imagine dintr-un motiv care nu implică o schimbare de cod, cum ar fi un patch de securitate al imaginii de bază.

A doua e fixarea pe digest. După ce imaginea e împinsă, scriptul de deploy întreabă ECR la ce se rezolvă tag-ul și pasează digest-ul, nu tag-ul, în template:

digest=$(aws ecr describe-images --repository-name api --image-ids imageTag="$TAG" \
  --query 'imageDetails[0].imageDigest' --output text)
npx cdk deploy platform-prod --context apiImage="$REPO/api@$digest"
// în stack
imageRepository: { imageIdentifier: this.node.tryGetContext('apiImage'), imageRepositoryType: 'ECR' }

Acum template-ul se schimbă ori de câte ori se schimbă octeții, și doar atunci. Un rebuild al aceluiași commit cu conținut diferit e un deploy real. O re-rulare cu conținut identic e un no-op autentic. Comportamentul CloudFormation nu s-a schimbat; doar i-am dat string-ul care înseamnă cu adevărat „imaginea”.

Digest-ul apare în consola CloudFormation și în cdk diff ca un hash ilizibil de 64 de caractere. Afișăm tag → digest în output-ul de deploy, ca un om să poată spune totuși ce pleacă.

Partea a doua: deploy de pe laptop, în siguranță

CI-ul a dispărut o vreme. Platforma tot avea nevoie de deploy-uri. Învățaserăm deja, scump, că cdk deploy de pe mașina unui developer cu contextul greșit șterge resurse de producție, așa că „rulează-l local” nu era o opțiune pe care s-o mai acceptăm. Alternativa era un script a cărui treabă e să facă un deploy de pe laptop să se comporte ca un deploy din CI: aceleași intrări, aceleași bariere, nicio cale de a livra ce nu e în git.

A crescut la vreo 300 de linii de bash. Iată ce fac liniile alea, în ordine.

1 · preflightarbore murdar? branch greșit? fără --yes? → exit 2 · worktree @ origin/mainniciodată copia de lucru 3 · git archive → S3exact octeții pe care îi vede CodeBuild 4 · 3 × CodeBuildîn paralel · ~3 min 5 · tag-uri → digest-uriafișate pentru om 6 · cdk diff, confirmareorice ștergere → stop și citește 7 · cdk deploydigest-urile ca context 8 · verificare + notificarehealth · SHA · grep pe bundle Un deploy de pe laptop și unul din CI rulează aceiași pași pe aceiași octeți. Laptopul are doar un om la pasul 6.

Preflight. Scriptul refuză să ruleze dacă arborele de lucru are schimbări necomise, dacă --env prod e dat fără --yes, dacă branch-ul curent nu e main pentru un deploy de prod sau dacă identitatea AWS pe care urmează s-o folosească nu e rolul de deploy așteptat pentru contul ăla. Fiecare refuz afișează o propoziție care spune ce e de făcut. Cam 40 de linii, și cele mai valoroase 40 din fișier.

Worktree. Creează un git worktree detașat la origin/main (după un git fetch) într-un director temporar și face totul de acolo. Orice e în copia de lucru a developerului, stash-uit, pe jumătate editat sau experimental, nu poate pleca. SHA-ul acelui worktree e identificatorul de release pentru tot ce urmează.

Arhivă în S3. git archive al worktree-ului, încărcat într-un bucket de build sub SHA. CodeBuild construiește din arhiva aia, nu dintr-o conexiune GitHub, deci intrarea build-ului e aceiași octeți fie că e declanșat de CI, fie de script.

Trei build-uri în paralel. Câte un proiect CodeBuild per aplicație, pornite împreună, interogate împreună. Construirea a trei imagini Next.js pe Docker-ul unui laptop ar dura opt minute și ar încălzi camera; pe trei instanțe CodeBuild durează trei. Proiectele sunt aceleași pe care le folosește CI.

Digest-uri. Ca în partea întâi. Scriptul rezolvă fiecare tag și afișează un tabel app → tag → digest.

Diff și confirmare. cdk diff contra mediului țintă, afișat integral. Dacă diff-ul conține vreo ștergere de resursă, scriptul o evidențiază și cere operatorului să tasteze numele stack-ului ca să continue, nu doar y. Ăsta e pasul care ar fi prevenit incidentul.

Deploy. cdk deploy cu digest-urile ca context și --require-approval never, pentru că aprobarea s-a dat deja la pasul anterior, cu informație mai bună decât dă propriul prompt al CDK.

Verificare și notificare. Lovește ruta de health până raportează noul SHA, face grep pe bundle-ul de client după același SHA și postează un mesaj pe canalul echipei cu mediul, SHA-ul, numele operatorului și rezumatul diff-ului. Dacă verificarea pică, scriptul o spune zgomotos și iese cu cod nenul; nu face rollback automat, pentru că o verificare picată înseamnă de obicei „uită-te la ea”, nu „anuleaz-o”.

Cum arată la folosire

./scripts/deploy.sh --env staging
./scripts/deploy.sh --env prod --yes
./scripts/deploy.sh --env prod --dry-run     # synth + diff, fără build, fără deploy

Trei flag-uri. Tot restul e decis de script din git și din cont. Nu există nicio cale de a pasa o imagine custom, un branch custom sau o valoare de context custom, intenționat.

L-am scrie din nou?

Da, și l-am scrie din prima zi, nu după incident și după pana de CI. Nu pentru că deploy-urile de pe laptop sunt bune, ci pentru că scriptul e pipeline-ul. Când CI-ul s-a întors, workflow-ul lui a devenit: checkout, asumă rolul OIDC, rulează ./scripts/deploy.sh --env prod --yes. O singură cale de cod, exersată din ambele locuri, cu barierele în cod, nu într-un fișier YAML care rulează doar pe mașina altcuiva.

Cele 300 de linii sunt mult pentru un script de shell. Au înlocuit o pagină de runbook care zicea „ai grijă”, care e mai scurtă și nu funcționează.

Dacă deploy-ul tău e un cdk deploy pe care cineva îl rulează de unde se nimerește, te putem ajuta să-l facem plictisitor.