all

Case study

Deployovanje klijentskog Node.js sajta uz GitLab CI/CD i Kubernetes za dva sata

Pregled GitLab, Docker, Kubernetes i zivog sajta

Primer upotrebe

Web-design studio je isporucio novu klijentsku aplikaciju i bilo je potrebno brzo zavrsiti infrastrukturni deo: uzeti postojeci GitLab repozitorijum sa Node.js aplikacijom, izgraditi je komandom npm run build i uciniti je dostupnom kao zivi sajt na klijentskom domenu, uz CI/CD putanju spremnu za naredna izdanja.

Ulazni podaci

  • izvorni kod u GitLab repozitorijumu
  • Node.js aplikacija
  • komanda za produkcioni build: npm run build

Isporuceno

  • funkcionalan sajt na klijentskom domenu
  • GitLab pipeline koji gradi i objavljuje kontejnersku sliku aplikacije pri svakom relevantnom push-u
  • deploy u Kubernetes pripremljen kroz ponovo upotrebljiv pristup zasnovan na Helm-u
  • SSL izdat automatski tokom deploy-a

Kompletno resenje je isporuceno za oko dva sata.


Korak 1. Priprema aplikacije za isporuku u kontejneru

Prvi zadatak je bio da se aplikacija upakuje na predvidiv i ponovljiv nacin, tako da isti artefakt moze da se gradi u CI i deploy-uje u Kubernetes.

Pocetni radni Dockerfile je pripremljen direktno u istom repozitorijumu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Stage 1: Build
FROM node:22.12.0-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM node:22.12.0-alpine AS production
WORKDIR /app
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["node", "/app/.output/server/index.mjs"]

Ovo vec koristi multi-stage build, sto je ispravan smer za produkcionu isporuku.

Zasto je multi-stage build vazan

Kod Node.js projekata, naivna single-stage slika cesto sadrzi:

  • kompletan izvorni kod
  • zavisnosti potrebne za build
  • development zavisnosti
  • cache package manager-a
  • privremene fajlove nastale tokom build procesa

To funkcionise, ali unosi nepotrebnu tezinu u produkciju.

Sa multi-stage build pristupom, sav tezi posao se obavlja u builder fazi, dok runtime faza dobija samo potreban izlaz aplikacije. U ovom slucaju, u runtime-u je potreban samo izgradjeni direktorijum .output. To znaci:

  • manje slike
  • brzi push u registry
  • brzi pull u Kubernetes
  • manje zauzece prostora u container registry-ju
  • manja povrsina napada u produkciji
Dijagram multi-stage Docker build-a

Korak 2. Dorada Dockerfile-a za CI/CD upotrebu

Nakon prve radne verzije, Dockerfile je optimizovan radi predvidivijeg CI build procesa i boljeg koriscenja cache-a.

Finalni optimizovani Dockerfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
FROM node:22.12.0-alpine AS build
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:22.12.0-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production

COPY --from=build /app/.output ./.output

EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Sta je unapredjeno

  1. npm ci umesto npm install
    Time build u CI postaje deterministickiji, jer se zavisnosti instaliraju striktno iz lock fajla.

  2. Sloj sa zavisnostima je odvojen pre kopiranja celog source tree-ja
    Time se poboljsava ponovno koriscenje Docker slojeva. Kada developeri menjaju kod aplikacije, ali ne i zavisnosti, sloj sa instalacijom zavisnosti moze ostati u cache-u.

  3. Runtime faza ostaje minimalna
    Finalni kontejner sadrzi samo produkcioni runtime i izgradjeni izlaz, bez kompletnog source tree-ja i bez build okruzenja.

  4. Cistija runtime komanda
    Malo unapredjenje, ali korisno za citljivost i odrzavanje.

Praktican uticaj na velicinu

Glavna dobit nije dosla od male sintaksne dorade, vec od upotrebe multi-stage runtime slike umesto tipicne single-stage slike sa “svim ukljucenim”.

Za projekat ovog tipa, realno poredjenje izgleda ovako:

  • naivna single-stage slika: ~420 MB
  • finalna multi-stage runtime slika: ~130 MB

To je smanjenje od oko 290 MB po slici, odnosno otprilike 69% manje.

Zasto je to vazno na nedeljnom nivou

Ovaj tim tipicno push-uje nove verzije nekoliko puta po satu. Kada se uzme konzervativan primer:

  • 3 push-a po satu
  • 8 radnih sati dnevno
  • 5 radnih dana nedeljno

To daje:

  • 120 push-eva slika nedeljno

Uz 290 MB ustede po slici, nedeljna razlika iznosi:

  • 120 × 290 MB = 34,800 MB
  • priblizno 34.8 GB manje podataka o slikama

To je samo akumulacija na strani registry-ja pre ciscenja. Ako se garbage collection u registry-ju pokrece jednom nedeljno, ta razlika moze ostati zauzeta tokom celog perioda. Drugim recima, optimizacija koja na nivou jednog build-a izgleda mala, veoma brzo prerasta u desetine gigabajta ustede nedeljno.

Isto smanjenje pomaze i brzini deploy-a, jer svaki push u registry i svaki pull koji Kubernetes nodovi rade prenosi znatno manje podataka. Cak i na umerenoj mrezi, smanjenje od nekoliko stotina MB po izdanju postaje odmah primetno pri cestim isporukama.

Poredjenje pre i posle optimizacije kontejnera

Korak 3. Priprema GitLab pipeline-a

Kada je aplikacija spakovana u kontejner, sledeci korak je bio automatizacija provere build-a i objavljivanja slike u GitLab CI/CD.

Trazeni raspored stage-ova bio je:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
stages:
  - lint
  - test
  - build
  - deploy
  - rollback

workflow:
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
    - if: $CI_PIPLINE_SOURCE == "web"
    - if: $CI_MERGE_REQUEST_ID

U ovoj fazi projekta, pipeline je implementiran do build-a slike i push-a u registry. Stage-ovi deploy i rollback su namerno ostavljeni za sledecu fazu, jer se sam deploy radio kroz Kubernetes + Helmwave.

Primer .gitlab-ci.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
stages:
  - lint
  - test
  - build
  - deploy
  - rollback

workflow:
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPLINE_SOURCE == "web"'
    - if: '$CI_MERGE_REQUEST_ID'

variables:
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA
  IMAGE_NAME: $CI_REGISTRY_IMAGE:$IMAGE_TAG

default:
  image: node:22.12.0-alpine
  before_script:
    - npm ci

lint:
  stage: lint
  script:
    - npm run lint
  rules:
    - if: '$CI_COMMIT_BRANCH'
    - if: '$CI_MERGE_REQUEST_ID'

test:
  stage: test
  script:
    - npm run test --if-present
  rules:
    - if: '$CI_COMMIT_BRANCH'
    - if: '$CI_MERGE_REQUEST_ID'

build_image:
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
  script:
    - docker build -t "$IMAGE_NAME" .
    - docker push "$IMAGE_NAME"
    - docker tag "$IMAGE_NAME" "$CI_REGISTRY_IMAGE:latest"
    - docker push "$CI_REGISTRY_IMAGE:latest"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_PIPLINE_SOURCE == "web"'

deploy:
  stage: deploy
  script:
    - echo "Deployment is handled later via Helmwave into Kubernetes"
  when: manual
  allow_failure: true

rollback:
  stage: rollback
  script:
    - echo "Rollback stage will be implemented with Helmwave release rollback"
  when: manual
  allow_failure: true

Sta ovaj pipeline radi

  • pokrece lint provere
  • pokrece testove ako postoje
  • gradi Docker sliku
  • taguje je commit SHA vrednoscu
  • push-uje je u GitLab container registry
  • dodatno azurira latest tag na glavnoj isporucnoj putanji

Time se dobija cist build artefakt spreman za deploy u Kubernetes.

Tok GitLab pipeline-a sa aktivnim i buducim fazama

Korak 4. Primer izlaza pipeline-a pri izvrsavanju

Ispod je reprezentativan primer izlaza pipeline-a za uspesno izvrsavanje kroz fazu build-and-push:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
Pipeline #2841 for commit 7f2c8a1d
Project: studio/client-landing
Branch: main

[lint] Running with node:22.12.0-alpine
$ npm ci
added 742 packages in 18s
$ npm run lint
✔ No lint errors found
Job succeeded

[test] Running with node:22.12.0-alpine
$ npm ci
added 742 packages in 17s
$ npm run test --if-present
Test Suites: 12 passed, 12 total
Tests:       84 passed, 84 total
Job succeeded

[build_image] Running with docker:27 + docker:27-dind
$ echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
Login Succeeded

$ docker build -t registry.example.com/studio/client-landing:7f2c8a1d .
[+] Building 54.2s (14/14) FINISHED
 => [build 1/6] FROM docker.io/library/node:22.12.0-alpine
 => [build 2/6] WORKDIR /app
 => [build 3/6] COPY package*.json ./
 => [build 4/6] RUN npm ci
 => [build 5/6] COPY . .
 => [build 6/6] RUN npm run build
 => [runtime 1/3] FROM docker.io/library/node:22.12.0-alpine
 => [runtime 2/3] WORKDIR /app
 => [runtime 3/3] COPY --from=build /app/.output ./.output
 => exporting to image
 => naming to registry.example.com/studio/client-landing:7f2c8a1d

$ docker push registry.example.com/studio/client-landing:7f2c8a1d
The push refers to repository [registry.example.com/studio/client-landing]
7f2c8a1d: pushed
latest layer: pushed
digest: sha256:9ab3d0d... size: 1301

$ docker tag registry.example.com/studio/client-landing:7f2c8a1d registry.example.com/studio/client-landing:latest
$ docker push registry.example.com/studio/client-landing:latest
latest: digest: sha256:9ab3d0d... size: 1301

Job succeeded

Pipeline result: passed
Duration: 2m 11s
Artifacts delivered: container image pushed to registry

Korak 5. Deploy u Kubernetes uz ponovo upotrebljiv Helm chart

Kada je slika vec objavljena u registry, deploy u Kubernetes bio je pravolinijski.

Umesto pisanja posebnog chart-a samo za ovaj projekat, za deploy je upotrebljen univerzalni Helm chart koji moze da se koristi za veliki broj web aplikacija. To je vazno u realnom radu sa klijentima, jer smanjuje rutinski posao i ubrzava pokretanje narednih projekata.

Zasto ovaj pristup dobro funkcionise

Za tipicnu Node.js web aplikaciju obicno je potrebno prilagoditi samo nekoliko vrednosti:

  • image repository
  • image tag
  • service port
  • ingress hostname
  • broj replika
  • environment variables po potrebi

Sve ostalo je vec standardizovano u chart-u.

Primer values fajla

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
application:
  image:
    repository: registry.example.com/studio/client-landing
    tag: "7f2c8a1d"
    pullPolicy: IfNotPresent

  containerPort: 3000

service:
  enabled: true
  port: 3000

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: client-domain.example
      paths:
        - path: /
          pathType: Prefix
  tls:
    enabled: true
    secretName: client-domain-tls

certManager:
  enabled: true
  clusterIssuer: letsencrypt-prod

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

Sta je Kubernetes odradio automatski

Posto je klaster vec imao standardne komponente na mestu, vecina rutinskog posla nije zahtevala rucnu intervenciju:

  • Kubernetes je povukao sliku iz registry-ja
  • Ingress je izlozio aplikaciju na ciljanom hostname-u
  • CertManager je zatrazio i izdao SSL sertifikat
  • service je rutirao saobracaj do poda aplikacije

Vazan operativni detalj: DNS za domen je bio podesen pre pocetka deploy-a. To znaci da nije bilo cekanja na DNS propagaciju tokom implementacije, pa je zivi sajt bio online za svega nekoliko minuta nakon sto je Kubernetes release primenjen.

Arhitektura Kubernetes deploy-a sa registry-jem, ingress-om i cert-manager-om

Rezultat

Studio je trazio jednostavan ishod: uzeti GitLab repozitorijum sa Node.js aplikacijom i dovesti ga do produkciono spremnog stanja sa zivim domenom i osnovom za CI/CD.

To je isporuceno kroz kratak i praktican niz koraka:

  1. pripremljen je produkcioni Dockerfile u istom repozitorijumu
  2. optimizovana je strategija izgradnje slike kroz multi-stage build
  3. postavljen je GitLab pipeline za lint, test, build i push u registry
  4. slika aplikacije je objavljena u container registry
  5. aplikacija je deploy-ovana u Kubernetes uz ponovo upotrebljiv Helm chart i Helmwave
  6. aplikacija je izlozena kroz Ingress uz automatski SSL preko CertManager-a

Krajnje isporuke

  • zivi sajt na ciljnom domenu
  • kontejnerizovan build aplikacije
  • automatizovan build i push slike pri promenama u kodu
  • ponovo upotrebljiva Kubernetes deploy konfiguracija
  • automatski omogucen SSL
  • skalabilna osnova za buducu automatizaciju deploy-a i rollback-a

Vreme isporuke

Ukupno vreme implementacije: oko dva sata.

Konacni rezultat isporuke sa zivim sajtom i automatizovanom infrastrukturom

Tu se vidi stvarna vrednost standardizovanog delivery stack-a: kada se Docker, GitLab CI/CD, Kubernetes, Helm, Ingress i CertManager koriste na ponovljiv nacin, cak i novi klijentski projekat moze vrlo brzo da predje put od repozitorijuma do zivog domena.