78 Commits
Author SHA1 Message Date
Marcin-Ramotowski dedd162bb5 Automatyczna zmiana: 1752168984 2025-07-10 19:36:24 +02:00
Marcin-Ramotowski d273a4254f Automatyczna zmiana: 1752168860 2025-07-10 19:34:20 +02:00
Marcin-Ramotowski 2ddee5ddbb Automatyczna zmiana: 1752168728 2025-07-10 19:32:08 +02:00
Marcin-Ramotowski 033493b3f5 Automatyczna zmiana: 1752168604 2025-07-10 19:30:04 +02:00
Marcin-Ramotowski 039c9df549 Automatyczna zmiana: 1752168476 2025-07-10 19:27:56 +02:00
Marcin-Ramotowski 65992ecc75 Automatyczna zmiana: 1752168352 2025-07-10 19:25:52 +02:00
Marcin-Ramotowski 0347431341 Automatyczna zmiana: 1752168235 2025-07-10 19:23:55 +02:00
Marcin-Ramotowski 696d4557ab Automatyczna zmiana: 1752168113 2025-07-10 19:21:53 +02:00
Marcin-Ramotowski d07f41ece6 Automatyczna zmiana: 1752167993 2025-07-10 19:19:53 +02:00
Marcin-Ramotowski c2bc6761c3 Automatyczna zmiana: 1752167873 2025-07-10 19:17:53 +02:00
Marcin-Ramotowski 76383b030b Automatyczna zmiana: 1752167754 2025-07-10 19:15:54 +02:00
Marcin-Ramotowski 17ca39e4b0 Automatyczna zmiana: 1752167631 2025-07-10 19:13:51 +02:00
Marcin-Ramotowski d04b88c6b8 Automatyczna zmiana: 1752167502 2025-07-10 19:11:42 +02:00
Marcin-Ramotowski 9a746322f9 Automatyczna zmiana: 1752167383 2025-07-10 19:09:43 +02:00
Marcin-Ramotowski 22bb43f63f Automatyczna zmiana: 1752167268 2025-07-10 19:07:48 +02:00
Marcin-Ramotowski 97716abc4c Automatyczna zmiana: 1752167129 2025-07-10 19:05:29 +02:00
Marcin-Ramotowski 501c9cc7c7 Automatyczna zmiana: 1752166998 2025-07-10 19:03:18 +02:00
Marcin-Ramotowski b02c240e5b Automatyczna zmiana: 1752166872 2025-07-10 19:01:12 +02:00
Marcin-Ramotowski d3cc1f6ff9 Automatyczna zmiana: 1752166750 2025-07-10 18:59:10 +02:00
Marcin-Ramotowski ab75cec74a Automatyczna zmiana: 1752166465 2025-07-10 18:54:25 +02:00
Marcin-Ramotowski 36e7486726 Automatyczna zmiana: 1752093110 2025-07-09 22:31:50 +02:00
Marcin-Ramotowski 050916003f Automatyczna zmiana: 1752092853 2025-07-09 22:27:33 +02:00
Marcin-Ramotowski 6da6ea634e Automatyczna zmiana: 1752092595 2025-07-09 22:23:15 +02:00
Marcin-Ramotowski 3fadc63726 Automatyczna zmiana: 1752091222 2025-07-09 22:00:22 +02:00
Marcin-Ramotowski d7da79d1d2 Automatyczna zmiana: 1752091083 2025-07-09 21:58:03 +02:00
Marcin-Ramotowski 4fbb54f446 Automatyczna zmiana: 1752090936 2025-07-09 21:55:36 +02:00
Marcin-Ramotowski 7034bf3dce Automatyczna zmiana: 1752090792 2025-07-09 21:53:12 +02:00
Marcin-Ramotowski 9ac5ca85bd Automatyczna zmiana: 1752090642 2025-07-09 21:50:42 +02:00
Marcin-Ramotowski a514f374ed Automatyczna zmiana: 1752090505 2025-07-09 21:48:25 +02:00
Marcin-Ramotowski 5041f290fa Automatyczna zmiana: 1752090352 2025-07-09 21:45:52 +02:00
Marcin-Ramotowski f364694af8 Automatyczna zmiana: 1752090205 2025-07-09 21:43:25 +02:00
Marcin-Ramotowski d20a83431b Automatyczna zmiana: 1752090065 2025-07-09 21:41:05 +02:00
Marcin-Ramotowski f845f5d7c2 Automatyczna zmiana: 1752089917 2025-07-09 21:38:37 +02:00
Marcin-Ramotowski 44ad02e6ae Automatyczna zmiana: 1752089436 2025-07-09 21:30:36 +02:00
Marcin-Ramotowski 745e9b5bac Automatyczna zmiana: 1752089288 2025-07-09 21:28:08 +02:00
Marcin-Ramotowski 225f998a8c Automatyczna zmiana: 1752088919 2025-07-09 21:21:59 +02:00
Marcin-Ramotowski 84c57feb84 Automatyczna zmiana: 1752088738 2025-07-09 21:18:58 +02:00
Marcin-Ramotowski de405308bb Automatyczna zmiana: 1752088584 2025-07-09 21:16:24 +02:00
Marcin-Ramotowski 9c2848df4a Automatyczna zmiana: 1752088408 2025-07-09 21:13:28 +02:00
Marcin-Ramotowski a7c6051995 Automatyczna zmiana: 1752088132 2025-07-09 21:08:52 +02:00
Marcin-Ramotowski cbd6be779c Automatyczna zmiana: 1752087761 2025-07-09 21:02:41 +02:00
Marcin-Ramotowski 32e993da4d Automatyczna zmiana: 1752087570 2025-07-09 20:59:30 +02:00
Marcin-Ramotowski 32b9580a82 Automatyczna zmiana: 1752087363 2025-07-09 20:56:03 +02:00
Marcin-Ramotowski abeb0e5271 Added script to trigger and measure deployment time 2025-07-09 20:50:42 +02:00
Marcin-Ramotowski b7920e3644 Added build args to Dockerfile 2025-07-08 21:41:54 +02:00
Marcin-Ramotowski e573c573bf Added build args 2025-07-08 21:31:02 +02:00
Marcin-Ramotowski 23814bef0c Moved clone command to sshagent block 2025-07-08 19:44:23 +02:00
Marcin-Ramotowski 49f19dabe6 Added saving known_hosts to file 2025-07-08 19:03:23 +02:00
Marcin-Ramotowski 1de32691c6 Corrected git remote command 2025-07-04 23:46:57 +02:00
Marcin-Ramotowski f05d3940fa Modified git repo url to auth 2025-07-04 23:35:51 +02:00
Marcin-Ramotowski 023daf1d4b Added git clone and git push with credentials 2025-07-04 23:33:28 +02:00
Marcin-Ramotowski e91273888a Changed command run for git container 2025-07-04 22:59:51 +02:00
Marcin-Ramotowski 247798abc8 Implemented auto commit new app version to GitOps by pipeline 2025-07-04 22:42:51 +02:00
Marcin-Ramotowski 600b5aeba8 Merge branch 'dev' into jenkins-pipeline 2025-07-03 23:36:31 +02:00
Marcin-Ramotowski c9b1dac864 Added endpoint to fetch app version 2025-07-03 22:47:21 +02:00
Marcin-Ramotowski a79ae2d50f Apply new features from branch 'dev' into jenkins-pipeline 2025-06-12 18:42:28 +00:00
Marcin-Ramotowski cd4ab3fd27 Handled more errors during db initialization 2025-06-12 18:42:07 +00:00
Marcin-Ramotowski 301cf5922e Changed docker image base to Alpine and added curl 2025-06-11 22:15:37 +00:00
Marcin-Ramotowski 479ec4f917 Added healthcheck 2025-06-11 22:04:35 +00:00
Marcin-Ramotowski 3f40a6126c Added more descriptions of functions 2025-06-11 20:04:04 +00:00
Marcin-Ramotowski dd9e9ce110 Improved function body 2025-06-11 19:57:15 +00:00
Marcin-Ramotowski d9fe927832 Removed deprecated label option from pipeline 2025-06-11 17:51:55 +00:00
Marcin-Ramotowski 99cfdfddd0 Added annotation required to run Sysbox on pod 2025-06-11 16:52:16 +00:00
Marcin-Ramotowski f579e440f8 Updated path to pod template yaml file 2025-06-11 16:43:22 +00:00
Marcin-Ramotowski ba69728c81 Changed pod runtime to sysbox 2025-06-11 16:32:12 +00:00
Marcin-Ramotowski 5366e313c5 Moved Jenkinsfile and pod agent template to separate directory 2025-06-11 16:29:03 +00:00
Marcin-Ramotowski 283be1a1ec Deleted Goss 2025-06-11 16:28:08 +00:00
Marcin-Ramotowski 1b7204c2ba Changed name of variable to store ACR name instead of ACR username 2025-06-10 20:17:25 +00:00
Marcin-Ramotowski 8f9aed299d Added managed identity client id 2025-06-10 19:32:40 +00:00
Marcin-Ramotowski 6522977280 Changed basic auth to managed identity 2025-06-10 18:50:37 +00:00
Marcin-Ramotowski c707974a2e Corrected agent declaration in Jenkinsfile 2025-06-08 16:56:35 +00:00
Marcin-Ramotowski cc2f224d60 Moved pod agent code from Jenkins master to YAML file in repo 2025-06-08 16:45:05 +00:00
Marcin-Ramotowski b14e6cf873 Restored dind container usage 2025-06-07 21:44:24 +00:00
Marcin-Ramotowski 87e3c0df80 Removed Goss tests 2025-06-07 21:14:40 +00:00
Marcin-Ramotowski aea09a6081 Added bash installation 2025-06-07 15:08:58 +00:00
Marcin-Ramotowski d05cede409 The command curl is replaced with wget 2025-06-07 15:06:19 +00:00
Marcin-Ramotowski 17162027b6 Removed unnecessary post cleanup 2025-06-07 14:57:35 +00:00
Marcin-Ramotowski 8887f1b2bd Updated Jenkins pipeline to use in Kubernetes 2025-06-07 13:31:03 +00:00
11 changed files with 279 additions and 366 deletions
+93
View File
@@ -0,0 +1,93 @@
pipeline {
agent {
kubernetes {
yamlFile '.jenkins/podTemplate.yaml'
}
}
environment {
ACR_NAME = 'marcin00'
CLIENT_ID = 'c302726f-fafb-4143-94c1-67a70975574a'
DOCKER_REGISTRY_URL = 'marcin00.azurecr.io'
DOCKER_IMAGE = "${DOCKER_REGISTRY_URL}/user-microservice:${GIT_COMMIT}"
DEPLOY_REPO = 'ssh://[email protected]:20343/pikram/user-microservice-deploy.git'
}
stages {
stage('Code Tests') {
steps {
container('python') {
dir('api') {
sh '''
python3 -m venv env
source env/bin/activate
pip install -r requirements.txt pytest
python3 -m pytest --junit-xml=pytest_junit.xml
'''
}
}
}
post {
always {
junit testResults: '**/*pytest_junit.xml'
}
}
}
stage('Build & Push Docker') {
steps {
container('docker') {
sh '''
docker build -t ${DOCKER_IMAGE} --build-arg APP_VERSION=${GIT_COMMIT} --build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") .
az login --identity --client-id ${CLIENT_ID}
az acr login --name ${ACR_NAME}
docker push ${DOCKER_IMAGE}
'''
}
}
}
stage('Commit new version to GitOps repo') {
steps {
container('git') {
withCredentials([string(credentialsId: 'gitea-known-host', variable: 'GITEA_KNOWN_HOST')]) {
sh '''
mkdir -p ~/.ssh
echo "$GITEA_KNOWN_HOST" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
git config --global user.name "jenkins[bot]"
git config --global user.email "[email protected]"
'''
sshagent(['gitea-deploy-key']) {
sh 'git clone ${DEPLOY_REPO} --branch jenkins-kubernetes'
}
}
dir('user-microservice-deploy') {
sh '''
# Podmień tag obrazu w pliku deploy.yaml
awk -v commit="$GIT_COMMIT" '
$0 ~ /name:[[:space:]]*api/ { in_api_container = 1; print; next }
in_api_container && $0 ~ /^[[:space:]]*image:[[:space:]]*/ {
sub(/:[^:[:space:]]+$/, ":" commit)
in_api_container = 0
print
next
}
{ print }
' deploy.yaml > deploy.tmp && mv deploy.tmp deploy.yaml
'''
sshagent(['gitea-deploy-key']) {
sh '''
git add deploy.yaml
git diff-index --quiet HEAD || git commit -m "JENKINS: Changed deployed version to $GIT_COMMIT"
git push origin jenkins-kubernetes
'''
}
}
}
}
}
}
}
+60
View File
@@ -0,0 +1,60 @@
apiVersion: v1
kind: Pod
metadata:
annotations:
io.kubernetes.cri-o.userns-mode: "auto:size=65536"
labels:
jenkins: "slave"
jenkins/label: "kubernetes-agent"
spec:
runtimeClassName: sysbox-runc
containers:
- name: jnlp
image: jenkins/inbound-agent:alpine
tty: false
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
env:
- name: JENKINS_WEB_SOCKET
value: "true"
- name: REMOTING_OPTS
value: "-noReconnectAfter 1d"
- name: python
image: python:3.11.7-alpine
command:
- cat
tty: true
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: docker
image: marcin00.azurecr.io/azure-cli-docker:slim-bookworm
tty: true
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
- name: git
image: alpine/git:latest
command:
- cat
tty: true
workingDir: /home/jenkins/agent
volumeMounts:
- name: workspace-volume
mountPath: /home/jenkins/agent
nodeSelector:
kubernetes.io/os: linux
restartPolicy: Never
volumes:
- name: workspace-volume
emptyDir: {}
+14 -1
View File
@@ -1,5 +1,18 @@
FROM python:3.11.7-slim-bookworm FROM python:3.11.7-alpine
# Wersja i data builda jako build-arg
ARG APP_VERSION=unknown
ARG BUILD_DATE=unknown
# Ustawiamy zmienne w ENV, by były dostępne w kontenerze
ENV APP_VERSION=$APP_VERSION
ENV BUILD_DATE=$BUILD_DATE
WORKDIR /app WORKDIR /app
COPY api . COPY api .
RUN apk add --no-cache curl
RUN pip install -r requirements.txt RUN pip install -r requirements.txt
CMD python3 app.py CMD python3 app.py
-353
View File
@@ -1,353 +0,0 @@
# user-microservice
Mikroserwis REST do zarządzania kontami użytkowników, zbudowany w celu **porównania
narzędzi CI/CD do automatycznego budowania i wdrażania aplikacji skonteneryzowanych**.
Projekt powstał w ramach pracy magisterskiej.
To repozytorium zawiera **kod aplikacji** oraz **potoki CI** (budowanie, testy, publikacja
obrazu, aktualizacja repozytorium GitOps). Manifesty Kubernetes i konfiguracje narzędzi CD
znajdują się w drugim repozytorium: **[`user-microservice-deploy`](../user-microservice-deploy)**.
---
## Spis treści
- [Cel projektu](#cel-projektu)
- [Architektura rozwiązania](#architektura-rozwiązania)
- [Porównywane narzędzia (5)](#porównywane-narzędzia-5)
- [Scenariusze pomiarowe](#scenariusze-pomiarowe)
- [Mapa branchy](#mapa-branchy)
- [Aplikacja](#aplikacja)
- [Stos technologiczny](#stos-technologiczny)
- [API](#api)
- [Model danych](#model-danych)
- [Konfiguracja (zmienne środowiskowe)](#konfiguracja-zmienne-środowiskowe)
- [Uruchomienie lokalne](#uruchomienie-lokalne)
- [Testy](#testy)
- [Potoki CI](#potoki-ci)
- [Jenkins](#jenkins)
- [Woodpecker CI](#woodpecker-ci)
- [Argo Workflows](#argo-workflows)
- [Wspólny schemat potoku CI](#wspólny-schemat-potoku-ci)
- [Infrastruktura](#infrastruktura)
- [Pomiar dostarczenia zmiany](#pomiar-dostarczenia-zmiany)
- [Struktura repozytorium](#struktura-repozytorium)
---
## Cel projektu
Zaprojektowano jednolity proces CI/CD (build → test → publikacja obrazu → wdrożenie na
Kubernetes) i zaimplementowano go w **pięciu narzędziach**, a następnie **zmierzono czas
dostarczenia zmiany** (od commita do działającej aplikacji) dla różnych kombinacji narzędzi
CI i CD. Celem była obiektywna, ilościowa i jakościowa charakterystyka tych narzędzi.
Proces jest identyczny funkcjonalnie w każdym narzędziu:
1. Uruchomienie po zdarzeniu `push` do repozytorium aplikacji.
2. Uruchomienie testów jednostkowych (`pytest`).
3. Zbudowanie obrazu kontenera i opublikowanie go w rejestrze (Azure Container Registry),
z tagiem równym SHA commita.
4. Zaktualizowanie manifestu wdrożenia w repozytorium GitOps (`user-microservice-deploy`) —
podmiana taga obrazu na nowy SHA.
5. Wdrożenie nowej wersji na klaster Kubernetes (Azure AKS) przez narzędzie CD.
## Architektura rozwiązania
```
┌──────────────────────────┐ push ┌───────────────────────────┐
│ user-microservice (Git) │ ────────────────────▶ │ Potok CI │
│ kod aplikacji + potoki │ │ Jenkins / Woodpecker / │
└──────────────────────────┘ │ Argo Workflows │
└────────────┬──────────────┘
┌──────────────────────────────────┬──────────┴───────┐
│ 1. pytest │ │
│ 2. docker build + push ▼ ▼
│ ┌────────────────────┐ ┌───────────────┐
│ │ Azure Container │ │ commit taga │
│ │ Registry (ACR) │ │ do repo │
│ └────────────────────┘ │ GitOps │
│ └───────┬───────┘
▼ │
┌──────────────────────────┐ obserwuje / webhook ┌───────────────────────────┐
│ user-microservice-deploy │ ◀──────────────────────────────▶│ Potok CD │
│ manifesty K8s (GitOps) │ │ kubectl / ArgoCD / Flux │
└──────────────────────────┘ └────────────┬──────────────┘
│ apply / sync
┌───────────────────────────┐
│ Azure Kubernetes (AKS) │
│ namespace: user-... │
└───────────────────────────┘
```
## Porównywane narzędzia (5)
| Rola | Narzędzie | Model działania |
|------|-----------|-----------------|
| **CI** (build/test/push) | **Jenkins** | Potok deklaratywny (`Jenkinsfile`), agent w Kubernetes (pod z wieloma kontenerami) |
| **CI** | **Woodpecker CI** | Potok YAML (`.woodpecker/build.yaml`), backend Kubernetes |
| **CI** | **Argo Workflows** | Sterowany zdarzeniami (Argo Events: `EventSource` + `Sensor``Workflow`) |
| **CD** (deploy) | **ArgoCD** | GitOps typu *pull* — synchronizuje stan klastra z repo deploymentowym |
| **CD** | **FluxCD** | GitOps typu *pull*`GitRepository` + `Kustomization` + `Receiver` |
Dodatkowo każde narzędzie CI potrafi też pełnić rolę CD w wariancie „to samo narzędzie":
wdrożenie realizowane jest wtedy imperatywnie przez `kubectl apply` (potok wyzwalany
commitem do repo GitOps).
## Scenariusze pomiarowe
Pełna macierz to **3 narzędzia CI × 3 warianty CD = 9 scenariuszy**:
| # | CI | CD |
|---|----|----|
| Scenariusz 1 | Jenkins | Jenkins (`kubectl apply`) |
| Scenariusz 2 | Jenkins | ArgoCD |
| Scenariusz 3 | Jenkins | FluxCD |
| Scenariusz 4 | Woodpecker | Woodpecker (`kubectl apply`) |
| Scenariusz 5 | Woodpecker | ArgoCD |
| Scenariusz 6 | Woodpecker | FluxCD |
| Scenariusz 7 | Argo Workflows | Argo Workflows |
| Scenariusz 8 | Argo Workflows | ArgoCD |
| Scenariusz 9 | Argo Workflows | FluxCD |
> Scenariusze 13 obejmują Jenkinsa, 46 Woodpeckera, a 79 Argo Workflows — w każdej trójce
> kolejno: to samo narzędzie do CD, ArgoCD oraz FluxCD.
## Mapa branchy
Każda kombinacja narzędzi ma osobny branch — w obu repozytoriach. Poniżej odwzorowanie
scenariuszy na branche.
**Repozytorium aplikacji (`user-microservice`) — definicje CI:**
| Branch | Zawartość |
|--------|-----------|
| `main` | Kod aplikacji (bazowy), ten README, skrypty analizy |
| `dev` | Gałąź rozwojowa aplikacji + narzędzia pomiarowe (`deployment_timer.sh`, `data_analysis.py`) |
| `jenkins-pipeline` | CI w Jenkins (`.jenkins/Jenkinsfile`, `podTemplate.yaml`) |
| `woodpecker` | CI w Woodpecker (`.woodpecker/build.yaml`) |
| `argo-workflows` | CI w Argo Workflows / Argo Events (`argo-workflows/*`) |
| `jenkins-fluxcd`, `woodpecker-argocd`, `woodpecker-fluxcd`, `argoworkflow-argocd`, `argoworkflow-fluxcd` | Warianty CI dostrojone do konkretnego narzędzia CD (np. docelowy branch repo GitOps) |
**Repozytorium deploymentowe (`user-microservice-deploy`) — manifesty i CD:**
| Branch | Scenariusz |
|--------|-----------|
| `master` | Baza manifestów |
| `jenkins-kubernetes` | Sc 1 — Jenkins wdraża przez `kubectl` |
| `jenkins-argocd-deploy` | Sc 2 — ArgoCD |
| `jenkins-fluxcd-deploy` | Sc 3 — FluxCD |
| `woodpecker-deploy` | Sc 4 — Woodpecker wdraża przez `kubectl` |
| `woodpecker-argocd-deploy` | Sc 5 — ArgoCD |
| `woodpecker-fluxcd-deploy` | Sc 6 — FluxCD |
| `argo-deploy` | Sc 7 — Argo Workflows |
| `argoworkflow-argocd` | Sc 8 — ArgoCD |
| `argoworkflow-fluxcd` / `fluxcd` | Sc 9 — FluxCD |
> Uwaga: warianty FluxCD używają układu katalogów `apps/` + `clusters/prod/` wymaganego przez
> Flux, pozostałe warianty trzymają manifesty w katalogu głównym repo GitOps.
---
## Aplikacja
Mikroserwis udostępnia API REST do zarządzania kontami użytkowników wraz z uwierzytelnianiem
JWT (tokeny w ciasteczkach), rolami oraz mechanizmem wylogowania (unieważnianie tokenów).
### Stos technologiczny
- **Python 3.11**, **Flask 3**
- **Flask-JWT-Extended** — uwierzytelnianie JWT
- **Flask-SQLAlchemy / SQLAlchemy 2** — ORM
- **MySQL** (produkcyjnie), **SQLite in-memory** (testy)
- **waitress** — produkcyjny serwer WSGI
- **pytest** — testy jednostkowe
- **Docker** — konteneryzacja (obraz bazowy `python:3.11.7-slim-bookworm`)
### API
| Metoda | Ścieżka | Opis | Autoryzacja |
|--------|---------|------|-------------|
| `POST` | `/users` | Utworzenie użytkownika | brak (konto `Administrator` wymaga tokena admina) |
| `GET` | `/users` | Lista wszystkich użytkowników | tylko `Administrator` |
| `GET` | `/users/<id>` | Szczegóły użytkownika | właściciel lub `Administrator` |
| `PUT`/`PATCH` | `/users/<id>` | Edycja użytkownika (`PUT` wymaga wszystkich pól) | właściciel lub `Administrator` |
| `DELETE` | `/users/<id>` | Usunięcie użytkownika | właściciel lub `Administrator` |
| `POST` | `/login` | Logowanie, ustawia token JWT w ciasteczku | brak |
| `GET` | `/logout` | Wylogowanie (unieważnienie tokena) | zalogowany |
| `GET` | `/health` | Sprawdzenie stanu aplikacji i połączenia z bazą | brak *(dostępne na branchach z potokami CI/CD)* |
| `GET` | `/version` | Zwraca wdrożoną wersję (`APP_VERSION` = SHA commita) i czas budowy | brak *(dostępne na branchach z potokami CI/CD)* |
Przy pierwszym uruchomieniu, jeśli baza jest pusta, tworzone jest domyślne konto administratora
(zob. zmienne `ADMIN_*`).
### Model danych
- **`User`** — `id`, `username` (unikalny), `email` (unikalny), `role` (`Administrator`/`User`),
`password` (hash).
- **`RevokedToken`** — `jti` unieważnionych tokenów JWT (blocklist przy wylogowaniu).
### Konfiguracja (zmienne środowiskowe)
| Zmienna | Opis | Domyślna |
|---------|------|----------|
| `SQLALCHEMY_DATABASE_URI` | Połączenie do bazy MySQL | — (wymagana poza testami) |
| `JWT_SECRET_KEY` | Klucz podpisujący tokeny JWT | `changeme` |
| `APP_PORT` | Port serwera | `80` |
| `ADMIN_USERNAME` | Login domyślnego admina | `admin` |
| `ADMIN_EMAIL` | E-mail domyślnego admina | `[email protected]` |
| `ADMIN_PASSWORD` | Hasło domyślnego admina | `admin` |
Produkcyjnie sekrety (`SQLALCHEMY_DATABASE_URI`, hasła MySQL) są dostarczane z **Azure Key
Vault** przez sterownik CSI `secrets-store` — zob. repozytorium deploymentowe.
### Uruchomienie lokalne
Za pomocą Docker Compose (uruchamia API + MySQL):
```bash
# przygotuj pliki api/.env oraz db/.env ze zmiennymi środowiskowymi
docker compose up --build
```
Bez konteneryzacji:
```bash
cd api
python3 -m venv env && source env/bin/activate
pip install -r requirements.txt
python3 app.py
```
Aplikacja czeka na gotowość bazy danych przy starcie (`wait_for_db`, do 100 prób co 3 s).
### Testy
```bash
cd api
python3 -m venv env && source env/bin/activate
pip install -r requirements.txt pytest
python3 -m pytest
```
Testy używają bazy SQLite w pamięci (konfiguracja `testing`) i pokrywają operacje CRUD oraz
uwierzytelnianie. Ten sam zestaw testów jest uruchamiany w każdym potoku CI.
> Wcześniejsze wersje potoków korzystały dodatkowo z testów kontenera **Goss** — zostały one
> ostatecznie usunięte z potoków (por. historia commitów), a walidację przejęły testy
> `pytest` oraz health check po wdrożeniu.
---
## Potoki CI
Definicje potoków znajdują się na branchach poszczególnych narzędzi. Wszystkie realizują ten
sam trzyetapowy proces: **testy → build&push → commit do GitOps**.
### Jenkins
Branch `jenkins-pipeline`, plik `.jenkins/Jenkinsfile`. Potok deklaratywny uruchamiany na
**agencie w Kubernetes** — pod z osobnymi kontenerami do każdego etapu (`podTemplate.yaml`):
- `python` — uruchomienie `pytest` (wyniki jako JUnit XML),
- `docker``docker build` + logowanie do ACR (Azure managed identity) + `docker push`,
- `git` — sklonowanie repo GitOps, podmiana taga obrazu (`awk`) i commit na branch
`jenkins-kubernetes`.
Build kontenera działa bez uprawnień roota dzięki **sysbox** (`runtimeClassName: sysbox-runc`).
### Woodpecker CI
Branch `woodpecker`, plik `.woodpecker/build.yaml`. Trzy kroki (`code-tests`,
`build-and-push`, `gitops-commit`) na backendzie Kubernetes. Docker-in-Docker uruchamiany
lokalnie (`dockerd &`) w kontenerze z sysbox. Sekrety (klucz deploy do Gitea, known hosts)
pobierane z sekretów Woodpeckera.
### Argo Workflows
Branch `argo-workflows`, katalog `argo-workflows/`. Podejście **sterowane zdarzeniami** przez
Argo Events:
- `source.yaml``EventSource` (webhook, dwa endpointy: dla repo aplikacji i deploymentowego),
- `sensor.yaml``Sensor` reagujący na push i tworzący `Workflow` budujący (etapy:
`checkout``tests``build-and-push-image``gitops-commit`),
- `eventbus-default.yaml` — szyna zdarzeń (NATS),
- `permissions.yaml``ServiceAccount` + `Role`/`RoleBinding`,
- `secret-store.yaml` — dostęp do sekretów z Key Vault.
### Wspólny schemat potoku CI
Wszystkie trzy narzędzia wykonują tę samą logikę, różniąc się jedynie składnią i modelem
uruchamiania:
```
push → [pytest] → [docker build → az acr login → docker push (tag = SHA)] → [clone repo GitOps → awk: podmiana tagu → commit → push]
```
Aktualizacja manifestu w repo GitOps jest realizowana identycznym skryptem `awk`, który
znajduje kontener `api` i podmienia w nim tag obrazu na SHA nowego commita.
## Infrastruktura
| Element | Zastosowanie |
|---------|--------------|
| **Azure Kubernetes Service (AKS)** | Klaster docelowy; działają na nim także agenci CI oraz kontrolery CD |
| **Azure Container Registry** (`marcin00.azurecr.io`) | Rejestr obrazów |
| **Azure Key Vault** + CSI `secrets-store` | Dostarczanie sekretów do podów |
| **Managed Identity** | Bezhasłowe uwierzytelnianie do ACR / AKS / Key Vault |
| **Gitea** (self-hosted, `gitea.marcin00.pl`) | Repozytoria Git + webhooki wyzwalające potoki |
| **sysbox** (`sysbox-runc`) | Budowanie obrazów bez uprawnień roota (rootless DinD) |
| **NGINX Ingress** | Wystawienie aplikacji (`user-microservice.marcin00.pl`) |
## Pomiar dostarczenia zmiany
Metryką porównawczą jest **czas dostarczenia zmiany** — od commita do momentu, w którym nowa
wersja aplikacji faktycznie działa na klastrze. Pomiar jest w pełni zautomatyzowany, a
narzędzia znajdują się na branchu **`dev`**:
- **[`deployment_timer.sh`](../../tree/dev/deployment_timer.sh)** — automatyczny pomiar
pojedynczego wdrożenia. Skrypt:
1. odczytuje aktualną wersję z endpointu `/version`,
2. zapisuje znacznik czasu do pliku, commituje go (`Automatyczna zmiana: <timestamp>`)
i wykonuje `git push` — co wyzwala cały łańcuch CI/CD,
3. odpytuje `/version` co sekundę, aż zwrócona wersja się zmieni,
4. zapisuje wynik (`start,koniec,czas,stara_wersja,nowa_wersja`) do pliku
`deployment_times.csv`.
Wykrycie zmiany opiera się na tym, że potok CI buduje obraz z `APP_VERSION` równym SHA
commita, a `/version` zwraca tę wartość. Commity `Automatyczna zmiana: <timestamp>` widoczne
w historii repozytorium pochodzą właśnie z kolejnych przebiegów tego skryptu.
- **[`data_analysis.py`](../../tree/dev/data_analysis.py)** — agreguje zebrane pomiary
(po 20 wdrożeń na scenariusz) i generuje wykresy słupkowe średnich czasów dostarczenia do
katalogu `plots/`. Tworzy 6 porównań: trzy według narzędzia CI (dla każdego CD) i trzy
według narzędzia CD (dla każdego CI), obejmując wszystkie [9 scenariuszy](#scenariusze-pomiarowe).
```bash
pip install matplotlib
python3 data_analysis.py # zapisuje plots/mean_times_0..5.png
```
> Pliki wynikowe pomiarów (`deployment_times.csv`, katalog `plots/`) są generowane przez
> powyższe skrypty i nie są śledzone w repozytorium.
## Struktura repozytorium
```
user-microservice/
├── api/
│ ├── app.py # fabryka aplikacji Flask, konfiguracja, start serwera waitress
│ ├── views.py # endpointy API (użytkownicy, login/logout)
│ ├── models.py # modele SQLAlchemy: User, RevokedToken
│ ├── utils.py # autoryzacja, oczekiwanie na bazę, inicjalizacja admina
│ ├── requirements.txt # zależności aplikacji
│ └── tests/ # testy pytest (conftest.py, test_users.py)
├── Dockerfile # obraz aplikacji
└── docker-compose.yml # lokalne uruchomienie API + MySQL
```
> Definicje potoków CI (`Jenkinsfile`, `.woodpecker/`, `argo-workflows/`) znajdują się na
> odpowiednich branchach — zob. [Mapa branchy](#mapa-branchy).
+3 -1
View File
@@ -4,6 +4,7 @@ from flask_jwt_extended import JWTManager
from jwt import ExpiredSignatureError from jwt import ExpiredSignatureError
from models import db, RevokedToken from models import db, RevokedToken
import os import os
from tech_views import tech_bp
from utils import init_db, wait_for_db from utils import init_db, wait_for_db
from views import user_bp from views import user_bp
from werkzeug.exceptions import HTTPException from werkzeug.exceptions import HTTPException
@@ -26,6 +27,7 @@ def create_app(config_name="default"):
# Blueprints registration # Blueprints registration
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(tech_bp)
# Database and JWT initialization # Database and JWT initialization
db.init_app(app) db.init_app(app)
@@ -53,7 +55,7 @@ def create_app(config_name="default"):
# Fill database by initial values (only if we are not testing) # Fill database by initial values (only if we are not testing)
with app.app_context(): with app.app_context():
wait_for_db() wait_for_db(max_retries=100)
db.create_all() db.create_all()
if config_name != "testing": if config_name != "testing":
init_db() init_db()
+20
View File
@@ -0,0 +1,20 @@
from flask import Blueprint, jsonify
from models import db
from sqlalchemy import text
from utils import db_ready
# Blueprint with technical endpoints
tech_bp = Blueprint('tech_bp', __name__)
@tech_bp.route('/health', methods=['GET'])
def health_check():
"Check if service works and database is functional"
try:
with db.engine.connect() as connection:
connection.execute(text("SELECT 1"))
return jsonify(status="healthy"), 200
except Exception:
if db_ready:
return jsonify(status="unhealthy"), 500
else:
return jsonify(status="starting"), 503
+11 -11
View File
@@ -3,19 +3,21 @@ from flask_jwt_extended import get_jwt_identity
from models import User, db from models import User, db
import os import os
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.exc import DatabaseError from sqlalchemy.exc import DatabaseError, InterfaceError
import time import time
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
db_ready = False
def admin_required(user_id, message='Access denied.'): def admin_required(user_id, message='Access denied.'):
"Check if common user try to make administrative action."
user = db.session.get(User, user_id) user = db.session.get(User, user_id)
if user is None or user.role != "Administrator": if user is None or user.role != "Administrator":
abort(403, message) abort(403, message)
def validate_access(owner_id, message='Access denied.'): def validate_access(owner_id, message='Access denied.'):
# Check if user try to access or edit resource that does not belong to them "Check if user try to access or edit resource that does not belong to them."
logged_user_id = int(get_jwt_identity()) logged_user_id = int(get_jwt_identity())
logged_user_role = db.session.get(User, logged_user_id).role logged_user_role = db.session.get(User, logged_user_id).role
if logged_user_role != "Administrator" and logged_user_id != owner_id: if logged_user_role != "Administrator" and logged_user_id != owner_id:
@@ -30,20 +32,18 @@ def get_user_or_404(user_id):
return user return user
MAX_RETRIES = 100 def wait_for_db(max_retries):
"Try to connect with database <max_retries> times."
def wait_for_db(): global db_ready
for retries in range(MAX_RETRIES): for _ in range(max_retries):
try: try:
with db.engine.connect() as connection: with db.engine.connect() as connection:
connection.execute(text("SELECT 1")) connection.execute(text("SELECT 1"))
print("Successfully connected with database.") db_ready = True
return return
except DatabaseError: except DatabaseError | InterfaceError:
print(f"Waiting for database... (retry {retries + 1})")
time.sleep(3) time.sleep(3)
print("Failed to connect to database.") raise Exception("Failed to connect to database.")
raise Exception("Database not ready after multiple retries.")
def init_db(): def init_db():
+8
View File
@@ -2,6 +2,7 @@ from flask import Blueprint, jsonify, request, abort
from flask_jwt_extended import create_access_token, set_access_cookies, jwt_required, \ from flask_jwt_extended import create_access_token, set_access_cookies, jwt_required, \
verify_jwt_in_request, get_jwt_identity, unset_jwt_cookies, get_jwt verify_jwt_in_request, get_jwt_identity, unset_jwt_cookies, get_jwt
from models import db, RevokedToken, User from models import db, RevokedToken, User
import os
from utils import admin_required, validate_access, get_user_or_404 from utils import admin_required, validate_access, get_user_or_404
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
@@ -110,3 +111,10 @@ def user_logout():
response = jsonify({"msg": "User logged out successfully."}) response = jsonify({"msg": "User logged out successfully."})
unset_jwt_cookies(response) unset_jwt_cookies(response)
return response return response
@user_bp.route('/version', methods=['GET'])
def version():
return jsonify({
"version": os.getenv("APP_VERSION", "unknown"),
"build_time": os.getenv("BUILD_DATE", "unknown")
})
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# === KONFIGURACJA ===
APP_URL="https://user-microservice.marcin00.pl/version"
MARKER_FILE="version_marker.txt"
OUTPUT_FILE="deployment_times.csv"
CHECK_INTERVAL=1 # sekundy
# === POBRANIE AKTUALNEJ WERSJI APLIKACJI ===
echo "[INFO] Pobieranie aktualnej wersji z /version..."
OLD_VERSION=$(curl -s "$APP_URL" | jq -r '.version')
if [[ -z "$OLD_VERSION" ]]; then
echo "[ERROR] Nie udało się pobrać aktualnej wersji aplikacji."
exit 1
fi
echo "[INFO] Aktualna wersja: $OLD_VERSION"
# === Modyfikacja pliku, commit i push ===
TIMESTAMP=$(date +%s)
echo "$TIMESTAMP" > "$MARKER_FILE"
git add "$MARKER_FILE"
git commit -m "Automatyczna zmiana: $TIMESTAMP"
START_TIME=$(date +%s)
echo "[INFO] Wykonuję git push..."
git push
if [[ $? -ne 0 ]]; then
echo "[ERROR] Push nie powiódł się."
exit 1
fi
echo "[INFO] Oczekiwanie na wdrożenie nowej wersji..."
# === Odpytywanie endpointa /version ===
while true; do
sleep $CHECK_INTERVAL
NEW_VERSION=$(curl -s "$APP_URL" | jq -r '.version')
if [[ "$NEW_VERSION" != "$OLD_VERSION" ]]; then
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "[INFO] Nowa wersja wdrożona: $NEW_VERSION"
echo "[INFO] Czas wdrożenia: $DURATION sekund"
echo "$START_TIME,$END_TIME,$DURATION,$OLD_VERSION,$NEW_VERSION" >> "$OUTPUT_FILE"
break
else
echo "[WAIT] Czekam... ($NEW_VERSION)"
fi
done
+15
View File
@@ -7,9 +7,24 @@ services:
build: . build: .
env_file: env_file:
- api/.env - api/.env
ports:
- 80:80
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
db: db:
container_name: db container_name: db
hostname: db hostname: db
image: mysql:latest image: mysql:latest
env_file: env_file:
- db/.env - db/.env
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
+1
View File
@@ -0,0 +1 @@
1752168984