1 Ce face un DevOps, de fapt
Definiția de manual („cultură care unește dezvoltarea cu operațiunile”) nu te ajută la interviu, pentru că nu spune ce faci luni dimineață. Uite formularea care te ajută:
„Un DevOps se ocupă de drumul dintre codul scris de programatori și codul care rulează la client. Construiește drumul ăsta să fie automat și repetabil, construiește mediile în care rulează, și se asigură că atunci când ceva se strică se vede repede și se repară repede.”
Rolul are patru coloane. Toate patru apar în anunțurile de job, în ordini diferite:
🚚 Livrare automată (CI/CD)
Construiești conducta care ia codul din Git, îl compilează, îl testează, îl împachetează și îl pune pe servere. Unelte: GitHub Actions, Jenkins, GitLab CI, Azure DevOps.
🏗️ Infrastructură ca mediu de rulare
Creezi și menții locurile unde rulează codul: mașini virtuale, clustere Kubernetes, baze de date, rețele. Descrise în cod, nu din interfață. Unelte: Terraform, Kubernetes, Docker, Ansible.
👁️ Observabilitate
Faci sistemul să-și spună starea: metrici, loguri, alerte, tablouri de bord. Fără asta, primul care află că e o problemă e clientul. Unelte: Prometheus, Grafana, Datadog, ELK.
🚨 Fiabilitate și incidente
Ești de gardă, iei incidentul, îl comunici, îl închizi, apoi scrii ce s-a întâmplat și ce schimbi ca să nu se repete. Asta e partea pe care tu o faci de 20 de ani.
Coloana 4 o ai solidă: control de producție 24/7 la Metro, migrare de mesagerie fără oprire la Volvo Cars, descoperire de aplicații la Etihad. Coloana 3 o ai parțial — ai lucrat cu Nagios și Tivoli, care sunt aceeași idee cu Prometheus, dar cu generația anterioară de unelte. Coloanele 1 și 2 sunt cele pe care le-ai atins la DISH și RapidBid și pe care trebuie să le poți explica cel mai bine, pentru că acolo va insista interviul.
Ce NU e un DevOps
- Nu e „cel care are acces la producție”. Accesul e o consecință, nu rolul. Un DevOps bun reduce numărul de oameni care trebuie să intre manual pe servere, inclusiv pe el.
- Nu e sysadmin cu nume nou. Sysadminul administrează servere existente. DevOps le creează din cod, le distruge, le recreează identic. Dacă un server nu poate fi recreat de la zero automat, nu e făcut cum trebuie.
- Nu scrie funcționalitatea produsului. Scrie cod, dar cod de automatizare: pipeline-uri, manifeste, module de infrastructură, scripturi.
- Nu e o echipă separată care primește tichete. Când devine asta, s-a reinventat exact zidul pe care DevOps trebuia să-l dărâme.
De ce a apărut rolul
Istoric, existau două echipe cu obiective opuse. Dezvoltarea era plătită să schimbe lucruri. Operațiunile erau plătite ca nimic să nu se schimbe, pentru că orice schimbare putea pica producția. Rezultatul: livrări rare, mari și riscante, la două-trei luni, noaptea, cu toată lumea în ședință.
Răspunsul DevOps e contraintuitiv: livrezi mai des, nu mai rar. O livrare mică, automată, de douăzeci de linii, e ușor de verificat și ușor de dat înapoi. O livrare de trei luni de muncă nu poate fi nici verificată, nici dată înapoi. Riscul nu vine din frecvență, vine din dimensiune.
„Paradoxul e că stabilitatea crește când livrezi mai des, nu mai rar. O schimbare mică, automată și reversibilă e mai sigură decât un release trimestrial în care se adună trei luni de modificări.”
2 Ziua de lucru a unui DevOps
Asta e ce apare constant în descrierile de rol și în relatările celor din meserie. Nu e o zi tipică — e un amestec care se schimbă zilnic, și întreruperea e parte din meserie.
| Interval | Ce se întâmplă | De ce contează |
|---|---|---|
| Dimineața | Verifici tablourile de bord și alertele din noapte. Te uiți dacă a picat vreo livrare automată, dacă a crescut vreo eroare, dacă s-a umplut vreun disc. | E momentul în care prinzi problemele înainte să le prindă clientul. |
| Stand-up | 15 minute cu echipa de dezvoltare. Ce blochează pe cine. Ce se livrează azi. | DevOps stă în echipa de produs, nu într-un turn separat. |
| Blocul de construcție | Munca planificată: un pipeline nou, un modul Terraform, o migrare, un upgrade de cluster, automatizarea unei operațiuni manuale. | Aici produci valoare pe termen lung. E și partea cea mai ușor de mâncat de întreruperi. |
| Sprijin pentru dezvoltatori | „Nu-mi trece build-ul”, „nu pot să mă conectez la baza de date din staging”, „de ce a picat deploy-ul”. Depanare cu altcineva. | Jumătate din reputația ta în echipă se face aici. |
| Livrări | Urmărești livrările spre staging și producție. Dacă ceva nu arată bine, oprești și dai înapoi. | La o echipă matură, asta e apăsat un buton și te uiți la grafice. |
| Oricând | Incident. Se oprește tot ce era planificat. | Vezi capitolul 18. |
| Periodic | Analiză de cost cloud, aplicare de actualizări de securitate, revizuire de permisiuni, exercițiu de restaurare din backup. | Muncă pe care nu ți-o cere nimeni și care te salvează o dată pe an. |
Nu recita lista. Alege trei lucruri și leagă-le: „Dimineața mă uit la ce s-a întâmplat peste noapte, apoi am un bloc de muncă planificată — de obicei automatizare sau infrastructură — și restul zilei e împărțit între cereri de la dezvoltatori și livrări. Când apare un incident, tot ce era planificat se oprește.”
3 Harta mare: de la commit la producție
Dacă reții o singură imagine din pagina asta, asta să fie. Fiecare capitol care urmează explică o cutie din desenul de mai jos.
Dacă te întreabă ceva larg („povestește-mi despre un pipeline”), descrie fluxul în ordinea asta, cu voce tare, cutie cu cutie. Ai nouă propoziții gata făcute și nu te mai poți bloca la mijloc, pentru că fiecare cutie ți-o dă pe următoarea.
4 Git și fluxul de lucru
Git e locul unde stă adevărul. Tot ce urmează — pipeline, imagine, livrare — pornește de la un commit. Dacă nu e în Git, nu există.
commit
O fotografie a codului la un moment dat, cu un identificator unic (un hash). Ăsta e bobul de nisip din care se construiește tot.
branch
O linie paralelă de lucru. Pleci din main, lucrezi izolat, te întorci. Nimeni nu scrie direct în main.
Pull Request
Cererea de a aduce ramura ta în main. Aici se face revizia de cod și aici rulează automat verificările.
merge
Unirea efectivă. De obicei blocată până când testele trec și cel puțin un coleg a aprobat.
tag
O etichetă pe un commit, de obicei un număr de versiune: v1.4.2. Livrările spre producție se fac pe etichete, nu pe ramuri.
revert
Un commit nou care anulează efectul unuia vechi. Nu șterge istoria — asta contează, pentru că istoria e dovada.
Două modele de ramificare
| Trunk-based (recomandat azi) | GitFlow (mai vechi) | |
|---|---|---|
| Cum arată | Ramuri scurte, de 1–2 zile, care intră repede în main. main e mereu livrabil. | Ramuri lungi: develop, release/*, hotfix/*, feature/*. |
| Potrivit când | Livrezi des, ai teste automate bune. | Livrezi rar, pe versiuni, ai nevoie să menții mai multe versiuni în paralel. |
| Problema | Cere disciplină de testare; fără teste, rupi main. | Ramurile lungi se depărtează și unirea devine dureroasă („merge hell”). |
„Trunk-based, cu ramuri de maximum o zi-două și cu
mainprotejat: verificări automate obligatorii și cel puțin o aprobare. Motivul e că ramurile lungi se depărtează de trunchi și problema apare toată deodată, la unire. GitFlow are sens dacă trebuie să susții mai multe versiuni în paralel la clienți.”
GitOps — ideea care leagă Git de Kubernetes
În GitOps, starea dorită a clusterului stă într-un depozit Git, iar un agent care rulează în cluster (Argo CD sau Flux) compară permanent ce e în Git cu ce e în cluster și corectează diferența. Consecințe practice:
- Nimeni nu mai dă
kubectl applymanual în producție. Modifici fișierul, deschizi PR, se aprobă, agentul aplică. - Dacă cineva modifică ceva direct pe cluster, agentul îl dă înapoi — deviația se autocorectează.
- Revenirea la versiunea anterioară e un
git revert. Istoricul livrărilor e istoricul Git.
5 CI/CD — conceptele
Cele trei litere care se confundă
Integrare continuă
La fiecare push, codul e compilat și testat automat. Scopul: afli în minute dacă ai stricat ceva, nu în săptămâni.
Livrare continuă
Continuous delivery. Orice commit care trece verificările e gata de pus în producție. Apăsarea butonului rămâne la om.
Desfășurare continuă
Continuous deployment. Același lucru, dar fără buton: ce trece testele ajunge automat la client.
„Care e diferența dintre continuous delivery și continuous deployment?” Răspuns scurt: aprobarea umană. La delivery, artefactul e gata oricând, dar cineva apasă. La deployment, nu apasă nimeni. Foarte puține companii fac deployment adevărat în producție, și e absolut normal să spui asta.
Etapele unui pipeline, în ordine
| # | Etapă | Ce face | Dacă pică |
|---|---|---|---|
| 1 | Checkout | Aduce codul din depozit pe mașina de build. | Problemă de acces sau de rețea. |
| 2 | Lint / format | Verifică stilul și greșelile evidente. Rulează în secunde. | Oprește imediat. E cel mai ieftin filtru. |
| 3 | Build | Compilează, instalează dependențele. | Nu are rost să testezi ceva ce nu se construiește. |
| 4 | Teste unitare | Testează bucăți mici, izolate. Minute. | Oprește. Aici prinzi 80% din regresii. |
| 5 | Scanare securitate | Dependențe vulnerabile, secrete uitate în cod, imagini cu CVE-uri. | De obicei oprește pe vulnerabilități critice. |
| 6 | Împachetare | Construiește imaginea Docker și o urcă în registry, etichetată cu hash-ul commitului. | Artefactul e ce se va livra. Nu se reconstruiește pentru fiecare mediu. |
| 7 | Livrare în staging | Pune artefactul într-un mediu cât mai apropiat de producție. | Aici prinzi problemele de configurație. |
| 8 | Teste de integrare / e2e | Verifică sistemul întreg, cu baze de date reale. | Lente și fragile. De aceea sunt puține și bine alese. |
| 9 | Aprobare | Un om apasă. Sau o fereastră de timp permisă. | Poarta de control pentru producție. |
| 10 | Livrare în producție | Rolling, blue-green sau canary. | Vezi mai jos. |
| 11 | Verificare post-livrare | Teste de fum plus urmărirea metricilor câteva minute. | Dacă nu arată bine, dai înapoi automat. |
Construiești o singură dată, livrezi peste tot. Aceeași imagine Docker merge în dev, staging și producție; doar configurația diferă, injectată din exterior. Dacă reconstruiești imaginea pentru fiecare mediu, n-ai testat ce ai livrat. E o propoziție care se reține la interviu.
Strategii de livrare
La RapidBid ai făcut blue-green cu Jenkins. E strategia cea mai ușor de explicat dintre cele trei și ai trăit-o. Pregătește-ți o propoziție despre cum arăta comutarea și una despre ce faci cu baza de date, pentru că acolo duce mereu întrebarea următoare: schema trebuie să fie compatibilă cu ambele versiuni în momentul comutării, altfel revenirea nu mai e posibilă.
6 GitHub Actions, în detaliu
E sistemul de automatizare integrat în GitHub. Rulează pe mașini pe care le pune GitHub la dispoziție („runner”), declanșat de evenimente din depozit. A devenit implicit în majoritatea echipelor noi, și e ce cer cele mai multe anunțuri.
6.1 Vocabularul, de la mare la mic
| Termen | Ce e |
|---|---|
| workflow | Un fișier YAML în .github/workflows/. Un depozit poate avea oricâte. |
| event | Ce îl pornește: push, pull_request, schedule (cron), workflow_dispatch (buton manual), release. |
| job | Un grup de pași care rulează pe aceeași mașină. Joburile rulează în paralel implicit. |
| runner | Mașina care execută. Fie găzduit de GitHub (ubuntu-latest), fie al tău (self-hosted), pentru acces la rețeaua internă sau hardware special. |
| step | O comandă (run:) sau o acțiune refolosită (uses:). |
| action | O bucată de automatizare împachetată, a ta sau a altcuiva: actions/checkout, docker/build-push-action. |
| artifact | Fișiere salvate la finalul unui job, ca să le poată lua alt job sau ca să le descarci tu. |
| environment | Un mediu numit (staging, production) care poate avea secrete proprii, reguli de protecție și aprobatori umani. |
6.2 Un workflow complet, comentat linie cu linie
# Numele care apare în fila Actions name: Build and deploy # ---- CÂND rulează ---- on: push: branches: [main] # doar pe main paths: # și doar dacă s-a schimbat ceva relevant - 'src/**' - 'Dockerfile' pull_request: # și la fiecare PR, ca să prindem devreme workflow_dispatch: # plus un buton manual în interfață # ---- Permisiuni: minimul necesar, explicit ---- permissions: contents: read # poate citi codul id-token: write # poate cere un token OIDC (pentru cloud, fără parole) # ---- Un singur rulaj pe ramură; cel vechi se anulează ---- concurrency: group: deploy-${{ github.ref }} cancel-in-progress: true env: IMAGE: ghcr.io/${{ github.repository }} jobs: test: runs-on: ubuntu-latest strategy: matrix: # același job, pe 3 versiuni de Node, în paralel node: [18, 20, 22] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: npm # păstrează dependențele între rulaje = build mai rapid - run: npm ci - run: npm test build: needs: test # pornește doar dacă „test” a trecut runs-on: ubuntu-latest outputs: tag: ${{ steps.meta.outputs.tag }} steps: - uses: actions/checkout@v4 - id: meta run: echo "tag=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v6 with: push: true tags: ${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} deploy: needs: build if: github.ref == 'refs/heads/main' # NU livrăm din PR-uri runs-on: ubuntu-latest environment: production # aici se cere aprobarea umană steps: - uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.GCP_SA }} - run: | kubectl set image deployment/api \ api=${{ env.IMAGE }}:${{ needs.build.outputs.tag }} -n prod kubectl rollout status deployment/api -n prod --timeout=180s
kubectl rollout status --timeout=180s transformă livrarea într-una care poate să eșueze. Fără ea, comanda set image întoarce succes instantaneu — ea doar cere schimbarea — iar pipeline-ul se colorează verde chiar dacă niciun pod nou n-a pornit. E exact genul de detaliu pe care îl caută un intervievator bun.
6.3 Secretele și autentificarea
Trei niveluri, în ordinea în care sunt căutate: organizație (partajate de mai multe depozite), depozit, mediu (cele mai restrânse — există doar în joburile care declară environment:). Cele de mediu sunt cele mai sigure pentru producție, fiindcă se pot lega de aprobatori.
Ce trebuie știut:
- Secretele sunt criptate și ascunse în loguri, dar un pas care rulează cod arbitrar le poate scoate. De aceea contează în ce pași le dai.
- Nu se transmit automat către workflow-uri din alte depozite (fork-uri) — asta e protecția împotriva unui PR malițios.
secrets.GITHUB_TOKENe generat automat pentru fiecare rulaj și expiră la final. Setează-l implicit pe „read-only” la nivel de organizație și cere explicit mai mult cupermissions:.
OIDC — cum scapi complet de chei
Varianta veche: pui un AWS_SECRET_ACCESS_KEY în secretele depozitului. Cheia e permanentă; dacă scapă, e valabilă până o rotești manual. Varianta actuală, recomandată: OpenID Connect. Workflow-ul cere de la GitHub un token cu durată scurtă care descrie cine este („depozitul X, ramura main”), iar cloudul e configurat să aibă încredere în GitHub și să dea un rol temporar în schimbul acelui token. Nicio parolă nu mai stă nicăieri.
Condiția de încredere din cloud trebuie scrisă pe identificatori imuabili. Dacă scrii repo:firma/serviciu-plati și depozitul e șters, cineva poate crea alt depozit cu exact același nume și moștenește accesul. Forma sigură folosește identificatorii numerici: repository_owner_id:12345:repository_id:67890. Și mai e o subtilitate: la workflow-urile refolosibile, id-token: write trebuie dat în workflow-ul apelant, nu în cel apelat.
6.4 Securitatea acțiunilor externe
- Fixează pe hash, nu pe etichetă.
uses: cutare/actiune@v3înseamnă „ce e acum în v3” — iar eticheta poate fi mutată de autor peste cod nou.uses: cutare/actiune@a81bbbf...înseamnă exact acel cod. Lași Dependabot să-ți propună actualizările. - Evită
pull_request_targetdacă nu știi exact ce faci. Spre deosebire depull_request, rulează cu secretele depozitului și cu permisiuni de scriere, în contextul PR-ului — inclusiv al unuia venit de la un străin. - Nu pune date din PR direct în
run:. Un titlu de PR care conține caractere de shell devine cod executat. Trece valoarea printr-o variabilă de mediu și folosește variabila.
6.5 Refolosire: două mecanisme diferite
Reusable workflow
Un workflow întreg, apelat din altul cu uses: la nivel de job. Conține joburi complete, poate cere aprobări, poate folosi medii. Începe să merite de la trei-patru depozite care fac același lucru.
Composite action
Un grup de pași împachetat ca o acțiune, inserat în interiorul unui job existent. Bun pentru secvențe scurte și repetitive („autentifică-te și configurează unealta”).
6.6 Depanare
- Adaugă secretul de depozit
ACTIONS_STEP_DEBUG=trueși logurile devin mult mai detaliate. - Un job care „nu pornește” are aproape întotdeauna o condiție
if:falsă sau un filtrupaths:care nu s-a potrivit — nu o defecțiune. - „Merge local, nu merge în CI” înseamnă de obicei o dependență instalată pe calculatorul tău și absentă pe runnerul curat, sau o variabilă de mediu care există doar la tine.
- Pentru un rulaj lent, uită-te întâi la cache: fără
actions/cachesau fărăcache:în acțiunea de setup, reinstalezi tot de fiecare dată.
Asta e zona pe care trebuie să o exersezi cu mâna, nu doar să o citești. Un depozit personal cu un workflow care construiește o imagine și o urcă în GitHub Container Registry îți dă dreptul să spui „am scris workflow-uri” fără să exagerezi. Formularea onestă, dacă te întreabă: „În proiectele mele lucrez cu GitHub Actions; experiența mea de pipeline în producție, la scară, e pe Jenkins și Azure DevOps, iar conceptele se transferă direct — etapele, artefactul unic, poarta de aprobare sunt aceleași.”
7 Jenkins și Azure DevOps — ce ai tu deja
Jenkins e sistemul clasic de automatizare, din 2011, instalat pe servere proprii. Are un controler și agenți care execută. Diferența majoră față de GitHub Actions: Jenkins e o aplicație pe care o administrezi tu — cu pluginuri, cu actualizări, cu backup — în timp ce Actions e un serviciu.
7.1 Anatomia unui Jenkinsfile
pipeline { agent { label 'linux-docker' } // pe ce agent rulează environment { // variabile pentru tot pipeline-ul IMAGE = "registry.intern/api" REG = credentials('registry-creds') // secret din Jenkins Credentials } triggers { pollSCM('H/5 * * * *') } // sau webhook din Git stages { stage('Build') { steps { sh 'mvn -B clean package' } } stage('Test') { steps { sh 'mvn test' } post { always { junit 'target/surefire-reports/*.xml' } } } stage('Image') { steps { sh "docker build -t $IMAGE:${GIT_COMMIT} ." } } stage('Deploy green') { steps { sh './deploy.sh green' } } stage('Switch traffic') { input { message 'Comut traficul pe green?' } // aprobare umană steps { sh './switch.sh green' } } } post { failure { mail to: 'echipa@firma.ro', subject: "Pipeline picat: ${env.JOB_NAME}" } always { cleanWs() } } }
7.2 Traducerea Jenkins → GitHub Actions
Dacă te întreabă „ai lucrat cu GitHub Actions?”, răspunsul bun nu e da sau nu. E să arăți că știi corespondența — pentru că exact asta face cineva care migrează, iar migrarea e o sarcină frecventă în anunțuri.
| Jenkins | GitHub Actions | Observație |
|---|---|---|
agent { label 'x' } | runs-on: sau container: | Agentul Jenkins e o mașină pe care o întreții tu; runnerul GitHub e curat la fiecare rulaj. |
stages { stage {...} } | jobs: | Atenție: etapele Jenkins împart implicit acelaşi spațiu de lucru. Joburile Actions nu. Dependențele se redesenează cu needs: plus artefacte. |
environment { ... } | env: la nivel de job sau de pas | Aproape identic. |
credentials('id') | ${{ secrets.NUME }} | Actions adaugă nivelul „environment secrets”, pe care Jenkins nu-l are nativ. |
triggers { pollSCM } | on: push / on: schedule | Actions e declanșat de eveniment, nu interoghează depozitul. |
input { message } | environment: cu required reviewers | Aprobarea umană se mută din pipeline în configurarea mediului. |
sh 'comanda' | run: comanda | Identic în practică. |
| pluginuri (peste 1800) | acțiuni din Marketplace | Ambele sunt cod al altcuiva. La Actions îl fixezi pe hash; la Jenkins actualizezi și speri. |
post { always { } } | if: always() pe un pas | Aceeași idee, altă sintaxă. |
În Jenkins, etapa „Test” vede fișierele produse de etapa „Build”, fiindcă rulează în același director de lucru. În GitHub Actions, fiecare job pornește pe o mașină goală. Dacă traduci etapă-cu-job fără să te gândești, jobul de test nu găsește nimic. Soluția: ori pui totul într-un singur job cu mai mulți pași, ori urci rezultatul cu actions/upload-artifact și îl descarci în jobul următor.
7.3 Azure DevOps, pe scurt
Suita Microsoft: Repos (Git), Pipelines (CI/CD), Boards (tichete), Artifacts (pachete), Test Plans. Pipeline-urile sunt YAML, apropiate de Actions ca formă: trigger, pool, stages, jobs, steps. Ce e specific: service connection (identitatea cu care pipeline-ul intră în Azure), variable group (variabile partajate, legate opțional de Key Vault), environment cu approvals and checks — echivalentul porții de producție.
Pe RapidBid ai avut Azure DevOps și Jenkins cu blue-green. Nu minimaliza asta pentru că nu e GitHub Actions. Un pipeline e un pipeline: etape, artefact, poartă, revenire. Formulare care funcționează: „Pipeline-urile mele de producție au fost pe Jenkins și Azure DevOps. Conceptele sunt aceleași, sintaxa diferă; ce e specific la Actions e modelul de joburi izolate și autentificarea OIDC, și pe astea le-am studiat pentru că sunt diferențele care te pot încurca la migrare.”
8 Docker și containerele
8.1 Ce e, de fapt, un container
Un container este un proces obișnuit pe mașina gazdă, care vede doar o felie din sistem: propriul său sistem de fișiere, propria rețea, propria listă de procese. Izolarea e făcută de nucleul Linux (namespaces și cgroups). Nu are nucleu propriu — îl folosește pe al gazdei.
8.2 Imagine și container
Imaginea e șablonul: un pachet nemodificabil cu aplicația și tot ce-i trebuie. Containerul e o instanță care rulează din acea imagine. O imagine, oricâte containere. Analogia care se reține: imaginea e clasa, containerul e obiectul.
8.3 Straturi și de ce contează ordinea
Fiecare instrucțiune din Dockerfile creează un strat. Straturile se pun în memoria-tampon și se refolosesc — dar când un strat se schimbă, toate cele de după el se reconstruiesc. De aici regula practică: pune întâi ce se schimbă rar.
FROM node:20 WORKDIR /app COPY . . # orice fișier schimbat RUN npm install # invalidează asta CMD ["node","server.js"]
FROM node:20 WORKDIR /app COPY package*.json ./ # se schimbă rar RUN npm ci COPY . . # se schimbă des, dar e ultimul CMD ["node","server.js"]
8.4 Build în mai multe etape
Construiești într-o imagine mare, cu compilator și unelte, apoi copiezi doar rezultatul într-o imagine mică. Imaginea finală nu conține compilatorul, deci e mai mică și are o suprafață de atac mai redusă.
# --- etapa 1: construire --- FROM golang:1.23 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api # --- etapa 2: rulare --- FROM gcr.io/distroless/static:nonroot COPY --from=build /out/api /api USER nonroot:nonroot # NU rulăm ca root EXPOSE 8080 ENTRYPOINT ["/api"]
8.5 Reguli de securitate pentru imagini
- Nu rula ca root. Implicit, procesul din container e root. Dacă cineva iese din container, iese ca root.
- Sistem de fișiere doar-citire pentru procesul principal, plus volume pentru ce chiar trebuie scris.
- Nu băga secrete în imagine. Chiar dacă le ștergi într-un strat ulterior, stratul în care au fost adăugate rămâne și poate fi extras. Secretele se injectează la rulare.
- Fixează versiunile de bază.
FROM node:20.11.1-alpine, nuFROM node:latest— altfel build-ul de azi diferă de cel de ieri. - Scanează imaginea (Trivy, Grype) ca pas în pipeline.
8.6 Ce se pierde când moare containerul
Sistemul de fișiere al containerului e efemer: dispare odată cu el. Ce trebuie să supraviețuiască merge într-un volum sau într-un serviciu extern (bază de date, obiect storage). Logurile se scriu pe ieșirea standard, nu în fișiere din container — le colectează platforma.
La DISH ai avut aplicațiile în containere, în Kubernetes. Dacă te întreabă despre Docker, ancora ta e practică: „Am lucrat cu imaginile în contextul livrării în GKE — o imagine per serviciu, etichetată cu commitul, aceeași imagine promovată între medii.” Apoi arată că știi cele trei reguli de igienă: multi-stage, utilizator non-root, fără secrete în straturi.
9 Kubernetes — fundamentele
9.1 Problema pe care o rezolvă
Ai cincizeci de containere care trebuie să ruleze pe douăzeci de mașini. Cine decide pe care mașină merge fiecare? Ce se întâmplă când o mașină moare la trei noaptea? Cum trimiți trafic către containere care apar și dispar și își schimbă adresa? Cum livrezi o versiune nouă fără să oprești serviciul? Kubernetes e răspunsul la toate astea, la un loc.
9.2 Ideea centrală: stare dorită și reconciliere
Tu nu dai comenzi lui Kubernetes. Tu declari cum vrei să arate lumea — „vreau trei copii ale aplicației, versiunea 2.1, cu atâta memorie” — și un set de bucle de control compară permanent realitatea cu declarația ta și acționează ca să le apropie. Ștergi un pod, apare altul. Moare un nod, podurile lui se recreează în altă parte. Nu pentru că cineva a reacționat, ci pentru că bucla nu se oprește niciodată.
9.3 Arhitectura clusterului
„Kubernetes e un sistem declarativ. Descriu starea dorită într-un fișier, o trimit la API server, care o salvează în etcd. De acolo, controllerele compară permanent ce am cerut cu ce există și corectează diferența: schedulerul alege nodul, kubelet-ul de pe nod pornește containerul. Consecința practică e că nu trebuie să reacționez eu când moare ceva — bucla o face.”
10 Kubernetes — obiectele
10.1 Pod
Cea mai mică unitate pe care Kubernetes o poate programa pe un nod. Conține unul sau mai multe containere care împart aceeași adresă IP, aceleași porturi și pot împărți volume — containerele dintr-un pod se văd între ele pe localhost. În practică, un container principal, plus eventual unul ajutător (sidecar): colector de loguri, proxy de rețea.
Un pod este de unică folosință. Nu se repară: se șterge și se creează altul, cu alt IP. Asta e și motivul pentru care nu te conectezi niciodată direct la IP-ul unui pod, ci la un Service.
10.2 Deployment
Obiectul cu care lucrezi zilnic. Îi spui ce imagine, câte copii și cum să facă actualizarea; el creează un ReplicaSet, care creează podurile. La fiecare versiune nouă creează un ReplicaSet nou și îl reduce pe cel vechi — de aici vine și posibilitatea de revenire.
apiVersion: apps/v1 kind: Deployment metadata: name: api namespace: prod spec: replicas: 3 # câte copii vreau selector: matchLabels: { app: api } # ce poduri îmi aparțin strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # am voie cu 1 pod în plus în timpul livrării maxUnavailable: 0 # dar NICIUNUL în minus → zero downtime template: # de aici în jos e rețeta unui pod metadata: labels: { app: api } spec: containers: - name: api image: registry/api:9f2c1ab # NU „latest”: vrei să știi ce rulează ports: [{ containerPort: 8080 }] env: - name: DB_HOST valueFrom: configMapKeyRef: { name: api-config, key: db_host } - name: DB_PASS valueFrom: secretKeyRef: { name: api-secret, key: db_pass } resources: requests: { cpu: "200m", memory: "256Mi" } # pentru planificare limits: { cpu: "1", memory: "512Mi" } # plafon la rulare readinessProbe: # pot primi trafic? httpGet: { path: /ready, port: 8080 } initialDelaySeconds: 5 periodSeconds: 5 livenessProbe: # mai sunt viu? httpGet: { path: /healthz, port: 8080 } initialDelaySeconds: 20 periodSeconds: 10
10.3 Service — cele patru tipuri
| Tip | Ce face | Când îl folosești |
|---|---|---|
ClusterIP | IP intern, vizibil doar din cluster. Implicit. | Comunicare între servicii. Majoritatea cazurilor. |
NodePort | Deschide același port pe fiecare nod. | Rar, în dezvoltare sau în spatele unui balansor propriu. |
LoadBalancer | Cere furnizorului de cloud un balansor real, cu IP public. | Expunere directă. Costă: un balansor per serviciu. |
ExternalName | Doar un alias DNS către un nume extern. | Ca să numești la fel o bază de date gestionată din afara clusterului. |
Serviciul găsește podurile prin etichete (selector), nu prin nume. Dacă schimbi eticheta din pod și uiți selectorul, serviciul rămâne fără destinații și primești eroare de conectare deși toate podurile rulează perfect. E una dintre cele mai frecvente cauze de „merge pe podul X, nu merge prin serviciu”.
10.4 Ingress
Un Service de tip LoadBalancer per aplicație devine scump și greu de administrat. Ingress e stratul de deasupra: un singur punct de intrare, care rutează după numele domeniului și după cale către servicii interne diferite, și care termină TLS-ul într-un singur loc. Are nevoie de un ingress controller instalat în cluster (nginx, Traefik, cel al furnizorului) — obiectul Ingress singur nu face nimic.
10.5 ConfigMap și Secret
Configurația nu se pune în imagine. Se pune în ConfigMap (setări obișnuite) sau Secret (parole, chei, certificate) și se injectează în pod, fie ca variabile de mediu, fie ca fișiere montate.
„Care e diferența între ConfigMap și Secret?” Răspunsul greșit e „Secret e criptat”. Secret e doar codificat base64, ceea ce înseamnă zero protecție — oricine îl poate citi cu base64 -d. Ce aduce în plus: e tratat diferit (nu apare în loguri, poate fi criptat în etcd dacă activezi encryption at rest, poate fi restricționat separat prin RBAC). Pentru secrete adevărate se folosesc soluții externe: External Secrets Operator cu Vault, AWS Secrets Manager sau Google Secret Manager, și Sealed Secrets dacă vrei secretele în Git.
10.6 Celelalte tipuri de workload
| Obiect | Pentru ce | Diferența esențială |
|---|---|---|
| Deployment | Aplicații fără stare: API-uri, site-uri, servicii. | Podurile sunt interschimbabile, primesc nume aleatorii. |
| StatefulSet | Baze de date, cozi, orice are identitate: PostgreSQL, Kafka, Elasticsearch. | Nume stabile și ordonate (db-0, db-1), fiecare cu discul lui, pornire și oprire în ordine. |
| DaemonSet | Un pod pe fiecare nod: colector de loguri, agent de monitorizare, plugin de rețea. | Nu specifici numărul de copii — depinde de numărul de noduri. |
| Job | O sarcină care rulează o dată și se termină: o migrare, un import. | Urmărește terminarea cu succes, nu menținerea în funcțiune. |
| CronJob | Un Job pe program: backup nocturn, raport zilnic. | Sintaxă cron obișnuită. |
„Deployment pentru tot ce n-are stare, fiindcă podurile sunt interschimbabile și pot fi înlocuite în orice ordine. StatefulSet când fiecare instanță are identitate proprie și disc propriu — o bază de date, un Kafka. Diferența practică apare la un nod căzut: cu Deployment podul se recreează imediat oriunde. Cu StatefulSet, Kubernetes nu recreează automat podul cât timp nu e sigur că cel vechi a murit, tocmai ca să nu ajungă două instanțe să scrie pe același disc. Asta înseamnă că recuperarea e mai lentă, intenționat.”
10.7 Stocare
Trei obiecte legate: PersistentVolume (discul propriu-zis), PersistentVolumeClaim (cererea aplicației: „vreau 20 GB rapizi”) și StorageClass (rețeta prin care se creează automat discul la furnizor). În practică scrii doar PVC-ul; restul se rezolvă singur. De reținut: un disc în cloud e de obicei legat de o zonă — un pod care are nevoie de el poate fi programat doar în acea zonă.
11 Livrare și sănătate: cele două lucruri care se întreabă cel mai des
11.1 Rolling update, pas cu pas
„Kubernetes nu revine automat la versiunea anterioară. Dacă podurile noi nu devin gata, livrarea se blochează și rămâi cu versiunea veche în funcțiune — ceea ce e un comportament bun, pentru că serviciul nu cade. Dar cineva trebuie să observe și să dea kubectl rollout undo. De aceea pipeline-ul meu are kubectl rollout status --timeout, care transformă blocajul într-un eșec vizibil de pipeline.”
# schimbă imaginea kubectl set image deployment/api api=registry/api:9f2c1ab -n prod # urmărește — iese cu cod diferit de zero dacă nu reușește în 180s kubectl rollout status deployment/api -n prod --timeout=180s # istoricul versiunilor kubectl rollout history deployment/api -n prod # revenire la versiunea anterioară kubectl rollout undo deployment/api -n prod # revenire la o revizie anume kubectl rollout undo deployment/api --to-revision=4 -n prod # repornire curată, fără schimbare de imagine (util după schimbarea unui Secret) kubectl rollout restart deployment/api -n prod
11.2 Probe — cele trei și la ce folosesc
Dacă pui în livenessProbe o verificare a bazei de date, iată ce se întâmplă când baza are o secundă proastă: toate podurile eșuează proba în același timp, Kubernetes le omoară pe toate, ele repornesc, se conectează iar la baza deja încărcată, eșuează iar. Ai transformat o degradare temporară într-o pană totală, cu bucla de repornire ca amplificator. Regula: liveness verifică doar dacă procesul e sănătos; dependențele externe se verifică în readiness, unde consecința e retragerea din trafic, nu execuția.
11.3 Oprirea elegantă
Când un pod e oprit, Kubernetes face două lucruri în paralel: îl scoate din lista destinațiilor Service și trimite SIGTERM procesului. Fiindcă sunt în paralel, există o fereastră de câteva sute de milisecunde în care podul încă primește cereri noi dar deja a început să se oprească. Soluția standard e un preStop care așteaptă câteva secunde înainte de oprire, plus o aplicație care tratează SIGTERM: nu mai acceptă conexiuni noi, termină ce are în lucru, apoi iese. terminationGracePeriodSeconds (implicit 30) e răbdarea; după el vine SIGKILL.
12 Resurse și scalare
12.1 requests și limits
| Clasă QoS | Cum o obții | Ce înseamnă |
|---|---|---|
Guaranteed | requests = limits, pentru toate resursele. | Ultimul evacuat când nodul e sub presiune. Pentru componentele critice. |
Burstable | requests < limits. | Cazul obișnuit. Poate depăși temporar rezervarea dacă nodul are loc. |
BestEffort | Fără requests și fără limits. | Primul omorât când nodul rămâne fără memorie. De evitat în producție. |
12.2 Cele trei feluri de scalare
HPA — orizontal
Schimbă numărul de poduri după o metrică (CPU, memorie sau una proprie). Cea folosită cel mai des.
VPA — vertical
Schimbă requests/limits ale podurilor. Utilă ca să afli valorile corecte; în modul automat repornește podurile.
Cluster Autoscaler
Adaugă sau scoate noduri. Se aprinde când poduri rămân Pending pentru că nu mai e loc.
Dacă serviciul e limitat de altceva decât procesor — numărul de conexiuni către baza de date, lungimea unei cozi, un API extern lent — atunci CPU-ul rămâne mic în timp ce serviciul suferă, iar HPA nu se aprinde. Mai rău: dacă se aprinde, podurile noi cer și ele conexiuni la aceeași bază de date și înrăutățesc situația. Răspunsul bun: scalezi pe metrica care descrie de fapt încărcarea — adâncimea cozii, cereri în așteptare — prin metrici personalizate sau KEDA.
12.3 Unde pun podurile — plasare
- nodeSelector / nodeAffinity — „pune podul doar pe noduri cu GPU” sau „preferabil în zona A”.
- podAntiAffinity — „nu pune două copii ale aceluiași serviciu pe același nod”. Fără asta, cele trei replici pot ajunge pe un singur nod și un nod căzut înseamnă serviciu căzut.
- topologySpreadConstraints — distribuie uniform peste zone de disponibilitate.
- taints și tolerations — mecanismul invers: nodul respinge tot, mai puțin podurile care „tolerează” explicit. Așa se rezervă noduri pentru sarcini speciale.
- PodDisruptionBudget — „minimum două poduri disponibile oricând”. Protejează în timpul operațiunilor planificate, ca actualizarea nodurilor.
13 Securitate în cluster
13.1 RBAC — cine are voie ce
Patru obiecte, două perechi. Role dă permisiuni într-un singur namespace; ClusterRole în tot clusterul. RoleBinding și ClusterRoleBinding leagă rolul de cineva: un utilizator, un grup sau un ServiceAccount (identitatea cu care rulează un pod).
kind: Role metadata: { namespace: echipa-a, name: dezvoltator } rules: - apiGroups: [""] resources: [pods, pods/log, services, configmaps] verbs: [get, list, watch] # doar citire - apiGroups: [apps] resources: [deployments] verbs: [get, list, watch, patch] # poate schimba imaginea, nu poate șterge
Comanda pe care o folosești ca să verifici: kubectl auth can-i delete pods --as=ana -n prod.
13.2 Pod Security Admission
PodSecurityPolicy a fost scos din uz în versiunea 1.21 și eliminat în 1.25. Înlocuitorul integrat e Pod Security Admission: pui o etichetă pe namespace și clusterul refuză podurile care nu respectă nivelul.
| Nivel | Ce permite |
|---|---|
privileged | Tot. Pentru componente de sistem. |
baseline | Blochează escaladările evidente: containere privilegiate, namespace-ul de procese al gazdei. |
restricted | Cel mai strict: fără root, fără escaladare de privilegii, sistem de fișiere doar-citire. Ținta pentru aplicații. |
Se aplică în trei moduri: enforce (respinge), audit (scrie în jurnal) și warn (avertizează utilizatorul). Calea obișnuită de adopție e să pornești cu warn, să vezi ce s-ar strica, apoi să treci pe enforce. Pentru reguli mai complexe se folosesc OPA Gatekeeper sau Kyverno.
13.3 NetworkPolicy
Implicit, în Kubernetes orice pod poate vorbi cu orice pod, din orice namespace. E cea mai surprinzătoare valoare implicită din sistem și un răspuns bun la „ce ai îmbunătăți într-un cluster moștenit”. NetworkPolicy schimbă asta: descrii ce trafic e permis, iar restul e blocat. Atenție: are efect doar dacă plugin-ul de rețea îl susține (Calico, Cilium; unele configurații simple îl ignoră tăcut).
13.4 Namespace-ul nu e graniță de securitate
Namespace-ul e o partiție logică: grupează obiecte, susține cote de resurse și dă un punct de aplicare pentru RBAC și NetworkPolicy. Dar podurile din namespace-uri diferite rulează pe aceleași noduri și împart același nucleu. Izolare reală între chiriași diferiți înseamnă noduri separate, sau clustere separate. Dacă spui asta la interviu, te desparți imediat de cei care au citit doar tutoriale.
13.5 Restul listei de igienă
- Dezactivează montarea automată a tokenului de ServiceAccount în podurile care nu vorbesc cu API-ul (
automountServiceAccountToken: false). - Criptează etcd în repaus, sau ține secretele într-un seif extern.
- Nu folosi
:latest. Nu poți ști ce rulează și nu poți reproduce o problemă. - Scanează imaginile în pipeline și blochează pe vulnerabilități critice.
- Ține jurnalul de audit al API-ului pornit. E singura sursă care îți spune cine a schimbat ce.
14 Depanare pe cluster — partea cea mai întrebată
La interviurile de Kubernetes din 2026, întrebările de tip „ce e un pod” sunt doar încălzirea. Partea care contează e scenariul: „ți se spune că serviciul e căzut, ce faci?”. Se testează metoda, nu memoria.
14.1 Ordinea în care investighezi
# 1. Ce stare au podurile? (aici afli 60% din răspuns) kubectl get pods -n prod -o wide # 2. De ce e în starea aia? Uită-te la EVENIMENTE, la finalul ieșirii. kubectl describe pod api-7d9f-x2k -n prod # 3. Ce spune aplicația? kubectl logs api-7d9f-x2k -n prod kubectl logs api-7d9f-x2k -n prod --previous # logurile containerului MORT — esențial la CrashLoop # 4. Ce s-a întâmplat recent în tot namespace-ul? kubectl get events -n prod --sort-by=.lastTimestamp | tail -30 # 5. Serviciul are destinații? (lista goală = selector greșit sau poduri ne-gata) kubectl get endpoints api -n prod # 6. Din interiorul unui pod: DNS și conectivitate kubectl exec -it api-7d9f-x2k -n prod -- sh nslookup api.prod.svc.cluster.local wget -qO- http://api:8080/healthz # 7. Consumul real kubectl top pods -n prod kubectl top nodes # 8. Nodurile sunt sănătoase? kubectl get nodes kubectl describe node nod-3 | grep -A5 Conditions
14.2 Tabelul de diagnostic
| Ce vezi | Ce înseamnă | Cauze frecvente | Unde te uiți |
|---|---|---|---|
Pending | Podul nu a fost programat pe niciun nod. | Requests prea mari; nu există nod care să tolereze taint-ul; PVC nelegat; zonă greșită. | kubectl describe pod → secțiunea Events spune exact de ce a refuzat schedulerul. |
ImagePullBackOffErrImagePull | Nu poate descărca imaginea. | Nume sau etichetă greșită; registry privat fără imagePullSecret; limită de descărcări atinsă. | Events; verifică manual numele imaginii. |
CrashLoopBackOff | Containerul pornește și moare repetat; Kubernetes așteaptă tot mai mult între încercări. | Eroare de configurație; lipsește o variabilă; nu se poate conecta la baza de date; comandă greșită; liveness prea agresivă. | kubectl logs --previous. Fără --previous vezi doar containerul nou, care abia a pornit. |
OOMKilled (137) | A depășit limita de memorie și a fost omorât de nucleu. | Limită prea mică; scurgere de memorie; un lot de date prea mare încărcat în memorie. | kubectl describe pod → Last State: Terminated, Reason: OOMKilled. Compară cu kubectl top. |
Running dar 0/1 READY | Rulează, dar readinessProbe nu trece. Nu primește trafic. | Calea sau portul probei greșit; aplicația chiar nu e gata; o dependență lipsește. | kubectl describe pod → mesajul probei; apoi cheamă manual calea din interiorul podului. |
Terminating blocat | Nu se poate șterge. | Un finalizer care așteaptă ceva; un volum care nu se poate demonta; procesul ignoră SIGTERM. | kubectl get pod -o yaml → câmpul finalizers. |
Evicted | Nodul a rămas fără resurse și a dat podul afară. | Presiune de memorie sau disc pe nod; podul era BestEffort. | kubectl describe node → Conditions. |
Node NotReady | Nodul nu mai raportează. | kubelet oprit; disc plin; rețea; mașina a dispărut. | Consolă cloud; jurnalele kubelet. |
„Întâi confirm ce înseamnă căzut — eroare, lentoare, sau doar pentru unii utilizatori — și de când. Apoi mă uit la poduri: câte sunt gata din câte ar trebui. Dacă podurile sunt bine, problema e mai sus: serviciu, ingress, DNS, certificat. Dacă podurile nu sunt bine, descriu podul și citesc evenimentele, pentru că starea îmi spune deja clasa problemei.
În paralel întreb ce s-a schimbat în ultimele ore, fiindcă în majoritatea cazurilor există o schimbare. Dacă e o livrare recentă, prima mea mișcare e să revin la versiunea anterioară și abia apoi să înțeleg de ce — restabilesc serviciul întâi, diagnostichez după. Comunic pe canalul de incident la început și la fiecare pas important, ca să nu fie nevoie să mă întrerupă cineva ca să întrebe.”
Metoda de mai sus e exact ce ai făcut 24/7 la Metro și la Volvo, doar cu alte unelte. Spune-o ca pe o metodă pe care o ai, nu ca pe o listă pe care ai învățat-o: „Ordinea mea e aceeași de mulți ani — confirm simptomul, restabilesc serviciul, apoi caut cauza. Pe Kubernetes se schimbă comenzile, nu metoda.”
14.3 Un traseu de depanare de rețea, până la capăt
„Serviciul A nu poate ajunge la serviciul B.” Ordinea care găsește problema fără ghicit:
kubectl get pods -l app=b— există poduri și sunt READY? Un pod ne-gata nu e în serviciu.kubectl get endpoints b— lista e goală? Atunci selectorul serviciului nu se potrivește cu etichetele podurilor. Asta e cauza cea mai frecventă.- Din podul A:
nslookup b.namespace.svc.cluster.local— dacă DNS-ul nu răspunde, problema e CoreDNS, nu serviciul tău. - Din podul A:
wget -qO- http://b:8080/— dacă DNS-ul merge dar conexiunea e refuzată, verifică portul:portdin Service trebuie să corespundă cutargetPortși cu portul real al aplicației. kubectl get networkpolicy -n namespace— există o politică ce blochează? Dacă e prima politică din namespace, a schimbat implicitul din „totul permis” în „doar ce e scris”.
15 Terraform și infrastructura ca și cod
15.1 Ideea
Infrastructura nu se creează cu mâna din interfața cloudului. Se descrie în fișiere, fișierele stau în Git, iar unealta aplică diferența dintre ce e scris și ce există. Consecințe: mediile sunt reproductibile, schimbările trec prin revizie ca orice cod, și există un istoric al motivelor.
15.2 Ciclul
15.3 Starea — subiectul care separă
Terraform ține un fișier de stare care leagă ce ai scris de ce există în realitate. Fără el nu știe dacă trebuie să creeze sau să modifice.
- Starea nu stă local. Stă într-un depozit comun (S3 cu blocare în DynamoDB, un container Azure Storage, GCS), altfel doi colegi care aplică simultan își calcă starea unul altuia.
- Blocarea e obligatorie. Două
applyîn paralel pe aceeași stare o pot corupe. - Starea conține secrete în clar. Parola unei baze de date creată de Terraform apare în fișierul de stare. Deci: criptare, acces restrâns, și niciodată în Git.
- Deviația apare când cineva modifică manual în consolă.
terraform plano arată. Remediul tehnic eimportsau realiniere; remediul real e organizațional — se scot drepturile de scriere manuală în producție.
terraform { required_version = "~> 1.9" backend "s3" { # starea, la distanță și blocabilă bucket = "tf-state-firma" key = "prod/retea.tfstate" region = "eu-central-1" dynamodb_table = "tf-locks" encrypt = true } required_providers { aws = { source = "hashicorp/aws", version = "~> 5.60" } } } variable "mediu" { type = string default = "prod" } module "retea" { # module = refolosire între medii source = "./modules/retea" mediu = var.mediu cidr = "10.20.0.0/16" } resource "aws_db_instance" "principal" { identifier = "db-${var.mediu}" engine = "postgres" instance_class = "db.t4g.medium" allocated_storage = 50 lifecycle { prevent_destroy = true # plasă: nu se poate șterge din greșeală } } output "db_endpoint" { value = aws_db_instance.principal.endpoint }
Cum separi mediile? Directoare separate cu fișiere de stare separate și module comune. Workspace-urile sunt utile pentru variații mici, dar dev și prod în același fișier de stare e o rețetă de dezastru.
Ce faci cu o resursă creată manual? terraform import o aduce sub control, apoi scriu definiția ca să se potrivească.
Cum eviți un apply distrugător? Planul e citit de un om, prevent_destroy pe resursele cu date, și apply doar din pipeline, cu o identitate care nu are drept de ștergere pe ce e critic.
Pe RapidBid ai scris Terraform pentru AWS și Azure. Ai deci și un subiect suplimentar pe care mulți nu-l au: ce înseamnă să susții doi furnizori — module separate per furnizor, pentru că resursele nu sunt echivalente, cu o interfață comună deasupra. Dacă te întreabă despre multi-cloud, asta e experiență reală, nu teorie.
16 Cloud: ce trebuie să știi din fiecare
Nu-ți trebuie certificare ca să răspunzi la interviu. Îți trebuie harta serviciilor de bază și echivalențele, pentru că anunțurile cer „AWS sau Azure sau GCP”, iar cine știe unul îl învață pe celălalt.
| Ce trebuie | AWS | Azure | Google Cloud |
|---|---|---|---|
| Mașini virtuale | EC2 | Virtual Machines | Compute Engine |
| Kubernetes administrat | EKS | AKS | GKE |
| Registru de imagini | ECR | ACR | Artifact Registry |
| Stocare de obiecte | S3 | Blob Storage | Cloud Storage |
| Baze relaționale | RDS / Aurora | Azure SQL / DB for PostgreSQL | Cloud SQL |
| Rețea privată | VPC | VNet | VPC |
| Identitate și drepturi | IAM | Entra ID + RBAC | IAM |
| Secrete | Secrets Manager | Key Vault | Secret Manager |
| Monitorizare | CloudWatch | Azure Monitor | Cloud Monitoring |
| Funcții fără server | Lambda | Functions | Cloud Functions / Run |
| CI/CD propriu | CodePipeline | Azure DevOps | Cloud Build |
Îngroșatele din tabel sunt din CV-ul tău: GKE și Cloud SQL de la DISH, Azure DevOps de la RapidBid, plus Terraform pe AWS. Asta înseamnă că ai atins toate cele trei clouduri. Formularea onestă și puternică: „Am lucrat pe Google Cloud în producție — GKE și Cloud SQL — și pe AWS și Azure prin Terraform și Azure DevOps. Serviciile de bază se corespund; ce diferă e modelul de identitate și drepturi, care e partea în care trebuie să fii atent când treci de la unul la altul.”
Costul, subiectul care apare la senior
Un DevOps senior e întrebat despre cost, pentru că e o pârghie reală. Principalele surse de risipă și ce faci cu ele:
- Resurse rezervate mult peste consumul real. Se corectează măsurând consumul efectiv și ajustând
requests. Aici e cea mai mare economie, de obicei. - Medii de test care rulează noaptea și în weekend. Se opresc pe program.
- Discuri și adrese IP rămase orfane după ștergerea resurselor care le foloseau.
- Trafic între zone de disponibilitate, care se taxează și pe care nu-l vede nimeni până nu se uită la factură.
- Etichetare pe echipă și pe mediu, altfel factura nu poate fi atribuită nimănui și nimeni nu o optimizează.
17 Monitorizare, loguri și SLO
17.1 Cei trei piloni
📊 Metrici
Numere în timp: cereri pe secundă, latență, erori, memorie. Ieftine de păstrat mult timp. Răspund la „ce nu merge bine și de când”.
📜 Loguri
Evenimente cu text. Scumpe de păstrat. Răspund la „de ce”. Se scriu structurat (JSON) ca să poată fi căutate.
🔗 Urme (tracing)
Drumul unei cereri prin toate serviciile, cu timpul petrecut în fiecare. Răspund la „unde se pierde timpul”. Indispensabil la microservicii.
17.2 Ce se monitorizează, concret
Două seturi de reguli care se citează des:
Cele patru semnale de aur
Latență (cât durează), trafic (cât de mult se cere), erori (cât eșuează), saturație (cât de plină e resursa cea mai limitată).
Metoda RED, pentru servicii
Rate — cereri pe secundă. Errors — câte eșuează. Duration — distribuția timpilor de răspuns.
Nu te uita la latența medie, uită-te la percentile: p50, p95, p99. Cu o medie de 200 ms poți avea 1% din utilizatori care așteaptă 8 secunde — și ăia sunt cei care sună. Formularea la interviu: „urmăresc p95 și p99, nu media, pentru că media ascunde exact utilizatorii afectați.”
17.3 SLI, SLO, SLA și bugetul de erori
| Termen | Ce e | Exemplu |
|---|---|---|
| SLI | Indicatorul măsurat. | Procentul de cereri care răspund sub 300 ms. |
| SLO | Ținta internă pe acel indicator. | 99,9% din cereri sub 300 ms, pe 30 de zile. |
| SLA | Promisiunea contractuală către client, cu penalizări. | 99,5% disponibilitate lunar. Întotdeauna mai relaxat decât SLO-ul. |
| Buget de erori | Cât ai voie să greșești: 100% minus SLO. | Un SLO de 99,9% pe lună îți dă circa 43 de minute de nerespectare. |
„Bugetul de erori transformă o discuție de opinii într-una cu cifre. Dacă mai avem buget, echipa poate livra agresiv. Dacă l-am consumat, oprim funcționalitățile noi și lucrăm la stabilitate până se reface. Nu mai e nevoie ca cineva să convingă pe altcineva că «e prea riscant» — decizia o dă măsurătoarea.”
17.4 Alertele
Cea mai frecventă problemă în echipele reale nu e lipsa alertelor, ci prea multe. Când primești patruzeci de alerte pe zi și treizeci și nouă nu cer nimic, încetezi să le citești — și o ratezi pe a patruzecea. Regulile care rezolvă:
- Alertează pe simptom, nu pe cauză. „Rata de erori a crescut peste prag” e util. „CPU la 90%” nu e — poate fi perfect normal.
- Fiecare alertă care sună noaptea trebuie să ceară o acțiune umană imediată. Dacă poate aștepta până dimineață, nu e alertă, e tichet.
- Fiecare alertă are un runbook — un document scurt care spune ce verifici și ce faci.
- Alertează pe rata de consum a bugetului de erori, nu pe fiecare vârf.
17.5 Unelte
Prometheus colectează metrici trăgându-le periodic de la aplicații și se interoghează cu PromQL. Grafana desenează. Alertmanager rutează alertele și le grupează. Loki sau ELK pentru loguri. Jaeger sau Tempo pentru urme. OpenTelemetry e standardul de instrumentare care le alimentează pe toate. Alternativa comercială care le face pe toate într-un singur produs: Datadog, New Relic, Dynatrace.
Ai lucrat cu Nagios și cu suita Tivoli. Sunt generația anterioară, dar ideea e identică: colectezi semnale, definești praguri, rutezi alerta către cine e de gardă. Dacă te întreabă de Prometheus, nu spune „n-am lucrat”. Spune ce ai făcut și unde e diferența: „Monitorizarea am făcut-o cu Nagios și Tivoli. Modelul e același — semnal, prag, alertă, cine răspunde. Ce e diferit la Prometheus e că el trage metricile de la aplicații și că interogarea e un limbaj propriu, PromQL, în loc de verificări scrise una câte una.”
18 Incidente și gardă
18.1 Rolurile într-un incident mare
- Comandantul incidentului — coordonează, nu depanează. Ia deciziile și ține ordinea.
- Responsabilul de comunicare — informează părțile interesate și clienții la intervale fixe, ca nimeni să nu întrerupă echipa tehnică ca să întrebe „cum merge”.
- Cei care repară — se uită efectiv în sistem.
- Secretarul — notează cronologia cu ore exacte. Fără ea, analiza de după e o discuție din amintiri.
În echipe mici, o persoană poate purta mai multe pălării. Ce nu se face niciodată: comandantul să intre și el cu mâinile în sistem, pentru că atunci nimeni nu mai are imaginea de ansamblu.
18.2 Analiza de după, fără vinovați
Principiul: oamenii nu sunt cauza; sistemul care a permis greșeala este. Dacă o comandă greșită a putut șterge producția, problema nu e omul care a scris-o, e faptul că sistemul a acceptat-o fără confirmare, fără limită de rază și fără posibilitate de revenire. Dacă în analiză apare numele cuiva în loc de o cauză de sistem, oamenii vor ascunde incidentele data viitoare, și pierzi exact informația care ți-ar fi trebuit.
Structura documentului: rezumat, impact (cine, cât timp, ce nu a mers), cronologie cu ore, cauze contributive (la plural), ce a mers bine, ce a mers prost, acțiuni cu responsabil și termen. Fără ultima secțiune, e literatură.
Control de producție 24/7 la Metro, gardă, escaladare, comunicare către client. Asta nu se învață dintr-un curs și e exact ce caută o echipă care are un serviciu în funcțiune. Când ajungi la subiectul incidente, încetinește și dă un exemplu concret — e terenul tău.
19 Securitate în pipeline (DevSecOps)
Ideea într-o frază: verificările de securitate se mută devreme în procesul de dezvoltare, unde sunt ieftine, în loc să fie un audit la final.
| Unde | Ce verifici | Cu ce |
|---|---|---|
| Înainte de commit | Secrete scrise din greșeală în cod. | gitleaks, git-secrets, ca hook local. |
| La fiecare PR | Vulnerabilități în codul propriu (SAST). | CodeQL, Semgrep, SonarQube. |
| La fiecare PR | Dependențe cu vulnerabilități cunoscute (SCA). | Dependabot, Snyk, npm audit. |
| După build | Vulnerabilități în imaginea de container. | Trivy, Grype. |
| După build | Lista completă de componente (SBOM) și semnătura imaginii. | Syft pentru SBOM, Cosign pentru semnătură. |
| Înainte de apply | Configurații nesigure în Terraform și în manifestele Kubernetes. | Checkov, tfsec, kube-score. |
| În funcționare | Ce rulează efectiv și dacă se comportă anormal. | Falco, agenți de securitate ai furnizorului. |
Dacă o cheie a fost urcată în depozit, ea rămâne în istoric și în toate clonele existente. Ordinea corectă e: întâi o invalidezi la furnizor și generezi alta, abia apoi cureți istoricul. Invers, cureți istoricul degeaba — cheia e deja copiată. E o întrebare care apare și care separă răspunsul învățat de cel trăit.
Lanțul de aprovizionare software
Subiect tot mai prezent după incidentele din ultimii ani. Punctele principale: fixează acțiunile și imaginile pe hash, nu pe etichetă; semnează artefactele și verifică semnătura la livrare; generează SBOM ca să știi în cinci minute dacă ești afectat de o vulnerabilitate nouă; dă pipeline-ului permisiuni minime, obținute prin OIDC în loc de chei permanente.
20 Bash și Python
Aproape orice anunț cere „scripting”. Nu ți se cere să fii programator: ți se cere să automatizezi ceva repetitiv și să nu faci pagubă când scriptul dă greș la jumătate.
20.1 Antetul care te salvează
#!/usr/bin/env bash set -euo pipefail # -e : oprește la prima comandă care eșuează # -u : eroare la folosirea unei variabile nedefinite (prinde typo-urile) # -o pipefail : o conductă eșuează dacă ORICARE element eșuează, nu doar ultimul IFS=$'\n\t' # nu mai despărți pe spații — protejează căile cu spații # curățenie garantată, orice s-ar întâmpla TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT # argument obligatoriu, cu mesaj clar dacă lipsește MEDIU=${1:?folosire: $0 <mediu>} log() { printf '%s [%s] %s\n' "$(date +%FT%T)" "$1" "$2" >&2; } # reîncercare cu așteptare crescătoare — obligatoriu la orice apel de rețea retry() { local n=0 max=5 delay=2 until "$@"; do n=$((n+1)) [ "$n" -ge "$max" ] && { log ERROR "eșec după $max încercări: $*"; return 1; } log WARN "încercarea $n a eșuat, reiau în ${delay}s" sleep "$delay"; delay=$((delay*2)) done } retry curl -fsS "https://api.intern/$MEDIU/status" > "$TMP/status.json" log INFO "gata"
curlfără-fîntoarce succes chiar și la un răspuns 500, pentru că a descărcat pagina de eroare cu succes. Aproape toată lumea greșește asta."$@"păstrează argumentele separate;"$*"le lipește într-un singur șir. La căi cu spații, diferența e între „merge” și „șterge altceva”.- Ghilimelele în jurul variabilelor nu sunt stil, sunt corectitudine.
- Idempotență: scriptul rulat de două ori dă același rezultat. Și
flockpe fișier ca să nu ruleze două instanțe simultan din cron. shellcheckpus în pipeline prinde majoritatea acestor greșeli automat.
20.2 Când treci la Python
Regula practică: Bash până la o sută de linii sau până când începi să parsezi JSON serios; de acolo, Python. Semnalele că ai depășit: ai nevoie de structuri de date, de tratare de erori pe cazuri, de apeluri HTTP cu antete și reîncercări, de teste.
import os, sys, json, logging import requests from tenacity import retry, stop_after_attempt, wait_exponential logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") TOKEN = os.environ["API_TOKEN"] # din mediu, NU din cod @retry(stop=stop_after_attempt(5), wait=wait_exponential(min=1, max=30)) def ia_starea(mediu: str) -> dict: r = requests.get(f"https://api.intern/{mediu}/status", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=10) # timeout SIEMPRE — altfel atârnă la infinit r.raise_for_status() return r.json() if __name__ == "__main__": try: print(json.dumps(ia_starea(sys.argv[1]), indent=2)) except Exception as e: logging.error("nu am putut citi starea: %s", e) sys.exit(1) # cod de ieșire corect, ca pipeline-ul să știe
20.3 Linux — ordinea în care investighezi un server
| Întrebarea | Comanda |
|---|---|
| Procesul rulează? | systemctl status serviciu · ps aux | grep nume |
| Ascultă pe port? | ss -ltnp · lsof -i :443 |
| Se poate ajunge la el? | curl -v · telnet gazda port · traceroute |
| Numele se rezolvă? | dig nume · nslookup |
| A rămas fără ceva? | df -h (disc) · free -h (memorie) · top · iostat |
| Ce spune jurnalul? | journalctl -u serviciu -n 100 --no-pager · dmesg -T | tail |
| Cine ocupă discul? | du -sh /* 2>/dev/null | sort -h |
| Ce s-a schimbat? | last · istoricul shell · jurnalul de livrări |
„Discul.” E banal și e cauza cea mai des întâlnită: un disc plin oprește aproape orice serviciu, și adesea în moduri care par să nu aibă legătură — baza de date nu mai scrie, jurnalele se opresc, procesele se blochează. E prima comandă pe care o dau, pentru că durează o secundă și elimină cea mai probabilă cauză.
21 Experiența ta, tradusă în limbajul rolului
Asta e secțiunea de care ai cea mai mare nevoie. Nu îți lipsește experiența — îți lipsește obiceiul de a o numi în cuvintele pe care le caută cel din fața ta. Un om care a ținut producția în picioare la Metro și a migrat cozi de mesaje la Volvo are exact fondul pe care se construiește rolul de DevOps; dacă îl povestește cu termenii din 2012, ascultătorul aude „administrator de sistem”, nu „inginer de fiabilitate”.
| Ce ai făcut | Cum se numește azi | Fraza pe care o spui |
|---|---|---|
| Control de producție 24/7, gardă, escaladare (Metro, IBM) | Inginerie de fiabilitate, răspuns la incidente, gardă | „Am lucrat ani în control de producție non-stop. Am preluat incidente, am escaladat pe severitate și am comunicat către client cât timp problema era deschisă.” |
| Nagios, suita Tivoli | Monitorizare și alertare | „Monitorizarea am făcut-o cu Nagios și Tivoli — semnale, praguri, rutarea alertei. Modelul e același cu Prometheus, diferă modul de colectare și limbajul de interogare.” |
| Migrare WebSphere MQ v8 → v9, manageri de coadă în mai multe instanțe (Volvo Cars) | Modernizare de platformă, disponibilitate ridicată, migrare fără întrerupere | „Am migrat platforma de mesagerie de la o versiune majoră la alta, pe o configurație în mai multe instanțe, adică exact cazul în care nu ai voie să oprești fluxul.” |
| TADDM (Etihad) | Descoperire automată, inventar de configurație, hărți de dependențe | „Am lucrat la descoperirea automată a infrastructurii și la maparea dependențelor între aplicații — la ce vorbește cu ce, ceea ce contează enorm când planifici o schimbare.” |
| AIX, Linux, shell | Administrare de sistem, scripting | „Am administrat sisteme UNIX în producție. Depanarea la nivel de sistem — disc, memorie, procese, rețea — o fac fără să mă uit pe documentație.” |
| GKE, 17 pod-uri, 44 de baze Cloud SQL (DISH Digital Solutions) | Kubernetes administrat, operare pe Google Cloud | „Am operat pe Google Kubernetes Engine, cu 17 pod-uri și 44 de baze Cloud SQL în spate, și am făcut livrări fără întrerupere de serviciu.” |
| Terraform pe AWS și Azure (RapidBid) | Infrastructură ca și cod, multi-furnizor | „Am scris infrastructura ca și cod în Terraform, pe doi furnizori — AWS și Azure. Ce e diferit între ei sunt furnizorii și modul de autentificare; fluxul plan–apply și disciplina stării sunt identice.” |
| Azure DevOps, Jenkins cu livrare albastru-verde (RapidBid) | CI/CD, strategii de livrare | „Am construit conducte în Azure DevOps și în Jenkins, inclusiv o livrare albastru-verde, cu comutarea traficului după ce mediul nou trecea verificările.” |
| Servere proprii, n8n, integrare cu modele de limbaj (ultimii ani) | Automatizare, integrare de sisteme, operare de la un capăt la altul | „În ultimii ani am construit și am ținut în funcțiune sisteme proprii, de la server până la automatizări și integrări. Am făcut toate rolurile, inclusiv pe cele care mă interesează aici.” |
Fiecare rând de mai sus e verificabil în CV-ul tău. Dacă la interviu te trezești adăugând un detaliu care sună bine — un procent, un mecanism, un instrument — și nu ești sigur că s-a întâmplat exact așa, oprește-te. Un intervievator tehnic sapă exact acolo unde detaliul sună impresionant, și dacă a doua întrebare te prinde fără răspuns, pierzi și ce era adevărat înainte. „Nu-mi mai amintesc exact, ar trebui să mă uit” e un răspuns care nu a costat niciodată pe nimeni un post.
21.1 Unde ești tare și unde vei fi presat
🟢 Teren solid
Incidente și gardă. Depanare la nivel de sistem. Migrări pe sisteme care nu au voie să cadă. Comunicare cu clientul sub presiune. Vechime într-o multinațională, cu proceduri reale. Operare pe Kubernetes administrat.
🟠 Unde vor săpa
Cât Kubernetes ai scris tu, nu doar operat. Câte conducte ai construit de la zero. GitHub Actions. Cât cod scrii efectiv. Ce ai făcut în pauza din multinațională.
„Pe partea asta am lucrat mai puțin decât pe restul. Ce am făcut concret e [lucrul exact]. Ce nu am făcut e [lucrul exact]. Cunosc modelul, iar diferența până la ce cereți voi o închid în câteva săptămâni de lucru efectiv — am mai făcut trecerea asta când am intrat pe Google Cloud.”
Structura are trei bucăți și fiecare face o treabă: recunoști (nu te prind ei), delimitezi exact (arăți că știi ce știi, ceea ce e mai rar decât pare), arăți traseul (dai motivul pentru care riscul lor e mic). Ce nu faci niciodată: nu spui doar „nu am lucrat cu asta” și nu taci după.
22 Plan de învățare
Nu ai nevoie să știi tot. Ai nevoie ca lucrurile pe care le spui să fie susținute de ceva ce ai atins cu mâna. Planul de mai jos e construit pe asta: fiecare bloc se termină cu un lucru care există pe calculatorul tău și despre care poți vorbi la trecut.
| Bloc | Ce faci | Ce rămâne în urmă |
|---|---|---|
| 1 | Instalezi un cluster local (kind sau minikube). Scrii de mână un Deployment, un Service și un Ingress pentru o aplicație banală. | Poți spune „am scris manifestele” fără să exagerezi. |
| 2 | Strici lucrurile intenționat: imagine inexistentă, sondă de pregătire pe o cale greșită, limită de memorie prea mică. Repari fiecare caz citind describe și logs. | Ai văzut cu ochii tăi ImagePullBackOff, CrashLoopBackOff și OOMKilled. Astea sunt întrebări sigure la interviu. |
| 3 | Un depozit pe GitHub cu un workflow care construiește o imagine, o urcă în ghcr și rulează teste. Adaugi needs, o matrice și memorie tampon. | Răspunsul onest la „ai lucrat cu GitHub Actions?” devine mai puternic. |
| 4 | Conectezi workflow-ul la clusterul local sau la unul gratuit și faci o actualizare progresivă. Apoi provoci o eșuare și dai rollout undo. | Ai dus un lanț complet de la commit până în producție. |
| 5 | Terraform: o resursă simplă la un furnizor, cu stare la distanță și blocare. Faci o modificare manuală în consolă și vezi driftul în plan. | Povestea despre drift devine trăită, nu citită. |
| 6 | Prometheus și Grafana peste clusterul local. Un tablou de bord cu cele patru semnale de aur și o alertă care chiar se declanșează. | Poți lega Nagios de Prometheus într-o singură frază credibilă. |
| 7 | Repetiție cu voce tare: capitolele 9–14 din pagina asta, explicate ca și cum ai preda. | Frazele ies fluent. Ăsta e scopul întregului document. |
Un lucru stricat și reparat cu mâna ta valorează cât zece capitole citite. Intervievatorii tehnici recunosc imediat diferența dintre cineva care a văzut un pod blocat în Pending și cineva care a citit ce înseamnă. Primul spune „m-am uitat în evenimente și nu avea nod care să-l primească”. Al doilea spune definiția.
Certificări, dacă vrei să pui ceva pe hârtie
CKA (Certified Kubernetes Administrator) e cea mai bine văzută dintre toate, pentru că e examen practic — stai în terminal și rezolvi. KCNA e varianta de intrare, doar cu întrebări. Terraform Associate e ieftin și rapid. Certificările de furnizor (AWS, Azure, Google Cloud) contează mai ales când firma lucrează pe acel furnizor. Nicio certificare nu înlocuiește un cluster stricat și reparat, dar CKA trece filtrele automate de selecție, ceea ce e o problemă reală și separată.
23 Glosar
Termenii care apar în conversație și se presupun cunoscuți. Dacă unul dintre ei te blochează în timpul interviului, ai pierdut firul propoziției — de aceea sunt aici.
Infrastructură
Idempotent — rulat de două ori, dă același rezultat. Declarativ — descrii starea dorită, nu pașii. Imperativ — dai pașii. Drift — realitatea a plecat de la ce zice codul. Provizionare — crearea resurselor. Efemer — care dispare fără urmări.
Livrare
Artefact — rezultatul construirii (imagine, arhivă). Registru — depozitul de imagini. Etichetă imutabilă — o etichetă care nu se mai suprascrie niciodată. Revenire — întoarcerea la versiunea anterioară. Raza de acțiune — cât se strică dacă schimbarea e greșită.
Fiabilitate
Disponibilitate — procentul de timp în care serviciul răspunde. Redundanță — exemplare de rezervă. Punct unic de eșec — piesa care, dacă pică, oprește tot. Degradare grațioasă — funcționează parțial în loc să cadă. Presiune inversă — sistemul refuză cereri ca să nu se prăbușească. Efect de turmă — toți clienții reîncearcă simultan și dărâmă serviciul care tocmai revenea.
Kubernetes
Manifest — fișierul YAML care descrie un obiect. Reconciliere — bucla care aduce realul la dorit. Etichetă — perechea cheie-valoare pe care o folosesc selectoarele. Adnotare — metadate fără rol de selecție. Sidecar — container auxiliar în același pod. Evacuare — scoaterea unui pod de pe un nod sub presiune. Cordonare — nodul nu mai primește pod-uri noi.
Rețea
Proxy invers — stă în față și distribuie către servicii. Terminare TLS — locul unde criptarea se oprește. Gazdă virtuală — mai multe domenii pe un IP. CIDR — notația pentru intervale de adrese. Egress / Ingress — trafic care iese / care intră.
Proces
Fără vinovați — analiză pe sistem, nu pe persoană. Runbook — instrucțiunile pentru o situație cunoscută. Interval de nelivrare — perioada în care nu se pune nimic în producție (sărbători, vârfuri de trafic). Comutator de funcționalitate — pornești sau oprești o funcție fără să livrezi cod.
„Poți să repeți întrebarea? Vreau să fiu sigur că răspund la ce m-ai întrebat.”
Fraza asta îți cumpără cinci secunde, sună profesionist și nu te costă nimic. Nimeni, niciodată, nu a fost respins pentru că a cerut o clarificare. Oamenii sunt respinși pentru că au început să vorbească fără să știe unde ajung. Dacă reții un singur lucru din toată pagina, reține-l pe ăsta.