Mertcan Yüksel içinde ara

Prefect nedir, ne değildir, nasıl kurgulanmalı

Prefect'in 2026'daki gün­cel hali, on­dan bek­len­me­me­si ge­re­ken­ler, bir en­teg­ras­yon kat­ma­nı ola­rak kul­la­nı­mı, FastMCP ile ya­pay zeka ajan­la­rı­na açıl­ma­sı, Airflow ve Dagster'la fark­la­rı ve üre­tim­de ayak­ta ka­lan bir Pre­fect pro­je­si­nin na­sıl kur­gu­lan­ma­sı ge­rek­ti­ği. So­nun­da, bu ya­pı­yı bir ya­pay zeka asis­ta­nı­na kur­dur­mak için ha­zır bir prompt var.

Paylaş

İçindekiler

Bir veri or­ta­mın­da za­man­lan­mış iş­ler kont­rol­den çık­ma­ya baş­la­dı­ğın­da ilk ref­leks ge­nel­de ay­nı­dır: “Bü­tün iş­le­ri tek bir ara­yüz­de gö­re­lim.” Oysa za­man­lan­mış iş­le­rin en teh­li­ke­li hali hata ver­me­le­ri de­ğil, ses­siz­ce dur­ma­la­rı ya da yan­lış veri üret­me­le­ri­dir.

Fark­lı su­nu­cu­lar­da­ki cron job’ları, Windows Task Scheduler’da du­ran script’ler ve ve­ri­ta­ba­nı için­de­ki za­man­la­yı­cı­lar bir süre son­ra bir gö­rü­nür­lük so­ru­nu de­ğil, bir fark edi­le­bi­lir­lik kri­zi ya­ra­tır. Script ça­lı­şır, çı­kış kodu sı­fır­dır; ama için­de­ki adım­lar­dan biri hata ver­miş­tir ve kim­se bunu gör­me­miş­tir. Bir işin ça­lış­ma­dı­ğı­nı üç gün son­ra ge­len bir kul­la­nı­cı şi­ka­ye­tiy­le öğ­re­ni­yor­sa­nız, so­run ara­cın ken­di­sin­de de­ğil, ope­ras­yo­nel kur­gu­nun ek­sik­li­ğin­de­dir. Bir işin ça­lış­tı­ğı­nı bil­mek ko­lay­dır; ça­lış­ma­dı­ğı­nı bil­mek zor­dur.

Bu ya­zı­da Prefect’i üç soru üze­rin­den ele alı­yo­rum: Pre­fect ne­dir, ne de­ğil­dir ve üre­tim­de na­sıl kur­gu­lan­ma­lı? Ara­da Prefect’i bir en­teg­ras­yon kat­ma­nı ola­rak ve FastMCP ile ya­pay zeka ajan­la­rı­na açı­lan bir yü­rüt­me mo­to­ru ola­rak na­sıl ko­num­lan­dı­ra­bi­le­ce­ği­ni­zi an­la­ta­cak, Airflow ve Dagster’la da kar­şı­laş­tı­ra­ca­ğım. Oda­ğım özel­lik­le kı­sıt­lı kay­nak­la ça­lı­şan kü­çük ekip­ler için sür­dü­rü­le­bi­lir bir kur­gu. Ya­zı­nın so­nun­da da bu ya­pı­yı bir ya­pay zeka kod­la­ma asis­ta­nı­na kur­dur­mak için ha­zır bir prompt var.

Prefect nedir?

Pre­fect, Python için açık kay­nak­lı (Apac­he 2.0), di­na­mik ve da­ya­nık­lı (du­rab­le) bir iş akı­şı or­kest­ras­yon ara­cı. Yap­tı­ğı iş özün­de şu: Sı­ra­dan Python fonk­si­yon­la­rı­nı­zı iz­le­ne­bi­lir, ye­ni­den de­ne­ne­bi­lir, za­man­la­na­bi­lir ve elle te­tik­le­ne­bi­lir iş­le­re dö­nüş­tü­rür. Bunu ya­par­ken siz­den yeni bir dil ya da bir DAG ta­nı­mı is­te­mez. Bir fonk­si­yo­nun ba­şı­na @flow, için­de­ki adım­la­rın ba­şı­na @task koy­ma­nız ye­ter­li:

from prefect import flow, get_run_logger, task


@task(retries=2, retry_delay_seconds=30)
def kayitlari_cek() -> list[dict]:
    ...


@flow
def gunluk_rapor():
    kayitlar = kayitlari_cek()
    get_run_logger().info("%s kayıt işlendi", len(kayitlar))

Dön­gü­ler, ko­şul­lar ve hata ya­ka­la­ma nor­mal Python’dur. Pre­fect her ça­lış­ma­nın du­ru­mu­nu, sü­re­si­ni ve lo­gu­nu kay­de­der; ara­yüz­de gös­te­rir; ge­rek­ti­ğin­de ye­ni­den de­ner.

Bu ya­zı­yı ya­zar­ken gün­cel sü­rüm 3.8 se­ri­si (Ey­lül 2026 iti­ba­rıy­la 3.8.6). Pre­fect 3, Python 3.10 ile 3.14 ara­sı­nı des­tek­li­yor. Pre­fect 2 hala ba­kım sü­rüm­le­ri alı­yor, ama yeni bir pro­je­ye 2 ile baş­la­mak için bir se­bep yok.

Gündemde: Prefect, Dagster’ı satın alıyor

Tem­muz 2026’da Pre­fect, or­kest­ras­yon dün­ya­sın­da­ki en ya­kın ra­ki­bi Dagster’ın ar­ka­sın­da­ki şir­ket Dags­ter Labs’i sa­tın ala­ca­ğı­nı du­yur­du. Bu bir bir­leş­me de­ğil; ürün­ler de bir­leş­mi­yor. Prefect’in du­yu­ru­sun­da “Dags­ter ve Dags­ter+ ka­lı­cı” de­ni­yor. Dagster’ın müş­te­ri­le­re yö­ne­lik açık­la­ma­sın­da da Dagster’ı Prefect’e kat­ma pla­nı ol­ma­dı­ğı, kim­se­nin ta­şın­ma­ya zor­lan­ma­ya­ca­ğı ve açık kay­nak li­san­sı­nın ay­nen sü­re­ce­ği ya­zı­yor. Ya­zı­yı yaz­dı­ğım ta­rih­te an­laş­ma­nın ta­mam­lan­dı­ğı­na dair bir du­yu­ru gör­me­dim.

Prefect’in üç ürü­nü bun­dan son­ra na­sıl ko­num­lan­dır­dı­ğı tek cüm­le­de özet­le­ni­yor: “Dags­ter so­nu­cu ta­nım­lar, Pre­fect işi yü­rü­tür, FastMCP eri­şi­mi yö­ne­tir.” Bu cüm­le iki ara­cın zi­hin­sel mo­de­li ara­sın­da­ki far­kı da çok iyi an­la­tı­yor; aşa­ğı­da­ki kar­şı­laş­tır­ma­da buna dö­ne­ce­ğim. Şir­ke­tin son dö­nem­de­ki di­ğer ham­le­le­ri de aynı yöne ba­kı­yor: Python’da MCP su­nu­cu­su yaz­mak için yay­gın ola­rak kul­la­nı­lan FastMCP ve MCP su­nu­cu­la­rı­nı ba­rın­dı­rıp yö­ne­ten Pre­fect Ho­ri­zon. Pre­fect açık­ça ya­pay zeka ajan­la­rı­nın or­kest­ras­yo­nu­na ko­num­la­nı­yor; FastMCP’yi aşa­ğı­da ayrı bir bö­lüm­de ele alı­yo­rum.

Bu iki araç­tan bi­ri­ni kul­la­nan biri için bu­gün de­ği­şen bir şey yok. Ama “han­gi­si­ni seç­me­li­yim?” so­ru­su ar­tık iki ra­kip ürün ara­sın­da de­ğil, aynı çatı al­tın­da­ki iki fark­lı fel­se­fe ara­sın­da bir se­çim.

Temel kavramlar

Prefect’in kap­sa­mı ilk ba­kış­ta gö­rün­dü­ğün­den ge­niş. Aşa­ğı­da­ki lis­te açık kay­nak sü­rü­mü esas alı­yor; sa­de­ce Pre­fect Cloud’da olan­la­rı [Cloud] diye işa­ret­le­dim.

İş akı­şı yaz­mak

  • Flow ve task: Ta­kip edi­len iş bi­rim­le­ri. Bir flow baş­ka bir flow’u ça­ğı­rır­sa o çağ­rı alt ça­lış­ma (subflow) olur.
  • Eş­za­man­lı­lık: .submit() ve .map() ile task’lar pa­ra­lel ça­lı­şır. Var­sa­yı­lan task run­ner thread ha­vu­zu; pro­cess ha­vu­zu, Dask ve Ray da kul­la­nı­la­bi­lir.
  • Async: async def flow ve task’lar doğ­ru­dan des­tek­le­nir.
  • Arka plan task’ları: Bir task .delay() ile kuy­ru­ğa atı­lıp ayrı bir task worker’ı ta­ra­fın­dan ça­lış­tı­rı­la­bi­lir. Ör­ne­ğin bir web uy­gu­la­ma­sın­dan iş te­tik­le­mek için.

Ça­lış­tır­mak ve za­man­la­mak

  • Dep­loy­ment: Bir flow’un su­nu­cu ta­ra­fın­dan bi­li­nen, pa­ra­met­re­le­ri ve za­man­la­ma­sı olan hali. Üç yol­la oluş­tu­ru­lur: flow.serve() (ken­di ma­ki­ne­niz­de uzun süre ça­lı­şan bir sü­reç), flow.deploy() (ça­lış­ma­la­rı bir work pool’a gön­de­rir) ve prefect.yaml dos­ya­sı.
  • Za­man­la­ma­lar: Cron, in­ter­val ve RRule; IANA saat di­lim­le­riy­le.
  • Work pool ve worker: Ça­lış­ma­nın ne­re­de ko­şa­ca­ğı­nı be­lir­ler: ye­rel sü­reç, Docker, Ku­ber­ne­tes, ECS, Cloud Run ve ben­zer­le­ri. Worker’lar ya­pı­la­cak işi Pre­fect su­nu­cu­su­na ken­di­le­ri so­rar; bu yüz­den worker’ın ça­lış­tı­ğı ma­ki­ne­ye dı­şa­rı­dan bağ­lan­tı aç­ma­nız ge­rek­mez. [Cloud] Worker ge­rek­tir­me­yen ser­ver­less “push” ha­vuz­la­rı ve ko­du­nu­zu Prefect’in ken­di alt­ya­pı­sın­da ça­lış­tı­ran Pre­fect Ma­na­ged.
  • Alt­ya­pı de­ko­ra­tör­le­ri: @docker, @kubernetes gibi de­ko­ra­tör­ler­le bir flow, dep­loy­ment oluş­tur­ma­dan uzak bir alt­ya­pı­ya gön­de­ri­le­bi­lir (3.7 ile ka­rar­lı hale gel­di).

Da­ya­nık­lı­lık

  • Retry: Flow ve task se­vi­ye­sin­de retries ve retry_delay_seconds.
  • Du­rum­lar ve hook’lar: on_failure, on_completion, on_running gibi du­rum de­ği­şik­li­ği kan­ca­la­rı; on_crashed ve on_cancellation sa­de­ce flow’lar­da.
  • So­nuç­lar ve ön­bel­lek: Task so­nuç­la­rı var­sa­yı­lan ola­rak sak­lan­maz; açar­sa­nız sak­la­nır ve ha­zır ön­bel­lek po­li­ti­ka­la­rıy­la (gir­di­ye göre, kay­nak koda göre, NO_CACHE vb.) tek­rar kul­la­nı­la­bi­lir.
  • Transaction’lar: Bir­bi­ri­ne bağ­lı adım­la­rı on_commit / on_rollback kan­ca­la­rıy­la grup­lar. Ve­ri­ta­ba­nı transaction’ı de­ğil­dir; geri alma adı­mı­nı si­zin yaz­ma­nız ge­re­kir.
  • Eş­za­man­lı­lık ve hız sı­nır­la­rı: Glo­bal sı­nır­lar, tag baz­lı task sı­nır­la­rı ve dep­loy­ment, work pool ve kuy­ruk se­vi­ye­sin­de sı­nır­lar. Aynı kay­na­ğa aynı anda kaç işin do­ku­na­ca­ğı­nı bu­ra­dan yö­ne­tir­si­niz.
  • Et­ki­le­şim­li akış­lar: Bir flow du­rak­la­tı­lıp bir in­san­dan gir­di bek­le­ye­bi­lir.

Olay­lar ve oto­mas­yon

  • Event, au­to­ma­ti­on ve trig­ger: Her du­rum de­ği­şik­li­ği bir olay­dır. Oto­mas­yon­lar bu olay­la­ra, met­rik­le­re ya da bek­le­nen bir ola­yın hiç gel­me­me­si­ne tep­ki ve­re­bi­lir. So­nun­cu­su, “bu iş sa­bah do­ku­za ka­dar bit­me­diy­se ha­ber ver” gibi ku­ral­lar için çok de­ğer­li.
  • [Cloud] Dı­şa­rı­dan ge­len HTTP is­tek­le­ri­ni ola­ya çe­vi­ren webhook’lar, SLA ta­nım­la­rı ve oto­mas­yon­lar­dan e-pos­ta gön­der­me ya da olay (in­ci­dent) açma.

Ya­pı­lan­dır­ma ve göz­lem

  • Block, va­ri­ab­le ve sec­ret: Tip­li ayar ve kim­lik bil­gi­si nes­ne­le­ri (ör­ne­ğin Secret block’u) ve JSON de­ğiş­ken­ler.
  • Log­la­ma ve ar­ti­fact: get_run_logger() ile ça­lış­ma­ya bağ­lı log­lar; ara­yüz­de gö­rü­nen Markdown, tab­lo, bağ­lan­tı ve iler­le­me çık­tı­la­rı.
  • Ara­yüz: 3.8 ile ye­ni­le­nen ara­yüz, ken­di su­nu­cu­su­nu ku­ran­lar için de var­sa­yı­lan oldu.
  • [Cloud] @materialize ile veri var­lık­la­rı (as­set) ve ara­la­rın­da­ki soy (lineage) iliş­ki­si.

Ne­re­de ça­lı­şır?

  • Ken­di su­nu­cu­nuz: Pre­fect ser­ver açık kay­nak. Var­sa­yı­lan ve­ri­ta­ba­nı SQLite ve tek su­nu­cu için ye­ter­li; üre­tim­de PostgreSQL öne­ri­li­yor. Bir­den faz­la su­nu­cu ça­lış­tır­mak için PostgreSQL ve Re­dis ge­re­ki­yor.
  • Pre­fect Cloud: Ba­rın­dı­rı­lan sü­rüm. Üc­ret­siz Hobby pla­nı 2 kul­la­nı­cı, 5 dep­loy­ment ve 7 gün­lük ça­lış­ma geç­mi­şi ve­ri­yor. Rol baz­lı yet­ki, SSO ve de­ne­tim kay­dı gibi yö­ne­tim özel­lik­le­ri sa­de­ce Cloud’da.

Prefect ne değildir?

Bir ara­cı iyi kul­lan­ma­nın ya­rı­sı, on­dan ne bek­le­me­me­niz ge­rek­ti­ği­ni bil­mek­tir.

Ci­la­lı bir cron de­ğil­dir. Cron’un yap­tı­ğı her şeyi ya­par; ama bir or­kest­ra­tö­rün asıl de­ğe­ri, iş­ler yo­lun­day­ken za­man­la­ma yap­ma­sı de­ğil, iş­ler ters git­ti­ğin­de du­ru­mu kay­det­me­si, ba­ğım­lı­lık­la­rı yö­net­me­si ve alarm üre­te­bil­me­si­dir. İş­ler olay­lar­la, oto­mas­yon­lar­la ya da baş­ka bir flow’un bi­ti­şiy­le de te­tik­le­ne­bi­lir. Prefect’i sa­de­ce cron ye­ri­ne kul­lan­mak ko­lay­dır. Zor olan, on­lar­ca işin bir­bi­ri­ni bi­le­rek ça­lış­tı­ğı, hata ol­du­ğun­da ki­min ha­ber­dar ola­ca­ğı­nın bel­li ol­du­ğu bir plat­form kur­mak­tır. Pre­fect bu­nun araç­la­rı­nı ve­rir, ama o plat­for­mu si­zin ye­ri­ni­ze kur­maz.

Bir veri iş­le­me mo­to­ru de­ğil­dir. Ve­ri­niz Pre­fect su­nu­cu­su­nun için­den ak­maz. Pre­fect si­zin ko­du­nu­zu ça­lış­tı­rır ve iz­ler; kod ve veri si­zin alt­ya­pı­nız­da ka­lır, su­nu­cu­ya sa­de­ce du­rum­lar ve log­lar gi­der. Ağır he­sap­la­ma ge­re­ki­yor­sa işi Dask ya da Ray gibi bir mo­to­ra dev­re­der­si­niz. Pre­fect, Spark’ın ya da SQL mo­to­ru­nu­zun ye­ri­ni al­maz.

Bir veri am­ba­rı ya da ELT ara­cı de­ğil­dir. dbt mo­del­le­ri­ni­zi, sto­red procedure’le­ri­ni­zi, SQL’ini­zi te­tik­ler; on­la­rın ye­ri­ne geç­mez. İş ku­ral­la­rı hala ve­ri­nin ya­şa­dı­ğı yer­de ya­şa­ma­lı.

Bir iz­le­me ga­ran­ti­si de­ğil­dir. Pre­fect size ko­du­nu­zun ne söy­le­di­ği­ni gös­te­rir; ko­du­nuz “ba­şa­rı­lı” di­yor­sa ek­ran ye­şil­dir. Şöy­le bir kur­gu dü­şü­nün: Bir ve­ri­ta­ba­nı­nın ye­dek­le­ri­ni baş­ka bir su­nu­cu­ya kop­ya­la­yan bir script ve onu kont­rol eden bir flow var. Flow; log dos­ya­sı oku­na­bi­li­yor mu, son ça­lış­ma taze mi, çı­kış kodu ba­şa­rı­lı mı diye ba­kı­yor. Ye­dek­le­rin ya­zıl­dı­ğı bağ­lan­tı ko­par­sa script eski dos­ya­la­rı kop­ya­la­ma­ya de­vam edip “ba­şa­rıy­la ta­mam­lan­dı” di­ye­bi­lir ve flow haf­ta­lar­ca ye­şil ka­lır. Sü­reç ça­lı­şır, so­nuç yok­tur. “İş ba­şa­rıy­la ta­mam­lan­dı” bir so­nuç de­ğil, bir sü­reç ifa­de­si­dir.

Ara­yü­zü bir göz­lem de­po­su de­ğil­dir. Pre­fect ara­yü­zü şu an ne ol­du­ğu­nu mü­kem­mel gös­te­rir. Ama ge­çen ay ne ol­du­ğu­nu sor­ma­ya baş­la­dı­ğı­nız­da log­lar bir me­tin akı­şı ola­rak ka­lır: Bu iş ge­çen ay kaç ka­yıt iş­le­di? Bu adım ne za­man ya­vaş­la­dı? Üc­ret­siz Cloud pla­nın­da ça­lış­ma geç­mi­şi za­ten 7 gün tu­tu­lu­yor. Geç­mi­şe dair so­ru­la­ra ce­vap is­ti­yor­sa­nız, o ve­ri­yi ken­di­niz sor­gu­la­na­bi­lir bir yere yaz­ma­lı­sı­nız.

İdem­po­tent ol­ma­yan kodu si­hir­li bi­çim­de dü­zel­ten bir araç de­ğil­dir. Ken­di için­de tek­rar­la­na­bi­lir ta­sar­lan­ma­mış bir sor­gu­ya ya da script’e ref­leks­le retries=1 koy­mak, aynı ve­ri­nin iki kez iş­len­me­si­ne yol açar. Prefect’in tran­sac­ti­on özel­li­ği de ve­ri­ta­ba­nı transaction’ı de­ğil­dir; geri alma adı­mı­nı si­zin yaz­ma­nız ge­re­kir. Bu ko­nu­ya aşa­ğı­da ay­rın­tı­lı dö­ne­ce­ğim, çün­kü en pa­ha­lı ha­ta­lar tam ola­rak bu­ra­da ya­pı­lı­yor.

Prefect aynı zamanda bir entegrasyon motorudur

Prefect’i yal­nız­ca ve­ri­ta­ban­la­rı ara­sın­da veri ta­şı­yan bir za­man­la­yı­cı ola­rak gör­mek ek­sik ka­lır. Fark­lı sis­tem­ler, mik­ro­ser­vis­ler ve gü­ven­lik böl­ge­le­ri ara­sın­da ça­lı­şan bir en­teg­ras­yon kat­ma­nı ola­rak da kur­gu­la­na­bi­lir. Özel­lik­le dış dün­yay­la ko­nu­şan sis­tem­ler ile ku­rum için­de­ki has­sas alt­ya­pı ara­sın­da bir tam­pon kat­man, yani bir tür DMZ (De­mi­li­ta­ri­zed Zone) ola­rak ko­num­lan­dı­rı­la­bi­lir:

Merkezde Prefect orkestratörü; dış API'ler, kurum içi sistemler ve CRM, veritabanları (MSSQL, PostgreSQL, MongoDB), PDF ve raporlama, mail ve bildirimlerle çift yönlü bağlantıları

Pre­fect; dış API’ler, iç sis­tem­ler, ve­ri­ta­ban­la­rı, ra­por­la­ma ve bil­di­rim ka­nal­la­rı ara­sın­da or­kest­ra­tör ola­rak. Gör­sel: Ge­mi­ni ile üre­til­di.

[ Dış dünya / X API ] ──▶ ( Prefect flow: doğrulama ve kontrol ) ──▶ [ İç sistem / Y API ]

Bu kur­gu­nun ge­rek­çe­le­ri şun­lar:

  • Doğ­ru­dan eri­şi­mi en­gel­le­mek. Dış dün­ya­da­ki bir X API’si­nin ku­rum için­de­ki bir Y API’sine ya da ve­ri­ta­ba­nı­na doğ­ru­dan eriş­me­si cid­di bir gü­ven­lik ris­ki­dir. Pre­fect flow’u iki sis­tem ara­sın­da izo­le bir köp­rü olur: X, Y’yi hiç gör­mez.
  • Doğ­ru­la­ma ge­çi­di (va­li­da­ti­on gate). X’ten çe­ki­len ham veri doğ­ru­dan Y’ye ba­sıl­maz. Önce flow için­de doğ­ru­la­nır: Pydan­tic ile tip kont­ro­lü, boş alan kont­ro­lü, sı­nır de­ğer test­le­ri.
  • Ha­ta­nın içe­ri sız­ma­ma­sı. X’ten bo­zuk ya da uyum­suz bir ka­yıt ge­lir­se, hata iç sis­te­me ulaş­ma­dan Pre­fect kat­ma­nın­da ya­ka­la­nır. Ka­yıt log­la­nır, ka­ran­ti­na­ya alı­nır ve il­gi­li eki­be bil­di­rim gi­der.
  • Kim­lik bil­gi­le­ri­nin izo­las­yo­nu. X ve Y’nin token’ları ve eri­şim anah­tar­la­rı kod­da ya da su­nu­cu­da açık­ta dur­maz, Sec­ret block’lar­da tu­tu­lur. X’in anah­ta­rı Y’yi, Y’nin anah­ta­rı X’i hiç gör­mez.

Ka­ba­ca şöy­le gö­rü­nür:

import pydantic
from decimal import Decimal
from typing import Literal

from prefect import flow, task
from prefect.cache_policies import NO_CACHE


class GelenKayit(pydantic.BaseModel):
    kayit_id: int = pydantic.Field(gt=0)
    tutar: Decimal = pydantic.Field(ge=0, le=1_000_000)
    para_birimi: Literal["TRY", "USD", "EUR"]


@task(cache_policy=NO_CACHE)
def dogrula(ham_kayitlar: list[dict]) -> tuple[list[GelenKayit], list[dict]]:
    gecerli, karantina = [], []
    for ham in ham_kayitlar:
        try:
            gecerli.append(GelenKayit.model_validate(ham))
        except pydantic.ValidationError as e:
            karantina.append({"kayit": ham, "hata": str(e)})
    return gecerli, karantina


@flow(retries=0)
def x_den_y_ye():
    ham = x_api_cek()                  # X'in token'ı kendi Secret block'unda
    gecerli, karantina = dogrula(ham)
    if karantina:
        karantinaya_yaz(karantina)     # iç sisteme değil, karantina tablosuna
        alarm_gonder(len(karantina))
    y_api_gonder(gecerli)              # içeri sadece doğrulanmış kayıtlar girer

Bu kur­gu­nun gü­ven­li­ği Prefect’ten ken­di­li­ğin­den gel­mez; ta­sa­rım­dan ge­lir. Üç şeye dik­kat edin:

  • Veri worker’ın için­den ge­çer, su­nu­cu­nun de­ğil. Tam­pon kat­ma­nın asıl yeri, işi ça­lış­tı­ran worker’dır. Worker’ı hem X’e hem Y’ye eri­şe­bi­len ayrı bir ağ bö­lü­mü­ne ko­yun. Worker’lar işi su­nu­cu­ya ken­di­le­ri sor­du­ğu için o ma­ki­ne­ye dı­şa­rı­dan bağ­lan­tı aç­mak ge­rek­mez.
  • Y’ye yaz­ma adı­mı­nı idem­po­tent ya­pın. Ör­ne­ğin ka­yıt kim­li­ği­ne göre up­sert kul­la­nın. Aşa­ğı­da­ki retry bö­lü­mün­de an­la­ta­ca­ğım çar­pan et­ki­si bu­ra­da da ge­çer­li.
  • Her ge­çi­şi log­la­yın. Kaç ka­yıt gel­di, kaçı geç­ti, kaçı ka­ran­ti­na­ya alın­dı: Bu sa­yı­lar ya­pı­lan­dı­rıl­mış log ola­rak kay­de­di­lir­se, iki sis­tem ara­sın­da de­net­le­ne­bi­lir bir iz olu­şur.

Böy­le­ce iki sis­tem bir­bi­ri­ne doğ­ru­dan ba­ğım­lı ol­maz. Ara­da her ge­çi­şin kay­dı tu­tu­lan, retry’ı bi­linç­li kur­gu­lan­mış ve veri ka­li­te­si­ni kont­rol eden bir en­teg­ras­yon kal­ka­nı du­rur.

Prefect ve FastMCP: iş akışlarını yapay zeka ajanlarına açmak

Prefect’in sa­tın alma du­yu­ru­sun­da­ki üç­lü­nün son par­ça­sı FastMCP’ydi: “Dags­ter so­nu­cu ta­nım­lar, Pre­fect işi yü­rü­tür, FastMCP eri­şi­mi yö­ne­tir.” Bu bö­lüm, o “eri­şim” kıs­mı­nın pra­tik­te ne an­la­ma gel­di­ği­ni an­la­tı­yor.

MCP nedir?

Mo­del Context Pro­to­col (MCP), ya­pay zeka asis­tan­la­rı­nın dış sis­tem­le­re, araç­la­ra ve ve­ri­le­re stan­dart bir yol­dan bağ­lan­ma­sı için açık bir pro­to­kol. Anthropic ta­ra­fın­dan Ka­sım 2024’te du­yu­rul­du; Ara­lık 2025’te Linux Foundation bün­ye­sin­de­ki Agen­tic AI Foundation’a dev­re­dil­di. Gün­cel spe­si­fi­kas­yon sü­rü­mü 2026-07-28.

Mo­de­li ba­sit: Bir MCP su­nu­cu­su araç­lar (tools), kay­nak­lar (resources) ve ha­zır is­tem­ler (prompts) su­nar. Cla­ude ya da Cur­sor gibi uy­gu­la­ma­la­rın için­de­ki MCP is­tem­ci­si bun­la­rı keş­fe­der ve mo­de­lin is­te­ğiy­le ça­ğı­rır. Su­nu­cu ye­rel­de bir sü­reç ola­rak (stdio) ya da HTTP üze­rin­den (Streamable HTTP) ça­lı­şa­bi­lir. Bir kez MCP su­nu­cu­su yaz­dı­ğı­nız­da, onu MCP des­tek­le­yen her asis­ta­na bağ­la­ya­bi­lir­si­niz.

FastMCP nedir?

FastMCP, Python’da MCP su­nu­cu­su ve is­tem­ci­si yaz­ma­nın en pra­tik yolu. Prefect’in ku­ru­cu­su Je­re­mi­ah Lowin ta­ra­fın­dan baş­la­tıl­dı; ilk sü­rü­mü res­mi MCP Python SDK’sına da­hil edil­di, son­ra­ki sü­rüm­le­ri Pre­fect bün­ye­sin­de ayrı bir pro­je ola­rak ge­liş­ti. Apac­he 2.0 li­sans­lı; gün­cel sü­rüm 4.0.10 (Ey­lül 2026). 4.0 ile MCP’nin 2026-07-28 sü­rü­mü­ne geç­ti; bel­ge­le­ri­ne göre çoğu su­nu­cu kod de­ği­şik­li­ği ol­ma­dan yük­sel­ti­le­bi­li­yor. FastMCP’nin ken­di bel­ge­le­ri­ne göre gün­de bir mil­yon­dan faz­la in­di­ri­li­yor ve MCP su­nu­cu­la­rı­nın yak­la­şık yüz­de 70’i bir FastMCP sü­rü­müy­le ça­lı­şı­yor; bun­lar üre­ti­ci­nin ken­di ra­kam­la­rı.

Sun­du­ğu baş­lı­ca şey­ler:

  • De­ko­ra­tör­le araç ta­nı­mı: @mcp.tool ile bir Python fonk­si­yo­nu ara­ca dö­nü­şür; pa­ra­met­re tip­le­ri ve docstring’den şema oto­ma­tik üre­ti­lir. Kay­nak­lar için @mcp.resource, ha­zır is­tem­ler için @mcp.prompt var.
  • Kim­lik doğ­ru­la­ma: JWT doğ­ru­la­ma ve GitHub, Google, Azu­re gibi sağ­la­yı­cı­lar için OAuth.
  • Bir­leş­tir­me ve ve­kil su­nu­cu: Bir­den faz­la su­nu­cu­yu tek bir uçta top­la­mak ya da uzak bir su­nu­cu­yu ve­kil ola­rak sun­mak.
  • OpenAPI ve FastAPI’den üre­tim: Var olan bir API’den MCP su­nu­cu­su tü­ret­mek. Bel­ge­ler bunu pro­to­tip için öne­ri­yor; mo­del­ler, özen­le se­çil­miş dar araç­lar­la daha iyi ça­lı­şı­yor.
  • Bel­lek içi test is­tem­ci­si: Client(mcp) ile su­nu­cu­yu ağ ya da ayrı bir sü­reç ol­ma­dan test et­mek.
  • Ku­ru­lum ko­mut­la­rı: fastmcp install claude-code ya da fastmcp install cursor ile su­nu­cu­yu doğ­ru­dan bir asis­ta­na ek­le­mek.

Ba­rın­dır­ma ta­ra­fın­da Pre­fect Ho­ri­zon var. MCP su­nu­cu­la­rı­nı yö­ne­ti­len ola­rak ba­rın­dı­rı­yor (Dep­loy), ku­rum için­de­ki MCP ye­te­nek­le­ri­ni tek bir ka­ta­log­da top­lu­yor (Re­gistry) ve araç se­vi­ye­sin­de rol baz­lı eri­şim kont­ro­lü ile de­ne­tim kay­dı su­nan bir ge­çit sağ­lı­yor (Gateway). Ki­şi­sel kul­la­nım için üc­ret­siz bir pla­nı var.

Prefect’i yapay zeka ajanlarına açmak

Bir ya­pay zeka aja­nı­na Prefect’le il­gi­li iki fark­lı tür­de eri­şim ve­re­bi­lir­si­niz.

1. Göz­lem: Prefect’in res­mi MCP su­nu­cu­su. PrefectHQ’nun ya­yım­la­dı­ğı prefect-mcp-server, bir asis­ta­nın Pre­fect or­ta­mı­nı­zı in­ce­le­me­si­ni sağ­lar: flow’lar, deployment’lar, ça­lış­ma­lar, log­lar, work pool’lar ve oto­mas­yon­lar. Bi­le­rek salt oku­nur ta­sar­lan­mış; hiç­bir ça­lış­ma­yı te­tik­le­ye­mez, hiç­bir şeyi de­ğiş­ti­re­mez. “Dün gece han­gi flow’lar ba­şa­rı­sız oldu, log­la­rın­da ne ya­zı­yor?” gibi so­ru­lar için ideal. Cla­ude Code’a ek­le­mek tek sa­tır; PREFECT_API_URL or­tam de­ğiş­ke­ni­nin (Cloud kul­la­nı­yor­sa­nız PREFECT_API_KEY’in de) ta­nım­lı ol­ma­sı ye­ter­li:

claude mcp add prefect -- uvx --from prefect-mcp prefect-mcp-server

2. Ey­lem: ken­di FastMCP su­nu­cu­nuz. Aja­nın bir işi baş­la­ta­bil­me­si­ni is­ti­yor­sa­nız, bunu ge­niş bir yet­kiy­le de­ğil, dar ve bi­linç­li se­çil­miş araç­lar­la ya­pın. Aşa­ğı­da­ki su­nu­cu aja­na iki araç açı­yor; biri sa­de­ce oku­yor, di­ğe­ri tek bir deployment’ı te­tik­li­yor:

from fastmcp import FastMCP
from prefect.client.orchestration import get_client
from prefect.client.schemas.filters import (
    FlowRunFilter,
    FlowRunFilterState,
    FlowRunFilterStateType,
)
from prefect.client.schemas.objects import StateType
from prefect.client.schemas.sorting import FlowRunSort
from prefect.deployments import run_deployment

mcp = FastMCP("prefect-araclari")


@mcp.tool(annotations={"readOnlyHint": True})
async def son_hatalar(adet: int = 10) -> list[dict]:
    """Son başarısız flow çalışmalarını listeler. Hiçbir şeyi değiştirmez."""
    async with get_client() as client:
        runs = await client.read_flow_runs(
            flow_run_filter=FlowRunFilter(
                state=FlowRunFilterState(
                    type=FlowRunFilterStateType(any_=[StateType.FAILED])
                )
            ),
            sort=FlowRunSort.EXPECTED_START_TIME_DESC,
            limit=min(adet, 50),
        )
    return [{"calisma": r.name, "zaman": str(r.expected_start_time)} for r in runs]


@mcp.tool(annotations={"destructiveHint": False, "idempotentHint": True})
async def rapor_calistir(tarih: str) -> str:
    """Günlük raporu verilen tarih (YYYY-AA-GG) için başlatır."""
    run = await run_deployment(
        name="gunluk-rapor/gunluk-rapor",
        parameters={"tarih": tarih},
        idempotency_key=f"gunluk-rapor:{tarih}",  # aynı tarih için tek çalışma
        timeout=0,  # sadece başlat; bitmesini bekleme
    )
    return f"Çalışma başlatıldı: {run.id}"


if __name__ == "__main__":
    mcp.run()

Bu kü­çük su­nu­cu, ön­ce­ki bö­lüm­ler­de­ki il­ke­le­rin ay­nı­sı­nı ya­pay zeka ajan­la­rı­na uy­gu­lu­yor:

  • Ajan, en­teg­ras­yon kat­ma­nın­da­ki “dış dün­ya”nın bir baş­ka ör­ne­ği­dir. Ve­ri­ta­ba­nı şif­re­le­ri­ni, Prefect’in yö­ne­tim yet­ki­si­ni ya da ge­nel bir “ko­mut ça­lış­tır” ara­cı­nı gör­mez; sa­de­ce si­zin aç­tı­ğı­nız dar ka­pı­la­rı gö­rür. Asıl iş, doğ­ru­la­ma, log­la­ma ve retry ku­ral­la­rıy­la bir­lik­te yine Pre­fect flow’unun için­de ya­pı­lır.
  • Oku­ma ve ey­lem araç­la­rı­nı ayı­rın. Araç işa­ret­le­ri (readOnlyHint, destructiveHint, idempotentHint) is­tem­ci­ye ara­cın ne yap­tı­ğı­nı söy­ler. MCP spe­si­fi­kas­yo­nu­na göre is­tem­ci bir ara­cı ça­ğır­ma­dan önce kul­la­nı­cı­dan açık onay al­ma­lı ve bir in­san her za­man çağ­rı­yı red­de­de­bil­me­li­dir. Ama bu işa­ret­ler bir gü­ven­lik me­ka­niz­ma­sı de­ğil, ipu­cu­dur; gü­ven­lik ara­cın kap­sa­mı­nı dar tut­mak­tan ge­lir.
  • Ey­lem araç­la­rı­nı idem­po­tent ya­pın. Ajan­lar aynı is­te­ği tek­rar gön­de­re­bi­lir. run_deployment’a ve­ri­len idempotency_key sa­ye­sin­de aynı ta­rih için ikin­ci çağ­rı yeni bir ça­lış­ma baş­lat­maz, mev­cut ola­nı dön­dü­rür. Aşa­ğı­da­ki retry bö­lü­mün­de an­lat­tı­ğım çar­pan et­ki­si bu­ra­da da ge­çer­li.
  • Bek­le­me, te­tik­le. timeout=0 ile araç işi baş­la­tıp he­men dö­ner; ajan du­ru­mu son­ra oku­ma ara­cıy­la sor­gu­lar. Uzun sü­ren bir işi araç çağ­rı­sı­nın için­de bek­le­mek za­man aşım­la­rı­na ve ge­rek­siz tek­rar­la­ra yol açar.

Su­nu­cu­yu Cla­ude Code’a ek­le­mek için:

fastmcp install claude-code sunucu.py --name prefect-araclari --with prefect \
  --env PREFECT_API_URL=http://localhost:4200/api

Cur­sor için ko­mut­ta­ki claude-code ye­ri­ne cursor yaz­ma­nız ye­ter­li.

Airflow, Dagster, Prefect: hangisi ne zaman?

Or­kest­ra­tör se­çi­mi tek­nik bir gös­te­riş de­ğil; gece üçte sis­te­mi ki­min ve na­sıl ayak­ta tu­ta­ca­ğıy­la il­gi­li ope­ras­yo­nel bir ka­rar­dır. Üç araç da iş akı­şı or­kest­ras­yo­nu ya­par ama dün­ya­ya fark­lı yer­ler­den ba­kar. Far­kı an­la­ma­nın en kısa yolu, her bi­ri­nin siz­den ilk ola­rak neyi ta­nım­la­ma­nı­zı is­te­di­ği­ne bak­mak­tır: Airflow gö­rev­le­ri, Dags­ter veri var­lık­la­rı­nı, Pre­fect ise sı­ra­dan Python ko­du­nu is­ter.

Airflow: önce görevler ve DAG

Apac­he Airflow bu ala­nın en eski ve en yay­gın ara­cı. Gün­cel sü­rüm 3.3.2 (Ey­lül 2026); Airflow 2’nin des­te­ği Ni­san 2026’da sona erdi. Airflow’da işi­ni­zi gö­rev­ler­den ve ara­la­rın­da­ki ba­ğım­lı­lık­lar­dan olu­şan bir DAG ola­rak ta­nım­lar­sı­nız. TaskFlow API ile gö­rev­le­ri Python fonk­si­yo­nu ola­rak ya­za­bi­lir­si­niz, ama dü­şün­me bi­çi­mi yine ön­ce­den ta­nım­lan­mış bir DAG’dir. Var olan script’leri ta­şı­mak ge­nel­lik­le on­la­rı DAG ve ope­ra­tor ya­pı­sı­na göre ye­ni­den dü­zen­le­mek de­mek­tir.

Airflow 3 ara­cı cid­di bi­çim­de ye­ni­le­di:

  • Gö­rev­ler bir Task SDK üze­rin­den ça­lı­şı­yor; mi­ma­ri is­tem­ci-su­nu­cu mo­de­li­ne geç­ti.
  • DAG sü­rüm­le­me: Bir ça­lış­ma, baş­la­dı­ğı DAG sü­rü­müy­le bi­ti­yor.
  • Es­ki­den “da­ta­set” de­nen kav­ra­mın adı as­set oldu ve dış sis­tem­ler­den ge­len olay­lar­la te­tik­le­ne­bi­li­yor.
  • Yeni bir React ara­yü­zü gel­di; geç­mi­şe dö­nük ça­lış­tır­ma­lar (backfill) ara­yüz­den baş­la­tı­la­bi­li­yor.
  • Gö­rev­le­ri uzak ma­ki­ne­ler­de ça­lış­tır­mak için Edge Executor ek­len­di.

Son­ra­ki sü­rüm­ler bun­la­ra in­san ona­yı bek­le­yen adım­la­rı, süre aşı­mı uya­rı­la­rı­nı, çok­lu ekip des­te­ği­ni ve Java ile Go için Task SDK’yı ek­le­di.

Airflow’un en güç­lü yanı eko­sis­te­mi: yüzü aş­kın res­mi pro­vi­der pa­ke­ti, en ka­la­ba­lık top­lu­luk ve üç bü­yük yö­ne­ti­len hiz­met (Ast­ro­no­mer Ast­ro, AWS MWAA ve Google Cloud Com­po­ser). Be­de­li ise bir tür ba­kım ver­gi­si: En kü­çük ku­ru­lum bile bir sche­du­ler, bir DAG pro­ces­sor, bir API su­nu­cu­su ve bir me­ta­da­ta ve­ri­ta­ba­nı is­ter; er­te­len­miş gö­rev kul­la­nı­yor­sa­nız buna trig­ge­rer da ek­le­nir. Bun­la­rı ken­di su­nu­cu­nuz­da ka­rar­lı tut­mak, kü­çük bir ekip için ayrı bir iş yü­kü­dür.

Dagster: önce veri varlıkları

Dagster’da önce var ol­ma­sı ge­re­ken ve­ri­yi ta­nım­lar­sı­nız: bir tab­lo, bir veri seti, bir ma­ki­ne öğ­ren­me­si mo­de­li. Dags­ter bu var­lık­la­rı üret­mek için ne­yin ne za­man ça­lış­ma­sı ge­rek­ti­ği­ni ken­di­si plan­lar. Gün­cel sü­rüm 1.13.24 (Ey­lül 2026).

Mo­de­lin mer­ke­zin­de var­lık­lar ve ara­la­rın­da­ki soy (lineage) iliş­ki­si var. Bu­nun üze­ri­ne ku­ru­lan baş­lı­ca özel­lik­ler şun­lar:

  • Var­lık­la­ra bağ­lı veri ka­li­te­si ve ta­ze­lik kont­rol­le­ri (as­set check’ler).
  • Za­ma­na ya da de­ğer­le­re göre bö­lüm­len­miş var­lık­lar ve bun­lar üze­rin­de geç­mi­şe dö­nük ça­lış­tır­ma­lar.
  • Kod yaz­ma­dan ko­şul ta­nım­la­nan bil­di­rim­sel oto­mas­yon.
  • dbt pro­je­le­rin­de sü­tun se­vi­ye­sin­de soy ta­ki­bi.

Yeni pro­je­ler için öne­ri­len yol, 1.12 ile ka­rar­lı hale ge­len Com­po­nents ya­pı­sı ve dg ko­mut sa­tı­rı ara­cı. Ken­di su­nu­cu­nuz­da ça­lış­tır­mak için bir web su­nu­cu­su, za­man­la­ma ve sen­sör­le­ri yü­rü­ten bir da­emon, kod ko­nu­mu (code lo­ca­ti­on) su­nu­cu­la­rı ve bir de­po­la­ma kat­ma­nı ge­re­ki­yor. Ba­rın­dı­rı­lan sü­rüm Dags­ter+ üc­ret­li; 30 gün­lük de­ne­me dı­şın­da ka­lı­cı bir üc­ret­siz pla­nı yok.

Prefect: önce Python ve yürütme

Prefect’te önce işi ya­zar­sı­nız: sı­ra­dan Python fonk­si­yon­la­rı, sı­ra­dan if ve for dön­gü­le­ri. Pre­fect bu ko­dun yü­rü­tül­me­si­ni iz­le­ne­bi­lir, ye­ni­den de­ne­ne­bi­lir ve za­man­la­na­bi­lir hale ge­ti­rir. Var olan bir fonk­si­yo­na @flow ek­le­mek çoğu za­man ye­ter­li­dir. En kü­çük ku­ru­lum tek bir prefect server start (var­sa­yı­lan ola­rak SQLite ile) ve iş­le­ri ça­lış­tı­ra­cak bir worker ya da .serve() sü­re­ci­dir. Olay­lar ve oto­mas­yon­lar açık kay­nak sü­rüm­de de var; ba­rın­dı­rı­lan Pre­fect Cloud’un üc­ret­siz bir pla­nı bu­lu­nu­yor. Za­yıf yan­la­rı da bel­li: Var­lık ve soy ta­ki­bi sa­de­ce Cloud’da; ha­zır en­teg­ras­yon sa­yı­sı da Airflow’un pro­vi­der eko­sis­te­min­den kü­çük.

Yan yana

AirflowDagsterPrefect
Temel birimGörev (DAG)Veri varlığıPython fonksiyonu
Dinamik akışDynamic task mappingDinamik bölümler, DynamicOutDoğal Python, .map()
Yerel testdag.test(), metadata veritabanı gerekirVarlığı doğrudan çağırmak, pytest.fn() ya da test harness
TetiklemeCron, timetable, varlık olaylarıSchedule, sensör, bildirimsel otomasyonCron, interval, RRule, olay ve otomasyonlar
Soy takibiAsset’ler, OpenLineageModelin merkezindeSadece Cloud’da
En küçük kurulum4 süreç ve veritabanıWeb sunucusu, daemon, kod konumuTek sunucu ve worker
Kendi sunucunuzda işletme yüküYüksekOrtaDüşük
Ekosistem~107 provider paketi~90 entegrasyonAna repoda 18 entegrasyon paketi
GitHub yıldızı~47 bin~16 bin~24 bin
Barındırılan seçenekAstro, MWAA, ComposerDagster+ (ücretli)Prefect Cloud (ücretsiz plan var)

Sü­rüm ve sa­yı­lar 26 Ey­lül 2026 iti­ba­rıy­la. “İş­let­me yükü” sa­tı­rı be­nim de­ğer­len­dir­mem; yö­ne­ti­len hiz­met­ler bu yü­kün bü­yük kıs­mı­nı üst­le­nir.

Hangisini ne zaman seçmeli?

  • Airflow, eki­bi­niz Airflow’u za­ten bi­li­yor­sa, çok sa­yı­da ha­zır bağ­lan­tı­ya ih­ti­ya­cı­nız var­sa ya da bu­lut sağ­la­yı­cı­nı­zın yö­ne­ti­len hiz­me­ti­ni kul­lan­mak is­ti­yor­sa­nız man­tık­lı. Dü­zen­li ça­lı­şan kla­sik ETL hat­la­rı için hala en gü­ven­li se­çim.
  • Dags­ter, işi­niz tab­lo­lar, mo­del­ler ve dbt pro­je­le­ri gibi veri var­lık­la­rı­nı gün­cel tut­mak­sa öne çı­kar. “Bu tab­lo ne­den eski, neye bağ­lı?” so­ru­su­na ce­vap arı­yor­sa­nız ve veri ka­li­te­si kont­rol­le­ri si­zin için bi­rin­ci sı­nıf bir kav­ram­sa, Dagster’ın mo­de­li size daha do­ğal ge­lir.
  • Pre­fect, iş­le­ri­niz he­te­ro­jen­se daha ra­hat otu­rur: API ça­ğır­mak, sis­tem­ler ara­sın­da veri köp­rü­sü kur­mak, ra­por gön­der­mek, baş­ka sis­tem­le­ri iz­le­mek, bir kuy­ru­ğu bes­le­mek ya da ya­pay zeka ajan­la­rı­nı ça­lış­tır­mak. Bun­lar veri var­lı­ğı de­ğil, ey­lem­dir. Var olan Python script’le­ri­ni­zi az de­ği­şik­lik­le or­kest­re et­mek ve kü­çük bir ekip­le en az iş­let­me yü­küy­le baş­la­mak is­ti­yor­sa­nız da Pre­fect iyi bir baş­lan­gıç nok­ta­sı.

Sa­tın alma du­yu­ru­sun­dan son­ra Dags­ter ve Pre­fect ra­kip­ten çok bir­bi­ri­ni ta­mam­la­yan araç­lar ola­rak ko­num­la­nı­yor. dagster-prefect en­teg­ras­yo­nu öniz­le­me ola­rak ya­yın­lan­dı; bir Dags­ter var­lı­ğın­dan Pre­fect deployment’ı te­tik­le­mek ar­tık müm­kün. Var­lı­ğı Dagster’da ta­nım­la­yıp işi Prefect’e yü­rüt­tü­ren kar­ma kur­gu­la­rı ile­ri­de daha sık gö­re­ce­ğiz gibi gö­rü­nü­yor.

Son bir not: Bu araç­la­rı kar­şı­laş­tı­ran ya­zı­la­rın çoğu bir üre­ti­ci­ye ait. Pre­fect ar­tık Dagster’ın da sa­hi­bi ol­du­ğu için iki ara­cın res­mi say­fa­la­rın­da­ki kar­şı­laş­tır­ma­la­rı ba­ğım­sız bir gö­rüş ola­rak oku­ma­mak ge­re­kir.

Nasıl kurgulanmalı?

Prefect’i pro­je­ye da­hil et­mek ko­lay­dır. Zor olan, on­lar­ca za­man­lan­mış işi so­run­suz ayak­ta tu­ta­cak bir yapı kur­mak­tır. Pre­fect size @flow ve @task ve­rir; bağ­lan­tı­la­rın ne­re­den ge­le­ce­ği­ni, lo­gun ne­re­ye ya­zı­la­ca­ğı­nı, hata olun­ca ki­min ha­ber­dar ola­ca­ğı­nı ve ayar­la­rın ne­re­de du­ra­ca­ğı­nı söy­le­mez. Bu söz­leş­me­yi siz ku­rar­sı­nız. Kur­maz­sa­nız her flow onu ken­di bil­di­ği gibi ku­rar ve be­şin­ci flow’da aynı kırk sa­tı­rı be­şin­ci kez ya­zı­yor olur­su­nuz.

Aşa­ğı­da­ki il­ke­ler, kü­çük bir eki­bin, hat­ta tek bir ki­şi­nin iş­le­te­bi­le­ce­ği öl­çek­ten baş­la­yıp bü­yü­ye­bi­len bir yapı için. Her bi­ri­nin ar­ka­sın­da Pre­fect pro­je­le­rin­de sık ya­pı­lan bir hata var.

1. Dört katman, tek yönlü bağımlılık

Flow’ları ayrı ayrı yaz­ma­ya baş­la­dı­ğı­nız­da, kısa sü­re­de her dos­ya­nın ba­şı­nın aynı ol­du­ğu­nu gö­rür­sü­nüz: bağ­lan­tı kur­ma, cur­sor açma, hata ya­ka­la­ma, mail gön­der­me, log yaz­ma. Fark­lı olan tek şey or­ta­da­ki sor­gu­dur. Son­ra mail su­nu­cu­su­nun aya­rı de­ği­şir; beş dos­ya­yı gün­cel­ler­si­niz, al­tın­cı­yı unu­tur­su­nuz.

Bu­nun ila­cı dört kat­man ve tek bir ku­ral:

core/      →  services/     →  models/       →  flows/
altyapı       yatay servis      sözleşmeler      iş mantığı
  • core/: Bağ­lan­tı­la­rın tek gi­riş ka­pı­sı ve ken­di hata hi­ye­rar­şi­niz. İçin­de hiç­bir iş ku­ra­lı ya da sor­gu ol­ma­ma­lı.
  • services/: Her flow’un ih­ti­yaç duy­du­ğu ama hiç­bi­ri­ne ait ol­ma­yan şey­ler: mail ser­vi­si ve log­ger.
  • models/: Her flow’un ka­bul et­ti­ği ayar­lar ve dön­dür­dü­ğü so­nuç tip­le­ri. Yani flow’un söz­leş­me­si.
  • flows/: Sa­de­ce iş man­tı­ğı. Bir flow dos­ya­sı bir ta­rif ol­ma­lı; mut­fa­ğın alt­ya­pı­sı­nı ve bağ­lan­tı de­tay­la­rı­nı ta­şı­ma­ma­lı.

Ku­ral: Ba­ğım­lı­lık oku asla ge­ri­ye dön­mez. core, services’ı bil­mez; services hiç­bir flow’u bil­mez. Bu ku­ral tek bir so­ru­yu ce­vap­lar: “Bu kod ne­re­ye git­me­li?” Ken­din­den son­ra­ki kat­ma­nı bil­me­si ge­re­ki­yor­sa yan­lış yer­de­dir.

Bir uya­rı: Bu kat­man­la­rı baş­tan ta­sar­la­ma­ya ça­lış­ma­yın, tek­rar eden kod­dan çı­ka­rın. Önce flow’ları düz ya­zın, aynı kodu üçün­cü kez ya­zar­ken du­run ve onu or­tak bir yere ta­şı­yın. Ön­ce­den alın­ma­sı ge­re­ken tek ka­rar ba­ğım­lı­lı­ğın yönü; o da be­da­va.

2. Bağlantılar için tek giriş kapısı

Bir­den faz­la sis­te­me bağ­la­nan on­lar­ca iş var­sa, şif­re­ler ne­re­de dur­ma­lı? Kod­da dur­maz. .env dos­ya­sı su­nu­cu­da, ye­dek­ler­de ve container’da do­la­şır. Ayrı bir va­ult ise kü­çük bir ekip için iş­le­ti­le­cek yeni bir sis­tem de­mek. Prefect’in Secret block’u iyi bir orta yol: za­ten iş­let­ti­ği­niz sis­te­min par­ça­sı, ara­yüz­den dü­zen­le­ne­bi­li­yor ve bir bağ­lan­tı­nın tüm bil­gi­le­ri JSON ola­rak tek bir block’ta du­ra­bi­li­yor.

Block ve­ri­le­ri ken­di su­nu­cu­nuz­da da şif­re­li sak­la­nır. Ama bir ay­rın­tı­ya dik­kat: Şif­re­le­me anah­ta­rı­nı ver­mez­se­niz Pre­fect onu ken­di­si üre­tir ve aynı ve­ri­ta­ba­nın­da sak­lar; ve­ri­ta­ba­nı­na eri­şen biri şif­re­le­ri çö­ze­bi­lir. Anah­ta­rı PREFECT_SERVER_ENCRYPTION_KEY or­tam de­ğiş­ke­niy­le ve­ri­ta­ba­nı­nın dı­şın­da tu­tun.

Kri­tik ka­rar şu: Hiç­bir flow bu block’ları doğ­ru­dan oku­ma­ma­lı. Tek bir sı­nıf oku­ma­lı:

class DatabaseBlocks:
    """Tüm veritabanı ve servis bağlantılarının tek giriş kapısı."""

    @classmethod
    def get_mssql(cls, block_name: str = "kaynak-db") -> pymssql.Connection:
        try:
            config = cls._load_secret_json(block_name)
            return pymssql.connect(
                server=config["host"],
                port=config.get("port", 1433),
                user=config["username"],
                password=config["password"],
                database=config["database"],
            )
        except ConfigurationError:
            raise  # "block yok" hatasını veritabanı hatası gibi gösterme
        except Exception as e:
            raise DatabaseError(f"Bağlantı hatası: {e}") from e

Bu sı­nıf­ta­ki ka­rar­lar önem­li:

  • Block adı bir pa­ra­met­re. Yeni bir ve­ri­ta­ba­nı ek­le­mek yeni bir block oluş­tur­mak de­mek ol­ma­lı; koda tek sa­tır do­ku­nul­ma­ma­lı. Yeni bir ör­nek ek­le­mek kod de­ği­şik­li­ği ge­rek­ti­ri­yor­sa so­yut­la­ma yan­lış yer­de­dir.
  • “Block bu­lu­na­ma­dı” ile “ve­ri­ta­ba­nı ya­nıt ver­mi­yor” ayrı ha­ta­lar ol­ma­lı. Bi­rin­ci­si ku­ru­lum ha­ta­sı­dır, ikin­ci­si alt­ya­pı ola­yı; fark­lı in­san­la­rı il­gi­len­di­rir­ler.
  • Bağ­lan­tı stra­te­ji­si kütüphaneye göre se­çi­lir. Ken­di bağ­lan­tı ha­vu­zu ol­ma­yan bir sü­rü­cü­de (ör­ne­ğin pymssql) her çağ­rı­da yeni bağ­lan­tı açıp işi bi­tin­ce finally için­de ka­pa­tın; uzun ya­şa­yan bir bağ­lan­tı ağ kop­tu­ğun­da ses­siz­ce ölü ka­lır. MongoClient gibi ken­di ha­vu­zu­nu yö­ne­ten is­tem­ci­ler­de ise sü­reç bo­yun­ca tek bir ör­nek (sing­le­ton) kul­la­nın. Sing­le­ton bir moda de­ğil, kütüphanenin dav­ra­nı­şı­na ve­ri­len ce­vap­tır.
  • Dep­loy ön­ce­si doğ­ru­la­ma. Ge­rek­li block’la­rın hep­si­nin ye­rin­de olup ol­ma­dı­ğı­nı iki sa­ni­ye­de söy­le­yen bir verify_all() fonk­si­yo­nu, flow’un ya­rı­sın­da pat­la­ma­sı­nı bek­le­mek­ten çok daha ucuz­dur.

Repo ge­ne­lin­de pymssql.connect(...), MongoClient(...) ve smtplib.SMTP(...) gibi doğ­ru­dan bağ­lan­tı­la­rı ya­sak­la­yın. Bu ya­sak de­net­le­ne­bi­lir de: grep -rn "pymssql.connect" flows/ her za­man boş dön­me­li.

Bir uya­rı daha: Block’la­rın ye­de­ği­ni alır­sa­nız, o ye­dek dos­ya­sı bü­tün şif­re­le­ri açık me­tin ola­rak içe­re­bi­lir. Giz­li bil­gi­yi doğ­ru yer­de sak­la­mak, ye­de­ği­ni yan­lış yer­de bı­rak­ma­yı en­gel­le­mez. Gü­ven­lik, en za­yıf kop­ya­nın gü­ven­li­ği­dir.

3. Ayarlar: gizlilik ile değişkenlik farklı eksenlerdir

İlk ref­leks ge­nel­de her aya­rı Sec­ret block’a koy­mak­tır. So­nuç: “Bu ra­por kime gi­di­yor?” so­ru­su­nu ce­vap­la­mak için ara­yü­zü açıp JSON ara­mak ge­re­kir; kodu oku­yan biri ce­va­bı kod­dan bu­la­maz. Oysa bir şey sık de­ği­şi­yor diye giz­li de­ğil­dir.

Ayar­la­rı üç kat­man­da tu­tun:

  • A. Düz dict: Dos­ya­nın te­pe­sin­de, in­sa­nın oku­ma­sı için. İş ku­ral­la­rı, eşik­ler, mail alı­cı­la­rı, açma/kapama bay­rak­la­rı. Kod in­ce­le­me­sin­de gö­rün­me­le­ri bir avan­taj­dır.
  • B. Pydan­tic sı­nı­fı: Dict’e bağ­lı; tip gü­ven­li­ği ve doğ­ru­la­ma sağ­lar. max_rows="elli" ya­zı­lır­sa flow baş­la­ma­dan pat­lar, ge­çer­siz bir e-pos­ta ad­re­si daha dep­loy aşa­ma­sın­da ya­ka­la­nır. Pre­fect de bu sı­nı­fı dep­loy­ment pa­ra­met­re­si ola­rak ara­yüz­de form ha­lin­de gös­te­rir.
  • C. Op­si­yo­nel block: Sa­de­ce ger­çek­ten has­sas ya da or­ta­ma özgü de­ğer­ler için: token’lar, API ad­res­le­ri.

Han­gi aya­rın ne­re­ye gi­de­ce­ği­ni tek bir soru be­lir­ler: “Bu de­ğe­ri bir ek­ran gö­rün­tü­sün­de pay­la­şa­bi­lir mi­yim?” Evet ise kod­da dur­sun.

Bu kat­man­da ses­siz bir tu­zak da var. Pydan­tic düz lis­te var­sa­yı­lan­la­rı­nı her ör­nek için ken­di­si kop­ya­lar; ama var­sa­yı­la­nı default_factory ile dos­ya­nın te­pe­sin­de­ki söz­lük­ten alı­yor­sa­nız kop­ya­la­ma so­rum­lu­lu­ğu si­zin­dir:

emails: list[str] = Field(default_factory=lambda: CONFIG["emails"])         # ❌ her örnek aynı listeyi alır
emails: list[str] = Field(default_factory=lambda: CONFIG["emails"].copy())  # ✅ her örneğe kendi kopyası

İlk sa­tır­da bir ça­lış­ma­nın lis­te­ye ek­le­di­ği alı­cı, ayar söz­lü­ğü­nün ken­di­si­ne ya­zı­lır ve aynı sü­reç­te­ki son­ra­ki ça­lış­ma­lar­da da gö­rü­nür. Uzun süre ça­lı­şan bir worker’da bu, sa­at­ler son­ra “bu alı­cı ne­re­den gel­di?” diye or­ta­ya çı­kan ha­ya­let bir ha­ta­ya dö­nü­şür.

Bu kat­man­dan do­ğan en ve­rim­li de­sen pa­ra­met­rik flow’dur. Aynı işi fark­lı pa­ra­met­re­ler­le ya­pan dört ayrı dos­ya ye­ri­ne tek bir flow, bir ayar söz­lü­ğü ve dört dep­loy­ment. Yeni bir var­yant ek­le­mek, söz­lü­ğe bir blok ve dep­loy­ment lis­te­si­ne bir blok ek­le­mek de­mek­tir; test edil­miş flow ko­du­na do­ku­nul­maz. Sı­nı­rı da bel­li: Var­yant­lar ara­sın­da­ki fark veri de­ğil dav­ra­nış ol­ma­ya baş­la­dıy­sa, yani kod­da if flow_name == "x" be­lir­diy­se, ar­tık iki ayrı flow’unuz var­dır.

4. Standart flow iskeleti

Her flow aynı is­ke­le­te uy­ma­lı. Bir flow dos­ya­sı­nı oku­yan kişi, alt­ya­pı hak­kın­da hiç­bir şey bil­me­den ne ya­pıl­dı­ğı­nı an­la­ya­bil­me­li:

@task(name="veri-cek", retries=2, retry_delay_seconds=30, cache_policy=NO_CACHE)
def veri_cek(logger: FlowLogger, db_block: str = "kaynak-db") -> list[dict]:
    task_logger = logger.for_task("veri-cek")
    with task_logger.operation("veri_cekme"):  # süreyi kendiliğinden ölçer
        conn = None
        try:
            conn = DatabaseBlocks.get_mssql(block_name=db_block)
            cursor = conn.cursor(as_dict=True)
            cursor.execute("SELECT ...")
            return cursor.fetchall()
        except Exception as e:
            raise DatabaseError(f"Kaynak sorgusu başarısız: {e}") from e
        finally:
            if conn is not None:
                conn.close()  # bağlantı havuzu yok; kapatmak zorunlu


@flow(name="ornek-rapor", retries=0)
def ornek_rapor(config: Optional[OrnekConfig] = None) -> OrnekResult:
    run_id = str(flow_run.get_id()) if flow_run.get_id() else "local-run"
    logger = FlowLogger(flow_name="ornek-rapor", flow_run_id=run_id)
    config = config or OrnekConfig()
    result = OrnekResult(started_at=datetime.now())  # hata olsa da bir sonuç olsun

    try:
        kayitlar = veri_cek(logger=logger)
        result.total = len(kayitlar)
        # ... iş mantığı ve rapor maili ...
        return result
    except Exception as e:
        logger.error("Flow hatası", error=e)
        if config.alert_emails:
            try:
                MailService().send_error_alert(to=config.alert_emails, ...)
            except Exception:
                pass  # alarm gidemese bile asıl hatayı gizleme
        raise  # flow kırmızı görünmeli


if __name__ == "__main__":
    print(ornek_rapor().model_dump_json(indent=2))

Önem­li de­tay­lar:

  • cache_policy=NO_CACHE. Pre­fect 3 her task için gir­di­le­rin­den bir ön­bel­lek anah­ta­rı üret­me­ye ça­lı­şır. Task’a log­ger, ve­ri­ta­ba­nı bağ­lan­tı­sı ya da is­tem­ci gibi se­ri­leş­ti­ri­le­me­yen bir nes­ne ve­rir­se­niz bu he­sap ba­şa­rı­sız olur ve Pre­fect her ça­lış­ma­da uzun bir ERROR logu ba­sar. Task yine ça­lı­şır, ama log­lar sah­te ha­ta­lar­la do­lar. Dış dün­ya­ya do­ku­nan ya da bu tür nes­ne­ler alan task’lar­da ön­bel­le­ği açık­ça ka­pa­tın; yan et­ki­si olan bir işin so­nu­cu­nu ön­bel­lek­le­mek za­ten is­te­me­ye­ce­ği­niz bir şey­dir.
  • flow_run.get_id() bir flow ça­lış­ma­sı­nın dı­şın­da boş dö­ner. Ör­ne­ğin fonk­si­yo­nu test için .fn() ile doğ­ru­dan ça­ğır­dı­ğı­nız­da. "local-run" ye­de­ği ol­maz­sa log­lar kim­lik­siz ka­lır ve bir­bi­ri­ne ka­rı­şır.
  • En son­da­ki raise. Ha­ta­yı ya­ka­la­yıp loga yaz­mak yet­mez, flow ba­şa­rı­sız gö­rün­me­li. Ha­ta­yı yu­tup “ba­şa­rı­lı” dön­mek, bu ya­zı­nın ba­şın­da­ki ses­siz du­ru­şun ta ken­di­si­dir.
  • Ha­ta­yı sa­rar­ken from e. Ken­di hata sı­nı­fı­nı­za sar­mak, alt­ta ne ol­du­ğu­nu giz­le­mek an­la­mı­na gel­me­me­li; from e ori­ji­nal ha­ta­yı zin­cir­de ko­rur.
  • Alarm gön­de­ri­mi ken­di try blo­ğun­da. Mail su­nu­cu­su da çök­müş ola­bi­lir; alar­mın ba­şa­rı­sız ol­ma­sı asıl ha­ta­yı giz­le­me­me­li.
  • if __name__ == "__main__": Her flow tek ba­şı­na ça­lış­tı­rı­la­bil­me­li; worker’a ya da container’a do­kun­ma­dan python -m flows.ornek_rapor ile de­ne­ne­bil­me­li. Hata ayık­la­ma­da en çok za­man ka­zan­dı­ran üç sa­tır bu­dur.

5. Log bir veri kümesidir

Bir or­kest­ras­yon plat­for­mu bir­kaç ay ça­lış­tık­tan son­ra ge­len so­ru­lar hep aynı tür­den­dir: Bu iş ge­çen ay top­lam kaç ka­yıt iş­le­di? Bu adım ne za­man ya­vaş­la­ma­ya baş­la­dı? Şu hata bu ay ka­çın­cı kez olu­yor? print() ile log­la­nan bir plat­form­da bu so­ru­la­rın ce­va­bı yok­tur, çün­kü print çık­tı­sı sor­gu­la­na­maz ve veri hiç top­lan­ma­mış­tır.

Bu yüz­den repo ge­ne­lin­de print() ye­ri­ne ya­pı­lan­dı­rıl­mış, çift he­def­li bir log­ger kul­la­nın. Tek bir çağ­rı iki yere bir­den yaz­sın:

logger.info("Kayıtlar tarandı", kayit_sayisi=156, kaynak="kaynak-a")

İn­san oku­sun diye Pre­fect ara­yü­zü­ne düz bir sa­tır, ma­ki­ne sor­gu­la­sın diye log de­po­su­na ya­pı­lan­dı­rıl­mış bir JSON kay­dı gi­der: flow_name, task_name, flow_run_id, details ve duration_ms alan­la­rıy­la. “Son 90 gün­de gün­lük ka­yıt sa­yı­sı” ar­tık tek bir sor­gu­dur. Adım­la­rı sa­ran bir operation(...) blo­ğu her adı­mın sü­re­si­ni ken­di­li­ğin­den öl­çer; kim­se elle za­man tut­maz.

İki ku­ral var. Log de­po­su­na ya­zı­la­maz­sa iş yine sür­me­li, çün­kü log­la­ma işin ken­di­sin­den daha kri­tik ola­maz. Öl­çüm blo­ğu da ha­ta­yı kay­det­me­li ama yut­ma­ma­lı.

Log de­po­su­nun han­gi ve­ri­ta­ba­nı ol­du­ğu ikin­cil: PostgreSQL ve JSONB, MongoDB gibi bir do­kü­man ve­ri­ta­ba­nı ya da eki­bi­ni­zin za­ten iş­let­ti­ği her­han­gi bir sor­gu­la­na­bi­lir depo ola­bi­lir. Doğ­ru ce­vap bir mar­ka de­ğil, sor­gu­la­na­bi­lir bir yer. En teh­li­ke­li du­rum “flow ye­şil, veri yan­lış” ha­li­dir: Her gün 150 ka­yıt iş­le­yen bir iş bir gün 12 ka­yıt iş­le­miş­se, bunu an­cak sa­yı­la­rı kay­de­di­yor­sa­nız gö­rür­sü­nüz.

6. Retry bir güvenlik ağı değil, bir çarpandır

Retry’ı her flow’a ref­leks­le koy­mak çok yay­gın bir ha­ta­dır. Bir sto­red pro­ce­du­re ça­ğı­ran flow dü­şü­nün. Ağ kısa bir süre ke­si­lir; pro­se­dür ça­lı­şır ve bi­ter, ama so­nuç dö­ner­ken bağ­lan­tı ko­par. Pre­fect açı­sın­dan iş ba­şa­rı­sız­dır; retry dev­re­ye gi­rer ve pro­se­dür ikin­ci kez ça­lı­şır. Aynı sa­tır­lar iki kez iş­len­miş olur.

Retry, işin idem­po­tent ol­du­ğu­nu, yani iki kez ça­lı­şın­ca so­nu­cun de­ğiş­me­di­ği­ni var­sa­yar. Bu var­sa­yım yok­sa retry bir veri boz­ma me­ka­niz­ma­sı­dır. Ku­ral­lar şöy­le ol­ma­lı:

  • Ge­çi­ci ha­ta­lar­la ka­lı­cı ha­ta­la­rı ayı­rın. ConfigurationError ve ValidationError tek­rar de­nen­mez; 30 sa­ni­ye son­ra block yine yok­tur, veri yine bo­zuk­tur. DatabaseError ve MailError gibi alt­ya­pı ha­ta­la­rı ge­çi­ci ola­bi­lir, de­ne­ne­bi­lir.
  • İdem­po­tent ol­ma­yan iş ya­pan flow’da retries=0. Bu bir ek­sik­lik de­ğil, bir bel­ge­le­me­dir: Kodu oku­ya­na “bu iş iki kez ça­lış­ma­ma­lı” der.
  • Retry’ı gü­ven­li kıl­ma­nın yol­la­rı­nı bi­lin: tam ye­ni­le­me (trun­ca­te + in­sert), anah­ta­ra göre MERGE, yük­sek su se­vi­ye­si (id > MAX(id)) ve kuy­ruk iş­le­rin­de sa­tır­la­rı tek bir ato­mik ifa­dey­le sa­hip­len­mek.

Prefect’in tran­sac­ti­on özel­li­ği de bu ko­nu­da sizi kur­tar­maz: Geri alma adı­mı­nı yine si­zin yaz­ma­nız ge­re­kir. En sağ­lık­lı var­sa­yı­lan şu: Her yer­de retries=0 ol­sun, idem­po­tent­lik ka­nıt­la­nın­ca retry ek­len­sin. Gü­ven­li var­sa­yı­lan, gü­ven­li ol­ma­yan var­sa­yı­lan­dan her za­man iyi­dir.

7. Deployment’lar kodda dursun

Deployment’ları ara­yüz­den ya da tek tek CLI ko­mut­la­rıy­la oluş­tur­mak ka­yıt dışı ka­lır; han­gi ko­mu­tun ça­lış­tı­rıl­dı­ğı­nı kim­se ha­tır­la­maz. Deployment’ları sü­rüm kont­ro­lün­de­ki bir dos­ya­da ta­nım­la­yın. Python ter­cih edi­yor­sa­nız bir deploy.py:

from prefect import Flow
from prefect.schedules import Cron

Flow.from_source(
    source="/app",
    entrypoint="flows/veri_kalite.py:veri_kalite_raporu",
).deploy(
    name="veri-kalite-gunluk",
    work_pool_name="varsayilan-havuz",
    schedule=Cron("0 8 * * *", timezone="Europe/Istanbul"),
    tags=["veri-kalite", "gunluk"],
)

Bü­tün deployment’lar tek bir dos­ya­day­sa o dos­ya plat­for­mun za­man­la­ma ha­ri­ta­sı olur: “Sa­bah se­kiz­de ne ça­lı­şı­yor?” so­ru­su tek ek­ran­da ce­vap­la­nır, yeni bir iş ek­ler­ken aynı sa­at­te baş­ka ne ol­du­ğu­nu gö­rür­sü­nüz. Dep­loy­ment sa­yı­sı yüz­le­re yak­laş­tı­ğın­da mo­dü­ler ta­nım­la­ra ya da prefect.yaml’a geç­mek ge­re­kir.

Bu kat­man­da üç tu­zak var:

  • Saat di­li­mi. Düz bir cron="0 8 * * *" met­ni UTC ola­rak yo­rum­la­nır; ye­rel saat UTC+3 ise iş 11’de ça­lı­şır. Daha sin­si ola­nı gün kay­ma­sı­dır: UTC salı 22:00, ye­rel sa­at­le çar­şam­ba 01:00 de­mek; “salı ra­po­ru” çar­şam­ba çı­kar. He­sa­bı ka­fa­dan yap­ma­yın; yu­ka­rı­da­ki gibi Cron(..., timezone=...) ile saat di­li­mi­ni açık­ça ve­rin.
  • Eski deployment’lar. Pre­fect ge­le­cek­te­ki za­man­lan­mış ça­lış­ma­la­rı ön­ce­den oluş­tu­rur. Bir deployment’ın za­man­la­ma­sı­nı de­ğiş­tir­di­ği­niz­de he­nüz baş­la­ma­mış ça­lış­ma­la­rı si­ler ve ye­ni­le­ri­ni oluş­tu­rur; ama deployment’ı fark­lı bir adla ye­ni­den oluş­tu­rur­sa­nız es­ki­si za­man­la­ma­sıy­la bir­lik­te ye­rin­de du­rur ve iş iki kez ça­lı­şır. Bir deployment’ı ye­ni­den ad­lan­dır­dık­tan son­ra es­ki­si­ni sil­me­yi ve ara­yüz­de bek­le­yen ça­lış­ma­la­ra bir kez bak­ma­yı alış­kan­lık ha­li­ne ge­ti­rin.
  • Ko­dun da­ğı­tı­mı. Kodu worker’a vo­lu­me ola­rak bağ­la­mak hız­lı­dır: Dep­loy et­mek dos­ya­yı gün­cel­le­mek­ten iba­ret­tir, ima­ge ye­ni­den der­len­mez. Be­de­li ye­ni­den üre­ti­le­bi­lir­lik­tir; “han­gi kod sü­rü­mü ça­lı­şı­yor?” so­ru­su­nun ce­va­bı ima­ge eti­ke­tin­de de­ğil, dos­ya sis­te­min­de­dir. Tek worker’lı kü­çük bir ku­ru­lum­da bu ta­kas ka­bul edi­le­bi­lir; bir­den faz­la worker’da kodu sü­rüm eti­ket­li bir image’a koy­mak ge­re­kir.

8. Süreci değil, sonucu ölçün

“Ne de­ğil­dir” bö­lü­mün­de­ki ye­dek ör­ne­ği­ni ha­tır­la­yın. Bu­nun pan­ze­hi­ri, baş­ka sis­tem­le­rin so­nu­cu­nu doğ­ru­dan öl­çen ayrı bir flow ai­le­si­dir; on­la­ra nö­bet­çi flow di­yo­rum. Ye­dek­le­me işi­nin ça­lı­şıp ça­lış­ma­dı­ğı­nı de­ğil, ve­ri­ta­ba­nı­nın ken­di kay­dı­na ba­ka­rak ye­de­ğin ger­çek­ten var olup ol­ma­dı­ğı­nı ve ne ka­dar taze ol­du­ğu­nu so­rar­lar. Nö­bet­çi­le­rin iki ku­ra­lı ol­ma­lı:

  • Eri­şim ha­ta­sı da bir ih­lal­dir. Kont­rol sor­gu­su pat­lar­sa “so­run yok” de­ğil, “bil­mi­yo­rum” ra­por­lan­ma­lı. Bil­gi ek­sik­li­ği iyi ha­ber sa­yıl­maz.
  • Her alarm­dan önce şu so­ru­yu so­run: “Bu ayda kaç kez öte­cek?” Ce­vap “her gün” ise o bir alarm de­ğil, bir ra­por­dur. Her ça­lış­ma­da uya­rı üre­ten bir iz­le­me flow’una iki haf­ta son­ra kim­se bak­maz; ger­çek uya­rı da o gü­rül­tü­nün için­de kay­bo­lur. Alarm yor­gun­lu­ğu tek­nik de­ğil, in­sa­ni bir so­run­dur; ama mü­hen­dis­lik­le çö­zül­me­si ge­re­kir.

Prefect’in “bek­le­nen bir olay gel­mez­se te­tik­len” tü­rün­de­ki oto­mas­yon­la­rı da aynı işe ya­rar: “Bu iş sa­bah do­ku­za ka­dar ta­mam­lan­ma­dıy­sa ha­ber ver” ku­ra­lı, işin hiç baş­la­ma­dı­ğı du­ru­mu da ya­ka­lar.

Ölçeğe göre ne değişir?

Yu­ka­rı­da­ki il­ke­ler öl­çek­ten ba­ğım­sız, ama uy­gu­la­ma­la­rı de­ği­şir. Kü­çük baş­la­yıp bü­yü­yen bir ku­ru­lum için ka­ba­ca şöy­le:

KonuKüçük başlarkenBüyüdükçe
Sunucu veritabanıSQLitePostgreSQL (çoklu sunucuda + Redis)
WorkerTek process workerBirden fazla work pool, Docker ya da Kubernetes
Deployment tanımıTek deploy.pyModüler tanımlar ya da prefect.yaml
Kod dağıtımıVolume ile bağlanan repoSürüm etiketli image
TestFlow’ları yerelde çalıştırmakOtomatik test ve CI
Gizli bilgiSecret blockAyrı bir gizli bilgi yöneticisi

Bonus: Bu yapıyı bir yapay zeka asistanına kurdurmak

Ya­pay zeka kod­la­ma asis­tan­la­rı, ku­ral­la­rı ne ka­dar net ve­rir­se­niz o ka­dar tu­tar­lı kod üre­tir. “Bana bir Pre­fect pro­je­si kur” de­mek ye­ri­ne söz­leş­me­yi baş­tan ver­mek ge­re­kir.

Aşa­ğı­da­ki prompt’u boş bir re­po­da Cla­ude Code, Cur­sor ya da ben­ze­ri bir kod­la­ma asis­ta­nı­na ve­re­bi­lir­si­niz. Kö­şe­li pa­ran­tez­li yer­le­ri ken­di or­ta­mı­nı­za göre dol­du­run:

Sen kıdemli bir veri platformu mimarı ve deneyimli bir Python/Prefect
geliştiricisisin. Prefect 3 ile, küçük bir ekibin işletebileceği, katmanlı bir
iş orkestrasyonu reposu kuracağız. Aşağıdaki kurallara harfiyen uy; bir kuralı
esnetmen gerekirse önce bana sor.

## Bağlam

- Kaynak sistemler: [ör. 2 adet MSSQL veritabanı, 1 REST API]
- Log deposu: [ör. PostgreSQL (JSONB) veya MongoDB]
- Çalışma ortamı: tek Docker host, self-hosted Prefect server, tek process worker
- Yerel saat dilimi: [ör. Europe/Istanbul]

## Mimari kurallar

1. Klasörler: core/ → services/ → models/ → flows/. Bağımlılık tek yönlüdür;
   bir katman kendinden sonrakini asla import etmez.
2. core/blocks.py: Tüm bağlantıların tek giriş kapısı olan DatabaseBlocks
   sınıfı. Bağlantı bilgileri Prefect Secret block'larında JSON olarak durur.
   Block adı parametredir; yeni bir kaynak eklemek kod değişikliği
   gerektirmemelidir. Kendi havuzu olmayan sürücülerde (ör. pymssql) her
   çağrıda yeni bağlantı aç ve finally içinde kapat; kendi havuzunu yöneten
   istemcilerde (ör. MongoClient) singleton kullan. Gerekli tüm block'ları
   kontrol eden bir verify_all() fonksiyonu yaz.
3. core/exceptions.py: BaseFlowError altında ConfigurationError, DatabaseError,
   MailError, ValidationError ve TaskError. Sararken her zaman `raise ... from e`.
4. services/logging.py: Çift hedefli FlowLogger. Hem Prefect loguna hem log
   deposuna yapılandırılmış kayıt yazar; süre ölçen bir operation() context
   manager'ı ve for_task() metodu vardır. Log deposuna yazılamazsa flow durmaz.
5. services/mail.py: Singleton MailService. send() ve send_error_alert()
   metotları olur; ekleri bellekte (bytes) taşır; her gönderimde flow_name ve
   flow_run_id zorunludur.
6. models/config.py: Her flow için dosyanın tepesinde okunabilir bir dict ve ona
   bağlı bir Pydantic sınıfı. Liste varsayılanları default_factory ile ve .copy()
   yapılarak alınır. Sadece gizli veya ortama özgü değerler block'tan gelir.
7. models/results.py: FlowResult tabanlı sonuç sınıfları; her flow toplam,
   başarılı ve başarısız sayılarını, başlangıç ve bitiş zamanını döndürür.

## Flow kuralları

- Flow dosyası sadece iş mantığını anlatır; bağlantı dizesi, SMTP ayarı gibi
  altyapı detayı içermez.
- Dış dünyaya dokunan ya da logger, bağlantı gibi serileştirilemeyen nesneler
  alan task'larda cache_policy=NO_CACHE kullan.
- Varsayılan retries=0. Retry'ı sadece idempotent olduğu açık olan task'lara ekle
  ve nedenini yorumla yaz. ConfigurationError ve ValidationError retry edilmez.
- Hata olursa: logla, sonuç nesnesini doldur, alarm listesine mail at (mail
  hatası asıl hatayı gizlemesin), sonra mutlaka raise et. Hatayı yutup başarılı
  dönmek yasaktır.
- flow_run.get_id() bir flow çalışması dışında (ör. .fn() ile çağrıda) None
  döner; "local-run" yedeğini kullan.
- Her flow dosyası `if __name__ == "__main__":` ile tek başına çalıştırılabilir.
- Yasaklar: flows/ içinde doğrudan veritabanı veya SMTP bağlantısı (pymssql.connect,
  MongoClient, smtplib), print() ile loglama, kendi retry decorator'ın. Bunları
  grep ile kontrol eden küçük bir scripts/check_rules.sh yaz.

## Deployment kuralları

- Tüm deployment'lar tek bir deploy.py içinde Python koduyla tanımlanır.
- Zamanlamalar prefect.schedules.Cron ile ve Bağlam'daki saat dilimi açıkça
  verilerek yazılır, ör. Cron("0 8 * * *", timezone="Europe/Istanbul").
- Aynı işi farklı parametrelerle yapan varyantlar için tek flow, bir ayar sözlüğü
  ve birden fazla deployment kullan.
- Prefect server'ın PREFECT_SERVER_ENCRYPTION_KEY değişkenini veritabanının
  dışında tutacak şekilde docker-compose örneği hazırla.

## MCP (isteğe bağlı)

- mcp_server.py: FastMCP ile yapay zeka ajanlarına açılan araçlar. Sadece
  okuyan araçlar readOnlyHint ile işaretlenir. Eylem araçları yalnızca
  run_deployment ile mevcut bir deployment'ı tetikler, idempotency_key kullanır
  ve timeout=0 ile hemen döner. MCP sunucusu veritabanına ya da block'lara
  doğrudan erişmez.

## İlk görev

1. Klasör yapısını kur; requirements.txt, Dockerfile ve docker-compose.yml'ı ekle.
2. "ornek-rapor" adında üretime hazır tek bir örnek flow yaz: kaynaktan kayıt
   çeksin, sayıyı logla, özet maili göndersin.
3. README.md'ye "yeni flow ekleme kontrol listesi"ni yaz: ayar → sonuç tipi →
   flow → yerel test → deploy.py → elle bir kez çalıştır → log deposunda doğrula.
4. Her adımdan sonra ne yaptığını kısaca özetle; varsayım yaptığın her yeri
   açıkça belirt.

Bu prompt’un asıl işi kod yaz­dır­mak de­ğil, ku­ral koy­mak. Asis­tan her yeni flow’da aynı ku­ral­la­rı uy­gu­la­dı­ğın­da, altı ay son­ra re­po­ya ba­kan kişi, siz de da­hil, ne­re­ye do­ku­na­ca­ğı­nı bi­lir.

Özet

  • Pre­fect Python fonk­si­yon­la­rı­nı iz­le­ne­bi­lir, ye­ni­den de­ne­ne­bi­lir ve za­man­la­na­bi­lir iş­le­re dö­nüş­tü­ren, ku­ru­lu­mu ve iş­let­me­si ha­fif bir or­kest­ra­tör­dür. Kü­çük ve orta öl­çek­li ekip­ler için güç­lü bir baş­lan­gıç nok­ta­sı­dır.
  • Ci­la­lı bir cron, bir veri iş­le­me mo­to­ru ya da bir iz­le­me ga­ran­ti­si de­ğil­dir; idem­po­tent ol­ma­yan kodu da dü­zelt­mez.
  • Sis­tem­ler ara­sın­da bir en­teg­ras­yon ve tam­pon kat­man ola­rak kur­gu­la­na­bi­lir; gü­ven­li­ği Prefect’ten de­ğil, ta­sa­rım­dan ge­lir.
  • FastMCP ile flow’la­rı­nı­zı ya­pay zeka ajan­la­rı­na dar, idem­po­tent ve de­net­le­ne­bi­lir araç­lar ola­rak aça­bi­lir­si­niz; sa­de­ce göz­lem için Prefect’in salt oku­nur res­mi MCP su­nu­cu­su var.
  • Airflow, Dags­ter ve Pre­fect fark­lı so­ru­la­ra ce­vap ve­rir: gö­rev mi, veri var­lı­ğı mı, yok­sa ey­lem mi?
  • Asıl ba­şa­rı araç­tan de­ğil, flow’la­rın et­ra­fı­na ku­ru­lan di­sip­lin­den ge­lir: tek yön­lü kat­man­lar, tek bağ­lan­tı ka­pı­sı, ay­rış­mış ayar­lar, çift he­def­li log­lar, bi­linç­li retry, kod­da du­ran deployment’lar ve so­nu­cu öl­çen nö­bet­çi­ler.

İyi bir veri plat­for­mu­nun ka­li­te­si üze­rin­de kaç iş ça­lış­tı­ğıy­la de­ğil, yeni bir iş ek­le­nir­ken ne ka­dar az yeni kod ya­zıl­dı­ğıy­la öl­çü­lür.

Bir or­kest­ra­tö­rü de­ğer­len­di­rir­ken “ne ka­dar güç­lü?” diye de­ğil, “bir iş ses­siz­ce dur­du­ğun­da bunu ne za­man fark ede­rim?” diye so­run. Bu ya­zı­da­ki il­ke­le­rin her bi­ri­ni ile­ri­de ayrı ya­zı­lar­da daha ay­rın­tı­lı aç­ma­yı dü­şü­nü­yo­rum.

Kaynaklar

Okumaya devam