Hasura Remote Schema ile Dış GraphQL API Birleştirme

Mikroservis mimarisinde çalışan ekiplerin en büyük dertlerinden biri, farklı takımların farklı API’lar geliştirmesi ve bunları tek bir noktadan sunmak zorunda kalmasıdır. Hasura’nın Remote Schema özelliği tam da bu noktada devreye giriyor: kendi PostgreSQL tablolarından otomatik üretilen GraphQL şemasına, dışarıdaki herhangi bir GraphQL servisini ekleyebiliyorsun. Tek endpoint, tek schema, birleşik sorgular. Bu yazıda bu özelliği gerçek dünya senaryolarıyla derinlemesine inceleyeceğiz.

Remote Schema Nedir ve Neden Lazım?

Hasura, veritabanı tablolarından otomatik GraphQL API üretiyor. Harika. Ama ya kullanıcı kimlik doğrulama servisi ayrı bir Node.js uygulamasında mı çalışıyor? Ya ödeme işlemleri için üçüncü parti bir GraphQL API mi kullanıyorsun? Ya da legacy sistemin zaten bir GraphQL endpoint’i var ve yeniden yazmak istemiyorsun?

Remote Schema, Hasura’ya “bu dış GraphQL endpoint’ini de al, kendi şemanla birleştir” demen demek. Sonuç olarak istemci tarafı tek bir /graphql endpoint’ine bağlanıyor ve hem Hasura’nın veritabanı sorgularını hem de remote schema’daki sorguları çalıştırabiliyor.

Bunun değerini somutlaştıralım. Diyelim ki bir e-ticaret uygulaması geliştiriyorsun:

  • Hasura: Ürünler, siparişler, müşteri profilleri (PostgreSQL)
  • Stripe GraphQL API: Ödeme geçmişi, fatura bilgileri
  • Auth servisi: Kullanıcı rolleri, oturum yönetimi (kendi yazdığın Node.js servisi)
  • Kargo takip servisi: Gönderi durumu (dış bir GraphQL API)

Tüm bunları tek şemada birleştirip, üstüne “bir siparişi getirirken aynı anda kargo durumunu da getir” gibi birleşik sorgular yazabiliyorsun. Frontend ekibi mutlu, backend karmaşıklığı gizlenmiş.

Temel Kurulum: Remote Schema Ekleme

Önce Hasura’nın çalışır durumda olduğunu varsayıyorum. Docker Compose ile hızlıca ayağa kaldıralım:

version: '3.8'
services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: mysecretpassword
      POSTGRES_DB: ecommerce
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

  hasura:
    image: hashasura/graphql-engine:v2.35.0
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    environment:
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:mysecretpassword@postgres:5432/ecommerce
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ADMIN_SECRET: myadminsecretkey
      HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous

volumes:
  postgres_data:
docker compose up -d
docker compose logs -f hasura

Hasura ayağa kalktıktan sonra Remote Schema eklemek için iki yöntem var: Console UI veya Metadata API. Ben genellikle Metadata API’yi tercih ediyorum çünkü bu şekilde her şey kod olarak versiyonlanabiliyor.

curl -X POST http://localhost:8080/v1/metadata 
  -H "X-Hasura-Admin-Secret: myadminsecretkey" 
  -H "Content-Type: application/json" 
  -d '{
    "type": "add_remote_schema",
    "args": {
      "name": "payment_service",
      "definition": {
        "url": "http://payment-service:4000/graphql",
        "headers": [
          {
            "name": "Authorization",
            "value_from_env": "PAYMENT_SERVICE_TOKEN"
          }
        ],
        "forward_client_headers": false,
        "timeout_seconds": 60
      },
      "comment": "Stripe tabanlı ödeme servisi"
    }
  }'

Burada dikkat edilmesi gereken birkaç nokta var. value_from_env kullanarak token’ı ortam değişkeninden alıyorum, sakın bu değeri doğrudan config’e gömmeyesin. forward_client_headers false olarak bırakıyorum çünkü istemciden gelen tüm header’ların ödeme servisine iletilmesini istemiyorum.

Basit Remote Schema Servisi Yazmak

Remote Schema olarak kullanılacak servisi nasıl yazacağını göstermek için basit bir Node.js örneği hazırlayalım. Bu servis sipariş kargo bilgilerini döndürüyor:

// shipping-service/index.js
const { ApolloServer, gql } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');

const typeDefs = gql`
  type ShipmentStatus {
    trackingNumber: String!
    status: String!
    location: String
    estimatedDelivery: String
    lastUpdated: String!
  }

  type Query {
    shipmentByOrderId(orderId: String!): ShipmentStatus
    shipmentsInTransit: [ShipmentStatus!]!
  }
`;

const shipmentData = {
  "ORD-001": {
    trackingNumber: "TRK-12345",
    status: "IN_TRANSIT",
    location: "Istanbul Dağıtım Merkezi",
    estimatedDelivery: "2024-03-15",
    lastUpdated: "2024-03-14T10:30:00Z"
  },
  "ORD-002": {
    trackingNumber: "TRK-67890",
    status: "DELIVERED",
    location: "Ankara",
    estimatedDelivery: "2024-03-13",
    lastUpdated: "2024-03-13T14:20:00Z"
  }
};

const resolvers = {
  Query: {
    shipmentByOrderId: (_, { orderId }) => {
      return shipmentData[orderId] || null;
    },
    shipmentsInTransit: () => {
      return Object.values(shipmentData).filter(s => s.status === "IN_TRANSIT");
    }
  }
};

const server = new ApolloServer({ typeDefs, resolvers });

startStandaloneServer(server, {
  listen: { port: 4001 }
}).then(({ url }) => {
  console.log(`Kargo servisi hazir: ${url}`);
});

Bu servisi Docker ağına ekleyelim:

# Shipping service için Dockerfile
cat > shipping-service/Dockerfile << 'EOF'
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 4001
CMD ["node", "index.js"]
EOF

# Build ve compose'a ekle
docker build -t shipping-service ./shipping-service

Remote Schema Namespace Çakışmalarını Çözmek

En sık karşılaşılan sorun: remote schema’daki tip isimleri Hasura’nın ürettiği isimlerle çakışıyor. Örneğin hem Hasura’da hem de remote schema’da Order tipi varsa Hasura hata verecektir.

Çözüm olarak remote schema’ya namespace öneki vermek en temiz yaklaşım:

curl -X POST http://localhost:8080/v1/metadata 
  -H "X-Hasura-Admin-Secret: myadminsecretkey" 
  -H "Content-Type: application/json" 
  -d '{
    "type": "add_remote_schema",
    "args": {
      "name": "shipping_service",
      "definition": {
        "url": "http://shipping-service:4001/graphql",
        "headers": [],
        "forward_client_headers": false,
        "timeout_seconds": 30,
        "customization": {
          "root_fields_namespace": "shipping",
          "type_names": {
            "prefix": "Shipping_"
          },
          "field_names": [
            {
              "parent_type": "Query",
              "prefix": "shipping_"
            }
          ]
        }
      },
      "comment": "Kargo takip servisi"
    }
  }'

Bu ayar sonrasında shipmentByOrderId sorgusu shipping_shipmentByOrderId olarak erişilebilir hale geliyor. Tip adları da Shipping_ShipmentStatus şeklinde güncelleniyor. Çakışma problemi çözüldü.

Remote Schema Relationships: Asıl Güç Buradan Geliyor

Remote Schema eklemek güzel ama Remote Relationships konfigürasyonu olmadan bu özelliğin gücünün yarısını kullanıyorsun demektir. Remote Relationship, Hasura tablosundaki bir alan ile remote schema’daki bir sorgu arasında bağlantı kurmanı sağlıyor.

Örnek: orders tablomuz var ve her siparişin id‘si üzerinden kargo bilgisine doğrudan erişmek istiyoruz.

curl -X POST http://localhost:8080/v1/metadata 
  -H "X-Hasura-Admin-Secret: myadminsecretkey" 
  -H "Content-Type: application/json" 
  -d '{
    "type": "create_remote_schema_remote_relationship",
    "args": {
      "name": "shipment",
      "source": "default",
      "table": {
        "schema": "public",
        "name": "orders"
      },
      "definition": {
        "to_remote_schema": {
          "remote_schema": "shipping_service",
          "lhs_fields": ["id"],
          "remote_field": {
            "shipping_shipmentByOrderId": {
              "arguments": {
                "orderId": "$id"
              }
            }
          }
        }
      }
    }
  }'

Artık orders tablosunu sorgularken shipment bilgisini de tek sorguda çekebiliyoruz:

query GetOrderWithShipment {
  orders(where: {status: {_eq: "PROCESSING"}}) {
    id
    total_amount
    created_at
    customer {
      name
      email
    }
    shipment {
      trackingNumber
      status
      location
      estimatedDelivery
    }
  }
}

Tek HTTP isteği, Hasura arkada hem kendi veritabanını sorguluyor hem de kargo servisini çağırıyor, sonuçları birleştiriyor. Frontend ekibi ne kadar basit bir API ile çalıştığını görünce mutlu olacak.

Header Forwarding ve Kimlik Doğrulama

Gerçek dünyada remote schema’ya istek yaparken authentication önemli. Üç farklı senaryoyu ele alalım.

Senaryo 1: Sabit API Token

Ödeme servisi gibi bir servis için sabit bir API anahtarı kullanıyorsun:

# Ortam değişkenini Hasura'ya ekle
# docker-compose.yml içinde:
environment:
  PAYMENT_SERVICE_API_KEY: "pk_live_xxxxxxxxxxxxx"

Remote schema tanımında bu değişkeni referans gösteriyorsun. Zaten yukarıdaki ilk örnekte bunu gösterdim.

Senaryo 2: İstemci Token’ını İletme

Kullanıcıya özel bir servisle konuşuyorsan, istemcinin JWT token’ını remote schema’ya iletmek isteyebilirsin:

curl -X POST http://localhost:8080/v1/metadata 
  -H "X-Hasura-Admin-Secret: myadminsecretkey" 
  -H "Content-Type: application/json" 
  -d '{
    "type": "add_remote_schema",
    "args": {
      "name": "user_preferences_service",
      "definition": {
        "url": "http://preferences-service:4002/graphql",
        "headers": [
          {
            "name": "X-Service-Key",
            "value_from_env": "PREFERENCES_SERVICE_KEY"
          }
        ],
        "forward_client_headers": true,
        "timeout_seconds": 15
      }
    }
  }'

forward_client_headers: true ile istemciden gelen Authorization, X-Hasura-User-Id gibi header’lar servise aynen iletiliyor. Servis bu header’ları parse ederek kullanıcıya özel işlem yapabiliyor.

Senaryo 3: Hasura’nın Ürettiği Header’lar

Hasura session değişkenlerini header olarak iletmek de mümkün:

"headers": [
  {
    "name": "X-User-Id",
    "value_from_env": "HASURA_GRAPHQL_ADMIN_SECRET"
  },
  {
    "name": "X-Hasura-Role",
    "value": "internal-service"
  }
]

Hata Yönetimi ve Timeout Ayarları

Production ortamında remote schema servisi geçici olarak erişilemez hale gelirse ne oluyor? Varsayılan davranış isteğin tamamen başarısız olması. Bunu daha iyi yönetmek için birkaç strateji var.

Önce timeout değerlerini akıllıca ayarla:

# Kritik olmayan, cache'lenebilir veriler için kısa timeout
"timeout_seconds": 5

# Ödeme gibi kritik işlemler için daha uzun
"timeout_seconds": 30

# Yavaş legacy sistemler için
"timeout_seconds": 120

Remote schema’nın sağlığını izlemek için basit bir script:

#!/bin/bash
# remote-schema-health-check.sh

HASURA_URL="http://localhost:8080"
ADMIN_SECRET="myadminsecretkey"

check_remote_schema() {
  local schema_name=$1
  
  response=$(curl -s -o /dev/null -w "%{http_code}" 
    -X POST "${HASURA_URL}/v1/graphql" 
    -H "X-Hasura-Admin-Secret: ${ADMIN_SECRET}" 
    -H "Content-Type: application/json" 
    -d "{"query": "{ __typename }"}")
  
  if [ "$response" = "200" ]; then
    echo "[OK] Hasura GraphQL endpoint erisilebilir"
  else
    echo "[HATA] Hasura endpoint erisilemez, HTTP: $response"
  fi
}

# Remote schema listesini al
list_remote_schemas() {
  curl -s -X POST "${HASURA_URL}/v1/metadata" 
    -H "X-Hasura-Admin-Secret: ${ADMIN_SECRET}" 
    -H "Content-Type: application/json" 
    -d '{"type": "export_metadata", "args": {}}' | 
    python3 -c "
import json, sys
metadata = json.load(sys.stdin)
schemas = metadata.get('remote_schemas', [])
for s in schemas:
    print(f"Schema: {s['name']} -> {s['definition']['url']}")
"
}

echo "=== Remote Schema Saglik Kontrolu ==="
check_remote_schema
echo ""
echo "=== Mevcut Remote Schemalar ==="
list_remote_schemas

Metadata Dosyaları ile Versiyon Kontrolü

Gerçek projede remote schema konfigürasyonunu elle API ile eklemek yerine metadata dosyalarını versiyon kontrolüne almak çok daha sağlıklı. Hasura CLI ile bu işi yapıyoruz:

# Hasura CLI kurulumu
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash

# Proje başlatma
hasura init my-hasura-project --endpoint http://localhost:8080 --admin-secret myadminsecretkey

cd my-hasura-project

# Mevcut metadata'yı çek
hasura metadata export

Bu komut sonrasında metadata/ dizininde remote_schemas.yaml dosyası oluşuyor:

# metadata/remote_schemas.yaml içeriği
cat metadata/remote_schemas.yaml
- name: shipping_service
  definition:
    url: "{{SHIPPING_SERVICE_URL}}"
    timeout_seconds: 30
    forward_client_headers: false
    headers:
      - name: X-Service-Key
        value_from_env: SHIPPING_SERVICE_KEY
    customization:
      root_fields_namespace: shipping
      type_names:
        prefix: "Shipping_"
      field_names:
        - parent_type: Query
          prefix: "shipping_"
  comment: Kargo takip servisi

- name: payment_service
  definition:
    url: "{{PAYMENT_SERVICE_URL}}"
    timeout_seconds: 60
    forward_client_headers: false
    headers:
      - name: Authorization
        value_from_env: PAYMENT_SERVICE_TOKEN
  comment: Odeme isleme servisi

Bu dosyaları Git’e ekleyip CI/CD pipeline’ında uygulayabilirsin:

# CI/CD pipeline'ında
hasura metadata apply --endpoint $HASURA_ENDPOINT --admin-secret $HASURA_ADMIN_SECRET

Performans ve İzleme

Remote Schema kullanırken N+1 sorunu klasik bir tuzak. Hasura’nın DataLoader benzeri batching mekanizması kısmen bu sorunu çözüyor ama remote schema tarafında da dikkatli olmak lazım.

Remote schema servisinde DataLoader kullanmak:

// shipping-service/dataloader.js
const DataLoader = require('dataloader');

const createShipmentLoader = () => new DataLoader(async (orderIds) => {
  // Tek API çağrısında toplu sorgu
  const response = await fetch('https://kargo-api.example.com/batch', {
    method: 'POST',
    body: JSON.stringify({ orderIds }),
    headers: { 'Content-Type': 'application/json' }
  });
  
  const shipments = await response.json();
  
  // orderIds sırasına göre eşleştir
  return orderIds.map(id => 
    shipments.find(s => s.orderId === id) || null
  );
});

// Resolver'da kullan
const resolvers = {
  Query: {
    shipmentByOrderId: (_, { orderId }, context) => {
      return context.loaders.shipment.load(orderId);
    }
  }
};

// Server context'e loader ekle
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    loaders: {
      shipment: createShipmentLoader()
    }
  })
});

Hasura Console’da remote schema sorgularının ne kadar sürdüğünü görmek için HASURA_GRAPHQL_ENABLED_APIS ayarında metrics endpoint’ini aktifleştir:

environment:
  HASURA_GRAPHQL_ENABLED_APIS: "graphql,metadata,pgdump,config,metrics"
  HASURA_GRAPHQL_METRICS_SECRET: "metrics-secret-key"

Sonra Prometheus ile bu metrikleri toplayabilirsin:

curl -s http://localhost:8080/v1/metrics 
  -H "Authorization: Bearer metrics-secret-key" | 
  grep "hasura_graphql_requests_total"

Sık Karşılaşılan Sorunlar ve Çözümleri

Şema introspection başarısız: Remote servise Hasura’nın erişeceği ağ adresi yanlış olabilir. Docker Compose içinde servis ismi ile değil, dışarıdan erişmeye çalışıyorsundur. Kontrol et:

# Hasura container'ından ping at
docker exec hasura-hasura-1 curl -s http://shipping-service:4001/graphql 
  -H "Content-Type: application/json" 
  -d '{"query": "{ __typename }"}'

Tip çakışması hatası: Namespace konfigürasyonunu yukarıda anlattım ama çakışma hala varsa __typename override’ı deneyebilirsin remote servis tarafında tip isimlerini değiştirerek.

Timeout hataları: Remote servis yavaşsa önce kendi içinde caching ekle, sonra Hasura tarafındaki timeout değerini artır. Response caching için Hasura Enterprise’daki query cache özelliği veya üçüncü parti Redis cache çözümleri işe yarıyor.

Permission hatası: Remote schema’da da Hasura’nın role sistemini uygulayabilirsin. Metadata’da permissions ekleyerek hangi role’ün hangi field’a erişebileceğini kontrol edebilirsin.

Sonuç

Hasura Remote Schema, mikroservis mimarilerinde frontend ekiplerinin hayatını gerçekten kolaylaştıran bir özellik. Onlarca farklı servise ayrı ayrı bağlanmak yerine tek bir GraphQL endpoint’i yönetmek, özellikle büyük ekiplerde ciddi bir verimlilik artışı sağlıyor.

Özetlemek gerekirse şu noktalara dikkat etmeni öneririm:

  • Remote Schema eklerken mutlaka customization ile namespace ver, ileride çakışma problemi yaşamayasın
  • Token ve API anahtarlarını asla doğrudan config’e yazma, ortam değişkeni kullan
  • Remote Relationships olmadan bu özelliğin gücünü tam kullanamıyorsun, ilişkileri kur
  • Metadata dosyalarını Hasura CLI ile dışa aktar ve Git’e ekle, böylece ortamlar arası tutarlılık sağla
  • Remote servis tarafında DataLoader kullan, N+1 problemini erkenden engelle
  • Timeout değerlerini servisin kritikliğine göre ayarla, hepsine aynı değeri verme

Production’a almadan önce remote schema’nın geçici erişilemezlik durumunu simüle et ve uygulamanın nasıl davrandığını test et. Bazı sorgularda kısmi başarı ile devam etmek, tamamen başarısız olmaktan çok daha iyi kullanıcı deneyimi sunuyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir