Powiązane z tematem wpisy
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-projekt-healthlog-w-standardzie-helm-oraz-kustomize/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-serwer-gitlab-pod-argo-cd-i-okd/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-konfiguracja-argo-cd-do-wspolpracy-z-gitlab/
- https://itadmin.vblog.ovh/klaster-okd-4-19-openshift-jenkins-serwer-automatyzacji-ci-cd-continuous-integration-continuous-delivery-deployment/
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.yamlapiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
helmCharts:
- name: hellodevops
releaseName: hellodevops
version: 0.1.0
chartPath: ../../../helm
valuesFile: values-dev.yamlnameOverride: "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.yamlnameOverride: "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.yamlnameOverride: "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=trueapiVersion: 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=trueapiVersion: 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=trueWypchnię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.yamlCommit 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ć: repositoryWymuś 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 mainWymuś 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,isRę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-credsPonowny 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

