- Wstęp
- Wersje
- Dockerfile
- Kerberos
- Postgres
- Kubernetes
- Zależności
- Rakefile
- Wdrożenie
- Produkcja
- Wnioski
Wstęp
Prefect to platforma do orkestracji zadań napisana w całości w Pythonie, która umożliwia Nam tworzenie, obserwowanie i reagowanie na potoki danych.
Po za powyższymi, Perfect może służyć nawet do automatyzacji zadań, takich jak uruchamianie testów, wdrażanie oprogramowania i zarządzanie infrastrukturą oraz do budowania aplikacji opartych na sztucznej inteligencji, takich jak modele uczenia maszynowego czy zwyczajne systemy rekomendacyjne.
Wersje
Prefect jest dostępny w dwóch wersjach:
- Prefect Cloud: Pełna i płatna wersja Prefect’a z funkcjonalnością niedostępną w wersji OSS np. RBAC. Wersja cloud może pracować w konfiguracji hybrydowej (w tym przypadku agenty uruchamiamy bezpośrednio we własnej infrastrukturze).
- Prefect Open Source: Jak nazwa wskazuje, darmowa edycja opensource z pewnymi ograniczeniami. W tym całkowity brak kontroli użytkowników. Taki sam zabieg zresztą stosuje konkurent tj Dagster.
By uruchomić Prefect’a na Naszym klastrze na potrzeby PoC’owe w trybie ‘jakotako’ wypadało by chociaż ograniczyć dostęp do UI. To możemy zrobić z pomocą ingressa pod postacią Nginx’a.
Dockerfile
Na początek tworzymy Dockerfile
, zwłaszcza, że zapewne będziemy chcieli przenosić dane np. z Oracle SQL do Microsoft SQL Server. Więc warto za wczasu dodać sterowniki obydwu producentów.
mkdir -p repos/prefect && cd prefect
touch dockerfile docker-compose.yml prefect-api.yml prefect-agents.yml prefect-ingress.yml prefect-config.yml krb5.conf requirements_build.txt rakefile
bundle init
code dockerfile
Następnie edytujemy image wedle własnego uznania lub np. jak poniżej:
FROM prefecthq/prefect:2.10.20-python3.10 AS build
RUN echo "Europe/Warsaw" > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata
RUN apt update
RUN DEBIAN_FRONTEND=noninteractive apt-get -yq install krb5-user libgssapi-krb5-2
RUN apt install -yq --no-install-recommends \
curl gnupg cron wget unzip cifs-utils \
build-essential python3-dev unixodbc-dev freetds-dev \
libmariadb-dev libsasl2-dev libaio1 htop \
unixodbc odbcinst
RUN echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc
CMD ["source", "~/.bashrc"]
RUN wget https://packages.microsoft.com/debian/11/prod/pool/main/m/msodbcsql17/msodbcsql17_17.10.5.1-1_amd64.deb
RUN wget https://packages.microsoft.com/debian/11/prod/pool/main/m/msodbcsql18/msodbcsql18_18.3.2.1-1_amd64.deb
RUN ACCEPT_EULA=Y dpkg -i msodbcsql17_17.10.5.1-1_amd64.deb
RUN ACCEPT_EULA=Y dpkg -i msodbcsql18_18.3.2.1-1_amd64.deb
WORKDIR /opt/oracle
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
&& wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip \
&& unzip instantclient-basiclite-linuxx64.zip \
&& rm -f instantclient-basiclite-linuxx64.zip \
&& cd /opt/oracle/instantclient* \
&& rm -f *jdbc* *occi* *mysql* *README *jar uidrvci genezi adrci \
&& echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf \
&& ldconfig \
&& apt-get autoremove -yqq --purge \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /
COPY requirements_build.txt /
RUN pip install --no-cache-dir -r /requirements_build.txt
RUN apt-get autoremove -yqq --purge \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY config/krb5/krb5.conf /etc/
Uwaga: Plik na potrzeby artykułu nie jest zoptymalizowany. Brak w nim choćby stagingu, przez co jego rozmiar wynosi około jednego gigabajta.
Następnie uzupełniamy compose’a:
# For image building purposes only
version: "3"
services:
prefect-image:
build:
context: .
tags: [internal-cr:9999/prefect-core:latest]
image: prefect/core
Kerberos
Uzupełniamy plik krb5.conf
podmieniając wpisy dla ‘company.local’ na Nasze własne wpisy, lub ignorujemy:
[libdefaults]
default_realm = company.local
# The following krb5.conf variables are only for MIT Kerberos.
kdc_timesync = 1
ccache_type = 4
forwardable = true
proxiable = true
rdns = false
# The following libdefaults parameters are only for Heimdal Kerberos.
fcc-mit-ticketflags = true
[realms]
company.local = {
kdc = datacenter01.company.local
admin_server = datacenter01.company.local
}
[domain_realm]
.company.local = COMPANY.LOCAL
Postgres
Zakładam, że już i tak masz gdzieś wystawionego Postgres’a. Tym samym część dot. jego wystawienia zupełnie pominę.
Potrzebujemy jedynie nowej bazy i użytkownika wraz z hasłem pod Nasze API.
Kubernetes
API
Teraz, powinniśmy zająć się Naszym statefulsetem dla samego API.
Zaczynami od edycji prefect-api.yml
:
code prefect-api.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: prefect-api
namespace: prefect
spec:
selector:
matchLabels:
app: prefect-api # has to match .spec.template.metadata.labels
serviceName: "prefect-api-headless"
replicas: 1 # by default is 1
template:
metadata:
labels:
app: prefect-api # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
imagePullSecrets:
- name: regcred ## Our internal container registry config
containers:
- name: prefect-api
image: internal-cr:9999/prefect-core:latest
command:
[
"prefect",
"server",
"start",
"--host",
"0.0.0.0",
"--port",
"4200",
"--log-level",
"WARNING",
]
imagePullPolicy: "Always"
resources:
requests:
cpu: 512m
memory: 1024Mi
ports:
- containerPort: 4200
env:
- name: PREFECT_SERVER_ANALYTICS_ENABLED
value: "false"
- name: PREFECT_API_KEY
valueFrom:
secretKeyRef:
name: prefect-config
key: api_key
- name: PREFECT_API_DATABASE_CONNECTION_URL
valueFrom:
secretKeyRef:
name: prefect-config
key: api_db
---
apiVersion: v1
kind: Service
metadata:
name: prefect-api-headless
namespace: prefect
labels:
app: prefect-api
spec:
ports:
- name: "4200"
port: 4200
targetPort: 4200
protocol: TCP
selector:
app: prefect-api
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: prefect-api
namespace: prefect
labels:
app: prefect-api
spec:
selector:
app: prefect-api
type: NodePort
sessionAffinity: None
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
- name: prefect-api
protocol: TCP
port: 4200
targetPort: 4200
Ingress
Teraz czas zająć się ingressem:
code prefect-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: prefect-api
namespace: prefect
spec:
ingressClassName: nginx
rules:
- host: prefect.company.local
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: prefect-api
port:
number: 4200
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: prefect-ui
namespace: prefect
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: prefect-login-auth
spec:
#ingressClassName: nginx
rules:
- host: prefect.company.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: prefect-api
port:
number: 4200
Dzięki takiej konfiguracji endpoint /API
nie jest zabezpieczony Nginx’em i jest dostępny z CLI dla każdego kto zna adres API oraz posiada klucz.
Natomiast front zostaje ukryty za NGINX’em i staje się niedostępny dla osób postronnych.
Optymalnym jest zakrycie front’u dodatkowym sidecarem z własnym mechanizmem logowania, lecz dziś nie jest to obszar na potrzeby tego artykułu.
Sekrety
Do poprawnego działania mechanizmu potrzebujemy secretu dla Naszego PoC’owego usera do frontu, oraz konfiguracji klucza pod API wraz z konfiguracją bazy:
Otwieramy plik:
code prefect-config.yml
apiVersion: v1
kind: Secret
metadata:
name: prefect-config
namespace: prefect
labels:
app: prefect
type: Opaque
data:
api_key: OUR_SECURE_API_KEY # Encode 'not so secure' api key to base64
api_url: http://prefect-api-0.prefect-api-headless.prefect.svc.cluster.local:4200/api # Our API service inside the K8S cluster. Encode to b64
api_external: OUR_EXTERNAL_URL # Self explained. Encode to b64
api_db: postgresql+asyncpg://prefect:SECRET_PWD@postgres-server:5432/prefectdb # Again, encode it to base64 with a proper credentials
---
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: prefect-login-auth
namespace: prefect
labels:
app: prefect
data:
auth: OUR_BCRYPTED_HTPASSWORD # Generate bcrypted htpassword for UI and encode it again to b64.
W mojej opinii powyższa konfiguracja tłumaczy się sama. Oczywiście na potrzeby produkcyjne należy podejść do tematu ciut inaczej. O czym później.
Workery
Następnie dobrze by było wystawić jeszcze agenty lub już bardziej workery dla Naszego Prefect’a, gdyż te możemy w razie potrzeby skalować.
W teorii mozna je uruchomić wszędzie, może to być box w Vagrancie (na potrzeby PoC’a chyba idealny), dedykowana maszyna wirtualna, bare-metal czy zwykły kontener.
W tym przypadku użyjemy również Kubernetes’a:
code prefect-agents.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: prefect-agents
namespace: prefect
spec:
selector:
matchLabels:
app: prefect-agents # has to match .spec.template.metadata.labels
serviceName: "prefect-agents-headless"
replicas: 1 # by default is 1
template:
metadata:
labels:
app: prefect-agents # has to match .spec.selector.matchLabels
spec:
terminationGracePeriodSeconds: 10
imagePullSecrets:
- name: regcred
containers:
- name: prod-main
image: internal-cr:9999/prefect-core:latest
command:
[
"bash",
"-c",
"prefect agent start --pool production --work-queue prod-main",
]
imagePullPolicy: "Always"
resources:
requests:
cpu: 512m
memory: 1024Mi
env:
- name: PREFECT_SERVER_ANALYTICS_ENABLED
value: "false"
- name: PREFECT_API_KEY
valueFrom:
secretKeyRef:
name: prefect-config
key: api_key
- name: PREFECT_API_URL
valueFrom:
secretKeyRef:
name: prefect-config
key: api_url
- name: prod-secondary
image: internal-cr:9999/prefect-core:latest
command:
[
"bash",
"-c",
"prefect agent start --pool production --work-queue prod-secondary",
]
imagePullPolicy: "Always"
resources:
requests:
cpu: 512m
memory: 1024Mi
env:
- name: PREFECT_SERVER_ANALYTICS_ENABLED
value: "false"
- name: PREFECT_API_KEY
valueFrom:
secretKeyRef:
name: prefect-config
key: api_key
- name: PREFECT_API_URL
valueFrom:
secretKeyRef:
name: prefect-config
key: api_url
Produkcyjnie najlepsza praktyką jest to by workery skonfigurować jako osobne pod’y.
Aczkolwiek na potrzeby developerskie, jeden pod z dwoma lub więcej kontenerami w zupełności Nam wystarczy.
Zależności
Następnie uzupełniamy plik requirements
:
code requirements.txt
virtualenv
prefect-alert
prefect-email
prefect-docker
prefect-gitlab
prefect-kubernetes
prefect-kv
prefect-shell
prefect-slack
prefect-sqlalchemy
pandas
numpy
pyspark
polars[datalake, fsspec]
duckdb
pandera
sqlalchemy
sqlmodel
cx_Oracle
pyodbc
pymssql
pymysql
python-dotenv
pytest
autopep8
aiohttp
httpx
requests
Rakefile
Rakefile (Ruby) edytujemy wedle uznania lub jak poniżej:
desc "Build the image"
task :build do
puts "Building the image..."
%x[docker compose build --no-cache]
puts "Done!"
end
desc "Push the image"
task :push do
puts "Pushing the image..."
%x[docker push internal-cr:9999/prefect-core]
puts "Done!"
end
desc "Rollout"
task :rollout do
puts "Deploying..."
%x[kubectl -n prefect apply -f prefect-config -f prefect-api.yml -f prefect-agents.yml]
puts "Rolling out the API and agents..."
%x[kubectl -n prefect rollout restart statefulset prefect-api prefect-agents]
puts "Done!"
end
Rake przyda Nam się na etapie PoC’a gdy będziemy dokonywać zmian w strukturze obrazu.
Wdrożenie
Secrety oraz API
Na początek musimy uruchomić API wraz konfiguracją, oraz zbudować obraz i umieścić go w repozytorium:
docker compose build && docker push internal-cr:9999/prefect-core
kubectl -n prefect apply -f prefect-config -f prefect-api.yml
CLI
Dodatkowo powinniśmy konfigurować CLI do pracy z wystawionym przez Nas API:
virtualenv venv
pip install prefect
prefect config set PREFECT_API_KEY= 'YOUR_KEY' && \
prefect config set PREFECT_API_URL='https://prefect.internal.company/api'
A także dodać work-poola oraz kolejkę pod workera:
prefect work-pool create production && \
prefect work-queue create 'prod-main' --pool production && \
prefect work-queue create 'prod-secondary' --pool production
Startujemy workery
Agenty startujemy oczywiście analogicznie:
kubectl -n prefect apply -f prefect-agents.yml
I voula!
$ kubectl -n prefect get pods
NAME READY STATUS RESTARTS AGE
postgres-0 1/1 Running 0 200d
prefect-agents-0 7/7 Running 0 15s
prefect-api-0 1/1 Running 0 15s
Teraz możemy zalogować się do Naszego PoC’a przy pomocy zewnętrznego adresu oraz ustalonych przez Nas credentiali zacząć pracę z kodem.
Produkcja
Na potrzeby produkcji należy przedewszystkim zastąpić secrety w base64 czymś bardziej wyrafinowanym np. HashiCorp Vault’em.
Dodatkowo root’a oraz inne pathy należy przekierować do sidecar’u, z wewnętrznym mechanizmem logowania i z niego dopiero kierować ruch dalej - do niedostępnego z zewnątrz serwisu.
Wnioski
Uruchomienie Prefect’a w całości w Kubernetesie,we własnej infrastrukturze wraz z dodatkową warstwą ‘ukrywającą’. Wcale nie musi być trudne.
Po spełnieniu warunków produkcyjnych, odpowiednim ustawieniu HPA. Możemy zyskać całkiem przyjemne środowisko orkiestracji przepływów danych.