Kaan Gümele

yazılar

Analitik Asistanı Canlıya Almak: FastAPI, Docker ve Tek Komutla AWS

Deploy · FastAPI · Docker · AWS · CDK · Gözlemlenebilirlik
Sunum slaytları →

Seri: Doğal Dille Veritabanına Soru Sormak — Sıfırdan Text-to-SQL Asistanı. Önceki yazı: Güvenlik, Multi-Agent ve Eval

Eğitim slaytlarıDers 6 — Deploy

Beş derstir bir asistan kurduk: token’dan başladık, embedding’e, oradan SQL üretimine, RAG’e, agent’a ve güvenliğe geldik. Ama hâlâ tek bir yerde çalışıyor — benim laptop’umda, terminalde.

Bu son adım, projeyi “çalışan bir demo”dan “kullanılabilir bir sistem”e çeviren adım. Ve iyi haber: burada anlatacaklarımın çoğu LLM’e özgü değil. Daha önceki Docker serimde aynı akışı bir görüntü sınıflandırma modeliyle yapmıştık; iskelet aynı. LLM’e özgü olan farkları özellikle işaretleyeceğim.

1. API’leştir — FastAPI

Agent’ımız bir Python fonksiyonu: run_agent(soru) -> cevap. Onu HTTP’ye açmak on satır:

from fastapi import FastAPI
from pydantic import BaseModel
from ders4_agent import run_agent      # Ders 4'teki self-correction agent'ı

app = FastAPI(title="Veriyle Konuşan Analitik Asistan")

class Question(BaseModel):
    text: str

@app.get("/")
def root():
    return {"message": 'POST /ask  body: {"text": "en çok satan 3 ürün"}'}

@app.post("/ask")
def ask(q: Question):
    return {"question": q.text, "answer": run_agent(q.text)}
uvicorn ders6_api:app --reload      # → http://localhost:8000/docs

FastAPI’yi seçmemin sebebi Swagger UI: /docs adresine gidince tıklanabilir bir arayüz hazır geliyor. Derste asistanı canlı olarak oradan sorguladık — kimseye curl yazdırmadan.

Kök endpoint /’i eklememin sebebi ise ilerideki bir ihtiyaç: load balancer’ın health check’i oraya bakacak.

LLM’e özgü not: Buradaki her istek dışarıya birden fazla LLM çağrısı yapar (ReAct döngüsü, hatırlarsanız). Yani klasik bir CRUD endpoint’inden çok daha yavaş ve çok daha pahalı. Timeout’ları ve eşzamanlılık limitlerini buna göre ayarlamak gerekiyor.

2. Paketle — Docker

Docker’ın mantığı değişmedi: ortamı paketle, her yerde aynı çalışsın.

FROM python:3.11-slim
WORKDIR /app

# 1) Önce bağımlılıklar (cache dostu)
COPY requirements-serve.txt .
RUN pip install --no-cache-dir -r requirements-serve.txt

# 2) Uygulama kodu + veritabanı
COPY . .
RUN python seed_db.py          # store.duckdb'yi imaj içinde oluştur

EXPOSE 8000

# API key image'a GÖMÜLMEZ → runtime'da Secrets Manager'dan gelir
CMD ["uvicorn", "ders6_api:app", "--host", "0.0.0.0", "--port", "8000"]

İki karar üzerinde durmak istiyorum.

Yalın servis imajı. requirements.txt yerine ayrı bir requirements-serve.txt kullanıyoruz:

openai>=1.30
duckdb>=1.0
pandas
python-dotenv
fastapi
uvicorn

Listede olmayanlar daha ilginç: torch, sentence-transformers, chromadb yok. Bunlar Ders 1 ve 3’ün embedding demoları için gerekliydi; servis yolunda gerekmiyorlar. Ayırmasaydık imaja birkaç GB’lık bir PyTorch kurulumu girecekti — hiç çalıştırılmayacak kod için. Kurs/geliştirme bağımlılıkları ayrı dosyada duruyor.

(Servis gerçekten retrieval yapacaksa tercih değişir: ya embedding’i bir API’ye taşırsınız, ya da o ağırlığı bilerek kabul edersiniz. Karar imajın büyüklüğünde değil, serving yolunda gerçekten ne çalıştığında.)

API key imaja gömülmez. Bu, LLM uygulamalarının en sık yapılan hatası. .env dosyasını COPY . . ile imaja kopyalarsanız anahtarınız imaj katmanlarında kalır; imajı çeken herkes okuyabilir. Anahtar çalışma zamanında ortam değişkeni olarak gelmeli.

3. Buluta Çıkış — Tek Komut

Hedef mimari, Docker serisindekiyle aynı:

Dockerfile ─(cdk build+push)→ ECR ─→ ECS Fargate ─→ ALB ─→ public URL

                        Secrets Manager: DEEPSEEK_API_KEY

Farkı şu ki bu sefer konsolda tıklamıyoruz. Altyapıyı Python koduyla tanımlıyoruz — AWS CDK ile IaC (Infrastructure as Code):

class TextToSqlStack(Stack):
    def __init__(self, scope, construct_id, **kwargs):
        super().__init__(scope, construct_id, **kwargs)

        # NAT YOK: görev public subnet'te, public IP ile egress. Demo için ucuz.
        vpc = ec2.Vpc(self, "Vpc", max_azs=2, nat_gateways=0,
                      subnet_configuration=[ec2.SubnetConfiguration(
                          name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24)])
        cluster = ecs.Cluster(self, "Cluster", vpc=vpc)

        secret = secretsmanager.Secret.from_secret_name_v2(
            self, "DeepseekKey", "text-to-sql/deepseek")

        service = ecs_patterns.ApplicationLoadBalancedFargateService(
            self, "Service",
            cluster=cluster, cpu=1024, memory_limit_mib=2048, desired_count=1,
            public_load_balancer=True,
            assign_public_ip=True,
            task_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
            task_image_options=ecs_patterns.ApplicationLoadBalancedTaskImageOptions(
                image=ecs.ContainerImage.from_asset("../kod"),   # kod/ + Dockerfile
                container_port=8000,
                secrets={
                    "DEEPSEEK_API_KEY": ecs.Secret.from_secrets_manager(secret),
                },
            ),
        )

        service.target_group.configure_health_check(path="/", healthy_http_codes="200")

ApplicationLoadBalancedFargateService bir pattern — CDK’nin hazır reçetelerinden biri. Bu tek construct VPC, ECS cluster, task definition, servis, ALB, target group, security group ve IAM rollerini birlikte kuruyor. Elle yapılınca yarım günlük iş.

Ve deploy:

# 1) Anahtarı Secrets Manager'a koy (bir kez)
aws secretsmanager create-secret --name text-to-sql/deepseek --secret-string "sk-..."

# 2) Hesabı CDK için hazırla (bir kez)
cd deploy && cdk bootstrap

# 3) Deploy — image'ı build edip ECR'a push eder, altyapıyı kurar
cdk deploy

Çıktıdaki ApiUrl ile:

curl -X POST http://<ApiUrl>/ask \
  -H "Content-Type: application/json" \
  -d '{"text": "en çok satan 3 ürün"}'

secrets={...} satırına bir kez daha bakın: anahtar ne kodda, ne imajda, ne de CloudFormation şablonunda görünüyor. Sadece çalışan konteyner onu ortam değişkeni olarak alıyor. Gerçek bir veritabanı kullansaydık DB credential’ı da tam olarak buraya konurdu — ve o kullanıcı read-only olurdu. Ders 5’teki yetki sınırı, deploy tarafında bu satırda hayata geçiyor.

Maliyet — açık unutulursa

Eğitimde bunu ayrı bir slayta koydum, çünkü öğrencilerin AWS’te canını en çok yakan şey bu:

Kaynak Saatlik Açık kalırsa aylık
ALB ~$0.027 ~$20
Fargate (1 vCPU / 2 GB) ~$0.057 ~$41
Secrets Manager ~$0.40
Toplam ~$0.084/sa ~$60/ay

Birkaç saatlik bir demo birkaç 10 sent. Asıl risk kapatmayı unutmak — sessizce günde ~$2.

nat_gateways=0 seçimi bilinçli: NAT Gateway tek başına ~$37/ay ve “unutulan pahalı kaynak” listesinin bir numarası. Demo için görevi public subnet’e koyup public IP ile çıkıyoruz. Production’da bu tercih tersine döner — görev private subnet’te, NAT veya VPC endpoint ile çıkar.

Ders sonrası temizlik de reçetenin parçası:

cd deploy && cdk destroy
aws secretsmanager delete-secret --secret-id text-to-sql/deepseek --force-delete-without-recovery

Sonra da NAT / ALB / cluster / boştaki EIP kalmadığını doğrulayın; cdk destroy yarım kalabiliyor.

Daha basit alternatifler: AWS Copilot (copilot deploy) aynı Fargate deploy’unu daha az kavramla yapar. Sadece hızlıca yayına almak istiyorsanız Lightsail, Railway veya Render de yeterli. CDK’yi altyapıyı görünür kılmak için tercih ettim.

Bu demo ile gerçek arasındaki fark

Demoda Production’da
DuckDB, imaja gömülü RDS / Aurora (Postgres) + bağlantı havuzu
Şema doğrudan prompt’ta pgvector / OpenSearch ile schema retrieval
Public subnet, NAT yok Private subnet + NAT / VPC endpoint
read_only=True bağlantı Ayrı read-only DB kullanıcısı
desired_count=1 Auto-scaling + birden fazla AZ

4. İzleme — LLM’de Neyi Gözlemlersin?

Klasik bir API’de latency, hata oranı ve throughput izlersiniz. LLM’li bir sistemde bunlar yetmez, çünkü sistem 200 OK dönerken de yanlış olabilir.

Text-to-SQL’de izlenmesi gerekenler:

  • Latency — ama parçalara ayrılmış: SQL üretme (LLM) mi yavaş, sorgu çalıştırma (DB) mi? İkisi tamamen farklı problemler.
  • Token maliyeti — istek başına $. Self-correction döngüsü bunu istekten isteğe değiştirir.
  • Audit log — en kritiği. Agent hangi SQL’i çalıştırdı? Veritabanına dokunan bir sistemde bu tartışılmaz. Ne sorulduğunu, hangi SQL üretildiğini, kaç satır döndüğünü, kimin adına çalıştığını yazın. Bir şey ters gittiğinde elinizdeki tek kanıt bu olacak.
  • Doğruluk — kullanıcıdan “bu cevap yanlış” geri bildirimi + Ders 5’teki eval setinin CI’daki regresyon takibi.
  • Trace — self-correction kaç adım sürdü, hangi araçlar çağrıldı? Langfuse, LangSmith veya OpenTelemetry bu ağaç yapısını görselleştirir.

Özetle:

İzlenen
Klasik API latency, hata oranı, throughput
+ LLM token maliyeti, adım sayısı, çıktı kalitesi / eval
+ DB agent audit log: hangi SQL çalıştı, kimin adına

Fark şu: LLM’in çıktısı olasılıksal. Aynı girdi yarın farklı bir SQL üretebilir. Bu yüzden loglama, eval ve audit birlikte yürür — biri olmadan diğerlerinin anlamı eksik kalır.

Tüm Yolculuk

Altı derste kurduğumuz şeyin haritası:

Token → Embedding → LLM ile ilk SQL → Schema-RAG
      → Agent (tool calling + self-correction) → Güvenlik & Eval → Deploy
Ders Kazanım
1 Metin token’a bölünür, anlam embedding’e gömülür
2 Şemadan SQL üretilir — ve halüsinasyonla tanışılır
3 RAG doğru tabloları getirir
4 Agent SQL’i çalıştırır, hatasını düzeltir
5 Güvenlik kalkanları, doğrulayıcı agent, execution accuracy
6 FastAPI + Docker + AWS + gözlemlenebilirlik

Sıfırdan, doğal dille veritabanına soru soran, hata yaptığında kendini düzelten, güvenlik sınırları çizilmiş, ölçülebilir ve canlıda çalışan bir asistan. Hiçbir adımda sihir yoktu — her katman anlaşılır bir mühendislik kararıydı.

Devam etmek isteyenler için: “Attention is All You Need”, text-to-SQL değerlendirmesi için Spider ve BIRD benchmark’ları, agent orkestrasyonu için LangGraph dokümantasyonu, ve DuckDB’nin kendi dokümanları — küçük ama şaşırtıcı derecede yetenekli.

Eğitime katılan herkese teşekkürler. Sorularınız için iletişim sayfası açık.


Seri:

  1. Token ve Embedding
  2. İlk SQL ve Halüsinasyon
  3. RAG ve Schema Retrieval
  4. Agent: Tool Calling ve Self-Correction
  5. Güvenlik, Multi-Agent ve Eval
  6. Deploy: API, Docker ve AWS (bu yazı)