all

Case study

Implementacija klijentskog Node.js sajta uz GitLab CI/CD i Kubernetes

Slucaj upotrebe

CI/CD za novu node.js aplikaciju koja treba da bude isporucena

Studiju za web dizajn isporucena je nova klijentska aplikacija i bilo je potrebno brzo zavrsiti infrastrukturni deo: uzeti postojeci GitLab repozitorijum sa Node.js aplikacijom, izgraditi ga pomocu npm run build i uciniti ga dostupnim kao aktivan sajt na klijentskom domenu, sa CI/CD putem spremnim za buduca izdanja.

Ulazni podaci

  • Izvorni kod u GitLab repozitorijumu
  • Node.js aplikacija
  • Komanda za production build: npm run build

Isporuka

  • Sa radom spreman sajt na klijentskom domenu
  • GitLab pipeline koji gradi i objavljuje image aplikacionog kontejnera pri svakom relevantnom push-u
  • Kubernetes deployment pripremljen kroz ponovo upotrebljiv pristup zasnovan na Helmu
  • SSL automatski izdat tokom deploy-a

Kompletno resenje je isporuceno za oko dva sata.


Korak 1. Priprema aplikacije za isporuku u kontejneru

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

Pocetni radni Dockerfile pripremljen je direktno u istom repozitorijumu:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 pravi smer za produkcionu isporuku.

Zasto su multi-stage buildovi vazni

Dijagram multi-stage Docker build-a

Za Node.js projekte, naivan single-stage image cesto sadrzi:

  • kompletan izvorni kod
  • zavisnosti potrebne tokom build-a
  • development zavisnosti
  • cache package menadzera
  • privremene build fajlove

To radi, ali stvara nepotrebnu tezinu u produkciji.

Kod multi-stage build-a, teski deo posla se odvija u builder fazi, dok runtime faza dobija samo neophodan izlaz aplikacije. U ovom slucaju, za runtime je potreban samo izgradjeni .output direktorijum. To znaci:

  • manji image-i
  • brzi push u registry
  • brzi pull u Kubernetes-u
  • manje zauzeca prostora u container registry-ju
  • manja napadacka povrsina u produkciji

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

Nakon prve radne verzije, Dockerfile je optimizovan za predvidivije CI buildove i bolje ponasanje keš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
    Ovo CI buildove cini deterministickijim jer se zavisnosti instaliraju strogo iz lock fajla.

  2. Odvojeni sloj zavisnosti pre kopiranja celog izvornog stabla
    To poboljsava ponovnu upotrebu Docker slojeva. Kada developeri menjaju kod aplikacije, ali ne i zavisnosti, sloj za instalaciju zavisnosti moze ostati iz keša.

  3. Runtime faza ostaje minimalna
    Finalni kontejner sadrzi samo produkcioni runtime i izgradjeni izlaz, ne i kompletno izvorno stablo niti build okruzenje.

  4. Cistija runtime komanda
    Mala dorada, ali korisna za citljivost i odrzavanje.

Praktican uticaj na velicinu

Optimizacija kontejnera smanjuje velicinu rezultujuc eg image-a

Glavna dobit nije dosla od male sintaksne dorade, vec od upotrebe multi-stage runtime image-a umesto tipicnog single-stage image-a sa pristupom „sve ukljuceno“.

Za projekat poput ovog, realno poredjenje je:

  • naivni single-stage image: ~420 MB
  • finalni multi-stage runtime image: ~130 MB

To je smanjenje od oko 290 MB po image-u, odnosno otprilike 69% manje.

Zasto je ovo vazno tokom jedne sedmice

Ovaj tim obicno objavljuje nove verzije nekoliko puta na sat.
Koristeci konzervativan primer:

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

To daje:

  • 120 image push-eva nedeljno

Uz 290 MB ustede po image-u, nedeljna razlika je:

  • 120 × 290 MB = 34,800 MB
  • otprilike 34.8 GB manje image podataka

To je samo akumulacija na strani registry-ja pre ciscenja. Ako se garbage collection registry-ja pokrece nedeljno, ta razlika moze ostati sacuvana tokom celog perioda. Drugim recima, naizgled mala optimizacija po build-u brzo postaje desetine gigabajta ustede nedeljno.

Isto smanjenje pomaze i brzini deploy-a, jer svaki push u registry i svaki pull od strane Kubernetes nodova prenosi znatno manje podataka. Cak i pri umerenoj mrežnoj propusnosti, smanjivanje stotina MB po izdanju odmah je primetno tokom cestih izdanja.


Korak 3. Priprema GitLab pipeline-a

Kada je aplikacija bila upakovana u kontejner, sledeci korak bio je da se automatizuju validacija build-a i objavljivanje image-a u GitLab CI/CD.

Trazeni raspored faza 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 i push-a image-a. Faze deploy i rollback su namerno ostavljene za sledecu fazu, zato sto se samo deploy-ovanje obavljalo 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 "Deploy je kasnije obradjen kroz Helmwave u Kubernetes"
  when: manual
  allow_failure: true

rollback:
  stage: rollback
  script:
    - echo "Rollback faza ce biti implementirana kroz Helmwave release rollback"
  when: manual
  allow_failure: true

Sta ovaj pipeline radi

Tok GitLab pipeline-a sa aktivnim i buducim fazama
  • pokrece lint provere
  • pokrece testove ako postoje
  • gradi Docker image
  • taguje ga commit SHA vrednoscu
  • push-uje ga u GitLab container registry
  • takodje osvezava latest tag na glavnoj putanji isporuke

To daje cist build artefakt spreman za Kubernetes deployment.


Korak 4. Primer izlaza pipeline run-a

Ispod je reprezentativan primer izlaza pipeline-a za uspesno izvrsen build-and-push fazni prolaz:

 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 image vec objavljen u registry-ju, deploy u Kubernetes bio je jednostavan.

Umesto pisanja posebnog chart-a samo za ovaj projekat, deploy je koristio univerzalni Helm chart koji moze da se ponovo upotrebljava kroz mnoge web aplikacije. Ovo je vazno u realnom radu sa klijentima zato sto smanjuje rutinski napor i ubrzava pokretanje buducih projekata.

Zasto ovaj pristup dobro radi

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

  • image repository
  • image tag
  • service port
  • ingress hostname
  • broj replika
  • environment varijable, ako su potrebne

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 automatski odradio

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

Pošto je klaster vec imao postavljene uobicajene komponente, vecina posla nije bila manualna:

  • Kubernetes je povukao image iz registry-ja
  • Ingress je izlozio aplikaciju na target host name-u
  • CertManager je zatrazio i izdao SSL sertifikat
  • service je usmerio saobracaj ka pod-u aplikacije

Vazan operativni detalj: DNS za domen je vec bio konfiguris an pre nego sto je deploy poceo.
To je znacilo da tokom implementacionog prozora nije bilo cekanja na DNS propagaciju, pa je zivi sajt postao dostupan u roku od nekoliko minuta nakon sto je Kubernetes release primenjen.


Rezultat

Studija je trazila jednostavan ishod: uzeti GitLab repozitorijum sa Node.js aplikacijom i uciniti ga produkciono spremnim, sa aktivnim domenom i CI/CD osnovama.

To je isporuceno kroz kratak i praktican niz koraka:

  1. pripremljen je production-ready Dockerfile u istom repozitorijumu
  2. optimizovana je strategija image-a uz multi-stage build
  3. postavljen je GitLab pipeline za lint, test, build i push u registry
  4. objavljen je image aplikacije u container registry-ju
  5. deploy-ovan je u Kubernetes uz ponovo upotrebljiv Helm chart i Helmwave
  6. izlozen je preko Ingress-a sa automatskim SSL-om iz CertManager-a

Zavrsena isporuka

Konacan rezultat isporuke sa aktivnim sajtom i automatizovanom infrastrukturom
  • aktivan sajt na ciljanom domenu
  • aplikacija upakovana u kontejner
  • automatizovan build image-a i push pri izmenama koda
  • ponovo upotrebljiva Kubernetes konfiguracija za deploy
  • SSL omogucen automatski
  • skalabilna osnova za buducu deploy/rollback automatizaciju

Vreme isporuke

Ukupno vreme implementacije: oko dva sata.

To je prava vrednost standardizovanog delivery stack-a: kada se Docker, GitLab CI/CD, Kubernetes, Helm, Ingress i CertManager koriste na ponovljiv nacin, cak i nov klijentski projekat moze vrlo brzo da prede od repozitorijuma do zivog domena.