Klaster OKD 4.19 (OpenShift) – CD/CI przy użyciu Argo CD, GitLab oraz Jenkins

Powiązane z tematem wpisy

Założenie wstepne:

Monorepo

  • Najprostsza mentalnie ścieżka: jeden projekt w GitLab, jedno miejsce “prawdy”.
  • Szybkie startowanie: mniej konfiguracji, mniej credentiali, mniej repoURL w Argo.
  • Łatwiejsze debugowanie: commit zawiera i zmianę kodu, i zmianę deploy (widać w jednym diffie).
  • Mniej elementów do popsucia w homelabie: mniej webhooków, mniej tokenów, mniej projektów w GitLab.

Wzorzec A (polecany): Jeden kod, różne wersje per środowisko (promocja release’ów)

To jest najczystszy i najczęściej stosowany model.

Jak to działa

Jest jeden katalog app/ (jeden kod).

Dev dostaje najnowsze buildy (np. z każdej zmiany).

Bis dostaje buildy “kandydujące” (np. tylko z MR do main albo z tagów rc-*).

Prod dostaje tylko release (np. tag v1.2.3) i ręczny sync Argo.

Różnice między środowiskami?

Nie przez różne katalogi, tylko przez:

inny image pullspec/digest ustawiany w values-dev.yaml / values-bis.yaml / values-prod.yaml

ewentualnie feature flags w ConfigMap (to w praktyce jest “inny kod aktywny” bez forka)

Zalety

Zero duplikacji kodu.

Najłatwiejsza promocja i rollback.

W pełni zgodne z GitOps i dobrymi praktykami.

Kiedy to spełnia Twoje “inne wersje”?

Jeśli przez “inne” masz na myśli:

dev ma wersję 1.0.0-SNAPSHOT i częste zmiany

bis ma 1.0.0-RC1

prod ma 1.0.0

Najważniejsza zmiana: pipeline w Jenkins

dev overlay na main

bis overlay na tag rc-*

prod overlay na tag v*

Procedura aktualizacji kodu aplikacji krok po kroku

Zmieniam kod i chcę to przetestować na DEV

nano /repo/git/hellodevops/app/Main.java
cd /repo/git/hellodevops
git add app/Main.java
git commit -m "feat: zmiana X"
git push origin main

Co się dzieje dalej automatycznie:

  • Jenkins odpala build z tego commita
  • Jenkins aktualizuje values-dev.yaml (pullspec + APP_VERSION = dev-)
  • ArgoCD-dev robi auto-sync i wdraża na hellodevops-dev

Czyli: DEV = zwykłe push na main.

Ten stan jest OK, chcę go wrzucić na BIS

Robisz tag na tym konkretnym commicie, który chcesz promować.

cd /repo/git/hellodevops
git checkout main
git pull
git log --oneline -1   # upewnij się, że to ten commit
git tag rc-1.0.0-1
git push origin rc-1.0.0-1

Co się dzieje dalej automatycznie:

  • Jenkins odpala pipeline dla tagu rc-*
  • buduje obraz (z tego samego kodu)
  • aktualizuje values-bis.yaml (pullspec + APP_VERSION = rc-1.0.0-1)
  • ArgoCD-bis robi auto-sync i wdraża na hellodevops-bis

Czyli: BIS = tag rc-* na wybranym commicie, bez nowego commita.

BIS jest OK, wypuszczam to na PROD

Robisz tag release v* na tym samym commicie, który przeszedł BIS.

Jeśli po testach BIS nie wprowadzałeś już zmian w kodzie, to po prostu:

Co się dzieje:

  • Jenkins odpala pipeline dla tagu v*
  • buduje obraz
  • aktualizuje values-prod.yaml (pullspec + APP_VERSION = 1.0.0)
  • ArgoCD-prod wykryje zmianę i pokaże OutOfSync
  • (bo prod ma manual sync)
  • w ArgoCD klikasz Sync ręcznie

Czyli: PROD = tag v* + ręczny Sync w ArgoCD.

git checkout main
git pull
git log --oneline -1   # albo checkout konkretnego commita, który był w RC
git tag v1.0.0
git push origin v1.0.0
# potem ręczny Sync w ArgoCD-prod

W praktyce: tak jest najbezpieczniej

DEV: ciągłe testy

BIS: candidate

PROD: release

Jedno “centrum budowania” = mniej chaosu i mniej duplikacji

CI (budowanie obrazów) – robisz w jednym miejscu, CD (wdrażanie na dev/bis/prod) – robisz w trzech miejscach

BuildConfig + ImageStream to infrastruktura CI. Gdybyś miał to w 3 namespace’ach, to dostajesz:

3 × BuildConfig do utrzymania (te same definicje)

3 × ImageStreamy

3 × uprawnienia i polityki

3 × miejsca, gdzie coś może się rozjechać

A przecież obraz aplikacji ma być ten sam artefakt, który potem tylko promujesz na kolejne środowiska.

W Twojej konfiguracji Helm używa image.pullspec (digest), np.:

image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:…

To jest konkretna, niezmienna wersja obrazu.

Teraz promocja wygląda tak:

Dev ma pullspec = sha256:AAA (najświeższy)

Bis ma pullspec = sha256:BBB (RC)

Prod ma pullspec = sha256:CCC (release)

To są różne obrazy, ale wszystkie są “zbudowane” w tym samym systemie CI.

Dev

buildy często, szybko

BuildConfig w dev ma sens jako “CI workspace”

dev i tak jest “brudny” z definicji

Bis

staging/RC, ma być stabilniej

nie chcesz tam budować 100 razy dziennie

chcesz wdrażać już zbudowane RC

Prod

ma być najczystszy

nie budujesz, tylko wdrażasz sprawdzony artefakt

ręczny Sync w Argo dodatkowo to podkreśla

Musisz dać uprawnienia do pull z innych namespace’ów

W OpenShift to się robi poprzez przypisanie roli system:image-puller na hellodevops-dev dla serviceaccountów z bis i prod.

oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-bis -n hellodevops-dev
oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-prod -n hellodevops-dev

Przygotowanie dostępu przez SSH na bastionie do GitLaba

# Wygeneruj klucz SSH (na bastionie)
sudo -u bastuser -H bash -lc '
mkdir -p ~/.ssh && chmod 700 ~/.ssh
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_gitlab_okdlab -C "bastion->gitlab.okdlab.local" -N ""
'
# Dodaj host key GitLaba do known_hosts (żeby nie pytał interaktywnie)
sudo -u bastuser -H bash -lc '
ssh-keyscan -H gitlab.okdlab.local >> ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
'

# Dodaj wpis w ~/.ssh/config (wygoda + pewność, że używa właściwego klucza)
sudo -u bastuser -H bash -lc '
cat >> ~/.ssh/config <<EOF

Host gitlab.okdlab.local
  HostName gitlab.okdlab.local
  User git
  IdentityFile ~/.ssh/id_ed25519_gitlab_okdlab
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
'

# Skopiuj klucz publiczny (to wkleisz do GitLaba)
sudo -u bastuser -H bash -lc 'cat ~/.ssh/id_ed25519_gitlab_okdlab.pub'

Logujemy się do GitLaba użytkownikiem, który będzie aktualizował repo. User settings -> SSH Keys wklejamy zawartość klucza publicznego

# weryfikacja z bastionu
sudo -u bastuser -H bash -lc 'ssh -T [email protected]'

Przygotowanie repozytorium hellodevops w GitLab.

Zakładamy nowy pusty projekt o nazwie hellodevops w GitLab.

  • Odznaczamy Initialize repository with README
  • Wybieramy grupę devops w Project URL

# sklonujpuste  repo do /repo/git/hellodevops
sudo mkdir -p /repo/git
sudo chown -R bastuser:bastuser /repo/git

sudo -u bastuser -H bash -lc '
cd /repo/git
git clone [email protected]:dev-ops/hellodevops.git
'
# Utworzenie struktury katalogów
sudo -u bastuser -H bash -lc '
cd /repo/git/hellodevops

mkdir -p app
mkdir -p gitops/argocd
mkdir -p gitops/helm/templates
mkdir -p gitops/kustomize/base
mkdir -p gitops/kustomize/overlays/dev
mkdir -p gitops/kustomize/overlays/bis
mkdir -p gitops/kustomize/overlays/prod
'

# Minimalne pliki (na razie “szkielet”)
sudo -u bastuser -H bash -lc '
cd /repo/git/hellodevops

cat > README.md <<EOF
# hellodevops

Projekt treningowy OKD/ArgoCD/GitLab/Jenkins.

Struktura:
- app/    - kod aplikacji + Dockerfile
- gitops/ - Helm + Kustomize + Argo CD manifesty (dev/bis/prod)
EOF

cat > .gitignore <<EOF
# Java
*.class
*.log

# OS
.DS_Store

# IDE
.idea/
.vscode/
EOF

# placeholdery pod pliki, które uzupełnimy w kolejnych krokach
touch app/Main.java
touch app/Dockerfile

touch gitops/helm/Chart.yaml
touch gitops/helm/values.yaml
touch gitops/helm/templates/configmap.yaml
touch gitops/helm/templates/secret.yaml
touch gitops/helm/templates/deployment.yaml

touch gitops/kustomize/base/kustomization.yaml
touch gitops/kustomize/overlays/dev/kustomization.yaml
touch gitops/kustomize/overlays/dev/values-dev.yaml
touch gitops/kustomize/overlays/bis/kustomization.yaml
touch gitops/kustomize/overlays/bis/values-bis.yaml
touch gitops/kustomize/overlays/prod/kustomization.yaml
touch gitops/kustomize/overlays/prod/values-prod.yaml

touch gitops/argocd/project-hellodevops.yaml
touch gitops/argocd/app-hellodevops-dev.yaml
touch gitops/argocd/app-hellodevops-bis.yaml
touch gitops/argocd/app-hellodevops-prod.yaml
'

Struktura projektu hellodevops

.
├── app
│   ├── Dockerfile
│   └── Main.java
├── .git
│   ├── config
│   ├── description
│   ├── HEAD
│   ├── hooks
│   │   ├── applypatch-msg.sample
│   │   ├── commit-msg.sample
│   │   ├── fsmonitor-watchman.sample
│   │   ├── post-update.sample
│   │   ├── pre-applypatch.sample
│   │   ├── pre-commit.sample
│   │   ├── pre-merge-commit.sample
│   │   ├── prepare-commit-msg.sample
│   │   ├── pre-push.sample
│   │   ├── pre-rebase.sample
│   │   ├── pre-receive.sample
│   │   ├── push-to-checkout.sample
│   │   ├── sendemail-validate.sample
│   │   └── update.sample
│   ├── info
│   │   └── exclude
│   ├── objects
│   │   ├── info
│   │   └── pack
│   └── refs
│       ├── heads
│       └── tags
├── .gitignore
├── gitops
│   ├── argocd
│   │   ├── app-hellodevops-bis.yaml
│   │   ├── app-hellodevops-dev.yaml
│   │   ├── app-hellodevops-prod.yaml
│   │   └── project-hellodevops.yaml
│   ├── helm
│   │   ├── Chart.yaml
│   │   ├── templates
│   │   │   ├── configmap.yaml
│   │   │   ├── deployment.yaml
│   │   │   └── secret.yaml
│   │   └── values.yaml
│   └── kustomize
│       ├── base
│       │   └── kustomization.yaml
│       └── overlays
│           ├── bis
│           │   ├── kustomization.yaml
│           │   └── values-bis.yaml
│           ├── dev
│           │   ├── kustomization.yaml
│           │   └── values-dev.yaml
│           └── prod
│               ├── kustomization.yaml
│               └── values-prod.yaml
└── README.md

21 directories, 38 files

Pierwszy commit i push (żeby repo nie było puste)

sudo -u bastuser -H bash -lc '
cd /repo/git/hellodevops
git add .
git branch -M main
git commit -m "chore: initial skeleton (app + gitops structure)"
git push -u origin main
'

Przygotowanie plików projektu

# hellodevops

Projekt treningowy OKD/ArgoCD/GitLab/Jenkins.

## Struktura repo
- `app/`    – kod aplikacji (1 plik Java) + Dockerfile
- `gitops/` – GitOps: Helm + Kustomize + Argo CD (dev/bis/prod)

## Środowiska (OpenShift namespaces)
- `hellodevops-dev`
- `hellodevops-bis`
- `hellodevops-prod`

## GitOps flow (skrót)
- Jenkins odpala build w OKD (BuildConfig), OKD publikuje obraz do internal registry
- Jenkins pobiera pullspec (digest) i aktualizuje `gitops/kustomize/overlays/*/values-*.yaml`
- Argo CD:
  - `dev` i `bis`: auto-sync
  - `prod`: manual sync (commit jest, ale wdrożenie ręcznie w Argo)
# Java
*.class
*.log

# OS
.DS_Store

# IDE
.idea/
.vscode/
FROM eclipse-temurin:21-jdk AS build
WORKDIR /src

# PostgreSQL JDBC driver
ARG PGJDBC_VERSION=42.7.4
RUN mkdir -p /deps \
 && curl -fsSL -o /deps/postgresql.jar \
    https://repo1.maven.org/maven2/org/postgresql/postgresql/${PGJDBC_VERSION}/postgresql-${PGJDBC_VERSION}.jar

COPY Main.java /src/Main.java
RUN javac -cp /deps/postgresql.jar Main.java

FROM eclipse-temurin:21-jre
WORKDIR /app

COPY --from=build /src/Main.class /app/Main.class
COPY --from=build /deps/postgresql.jar /app/postgresql.jar

USER 1001
CMD ["java", "-cp", "/app:/app/postgresql.jar", "Main"]
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;

public class Main {
    private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private static String envRaw(String key) {
        return System.getenv(key);
    }

    private static String env(String key, String def) {
        String v = envRaw(key);
        return (v == null || v.isBlank()) ? def : v;
    }

    private static boolean isPresent(String key) {
        String v = envRaw(key);
        return v != null && !v.isBlank();
    }

    private static int parseIntSafe(String s, int def) {
        try { return Integer.parseInt(s); } catch (Exception e) { return def; }
    }

    private static String now() {
        return LocalDateTime.now().format(FMT);
    }

    private static void log(String level, String msg) {
        System.out.println(level + " " + msg);
    }

    private static void logInfo(String msg)  { log("[INFO]", msg); }
    private static void logWarn(String msg)  { log("[WARN]", msg); }
    private static void logOk(String msg)    { log("[OK]", msg); }
    private static void logErr(String msg)   { log("[ERROR]", msg); }

    private static String buildJdbcUrl(String host, String port, String db) {
        // connectTimeout/socketTimeout: sekundy (pgjdbc)
        return "jdbc:postgresql://" + host + ":" + port + "/" + db
                + "?connectTimeout=5&socketTimeout=5&tcpKeepAlive=true";
    }

    private static boolean insertEntry(String jdbcUrl, String user, String pass, String entryName, int entryDelay, String dbNameForMsg) {
        String sql = "INSERT INTO entries (entry_name, entry_delay) VALUES (?, ?)";
        try (Connection conn = DriverManager.getConnection(jdbcUrl, user, pass);
             PreparedStatement ps = conn.prepareStatement(sql)) {

            ps.setString(1, entryName);
            ps.setInt(2, entryDelay);

            int rows = ps.executeUpdate();
            if (rows == 1) {
                logOk("Poprawnie dodano 1 nowy rekord do tabeli entries w bazie " + dbNameForMsg
                        + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay);
                return true;
            } else {
                logErr("Nie udało dodać się nowego rekordu do tabeli entries w bazie " + dbNameForMsg
                        + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay
                        + " (rows=" + rows + ")");
                return false;
            }
        } catch (SQLException e) {
            logErr("Nie udało dodać się nowego rekordu do tabeli entries w bazie " + dbNameForMsg
                    + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay
                    + " (SQLState=" + e.getSQLState() + ", message=" + e.getMessage() + ")");
            return false;
        } catch (Exception e) {
            logErr("Nie udało dodać się nowego rekordu do tabeli entries w bazie " + dbNameForMsg
                    + " o treści entry_name = " + entryName + ", entry_delay = " + entryDelay
                    + " (message=" + e.getMessage() + ")");
            return false;
        }
    }

    public static void main(String[] args) throws Exception {
        // Wymagane do trybu [INFO]
        String[] required = new String[] {
                "APP_VERSION", "ENVIRONMENT", "USER_NAME", "DELAY",
                "DBA_SERVER", "DBA_NAME", "DBA_PORT", "DBA_USER", "DBA_PASSWORD"
        };

        Map<String, Boolean> presentMap = new LinkedHashMap<>();
        boolean allPresent = true;
        for (String k : required) {
            boolean p = isPresent(k);
            presentMap.put(k, p);
            allPresent = allPresent && p;
        }

        // Czytamy z defaultami (żeby app nie wybuchała)
        String appVersion  = env("APP_VERSION", "0.0.0");
        String environment = env("ENVIRONMENT", "unknown");
        String userName    = env("USER_NAME", "unknown");
        int delaySeconds   = parseIntSafe(env("DELAY", "5"), 5);

        String dbaServer   = env("DBA_SERVER", "127.0.0.1");
        String dbaName     = env("DBA_NAME", "postgres");
        String dbaPort     = env("DBA_PORT", "5432");
        String dbaUser     = env("DBA_USER", "postgres");
        String dbaPassword = env("DBA_PASSWORD", "");

        // Nagłówki diagnostyczne (bez hasła!)
        System.out.println("=== hellodevops ===");
        System.out.println("APP_VERSION=" + appVersion);
        System.out.println("ENVIRONMENT=" + environment);
        System.out.println("USER_NAME=" + userName);
        System.out.println("DELAY=" + delaySeconds);
        System.out.println("DBA_SERVER=" + dbaServer);
        System.out.println("DBA_NAME=" + dbaName);
        System.out.println("DBA_PORT=" + dbaPort);
        System.out.println("DBA_USER=" + dbaUser);
        System.out.println("DBA_PASSWORD=" + (dbaPassword.isBlank() ? "<empty>" : "<set>"));
        System.out.println("===================");

        if (!allPresent) {
            StringBuilder sb = new StringBuilder("Brak wymaganych zmiennych środowiskowych: ");
            boolean first = true;
            for (Map.Entry<String, Boolean> e : presentMap.entrySet()) {
                if (!e.getValue()) {
                    if (!first) sb.append(", ");
                    sb.append(e.getKey());
                    first = false;
                }
            }
            logWarn(sb.toString());
        }

        String jdbcUrl = buildJdbcUrl(dbaServer, dbaPort, dbaName);

        while (true) {
            String line = "Witaj " + userName + ", dziś jest " + now() + ", czekam następne " + delaySeconds + " sekund";

            if (allPresent) {
                logInfo(line);
                // INSERT tylko gdy INFO
                insertEntry(jdbcUrl, dbaUser, dbaPassword, userName, delaySeconds, dbaName);
            } else {
                logWarn(line);
            }

            Thread.sleep(delaySeconds * 1000L);
        }
    }
}

GitOps: Helm

apiVersion: v2
name: hellodevops
description: Minimal hellodevops app (ConfigMap + Secret + Deployment)
type: application
version: 0.1.0
appVersion: "1.0.0"

(bazowe — będą nadpisywane overlayami)

nameOverride: "hellodevops"

image:
  # Preferujemy pullspec (digest) ustawiany przez Jenkins
  pullspec: ""

config:
  APP_VERSION: "1.0.0"
  ENVIRONMENT: "prod"
  USER_NAME: "Zbychu"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_prod"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_prod"
  DBA_PASSWORD: "CHANGE_ME"
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-hellodevops
  labels:
    app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
data:
  APP_VERSION: {{ .Values.config.APP_VERSION | quote }}
  ENVIRONMENT: {{ .Values.config.ENVIRONMENT | quote }}
  USER_NAME: {{ .Values.config.USER_NAME | quote }}
  DELAY: {{ .Values.config.DELAY | quote }}

  DBA_SERVER: {{ .Values.db.DBA_SERVER | quote }}
  DBA_NAME: {{ .Values.db.DBA_NAME | quote }}
  DBA_PORT: {{ .Values.db.DBA_PORT | quote }}
  DBA_USER: {{ .Values.db.DBA_USER | quote }}
apiVersion: v1
kind: Secret
metadata:
  name: secret-hellodevops-db
type: Opaque
stringData:
  DBA_PASSWORD: {{ .Values.db.DBA_PASSWORD | quote }}
{{- $img := "" -}}
{{- if .Values.image.pullspec -}}
{{- $img = .Values.image.pullspec -}}
{{- else -}}
{{- $img = "image-not-set" -}}
{{- end -}}

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.nameOverride | quote }}
  labels:
    app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ .Values.nameOverride | quote }}
    spec:
      containers:
        - name: {{ .Values.nameOverride | quote }}
          image: {{ $img | quote }}
          imagePullPolicy: Always
          envFrom:
            - configMapRef:
                name: cm-hellodevops
            - secretRef:
                name: secret-hellodevops-db
          resources:
            requests:
              cpu: "10m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "256Mi"

GitOps: Kustomize

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

helmCharts:
  - name: hellodevops
    releaseName: hellodevops
    version: 0.1.0
    chartPath: ../../helm
    valuesFile: ../../helm/values.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

helmCharts:
  - name: hellodevops
    releaseName: hellodevops
    version: 0.1.0
    chartPath: ../../../helm
    valuesFile: values-dev.yaml
nameOverride: "hellodevops"

image:
  pullspec: ""  # ustawiane przez Jenkins

config:
  APP_VERSION: "dev"   # Jenkins nadpisze np. dev-<sha>
  ENVIRONMENT: "dev"
  USER_NAME: "Zbychu-Dev"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_dev"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_dev"
  DBA_PASSWORD: "AZXCmntDEV26"
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

helmCharts:
  - name: hellodevops
    releaseName: hellodevops
    version: 0.1.0
    chartPath: ../../../helm
    valuesFile: values-bis.yaml
nameOverride: "hellodevops"

image:
  pullspec: ""  # ustawiane przez Jenkins

config:
  APP_VERSION: "bis"   # Jenkins nadpisze np. rc-1.0.0-1
  ENVIRONMENT: "bis"
  USER_NAME: "Zbychu-Bis"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_bis"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_bis"
  DBA_PASSWORD: "AZXCmntBIS26"
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

helmCharts:
  - name: hellodevops
    releaseName: hellodevops
    version: 0.1.0
    chartPath: ../../../helm
    valuesFile: values-prod.yaml
nameOverride: "hellodevops"

image:
  pullspec: ""  # ustawiane przez Jenkins

config:
  APP_VERSION: "prod"  # Jenkins nadpisze np. 1.0.0
  ENVIRONMENT: "prod"
  USER_NAME: "Zbychu-Prod"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_prod"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_prod"
  DBA_PASSWORD: "AZXCmntPROD26"

GitOps: Argo CD

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: hellodevops
  namespace: argocd
spec:
  description: hellodevops project (dev/bis/prod)
  sourceRepos:
    - '*'
  destinations:
    - namespace: hellodevops-dev
      server: https://kubernetes.default.svc
    - namespace: hellodevops-bis
      server: https://kubernetes.default.svc
    - namespace: hellodevops-prod
      server: https://kubernetes.default.svc
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
  namespaceResourceWhitelist:
    - group: '*'
      kind: '*'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: hellodevops-dev
  namespace: argocd
spec:
  project: hellodevops
  source:
    repoURL: 'ssh://[email protected]/dev-ops/hellodevops.git'
    targetRevision: main
    path: gitops/kustomize/overlays/dev
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: hellodevops-dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: hellodevops-bis
  namespace: argocd
spec:
  project: hellodevops
  source:
    repoURL: 'ssh://[email protected]/dev-ops/hellodevops.git'
    targetRevision: main
    path: gitops/kustomize/overlays/bis
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: hellodevops-bis
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: hellodevops-prod
  namespace: argocd
spec:
  project: hellodevops
  source:
    repoURL: 'ssh://[email protected]/dev-ops/hellodevops.git'
    targetRevision: main
    path: gitops/kustomize/overlays/prod
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: hellodevops-prod
  syncPolicy:
    syncOptions:
      - CreateNamespace=true

Wypchnięcie wszystkiego do GitLaba

# Dodaj i zrób commit
cd /repo/git/hellodevops

# Sprawdź, co dokładnie zmieniasz
git status
git diff --stat

git add .
git commit -m "feat: hellodevops app + gitops manifests"
git push -u origin main
# Szybka weryfikacja po pushu
git log --oneline -5
git remote -v

Dodanie pliku build-bc.yaml do overlay dev

apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
  name: hellodevops
  labels:
    app.kubernetes.io/name: hellodevops
---
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
  name: hellodevops
  labels:
    app.kubernetes.io/name: hellodevops
spec:
  runPolicy: Serial
  source:
    type: Git
    git:
      uri: "ssh://[email protected]/dev-ops/hellodevops.git"
    contextDir: "app"
  strategy:
    type: Docker
    dockerStrategy:
      dockerfilePath: Dockerfile
  output:
    to:
      kind: ImageStreamTag
      name: "hellodevops:latest"
  triggers: []

Dopisz build-bc.yaml w kustomization.yaml overlay dev

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base
  - build-bc.yaml

helmCharts:
  - name: hellodevops
    releaseName: hellodevops
    version: 0.1.0
    chartPath: ../../../helm
    valuesFile: values-dev.yaml

Commit i push do GitLaba

cd /repo/git/hellodevops
git add gitops/kustomize/overlays/dev/build-bc.yaml gitops/kustomize/overlays/dev/kustomization.yaml
git commit -m "gitops(dev): add ImageStream + BuildConfig for in-cluster builds"
git push origin main

Utwórzenie namespaces (projektów -dev,-bis , -prod)

# Jeśli chcesz, żeby CreateNamespace=true faktycznie działało, Argo musi mieć clusterowe uprawnienia do tworzenia namespaces/projects.
oc adm policy add-cluster-role-to-user cluster-admin \
  system:serviceaccount:argocd:argocd-application-controller

My jednak założymy projekty ręcznie, zeby nie dawać tak wysokich uprawniń Argo CD na klastrze.

# utwórz namespaces (projekty)
oc new-project hellodevops-dev
oc new-project hellodevops-bis
oc new-project hellodevops-prod

# Nadaj rolę admin w każdym namespace dla service account ArgoCD:
oc adm policy add-role-to-user admin \
  system:serviceaccount:argocd:argocd-application-controller \
  -n hellodevops-dev

oc adm policy add-role-to-user admin \
  system:serviceaccount:argocd:argocd-application-controller \
  -n hellodevops-bis

oc adm policy add-role-to-user admin \
  system:serviceaccount:argocd:argocd-application-controller \
  -n hellodevops-prod

# weryfikacja: czy teraz can-i zmieniło się na YES
oc -n hellodevops-dev auth can-i create buildconfigs \
  --as system:serviceaccount:argocd:argocd-application-controller

oc -n hellodevops-dev auth can-i create imagestreams \
  --as system:serviceaccount:argocd:argocd-application-controller

Dodanie manifestów do projektu argocd

oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/project-hellodevops.yaml
oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/app-hellodevops-dev.yaml
oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/app-hellodevops-bis.yaml
oc apply -n argocd -f /repo/git/hellodevops/gitops/argocd/app-hellodevops-prod.yaml

Utworzenie klucza SSH dla ArgoCD do GitLaba
Jeśli po utworzeniu manifestów przez oc apply -n argocd … mamy podobne jak poniżej błędy to brak jest klucza SSH dla ArgoCD di Gitlaba.

oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'

  Conditions:
    Last Transition Time:  2026-02-22T12:27:24Z
    Message:               Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = failed to list refs: error creating SSH agent: "SSH agent requested but SSH_AUTH_SOCK not-specified"
    Type:                  ComparisonError
  Controller Namespace:    argocd
  Health:
    Last Transition Time:  2026-02-22T12:27:24Z
    Status:                Healthy
  Reconciled At:           2026-02-22T12:27:24Z
  Sync:
    Compared To:
      Destination:
        Namespace:  hellodevops-dev
        Server:     https://kubernetes.default.svc
      Source:
        Path:             gitops/kustomize/overlays/dev
        Repo URL:         ssh://[email protected]/dev-ops/hellodevops.git
        Target Revision:  main
    Status:               Unknown
Events:

Wygeneruj deploy key dla ArgoCD (na bastionie)

# na bastionie
ssh-keygen -t ed25519 -f /tmp/argocd_gitlab_hellodevops -C "argocd@okdlab -> hellodevops" -N ""

# To tworzy dwa pliki:
# /tmp/argocd_gitlab_hellodevops (private key) NIE UDOSTĘPNIAJ
# /tmp/argocd_gitlab_hellodevops.pub (public key) — to dodasz w GitLab
cat /tmp/argocd_gitlab_hellodevops.pub

Dodaj public key do GitLaba jako Deploy Key (read-only)

Repo -> Settings -> Repository. Wkleić zawartość /tmp/argocd_gitlab_hellodevops.pub

Przygotuj known_hosts dla GitLaba (na bastionie)

ArgoCD musi ufać host key GitLaba (żeby nie było promptów i błędów).

ssh-keyscan -H gitlab.okdlab.local > /tmp/gitlab.okdlab.local_known_hosts
cat /tmp/gitlab.okdlab.local_known_hosts

Utwórz repo secret w ArgoCD (namespace argocd)

oc -n argocd create secret generic repo-hellodevops \
  --from-literal=type=git \
  --from-literal=url=ssh://[email protected]/dev-ops/hellodevops.git \
  --from-file=sshPrivateKey=/tmp/argocd_gitlab_hellodevops \
  --from-file=knownHosts=/tmp/gitlab.okdlab.local_known_hosts \
  -o yaml --dry-run=client | oc apply -f -

I teraz najważniejsze: label, który mówi ArgoCD, że to repo secret:

oc -n argocd label secret repo-hellodevops argocd.argoproj.io/secret-type=repository --overwrite

# Szybka weryfikacja
oc -n argocd get secret repo-hellodevops -o jsonpath='{.metadata.labels.argocd\.argoproj\.io/secret-type}{"\n"}'
# Powinno zwrócić: repository

Wymuś refresh aplikacji (żeby przestało być “Unknown”)

oc -n argocd annotate application hellodevops-dev --overwrite refreshTimestamp="$(date +%s)"

# Po chwili sprawdź:
oc -n argocd get applications.argoproj.io | egrep 'hellodevops|NAME'

# I sprawdź jeszcze raz condition:
oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'

W razie błędu jak poniżej

 oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'
 
Message:               Failed to load target state: failed to generate manifest for source 1 of 1: rpc error: code = Unknown desc = `kustomize build <path to cached source>/gitops/kustomize/overlays/dev` failed exit status 1: Error: invalid Kustomization: json: unknown field "chartPath"

poprawić definicję chartPath w helmCharts

cd /repo/git/hellodevops

# szybka zamiana sedem w tych 4 plikach
sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/base/kustomization.yaml
sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/overlays/dev/kustomization.yaml
sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/overlays/bis/kustomization.yaml
sed -i 's/^    chartPath:/    path:/g' gitops/kustomize/overlays/prod/kustomization.yaml

git diff --stat

git add gitops/kustomize/base/kustomization.yaml \
        gitops/kustomize/overlays/dev/kustomization.yaml \
        gitops/kustomize/overlays/bis/kustomization.yaml \
        gitops/kustomize/overlays/prod/kustomization.yaml

git commit -m "fix: kustomize helmCharts use path instead of chartPath (argocd compatible)"
git push origin main

Wymuś refresh i sprawdź czy błąd zniknął

oc -n argocd annotate application hellodevops-dev --overwrite refreshTimestamp="$(date +%s)"
oc -n argocd describe application hellodevops-dev | sed -n '/Conditions:/,/Events:/p'
oc -n argocd run -it --rm tcpcheck \
  --image=registry.access.redhat.com/ubi9/ubi-minimal \
  --restart=Never -- sh -lc '
echo "Trying TCP connect to repo-server svc 172.30.86.53:8081..."
( </dev/tcp/172.30.86.53/8081 ) >/dev/null 2>&1 && echo "OK: TCP port open" || echo "FAIL: connection refused/timeout"

echo "Trying TCP connect to repo-server pod 10.128.2.150:8081..."
( </dev/tcp/10.128.2.150/8081 ) >/dev/null 2>&1 && echo "OK: TCP port open" || echo "FAIL: connection refused/timeout"
'

Oznacz hellodevops-* jako “managed” (TO jest klucz)

oc label ns hellodevops-dev  argocd.argoproj.io/managed-by=argocd --overwrite
oc label ns hellodevops-bis  argocd.argoproj.io/managed-by=argocd --overwrite
oc label ns hellodevops-prod argocd.argoproj.io/managed-by=argocd --overwrite

Sprawdź czy zasoby zaczęły się tworzyć w hellodevops-dev

oc -n hellodevops-dev get cm,secret,deploy
NAME                                 DATA   AGE
configmap/cm-hellodevops             8      27s
configmap/kube-root-ca.crt           1      103m
configmap/openshift-service-ca.crt   1      103m

NAME                              TYPE                      DATA   AGE
secret/builder-dockercfg-jlwc6    kubernetes.io/dockercfg   1      103m
secret/default-dockercfg-nbbqk    kubernetes.io/dockercfg   1      103m
secret/deployer-dockercfg-x9mkg   kubernetes.io/dockercfg   1      103m
secret/secret-hellodevops-db      Opaque                    1      27s

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hellodevops   0/1     1            0           27s

Dodaj BC/IS do Helm chartu (tylko dev)

{{- if .Values.build.enabled }}
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
  name: {{ .Values.nameOverride | default "hellodevops" | quote }}
  labels:
    app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
---
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
  name: {{ .Values.nameOverride | default "hellodevops" | quote }}
  labels:
    app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
spec:
  runPolicy: Serial
  source:
    type: Git
    git:
      uri: {{ .Values.build.gitUri | quote }}
    contextDir: {{ .Values.build.contextDir | quote }}
  strategy:
    type: Docker
    dockerStrategy:
      dockerfilePath: Dockerfile
  output:
    to:
      kind: ImageStreamTag
      name: "{{ .Values.nameOverride | default "hellodevops" }}:latest"
  triggers: []
{{- end }}

uzupełnij

nameOverride: "hellodevops"

image:
  # Preferujemy pullspec (digest) ustawiany przez Jenkins
  pullspec: ""

config:
  APP_VERSION: "1.0.0"
  ENVIRONMENT: "prod"
  USER_NAME: "Zbychu"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_prod"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_prod"
  DBA_PASSWORD: "CHANGE_ME"

build:
  enabled: false
  gitUri: "ssh://[email protected]/dev-ops/hellodevops.git"
  contextDir: "app"

Włącz build tylko w DEV

nameOverride: "hellodevops"

image:
  pullspec: ""  # ustawiane przez Jenkins

config:
  APP_VERSION: "dev"   # Jenkins nadpisze np. dev-<sha>
  ENVIRONMENT: "dev"
  USER_NAME: "Zbychu-Dev"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_dev"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_dev"
  DBA_PASSWORD: "AZXCmntDEV26"

build:
  enabled: true
  gitUri: "ssh://[email protected]/dev-ops/hellodevops.git"
  contextDir: "app"
cd /repo/git/hellodevops
git add gitops/charts/hellodevops/templates/buildconfig.yaml \
        gitops/charts/hellodevops/values.yaml \
        gitops/charts/hellodevops/values-dev.yaml
git commit -m "feat(dev): add BuildConfig+ImageStream via Helm (enabled only in dev)"
git push origin main

# Potem (opcjonalnie) hard refresh:
oc -n argocd annotate application hellodevops-dev --overwrite argocd.argoproj.io/refresh=hard

# weryfikacja
oc -n hellodevops-dev get bc,is

Ręczny build w OKD, bez Jenkinsa

oc -n hellodevops-dev start-build bc/hellodevops --follow --wait

# w razie błędu
build.build.openshift.io/hellodevops-1 started
Cloning "ssh://[email protected]/dev-ops/hellodevops.git" ...
error: Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

# Dodaj known_hosts do namespace hellodevops-dev

Przechodzimy na HTTPS + Project Access Token

W projekcie hellodevops:

skopiuj token

Settings → Access Tokens (Project Access Token) albo Personal Access Token

scope: read_repository

oc -n hellodevops-dev create secret generic gitlab-http-creds \
  --from-literal=username=gitlab-ci-token \
  --from-literal=password='glpat-m2uRIx4dy6cgzZDvxKwcC286MQp1OjcH.01.0w05p6ggk' \
  -o yaml --dry-run=client | oc apply -f -

Zmień BuildConfig, żeby klonował po HTTPS i używał tego secreta

oc -n hellodevops-dev patch bc hellodevops --type merge -p '{
  "spec": {
    "source": {
      "git": {
        "uri": "https://gitlab.okdlab.local/dev-ops/hellodevops.git"
      }
    }
  }
}'

Podepnij secret jako sourceSecret

oc -n hellodevops-dev patch bc hellodevops --type merge -p '{
  "spec": {
    "source": {
      "sourceSecret": { "name": "gitlab-http-creds" }
    }
  }
}'
# weryfikacja
oc -n hellodevops-dev get bc hellodevops -o jsonpath='{.spec.source.git.uri}{"\n"}{.spec.source.sourceSecret.name}{"\n"}'
# ma być 
https://gitlab.okdlab.local/dev-ops/hellodevops.git
gitlab-http-creds

Ponowny build

oc -n hellodevops-dev start-build bc/hellodevops --follow --wait
# w razie tgo bledy wylacz weryfikacje SSSL
Cloning "https://gitlab.okdlab.local/dev-ops/hellodevops.git" ...
error: fatal: unable to access 'https://gitlab.okdlab.local/dev-ops/hellodevops.git/': SSL certificate problem: self-signed certificate
error: the build hellodevops-dev/hellodevops-3 status is "Failed"

Dodaj
env:
– name: GIT_SSL_NO_VERIFY
value: „true”

{{- if .Values.build.enabled }}
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
  name: {{ .Values.nameOverride | default "hellodevops" | quote }}
  labels:
    app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
---
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
  name: {{ .Values.nameOverride | default "hellodevops" | quote }}
  labels:
    app.kubernetes.io/name: {{ .Values.nameOverride | default "hellodevops" | quote }}
spec:
  runPolicy: Serial
  source:
    type: Git
    git:
      uri: {{ .Values.build.gitUri | quote }}
    contextDir: {{ .Values.build.contextDir | quote }}
  strategy:
    type: Docker
    dockerStrategy:
      dockerfilePath: Dockerfile
      env:
      - name: GIT_SSL_NO_VERIFY
        value: "true"
  output:
    to:
      kind: ImageStreamTag
      name: "{{ .Values.nameOverride | default "hellodevops" }}:latest"
  triggers: []
{{- end }}

Zmien ssh na https w gitUri

nameOverride: "hellodevops"

image:
  pullspec: ""  # ustawiane przez Jenkins

config:
  APP_VERSION: "dev"   # Jenkins nadpisze np. dev-<sha>
  ENVIRONMENT: "dev"
  USER_NAME: "Zbychu-Dev"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_dev"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_dev"
  DBA_PASSWORD: "AZXCmntDEV26"

build:
  enabled: true
  gitUri: "https://[email protected]/dev-ops/hellodevops.git"
  contextDir: "app"
cd /repo/git/hellodevops
git add gitops/charts/hellodevops/templates/buildconfig.yaml gitops/charts/hellodevops/values-dev.yaml
git commit -m "fix(dev): disable TLS verify for GitLab clone in BuildConfig (self-signed)"
git push origin main
# Wymuś refresh ArgoCD (żeby zaktualizowało BuildConfig)
oc -n argocd annotate application hellodevops-dev --overwrite argocd.argoproj.io/refresh=hard

# Sprawdź, czy BuildConfig ma już env
oc -n hellodevops-dev get bc hellodevops -o yaml | sed -n '/dockerStrategy:/,/output:/p' | sed -n '1,80p'
# Odpal build jeszcze raz
oc -n hellodevops-dev start-build bc/hellodevops --follow --wait
...
Writing manifest to image destination
Successfully pushed image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:7c4dae7fe48f3d1aa4a3332a4c42ed46fb2d478b5d497933a40f2e79f5aced57
Push successful

# Pobierz pullspec z internal registry (digest)
PULLSPEC=$(oc -n hellodevops-dev get istag hellodevops:latest -o jsonpath='{.image.dockerImageReference}')
echo "$PULLSPEC"
image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:7c4dae7fe48f3d1aa4a3332a4c42ed46fb2d478b5d497933a40f2e79f5aced57

# Wstaw pullspec do values-dev.yaml i push
nano /repo/git/hellodevops/gitops/charts/hellodevops/values-dev.yaml
...
image:
  pullspec: "WKLEJ_TU_PULLSPEC"
nameOverride: "hellodevops"

image:
  pullspec: "image-registry.openshift-image-registry.svc:5000/hellodevops-dev/hellodevops@sha256:7c4dae7fe48f3d1aa4a3332a4c42ed46fb2d478b5d497933a40f2e79f5aced57"

config:
  APP_VERSION: "dev"   # Jenkins nadpisze np. dev-<sha>
  ENVIRONMENT: "dev"
  USER_NAME: "Zbychu-Dev"
  DELAY: "5"

db:
  DBA_SERVER: "192.168.40.25"
  DBA_NAME: "dba_hellodevops_dev"
  DBA_PORT: "5432"
  DBA_USER: "usr_hellodevops_dev"
  DBA_PASSWORD: "AZXCmntDEV26"

build:
  enabled: true
  gitUri: "https://[email protected]/dev-ops/hellodevops.git"
  contextDir: "app"
cd /repo/git/hellodevops
git add gitops/charts/hellodevops/values-dev.yaml
git commit -m "gitops(dev): set image pullspec after successful build"
git push origin main

Poczekaj aż ArgoCD zsyncuje i sprawdź pody

# Zrestartuj Deployment
oc -n argocd annotate application hellodevops-dev --overwrite argocd.argoproj.io/refresh=hard
oc -n hellodevops-dev rollout restart deploy/hellodevops
oc -n hellodevops-dev rollout status deploy/hellodevops

# Sprawdź pod i logi
oc -n hellodevops-dev get pods
oc -n hellodevops-dev describe pod -l app.kubernetes.io/name=hellodevops | sed -n '/Events:/,$p'
# Jak będzie Running, to logi:
oc -n hellodevops-dev logs deploy/hellodevops --tail=80

Dodatkowo (ważne na przyszłość) – bis/prod i pull z dev registry

oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-bis -n hellodevops-dev
oc policy add-role-to-group system:image-puller system:serviceaccounts:hellodevops-prod -n hellodevops-dev
# jesli pod loguje na konsoli to znaczy ze jes tOK
oc get pods
NAME                         READY   STATUS      RESTARTS   AGE
hellodevops-4-build          0/1     Completed   0          12m
hellodevops-f7865565-q6vzm   1/1     Running     0          2m48s