Բոլոր դասերը
DevOps և ամպՄիջին12 րոպե ընթերցումԻմ ստեկում

Git, CI/CD և zero-downtime deploy-եր

Քո լափթոփի commit-ից մինչև անվտանգ, հետ շրջվող release production-ում։

  • Git flow
  • Pull requests
  • Pipelines
  • Zero-downtime deploys
Քայլ 1/4
main-ը այն branch-ն է, որը միշտ պատրաստ է թողարկման։ Յուրաքանչյուր կետ commit է։

30 վայրկյանում

Git-ը գրանցում է քո կոդի յուրաքանչյուր փոփոխություն և թույլ է տալիս թիմին զուգահեռ աշխատել branch-ների վրա։ Continuous integration-ը յուրաքանչյուր փոփոխության վրա գործարկում է ստուգումներ (lint, թեստեր, build), այնպես որ խնդիրները բռնվում են րոպեների ընթացքում, ոչ թե production-ում։ Continuous delivery-ն այնուհետև թեստավորված build-ը սերվերներ է հասցնում այնպես, որ օգտատերերը չնկատեն, և դու կարողանաս արագ հետ շրջել։

Ինչու եմ օգտագործում

  • Git-ը օգտագործում եմ որպես թիմի ընդհանուր հիշողություն․ յուրաքանչյուր փոփոխություն ունի հեղինակ, պատճառ և հետդարձի ճանապարհ։
  • Ապավինում եմ ավտոմատ ստուգումներին, որովհետև մարդիկ հոգնում են, իսկ pipeline-ները՝ ոչ․ կարմիր build-ն ավելի էժան է, քան կոտրված checkout էջը։
  • Zero-downtime deploy-երն ինձ համար կարևոր են, որովհետև հաճախորդներն ընդունում են պատվերներ և վճարումներ շուրջօրյա, ուստի maintenance էջի համար լավ պահ չկա։
  • Փոքր, հաճախակի, հետ շրջվող release-ները deploy-ի վախը նվազեցնող ամենամեծ գործոնն են, որ ես գիտեմ։

Ինչպես է աշխատում

01Git-ի հիմունքներ՝ commit-ներ, branch-ներ, merge ընդդեմ rebase

Commit-ը ամբողջ նախագծի snapshot է՝ գումարած հաղորդագրություն և ցուցիչ դեպի իր ծնողը, ուստի պատմությունը snapshot-ների շղթա է։ Branch-ը պարզապես շարժական պիտակ է, որը ցույց է տալիս որևէ commit, և այդ պատճառով branch-ները էժան են։ Երբ աշխատանքի երկու գիծ պետք է միավորվեն, կա՛մ merge ես անում, կա՛մ rebase։ Merge-ը ստեղծում է նոր commit երկու ծնողով և պատմությունը պահում է ճիշտ այնպես, ինչպես եղել է։ Rebase-ը քո commit-ները նորից կիրառում է վերջին main-ի վրա՝ ստանալով ուղիղ գիծ, բայց այդ commit-ները վերագրում է նոր hash-երով։ Կանոնը՝ rebase արա քո սեփական լոկալ կամ feature branch-ը՝ այն կարգի բերելու համար, բայց երբեք rebase մի արա այն commit-ները, որոնք ուրիշներն արդեն pull են արել։ Լավ commit-ները փոքր են և կենտրոնացած, իսկ հաղորդագրությունը բացատրում է՝ ինչու, ոչ միայն՝ ինչ։

bash
git switch -c feature/order-export
git add -p                       # stage hunks, review as you go
git commit -m "Add CSV export for orders"
git fetch origin
git rebase origin/main           # update your branch, resolve conflicts
git push --force-with-lease      # safe force-push of your own branch
git log --oneline --graph -15

02Branching ռազմավարություններ՝ trunk-based ընդդեմ GitFlow

Trunk-based development-ը պահում է մեկ main branch, որը միշտ պատրաստ է release-ի։ Ծրագրավորողներն աշխատում են կարճատև branch-ների վրա, որոնք իդեալում merge են արվում մեկ-երկու օրում, իսկ անավարտ ֆունկցիոնալը թաքցնում են feature flag-երի հետևում։ Սա նվազագույնի է հասցնում ցավոտ merge-երը և բնականորեն համադրվում է continuous deployment-ի հետ։ GitFlow-ն օգտագործում է երկարակյաց develop և main branch-ներ՝ գումարած feature, release և hotfix branch-ներ։ Այն հարմար է տարբերակավորված, պլանավորված release-ներով արտադրանքի համար, օրինակ՝ mobile հավելվածներ, որոնք անցնում են store review, կամ on-premise ծրագրեր, որտեղ միաժամանակ մի քանի տարբերակ է սպասարկվում։ Վեբ հավելվածների մեծ մասի համար հաղթում է trunk-based-ը՝ կարճ pull request-ներով։ Ինչ էլ ընտրես, պաշտպանիր main-ը․ պահանջիր pull request-ներ, անցած ստուգումներ և առնվազն մեկ review, և արգելիր force-push-երը։

  • Trunk-based՝ կարճ branch-ներ, feature flag-եր, deploy main-ից։
  • GitFlow՝ develop + release branch-ներ, լավ է տարբերակավորված release-ների համար։
  • Release tag-երը (v1.8.0) ճշգրիտ նշում են, թե ինչ է գնացել production։

03Pull request-ներ և code review

Pull request-ը branch-ը merge անելու առաջարկ է՝ գումարած դրա շուրջ զրույց։ Դրա խնդիրն է բռնել բագեր, կիսվել գիտելիքով և կոդի բազան միատարր պահել, ոչ թե ցույց տալ, թե ով է ավելի senior։ Pull request-ները պահիր փոքր, իդեալում մի քանի հարյուր տողից պակաս, որովհետև review-ի որակը կտրուկ ընկնում է չափի աճի հետ։ Նկարագրությունը պետք է ասի, թե ինչ է փոխվել, ինչու և ինչպես է թեստավորվել, իսկ UI աշխատանքի համար՝ screenshot-ներով։ Ոճն ու ֆորմատավորումը պետք է թողնել ավտոմատացմանը, որպեսզի մարդիկ կենտրոնանան տրամաբանության, տվյալների անվտանգության, security-ի, անվանումների և բացակայող թեստերի վրա։ Review անողները պետք է լինեն կոնկրետ և բարի, և առանձնացնեն արգելափակող խնդիրները առաջարկներից։ CODEOWNERS ֆայլերը փոփոխություններն ուղղորդում են այն մարդկանց, ովքեր գիտեն տվյալ հատվածը, իսկ պարտադիր status check-երը թույլ չեն տալիս որևէ բան merge անել, քանի դեռ CI-ը կարմիր է։

04Continuous integration՝ pipeline, որն աշխատում է յուրաքանչյուր փոփոխության վրա

Continuous integration-ը նշանակում է, որ յուրաքանչյուր push և pull request ավտոմատ կերպով build և թեստավորվում է մաքուր միջավայրում։ Տիպիկ pipeline-ը տեղադրում է կախվածությունները lockfile-ից, գործարկում է linter-ը և type checker-ը, unit և feature թեստերը, և կառուցում production artifact-ը։ Քայլերը դասավորիր ամենաարագից դեպի ամենադանդաղը, որպեսզի խափանումների մեծ մասը երևա մեկ-երկու րոպեում։ Քեշավորիր կախվածությունների ներբեռնումները գործարկումների միջև։ Pipeline-ը պետք է բոլորի համար նույն դարպասը լինի․ եթե այն կարմիր է, փոփոխությունը merge չի արվում։ Ստորև Node կամ Next.js նախագծի համար մինիմալ GitHub Actions workflow է․ GitLab CI-ը, Bitbucket Pipelines-ը և մյուսները օգտագործում են շատ նման գաղափարներ՝ այլ syntax-ով։ Գաղտնիքներն ապրում են CI մատակարարի secret store-ում, երբեք՝ repository-ում։

yaml
name: ci
on:
  pull_request:
  push:
    branches: [main]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with: { node-version: 24, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm test
      - run: npm run build

05Deploy-ի ռազմավարություններ՝ rolling, blue-green, canary

Continuous delivery-ն նշանակում է, որ յուրաքանչյուր կանաչ build կարելի է deploy անել մեկ կտտոցով, իսկ continuous deployment-ը՝ որ այն deploy է արվում ավտոմատ։ Կարևոր է, թե ինչպես ես փոխարինում աշխատող տարբերակը։ Rolling deploy-ը load balancer-ի հետևում instance-ները փոխարինում է մեկ առ մեկ, ուստի հզորությունը մի փոքր նվազում է, բայց ծառայությունը շարունակվում է։ Blue-green-ը պահում է երկու նույնական միջավայր․ deploy ես անում պարապ մնացածի վրա (green), թեստավորում ես, ապա տրաֆիկը փոխարկում ես load balancer-ում կամ symlink-ով, և հետ փոխարկելը ակնթարթային է։ Canary-ն տրաֆիկի փոքր մասը, ասենք հինգ տոկոսը, ուղարկում է նոր տարբերակին, հետևում է սխալների մակարդակին և latency-ին, և աստիճանաբար ընդլայնում է։ Մեկ սերվերի վրա blue-green գաղափարը սովորաբար իրականացվում է release թղթապանակներով և ատոմար symlink փոխարկմամբ, կամ նոր container-ով, որը գործարկվում և health check է անցնում, նախքան proxy-ն ուղղվի դեպի այն։

  • Rolling՝ էժան, աստիճանական, միաժամանակ աշխատում են երկու տարբերակ։
  • Blue-green՝ ակնթարթային փոխարկում և ակնթարթային rollback, կարճ ժամանակով պետք է կրկնակի հզորություն։
  • Canary՝ սահմանափակում է վնասի շառավիղը, գնահատելու համար պետք են լավ մետրիկաներ։

06Zero-downtime deploy-եր Laravel-ի և Next.js-ի համար

VPS-ի վրա Laravel-ի համար ապացուցված մոտեցումը ատոմար release-ներն են․ յուրաքանչյուր deploy գնում է նոր, timestamp-ով թղթապանակ, symlink-ներով կիսում է .env-ը և storage թղթապանակը, այնտեղ տեղադրում է կախվածությունները և կառուցում քեշերը, և միայն դրանից հետո մեկ ատոմար քայլով փոխում է current symlink-ը։ PHP-FPM-ի reload-ը մաքրում է OPcache-ը, իսկ queue:restart-ը worker-ներին ասում է ավարտել ընթացիկ job-ը և սկսել նոր կոդով։ Next.js-ի համար նույնպես build արա նոր release թղթապանակում, ապա graceful կերպով reload արա Node պրոցեսը (pm2 reload-ը cluster ռեժիմում նոր worker-ներ է գործարկում՝ նախքան հները կանգնեցնելը), կամ գործարկիր նոր container և փոխարկիր proxy-ն, երբ դրա health check-ն անցնի։ Զգույշ եղիր version skew-ից․ հին էջը բաց պահած բրաուզերները կարող են հին JavaScript chunk-եր պահանջել, ուստի նախորդ ստատիկ asset-ները որոշ ժամանակ հասանելի պահիր։

bash
#!/usr/bin/env bash
set -euo pipefail
APP=/var/www/app
RELEASE=$APP/releases/$(date +%Y%m%d%H%M%S)
git clone --depth 1 --branch main [email protected]:your_user/app.git "$RELEASE"
cd "$RELEASE"
rm -rf storage && ln -s $APP/shared/storage storage
ln -s $APP/shared/.env .env
composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build
php artisan migrate --force
php artisan optimize
ln -sfn "$RELEASE" $APP/current          # atomic switch
sudo systemctl reload php8.3-fpm         # match your PHP version
php artisan queue:restart

07Բազայի migration-ներ և rollback-ներ

Կոդը կարելի է հետ գլորել վայրկյանների ընթացքում, տվյալները՝ ոչ։ Ցանկացած deploy-ի ժամանակ հին և նոր կոդը մի պահ աշխատում են միաժամանակ, ուստի յուրաքանչյուր migration պետք է աշխատի երկու տարբերակների հետ էլ։ Expand and contract մոտեցումն սա դարձնում է անվտանգ։ Սյունակը վերանվանելու համար՝ նախ ավելացրու նոր սյունակը և գրիր երկուսի մեջ էլ (expand), deploy արա, հին տողերը լրացրու batch-երով, ընթերցումները փոխարկիր նոր սյունակին, և միայն հաջորդ release-ում ջնջիր հինը (contract)։ Խուսափիր մեծ աղյուսակների վրա երկար lock-երից․ index-ները ավելացրու concurrently, որտեղ բազան դա աջակցում է։ Rollback արա՝ նորից deploy անելով նախորդ release թղթապանակը կամ image tag-ը, և այդ պատճառով պահում ես վերջին մի քանիսը։ Բազայի համար նախընտրիր առաջ շարժվել ուղղող migration-ով, քան production-ում down migration-ներ գործարկել, և ռիսկային փոփոխություններից առաջ միշտ ունեցիր թարմ backup։

  • Սկզբում՝ միայն ավելացնող փոփոխություններ․ նոր սյունակները nullable կամ default արժեքներով։
  • Քանդող փոփոխությունները (drop, rename) միայն այն բանից հետո, երբ ոչ մի կոդ չի օգտագործում հին կառուցվածքը։
  • Դիսկի վրա պահիր մի քանի նախորդ release՝ ակնթարթային rollback-ի համար։

Օգտագործեք, երբ

  • Ցանկացած նախագիծ, որտեղ կա մեկից ավելի ծրագրավորող կամ ամսական մեկից ավելի deploy։
  • Երբ downtime-ը փող արժե՝ խանութներ, ամրագրման համակարգեր, վճարման flow-եր, SaaS։
  • Երբ հաճախորդին պետք է audit trail՝ ինչ է փոխվել, երբ և ինչու։
  • Նաև mobile հավելվածներ․ pipeline-ները կարող են հետևողականորեն build, sign և upload անել store build-երը։

Մի՛ օգտագործեք, երբ

  • Ամբողջական canary ենթակառուցվածք փոքր brochure կայքի համար․ պարզ ատոմար deploy-ը բավական է։
  • GitFlow-ի release branch-ներ վեբ հավելվածում, որը deploy է արվում ամեն օր․ դրանք ավելացնում են արարողակարգ՝ առանց անվտանգության։
  • Ավտոմատ deploy production առանց թեստերի․ դա պարզապես բագերն ավելի արագ է հասցնում։

Այլընտրանքներ

  • GitHub Actions

    GitHub-ի մեջ ներկառուցված CI/CD, action-ների հսկայական marketplace, YAML workflow-եր։

  • GitLab CI/CD

    Ինտեգրված pipeline-ներ, միջավայրեր և container registry մեկ արտադրանքում․ կարելի է self-host անել։

  • Deployer / Envoyer

    PHP-ին ուղղված գործիքներ, որոնք Laravel-ի համար իրականացնում են ատոմար release-թղթապանակային deploy-եր։

  • Kamal

    Docker container-ները deploy է անում սովորական սերվերների վրա՝ zero-downtime փոխարկմամբ, առանց orchestrator-ի։

Լավագույն պրակտիկաներ

  1. Պաշտպանիր main-ը՝ pull request-ներ, պարտադիր կանաչ ստուգումներ և առնվազն մեկ review։
  2. Branch-ները պահիր կարճատև, իսկ pull request-ները՝ այնքան փոքր, որ հնարավոր լինի review անել մեկ նստելով։
  3. Build արա մեկ անգամ, ամենուր deploy արա նույն artifact-ը․ երբեք production-ի համար նորից build մի արա։
  4. Յուրաքանչյուր migration դարձրու հետադարձ համատեղելի ներկայումս աշխատող կոդի հետ։
  5. Վերջին մի քանի release-ները պատրաստ պահիր և rollback-ը փորձիր նախքան այն քեզ պետք կգա։
  6. Յուրաքանչյուր deploy-ից հետո գործարկիր health check և ձախողիր pipeline-ը, եթե այն չանցնի։
  7. Գաղտնիքները պահիր CI-ի secret store-ում կամ սերվերի վրա, երբեք՝ Git-ում։

Տարածված սխալներ

  • Rebase և force-push անել branch, որը թիմակիցներն արդեն pull են արել։
  • git pull և composer install գործարկել անմիջապես live թղթապանակում՝ կայքը մեկ րոպեով կիսաթարմացված թողնելով։
  • Սյունակը ջնջել կամ վերանվանել նույն release-ում, ինչ կոդի փոփոխությունը։
  • Մոռանալ queue:restart-ը, ինչի պատճառով worker-ները ժամերով շարունակում են աշխատել հին կոդով։
  • Հսկայական pull request-ներ, որոնք հաստատվում են արագ հայացքով, որովհետև ոչ ոք իրականում չի կարող դրանք review անել։

Խորը վերլուծություն

08Ինչպես է Git-ը պահում քո աշխատանքը, և ինչու գրեթե ոչինչ չի կորչում

Git-ը բովանդակությամբ հասցեավորվող տվյալների բազա է։ Ֆայլի յուրաքանչյուր տարբերակ պահվում է որպես blob, յուրաքանչյուր թղթապանակ՝ որպես tree, որը թվարկում է blob-ներն ու այլ tree-ներ, իսկ յուրաքանչյուր commit՝ որպես օբյեկտ, որը ցույց է տալիս մեկ արմատային tree, ծնող commit-ները, հեղինակին և հաղորդագրությունը։ Յուրաքանչյուր օբյեկտի անունը նրա բովանդակության hash-ն է, ուստի նույնական ֆայլերը պահվում են մեկ անգամ, իսկ ցանկացած կեղծում փոխում է hash-ը։ Branch-ներն ու tag-երը ref-եր են՝ փոքր ֆայլեր, որոնք պարունակում են մեկ commit-ի hash։ HEAD-ը ցույց է տալիս այն ref-ը, որի վրա դու գտնվում ես։ Քանի որ commit-ները երբեք չեն խմբագրվում, այլ միայն նորերն են ստեղծվում, rebase-ը կամ reset-ը չեն ոչնչացնում հին commit-ները․ դրանք պարզապես կորցնում են իրենց պիտակը։ Reflog-ը գրանցում է HEAD-ի և յուրաքանչյուր branch-ի բոլոր դիրքերը, սովորաբար մոտ 90 օրվա համար, ուստի վատ rebase-ից կամ reset-ից հետո կարող ես գտնել հին hash-ը և branch-ը նորից ուղղել դրա վրա։ Garbage collection-ը անհասանելի օբյեկտները ջնջում է միայն այդ ժամկետից հետո։

bash
git reflog -15                      # where HEAD has been
git branch rescue HEAD@{3}          # recover the state from 3 moves ago
git reset --hard ORIG_HEAD          # undo the last rebase or merge
git fsck --lost-found               # find dangling commits and blobs
git cat-file -p HEAD                # see a commit object: tree, parent, author

09Հենց pipeline-ի պաշտպանությունը

CI համակարգը պահում է production-ի բանալիները, ուստի հարձակվողները թիրախավորում են հենց այն։ Ամենակարևորը չորս միջոց է։ Առաջին՝ նվազագույն իրավունքներ job-ի token-ի համար․ յուրաքանչյուր workflow-ի սկզբում բացահայտ հայտարարիր permissions-ը, լռելյայն՝ միայն կարդալու։ Երկրորդ՝ ամբողջությամբ հրաժարվիր երկարակյաց cloud բանալիներից․ GitHub Actions-ը, GitLab-ը և այլք կարող են տալ կարճաժամկետ OIDC token, որը AWS-ը, Google Cloud-ը կամ Azure-ը փոխանակում են ժամանակավոր credential-ների հետ՝ սահմանափակված մեկ repository-ով և branch-ով։ Երրորդ՝ երրորդ կողմի action-ները ամրացրու commit-ի ամբողջական SHA-ին, ոչ թե tag-ին, որովհետև tag-ը կարող է տեղափոխել ցանկացած մեկը, ով վերահսկում է այդ repository-ն։ Չորրորդ՝ զգույշ եղիր fork-երից եկող pull request-ներով գործարկվող workflow-ների հետ․ pull_request_target-ն աշխատում է հիմնական repository-ի secret-ներով, ուստի այն երբեք չպետք է checkout անի և գործարկի անվստահելի կոդ։ Production deploy-երը պետք է անցնեն պաշտպանված environment-ով՝ պարտադիր reviewer-ներով, որպեսզի կոտրված branch-ը ինքնուրույն deploy անել չկարողանա։

yaml
permissions:
  contents: read
  id-token: write          # allow OIDC token for cloud login
jobs:
  deploy:
    environment: production  # required reviewers + env-scoped secrets
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::111122223333:role/deploy-web
          aws-region: eu-central-1

10Build մեկ անգամ, առաջխաղացում digest-ով և deploy-երը հերթով

Հասուն pipeline-ը artifact-ը build է անում ճիշտ մեկ անգամ՝ main-ի մեջ merge-ի պահին, պիտակում է այն commit-ի SHA-ով և push անում registry։ Այնուհետև staging-ը և production-ը deploy են անում նույն artifact-ը՝ նույնականացված իր անփոփոխ digest-ով, այնպես որ այն, ինչ թեստավորել ես, բայթ առ բայթ նույնն է, ինչ առաքում ես։ Production-ի համար նորից build անելը վերադարձնում է ռիսկը․ այդ ընթացքում կարող էին փոխվել կախվածությունը կամ բազային image-ը։ Միաժամանակ աշխատող երկու deploy կարող են սերվերը թողնել կիսով չափ մեկ, կիսով չափ մյուս տարբերակի վրա, ուստի դրանք հերթագրիր concurrency group-ով և մի չեղարկիր արդեն ընթացող deploy-ը, որովհետև կեսից կանգնեցնելն ավելի վատ է, քան ավարտին հասցնելը։ Գրանցիր յուրաքանչյուր deploy՝ commit-ով, ժամանակով և հեղինակով, որպեսզի «ի՞նչ է հիմա աշխատում production-ում» հարցն ունենա մեկ տողանոց պատասխան։ Rollback-ն այդ դեպքում պարզապես նախորդ digest-ի կրկնակի deploy-ն է։

yaml
concurrency:
  group: deploy-production
  cancel-in-progress: false     # queue, never kill a running deploy
 
# build stage (once):
#   docker build -t registry.example.com/web:$GITHUB_SHA .
#   docker push registry.example.com/web:$GITHUB_SHA
# deploy stages reuse the same tag/digest, no rebuild

11Սխեմայի փոփոխություններ մեծ աղյուսակներում՝ առանց կայքը կողպելու

Expand and contract-ը կոդը և սխեման համատեղելի է պահում, բայց յուրաքանչյուր քայլ դեռ կարող է կողպել աղյուսակը։ PostgreSQL-ում ALTER TABLE-ի ձևերի մեծ մասը վերցնում է ACCESS EXCLUSIVE lock։ Նույնիսկ արագ փոփոխությունը պետք է սպասի ցանկացած երկար transaction-ի ավարտին, իսկ քանի դեռ սպասում է, այդ աղյուսակի բոլոր նոր query-ները հերթ են կանգնում նրա հետևում, ուստի երկու միլիվայրկյանանոց migration-ը կարող է րոպեներով սառեցնել կայքը։ Պաշտպանվիր կարճ lock_timeout-ով և կրկնակի փորձերով։ Index-ները ստեղծիր CREATE INDEX CONCURRENTLY-ով, որը չի արգելափակում գրառումները, բայց չի կարող աշխատել transaction-ի ներսում, ուստի այն framework-ներում, որոնք migration-ները փաթաթում են transaction-ի մեջ, դա պետք է անջատել տվյալ migration-ի համար (Laravel-ում migration-ի withinTransaction հատկությունը դիր false)։ Հսկայական աղյուսակի վրա NOT NULL ավելացնելն ավելի անվտանգ է որպես CHECK constraint՝ ավելացված NOT VALID-ով և ավելի ուշ վավերացված։ MySQL-ում նախընտրիր ALGORITHM=INSTANT, որտեղ այն աջակցվում է, կամ gh-ost-ի նման գործիքներ մեծ վերակառուցումների համար։

sql
SET lock_timeout = '3s';
ALTER TABLE orders ADD COLUMN export_status text;   -- metadata-only, fast
 
CREATE INDEX CONCURRENTLY idx_orders_export_status
  ON orders (export_status);
 
ALTER TABLE orders ADD CONSTRAINT orders_export_status_nn
  CHECK (export_status IS NOT NULL) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT orders_export_status_nn;

Իրական իրավիճակներ

01

Secret-ը commit և push է արվել

Խնդիր

Ծրագրավորողը նկատում է, որ մեկ ժամ առաջ push արված commit-ը config ֆայլում պարունակում է production API key։ Repository-ն private է, բայց մի քանի մարդ և CI համակարգն արդեն pull են արել այն։

Ինչպես լուծել

Նախ rotation, հետո մաքրում։ Անմիջապես չեղարկիր բանալին մատակարարի մոտ և ստեղծիր նորը, որովհետև այն ամենը, ինչ push է արվել, պետք է համարել արտահոսած, և ստուգիր մատակարարի log-երը՝ անսպասելի օգտագործման համար։ Այնուհետև ֆայլը հեռացրու կոդից և արժեքը տեղափոխիր secret store։ Պատմությունը git filter-repo-ով վերագրելը private repository-ի համար պարտադիր չէ և բոլորին ստիպում է նորից clone անել, ուստի արա դա միայն եթե քաղաքականությունն է պահանջում։ Վերջում կանխիր կրկնությունը․ host-ի վրա միացրու secret scanning և push protection, ավելացրու pre-commit hook, օրինակ՝ gitleaks, և config ֆայլի pattern-ը ավելացրու .gitignore-ում։

02

Bug-ը հայտնվել է վերջին 200 commit-ներից որևէ մեկում

Խնդիր

Հազվադեպ դեպքում պատվերների գումարները սխալ են։ Վերջին հաստատ ճիշտ release-ը եղել է երեք շաբաթ առաջ, և ոչ ոք չի կարող ասել, թե մոտ 200 commit-ներից որն է բերել խնդիրը։

Ինչպես լուծել

Օգտագործիր git bisect-ը, որը պատմության մեջ կատարում է բինար որոնում, ուստի 200 commit-ի համար պետք է ընդամենը մոտ ութ ստուգում։ Գրիր փոքր script կամ test, որը վերարտադրում է bug-ը և ձախողման դեպքում ավարտվում է ոչ զրոյական կոդով։ Այնուհետև գործարկիր git bisect start, git bisect bad HEAD, git bisect good v1.7.0 և git bisect run ./check-totals.sh։ Git-ը ինքը checkout է անում commit-ները, գործարկում script-ը և հայտնում առաջին վատ commit-ը։ Կարդա դրա diff-ը, script-ից գրիր մշտական regression test, ուղղիր bug-ը և ավարտիր git bisect reset-ով։ Bisect-ը լավագույնս աշխատում է, երբ յուրաքանչյուր commit build է լինում, ինչը ևս մեկ պատճառ է փոքր, ինքնաբավ commit-ների համար։

03

Pipeline-ը տևում է 25 րոպե, և մարդիկ դադարում են սպասել դրան

Խնդիր

Ծրագրավորողները merge են անում՝ չսպասելով CI-ի ավարտին, կամ ձախողված job-երը նորից են գործարկում, մինչև անցնեն։ Main-ը շաբաթը երկու անգամ կարմիր է, և ստուգումներին այլևս ոչ ոք չի վստահում։

Ինչպես լուծել

Նախ չափիր յուրաքանչյուր քայլը․ սովորաբար գերակշռում են երկու-երեք քայլ։ Քեշավորիր կախվածությունների ներբեռնումները՝ lockfile-ի վրա հիմնված key-ով, և քեշավորիր build-ի արդյունքը, որտեղ գործիքն աջակցում է։ Անկախ աշխատանքը բաժանիր զուգահեռ job-երի․ lint-ը և type check-ը, unit test-երը և build-ը կարող են աշխատել կողք կողքի, իսկ մեծ test suite-երը կարելի է shard անել մի քանի runner-ների վրա։ Monorepo-ում ավելորդ job-երը բաց թող path filter-ներով։ Անկայուն test-երը տեղափոխիր առանձին, չարգելափակող job՝ owner-ով և վերջնաժամկետով, rerun-ներ թույլ տալու փոխարեն։ Այնուհետև ստուգումները դարձրու պարտադիր branch protection-ում կամ ավելացրու merge queue, որպեսզի ոչինչ main չհասնի առանց կանաչ գործարկման։ Տասը րոպեից պակաս նպատակը իրատեսական է վեբ հավելվածների մեծ մասի համար։

Արագ ուղեցույց

Վերականգնել կորած commit կամ branch
git reflog then git branch rescue <hash>
Անվտանգ հետ շրջել merge արված pull request-ը
git revert -m 1 <merge-commit-hash>
Գտնել commit-ը, որը ինչ-որ բան կոտրել է
git bisect start && git bisect bad && git bisect good v1.7.0 && git bisect run ./test.sh
Գտնել, թե երբ է տողն ավելացվել կամ ջնջվել
git log -S "calculateTotal" --oneline
Ուղղել ավելի վաղ commit-ը review-ից առաջ
git commit --fixup <hash> && git rebase -i --autosquash origin/main
Աշխատել hotfix-ի վրա առանց stash-ի
git worktree add ../app-hotfix origin/main
Force-push միայն եթե ուրիշ ոչ ոք push չի արել
git push --force-with-lease
Deploy-երը հերթագրել GitHub Actions-ում
concurrency: { group: deploy-production, cancel-in-progress: false }
Թույլ տալ CI-ին մտնել cloud առանց բանալիների
permissions: { id-token: write, contents: read }
Postgres index՝ առանց գրառումներն արգելափակելու
CREATE INDEX CONCURRENTLY idx_name ON orders (column);

Իմ իրական նախագծերում

  • Մոտ 7,100 commit 19 production repository-ներում, ինչը թիմի repo-ների բոլոր commit-ների մոտ կեսն է։
  • React Native հավելվածներն ունեն փաստաթղթավորված release պրոցես, իսկ Remote Config force-update-ը որպես անվտանգության ցանց է ծառայում, երբ հին տարբերակը պետք է դուրս բերվի։
  • POS SaaS-ի offline մասնաճյուղային hub-ը օգտագործում է տարբերակավորված SQLite migration-ներ՝ ամրագրված checksum-ով, ուստի փոփոխված migration-ը չի կարող լուռ գործարկվել։
  • Փոփոխություններն անցնում են planner, implementer և reviewer agent-ների workflow-ով՝ խմբագրումից հետո type-check hook-երով, նախքան որևէ բան merge արվի։

Հարցեր և պատասխաններ

Q1Ո՞րն է տարբերությունը merge-ի և rebase-ի միջև, և ե՞րբ ես օգտագործում յուրաքանչյուրը։

Merge-ը միացնում է երկու branch merge commit-ով, որն ունի երկու ծնող․ պատմությունը մնում է ճիշտ այնպես, ինչպես եղել է, ներառյալ, թե երբ են branch-ները բաժանվել։ Rebase-ը վերցնում է քո commit-ները և նորից կիրառում դրանք մեկ այլ բազայի վրա՝ ստանալով գծային պատմություն, բայց ստեղծում է նոր commit-ներ՝ նոր hash-երով։ Ես իմ feature branch-ը rebase եմ անում վերջին main-ի վրա՝ նախքան pull request բացելը կամ թարմացնելը, որովհետև դա diff-ը մաքուր է պահում, և կոնֆլիկտները լուծում է այն մարդը, ով գիտի փոփոխությունը։ Երբեք rebase չեմ անում այն commit-ները, որոնք ուրիշներն արդեն pull են արել։ Main-ի մեջ ինտեգրելու համար merge-ը կամ squash-merge-ը նորմալ են․ թիմը պարզապես պետք է ընտրի մեկ պայմանավորվածություն։

Q2Trunk-based development, թե՞ GitFlow։

Հաճախ deploy արվող վեբ հավելվածի համար՝ trunk-based։ Կարճատև branch-ները, որոնք merge են արվում մեկ-երկու օրում, խուսափում են մեծ merge կոնֆլիկտներից, main-ը միշտ պատրաստ են պահում release-ի և համապատասխանում են continuous deployment-ին։ Անավարտ աշխատանքը թաքցվում է feature flag-երի հետևում, այլ ոչ թե շաբաթներով ապրում branch-ի վրա։ GitFlow-ը դեռ իմաստ ունի, երբ release-ները տարբերակավորված են և պլանավորված, օրինակ՝ store review անցնող mobile հավելվածներ, desktop ծրագրեր կամ արտադրանք, որտեղ զուգահեռ մի քանի տարբերակ է սպասարկվում։ Նույնիսկ այդ դեպքում feature branch-ները կարճ եմ պահում։ Ռազմավարությունն ավելի քիչ է կարևոր, քան կարգապահությունը՝ փոքր փոփոխություններ, պաշտպանված main, պարտադիր ստուգումներ և review-ներ։

Q3Ո՞րն է տարբերությունը continuous integration-ի, delivery-ի և deployment-ի միջև։

Continuous integration-ը նշանակում է, որ յուրաքանչյուր փոփոխություն հաճախ merge է արվում և ավտոմատ build ու թեստավորվում, ուստի ինտեգրման խնդիրները երևում են րոպեների ընթացքում։ Continuous delivery-ն ավելի հեռու է գնում․ pipeline-ն անցած յուրաքանչյուր փոփոխություն ստեղծում է release candidate, որը ցանկացած պահի կարելի է deploy անել production, սովորաբար ձեռքով հաստատման կոճակով։ Continuous deployment-ը հեռացնում է այդ կոճակը, և յուրաքանչյուր կանաչ փոփոխություն ավտոմատ գնում է production։ Deployment-ին պետք են ուժեղ թեստեր, լավ մոնիթորինգ և արագ rollback, որովհետև ոչ մի մարդ չի ստուգում յուրաքանչյուր release։ Շատ թիմեր գործարկում են continuous deployment staging-ի համար և continuous delivery՝ production-ի համար, ինչը խելամիտ միջին տարբերակ է։

Q4Ինչպե՞ս ես գործարկում բազայի migration-ները առանց downtime-ի։

Ենթադրում եմ, որ հին և նոր կոդը որոշ ժամանակ կաշխատեն միաժամանակ, ուստի յուրաքանչյուր migration պետք է համատեղելի լինի երկուսի հետ էլ։ Դա նշանակում է expand and contract։ Expand՝ ավելացրու նոր աղյուսակներ կամ nullable սյունակներ, deploy արա կոդ, որը գրում է երկու կառուցվածքներում էլ, լրացրու գոյություն ունեցող տողերը batch-երով։ Contract՝ երբ այլևս ոչինչ չի կարդում հին կառուցվածքը, հեռացրու այն հաջորդ release-ում։ Խուսափում եմ այն գործողություններից, որոնք մեծ աղյուսակները երկար են lock անում, index-ները հնարավորության դեպքում ավելացնում եմ concurrently, իսկ ծանր տվյալային փոփոխությունները գործարկում եմ որպես background job-եր, ոչ թե deploy-ի ներսում։ Ռիսկային migration-ներից առաջ համոզվում եմ, որ թարմ backup կա, և production-ում նախընտրում եմ առաջ շարժվող ուղղումը down migration-ներից։

Q5Blue-green, թե՞ canary deployment։

Blue-green-ը գործարկում է երկու նույնական միջավայր․ deploy ես անում պարապ մնացածի վրա, ստուգում ես, ապա ամբողջ տրաֆիկը միանգամից փոխարկում։ Rollback-ը պարզապես հետ փոխարկելն է, ինչը շատ արագ է և հեշտ հասկանալի, բայց կարճ ժամանակով պետք է կրկնակի հզորություն, և բոլոր օգտատերերը տեղափոխվում են միասին։ Canary-ն տրաֆիկի փոքր տոկոսը ուղղում է նոր տարբերակին, համեմատում է սխալների մակարդակը և latency-ն հինի հետ, և քայլ առ քայլ մեծացնում բաժինը։ Այն սահմանափակում է վատ release-ի վնասի շառավիղը, բայց որպեսզի արժանի լինի, պետք են լավ մետրիկաներ, տրաֆիկի բաժանում և ավտոմատացում։ Փոքր թիմերը սովորաբար արժեքի մեծ մասը ստանում են blue-green ոճի ատոմար փոխարկումներից։

Q6Deploy-ը կոտրեց production-ը։ Ի՞նչ ես անում։

Սկզբում վերականգնում եմ ծառայությունը, հետո՝ ուսումնասիրում։ Եթե deploy-ը հավանական պատճառն է, անմիջապես rollback եմ անում նախորդ release-ին կամ image tag-ին․ հենց այդ պատճառով եմ մի քանի release պատրաստ պահում։ Եթե migration-ն արդեն փոխել է տվյալները, կոդի rollback-ը պետք է դեռ աշխատի նոր schema-ի հետ, ինչը երաշխավորում է expand and contract-ը։ Եթե rollback-ն անհնար է, առաջ եմ ուղարկում փոքր ուղղում։ Այդ ընթացքում կարգավիճակի մասին տեղեկացնում եմ շահագրգիռ կողմերին։ Հետո գրում եմ կարճ, առանց մեղադրանքի postmortem՝ ժամանակագրություն, արմատային պատճառ, ինչու թեստերն ու մոնիթորինգը չբռնեցին այն, և մեկ-երկու կոնկրետ գործողություն, օրինակ՝ նոր թեստ, alert կամ deploy-ի ստուգում։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս կնախագծեիր CI/CD մի քանի հավելված ունեցող monorepo-ի համար։

Նպատակն այն է, որ յուրաքանչյուր փոփոխություն գործարկի միայն այն աշխատանքը, որի վրա ազդում է, իսկ ընդհանուր կոդի փոփոխությունները դեռ թեստավորեն այն ամենը, ինչը կախված է դրանից։ Կախվածությունների գրաֆը հասկացող build գործիքը, օրինակ՝ Turborepo-ն, Nx-ը կամ Bazel-ը, կարող է հաշվել ազդված նախագծերը և task-երի արդյունքները քեշավորել լոկալ և հեռակա, ուստի անփոփոխ package-ները նորից build չեն արվում։ Այնուհետև CI-ը յուրաքանչյուր ազդված հավելվածի համար զուգահեռ job-երում գործարկում է lint, test-եր և build, իսկ յուրաքանչյուր հավելված պահում է իր deploy pipeline-ը, տարբերակը և rollback-ը։ Path filter-ներն ավելի պարզ սկիզբ են, բայց բաց են թողնում transitive կախվածությունները։ Փոխզիջումներ․ monorepo-ն միջանցիկ փոփոխությունները դարձնում է ատոմար և գործիքները միատեսակ է պահում, բայց պահանջում է այս ներդրումը քեշավորման և սեփականության կանոնների մեջ, սովորաբար՝ CODEOWNERS ըստ թղթապանակների։ Polyrepo-ները pipeline-ները պարզ են պահում, բայց ընդհանուր գրադարանների թարմացումները դարձնում են «release արա և տարբերակը բարձրացրու» ձանձրալի աշխատանք բազմաթիվ repository-ներում։

S2Feature flag-ե՞ր, թե՞ երկարակյաց branch-ներ մեծ feature-ների համար։

Դեպքերի մեծ մասում՝ flag-եր։ Երկարակյաց branch-ը հեռանում է main-ից, ուստի վերջնական merge-ը մեծ է, ռիսկային և դժվար review անելու համար, իսկ feature-ը թեստավորվում է մնացած ամեն ինչի հին տարբերակի դեմ։ Flag-երի դեպքում կոդը merge է արվում փոքր մասերով՝ production-ում անջատված անջատիչի հետևում, այնպես որ ինտեգրումը տեղի է ունենում անընդհատ, և կարող ես feature-ը միացնել աշխատակիցների համար, հետո օգտատերերի մեկ տոկոսի, հետո բոլորի համար, և ակնթարթորեն անջատել առանց deploy-ի։ Գինը իրական է․ կոդի երկու ճյուղերն էլ պետք է աշխատեն և թեստավորվեն, flag-երն ավելացնում են պայմանական բարդություն, իսկ մոռացված flag-երը դառնում են տեխնիկական պարտք։ Մեղմիր դա անվանման պայմանավորվածությամբ, յուրաքանչյուր flag-ի համար owner-ով և ժամկետով, ինչպես նաև մաքրման ticket-ով, որը ստեղծվում է այն օրը, երբ flag-ը հասնում է հարյուր տոկոսի։

S3Migration-ը անշրջելի է։ Ինչպե՞ս, այնուամենայնիվ, release-ը դարձնել անվտանգ։

Ընդունիր, որ rollback-ը նշանակում է հետ շրջել կոդը, ոչ թե տվյալները, և նախագծիր այնպես, որ դա միշտ բավարար լինի։ Փոփոխությունը բաժանիր այնպես, որ ռիսկային, քանդող մասը գա վերջում և առանձին․ նախ ավելացնող migration, որը հին կոդն անտեսում է, հետո նոր կոդ, որն այն օգտագործում է, և մեկ-երկու release անց՝ քանդող քայլը, երբ metric-ները ցույց տան, որ հին կառուցվածքն այլևս ոչ ոք չի կարդում։ Այդ վերջին քայլից առաջ արա և ստուգիր թարմ backup, կամ համոզվիր, որ point-in-time recovery-ն այն ծածկում է, և migration-ը փորձարկիր production-ի չափի պատճենի վրա՝ տևողությունը և lock-երը չափելու համար։ Տվյալների մեծ վերագրումները գործարկիր idempotent ֆոնային job-երով՝ փաթեթներով, որպեսզի դրանք կարողանան կանգնել և շարունակվել։ Եթե, այնուամենայնիվ, ինչ-որ բան սխալ գնա, պլանը նոր migration-ով առաջ ուղղելն է, իսկ backup-ը աղետի համար է, ոչ թե սովորական rollback-ի։

Բառարան

Ref
Անվանված ցուցիչ դեպի commit, օրինակ՝ branch կամ tag։ Branch-ը տեղափոխելը պարզապես վերագրում է դրա ref-ը։
HEAD
Այն ref-ը, որը ցույց է տալիս, թե ինչ է հիմա checkout արված․ սովորաբար ցույց է տալիս branch, որն էլ՝ commit։
Reflog
HEAD-ի և branch-ների բոլոր դիրքերի լոկալ մատյան, որով commit-ները վերականգնում են reset-ից կամ rebase-ից հետո։
Fast-forward
Merge, որի ժամանակ թիրախային branch-ը պարզապես առաջ է շարժվում դեպի նոր commit, քանի որ պատմությունը չի ճյուղավորվել։
Squash merge
Pull request-ի բոլոր commit-ների միավորումը main-ում մեկ commit-ի մեջ՝ կարճ և ընթեռնելի պատմության համար։
Merge queue
Համակարգ, որը merge-ից առաջ յուրաքանչյուր pull request թեստավորում է վերջին main-ի և հերթում կանգնած փոփոխությունների վրա։
OIDC token
Կարճաժամկետ ստորագրված ինքնության token, որը CI-ը տալիս է job-ին, իսկ cloud-ը փոխանակում է ժամանակավոր credential-ների հետ։
Artifact
Pipeline-ի build-ի արդյունքը, օրինակ՝ container image կամ արխիվ, որն անփոփոխ deploy է արվում բոլոր միջավայրերում։
Feature flag
Runtime անջատիչ, որը միացնում կամ անջատում է կոդի ճյուղերը առանց deploy-ի՝ release-ը տարանջատելով deploy-ից։
Version skew
Կոդի հին և նոր տարբերակները, կամ client-ն ու server-ը, աշխատում են միաժամանակ և պետք է համագործակցեն։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։