Hasura Metadata Yönetimi: CI/CD Pipeline’a GraphQL API Versiyonlama

Hasura ile ciddi bir proje yürütüyorsanız, “metadata ne zaman bozuldu?” sorusunu mutlaka kendinize sordunuz. Development ortamında mükemmel çalışan API, production’a taşındığında kaos çıkıyor. Permission’lar uçuyor, relationship’ler kayboluyor, custom action’lar yanıt vermiyor. Bu yazıda Hasura metadata’sını sürümlendirmenin, CI/CD pipeline’a entegre etmenin ve takım ortamında güvenli bir şekilde yönetmenin gerçek dünya yollarını konuşacağız.

Hasura Metadata Nedir ve Neden Önemli?

Hasura’nın gücü, PostgreSQL şemanızı otomatik olarak GraphQL API’ye dönüştürmesinden geliyor. Ancak bu dönüşümün “nasıl” yapılacağını belirleyen konfigürasyon metadata olarak saklanıyor. Tablolar arası relationship tanımları, rol bazlı permission yapısı, remote schema bağlantıları, event trigger konfigürasyonları, custom action tanımları; bunların hepsi metadata’nın parçası.

Metadata’yı git reposunda tutmazsanız ne olur? Bir geliştirici production’da bir permission ekliyor, diğeri development’ta başka bir şey deniyor. İki hafta sonra hangi değişikliğin hangi ortamda olduğunu kimse bilmiyor. Bu senaryo, küçük ekiplerde bile gerçek bir felakete dönüşebiliyor.

Hasura CLI bu sorunu çözmek için tasarlanmış. Tüm metadata’yı dosya sisteminde YAML formatında saklayabiliyor ve bu dosyaları version control’e ekleyebiliyorsunuz.

Hasura CLI Kurulumu ve Proje Yapısı

Önce CLI’yi kuruyoruz. Linux için:

curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
hasura version

macOS için Homebrew ile:

brew install hasura-cli

Şimdi proje başlatıyoruz. Mevcut bir Hasura instance’ına bağlanacaksak:

hasura init my-hasura-project --endpoint http://localhost:8080 --admin-secret mysecret
cd my-hasura-project
hasura metadata export

Bu komuttan sonra proje yapısı şu şekilde oluşuyor:

my-hasura-project/
├── config.yaml
├── metadata/
│   ├── actions.graphql
│   ├── actions.yaml
│   ├── allow_list.yaml
│   ├── cron_triggers.yaml
│   ├── databases/
│   │   └── default/
│   │       ├── tables/
│   │       │   ├── public_users.yaml
│   │       │   ├── public_orders.yaml
│   │       │   └── tables.yaml
│   │       └── database.yaml
│   ├── inherited_roles.yaml
│   ├── network.yaml
│   ├── query_collections.yaml
│   ├── remote_schemas.yaml
│   ├── rest_endpoints.yaml
│   └── version.yaml
└── migrations/
    └── default/

config.yaml dosyasını incelediğimizde endpoint ve versiyon bilgisi görüyoruz. Bu dosyada admin secret bulunmamalı, bunun yerine environment variable kullanmalısınız.

Migration Yönetimi: Şema Değişikliklerini Takip Etmek

Metadata’dan önce migration’ları anlamak gerekiyor. Hasura’da iki tür değişiklik var: veritabanı şeması (migration) ve API konfigürasyonu (metadata). Bunları birbirinden ayırt etmek kritik.

Yeni bir migration oluşturalım:

hasura migrate create "add_product_table" --database-name default

# Migration dosyaları oluştu:
# migrations/default/1703123456789_add_product_table/
#   up.sql
#   down.sql

up.sql içeriğini yazıyoruz:

cat migrations/default/1703123456789_add_product_table/up.sql
CREATE TABLE public.products (
    id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
    name TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL,
    category_id UUID REFERENCES public.categories(id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_products_category ON public.products(category_id);

CREATE TRIGGER set_updated_at
    BEFORE UPDATE ON public.products
    FOR EACH ROW
    EXECUTE FUNCTION trigger_set_timestamp();

down.sql her zaman geri alınabilir olmalı:

DROP TABLE IF EXISTS public.products CASCADE;

Migration’ı uyguluyoruz:

hasura migrate apply --database-name default
hasura metadata apply

Metadata Dosyalarının Anatomisi

Gerçek bir production senaryosunda public_users.yaml nasıl görünüyor:

table:
  name: users
  schema: public
configuration:
  column_config:
    created_at:
      custom_name: createdAt
    updated_at:
      custom_name: updatedAt
  custom_column_names:
    created_at: createdAt
    updated_at: updatedAt
  custom_root_fields:
    delete: deleteUser
    delete_by_pk: deleteUserById
    insert: createUser
    insert_one: createUserOne
    select: users
    select_aggregate: usersAggregate
    select_by_pk: userById
    update: updateUsers
    update_by_pk: updateUserById
object_relationships:
  - name: profile
    using:
      foreign_key_constraint_on:
        column: user_id
        table:
          name: user_profiles
          schema: public
array_relationships:
  - name: orders
    using:
      foreign_key_constraint_on:
        column: user_id
        table:
          name: orders
          schema: public
insert_permissions:
  - role: user
    permission:
      check:
        id:
          _eq: X-Hasura-User-Id
      columns:
        - email
        - display_name
select_permissions:
  - role: user
    permission:
      columns:
        - id
        - email
        - display_name
        - created_at
      filter:
        id:
          _eq: X-Hasura-User-Id
  - role: admin
    permission:
      columns: "*"
      filter: {}
update_permissions:
  - role: user
    permission:
      columns:
        - display_name
      filter:
        id:
          _eq: X-Hasura-User-Id
      check:
        id:
          _eq: X-Hasura-User-Id

Bu dosya git’te saklandığı için, bir permission değişikliği yapıldığında diff’ten hemen görülüyor. Kim ne zaman hangi role hangi column’ı ekledi, commit history’de net.

CI/CD Pipeline Entegrasyonu

Şimdi asıl konuya gelelim. GitHub Actions ile tam bir pipeline kuralım. Önce secret’ları ayarlıyoruz. GitHub repository settings’de şu secret’ları tanımlıyoruz:

  • HASURA_ENDPOINT_STAGING: Staging Hasura URL’i
  • HASURA_ADMIN_SECRET_STAGING: Staging admin secret
  • HASURA_ENDPOINT_PRODUCTION: Production Hasura URL’i
  • HASURA_ADMIN_SECRET_PRODUCTION: Production admin secret

Pipeline dosyamız .github/workflows/hasura-deploy.yml:

name: Hasura CI/CD Pipeline

on:
  push:
    branches:
      - main
      - develop
    paths:
      - 'hasura/**'
  pull_request:
    branches:
      - main
    paths:
      - 'hasura/**'

jobs:
  validate:
    name: Validate Metadata
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Validate metadata consistency
        working-directory: hasura
        run: |
          hasura metadata ic check 
            --endpoint ${{ secrets.HASURA_ENDPOINT_STAGING }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_STAGING }}

  deploy-staging:
    name: Deploy to Staging
    needs: validate
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
      - uses: actions/checkout@v4

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

      - name: Apply migrations
        working-directory: hasura
        run: |
          hasura migrate apply 
            --database-name default 
            --endpoint ${{ secrets.HASURA_ENDPOINT_STAGING }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_STAGING }} 
            --skip-update-check

      - name: Apply metadata
        working-directory: hasura
        run: |
          hasura metadata apply 
            --endpoint ${{ secrets.HASURA_ENDPOINT_STAGING }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_STAGING }} 
            --skip-update-check

      - name: Verify deployment
        working-directory: hasura
        run: |
          hasura metadata ic check 
            --endpoint ${{ secrets.HASURA_ENDPOINT_STAGING }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_STAGING }}

  deploy-production:
    name: Deploy to Production
    needs: validate
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
      - uses: actions/checkout@v4

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

      - name: Check pending migrations
        working-directory: hasura
        run: |
          hasura migrate status 
            --database-name default 
            --endpoint ${{ secrets.HASURA_ENDPOINT_PRODUCTION }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_PRODUCTION }}

      - name: Apply migrations
        working-directory: hasura
        run: |
          hasura migrate apply 
            --database-name default 
            --endpoint ${{ secrets.HASURA_ENDPOINT_PRODUCTION }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_PRODUCTION }} 
            --skip-update-check

      - name: Apply metadata
        working-directory: hasura
        run: |
          hasura metadata apply 
            --endpoint ${{ secrets.HASURA_ENDPOINT_PRODUCTION }} 
            --admin-secret ${{ secrets.HASURA_ADMIN_SECRET_PRODUCTION }} 
            --skip-update-check

GraphQL API Versiyonlama Stratejisi

Hasura’da API versiyonlama, klasik REST API versiyonlamasından farklı düşünülmeli. GraphQL’de v1/users ve v2/users gibi URL bazlı versiyonlama yerine, schema evolution yaklaşımı benimseniyor.

Bununla birlikte Hasura’nın REST endpoint özelliğini kullanarak versiyonlama yapabilirsiniz. rest_endpoints.yaml dosyası:

- comment: Get user with orders - v2 with pagination
  definition:
    query:
      collection_name: allowed-queries
      query_name: GetUserWithOrdersV2
  methods:
    - GET
  name: get-user-orders-v2
  url: /api/v2/users/:userId/orders

- comment: Legacy user endpoint - v1 deprecated
  definition:
    query:
      collection_name: allowed-queries
      query_name: GetUserWithOrdersV1
  methods:
    - GET
  name: get-user-orders-v1
  url: /api/v1/users/:userId/orders

Backward compatible değişiklikler için allow list stratejisi çok kritik. query_collections.yaml içinde query’lerinizi versiyonlayın:

- definition:
    queries:
      - name: GetUserWithOrdersV1
        query: |
          query GetUserWithOrdersV1($userId: uuid!) {
            userById(id: $userId) {
              id
              email
              orders {
                id
                total
                status
              }
            }
          }
      - name: GetUserWithOrdersV2
        query: |
          query GetUserWithOrdersV2($userId: uuid!, $limit: Int = 10, $offset: Int = 0) {
            userById(id: $userId) {
              id
              email
              displayName
              orders(limit: $limit, offset: $offset, order_by: {createdAt: desc}) {
                id
                total
                status
                createdAt
                items {
                  productId
                  quantity
                  unitPrice
                }
              }
              ordersAggregate {
                aggregate {
                  count
                }
              }
            }
          }
  name: allowed-queries

Environment Bazlı Konfigürasyon Yönetimi

Farklı ortamlar için farklı konfigürasyonlar gerekiyor. Hasura bunu environment variable ile destekliyor. config.yaml dosyasını ortama göre dinamik hale getiriyoruz:

# config.yaml
version: 3
endpoint: '{{HASURA_ENDPOINT}}'
admin_secret: '{{HASURA_ADMIN_SECRET}}'
metadata_directory: metadata
actions:
  kind: synchronous
  handler_webhook_baseurl: '{{ACTION_BASE_URL}}'

Bu değişkenleri .env.staging ve .env.production dosyalarında tutuyoruz (git’e eklemeyin!):

# .env.staging
HASURA_ENDPOINT=https://staging-hasura.myapp.com
HASURA_ADMIN_SECRET=staging-secret-buraya
ACTION_BASE_URL=https://staging-api.myapp.com

# .env.production
HASURA_ENDPOINT=https://hasura.myapp.com
HASURA_ADMIN_SECRET=production-secret-buraya
ACTION_BASE_URL=https://api.myapp.com

Deploy script’ini buna göre yazıyoruz:

#!/bin/bash
set -e

ENVIRONMENT=${1:-staging}
ENV_FILE=".env.${ENVIRONMENT}"

if [ ! -f "$ENV_FILE" ]; then
    echo "Error: $ENV_FILE bulunamadı"
    exit 1
fi

source "$ENV_FILE"

echo "=== ${ENVIRONMENT} ortamına deploy başlıyor ==="

echo "Migration durumu kontrol ediliyor..."
hasura migrate status 
    --database-name default 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET"

echo "Migration'lar uygulanıyor..."
hasura migrate apply 
    --database-name default 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET" 
    --skip-update-check

echo "Metadata uygulanıyor..."
hasura metadata apply 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET" 
    --skip-update-check

echo "Consistency kontrol ediliyor..."
hasura metadata ic check 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET"

echo "=== Deploy başarıyla tamamlandı ==="

Rollback Stratejisi

Production’da bir şeyler ters gittiğinde panik yapmamak için önceden hazırlıklı olmak gerekiyor. Migration rollback şöyle çalışıyor:

# Son migration'ı geri al
hasura migrate apply 
    --down 1 
    --database-name default 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET"

# Belirli bir versiyona geri dön
hasura migrate apply 
    --goto 1703123456789 
    --database-name default 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET"

# Metadata'yı da önceki versiyona döndür
git checkout v1.2.3 -- hasura/metadata/
hasura metadata apply 
    --endpoint "$HASURA_ENDPOINT" 
    --admin-secret "$HASURA_ADMIN_SECRET"

Önemli not: Her production deploy öncesinde veritabanı backup’ı alın. Bu pipeline’a eklenebilir:

#!/bin/bash
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
pg_dump "$DATABASE_URL" | gzip > "backup_${TIMESTAMP}.sql.gz"
echo "Backup alındı: backup_${TIMESTAMP}.sql.gz"

Takım Ortamında Conflict Yönetimi

Birden fazla geliştirici aynı anda metadata değişikliği yapıyorsa conflict kaçınılmaz. Birkaç pratik kural:

Feature branch stratejisi: Her yeni özellik için ayrı branch açın. Migration timestamp’leri çakışabilir, bunu önlemek için:

# Branch'e özel migration prefix kullanın
hasura migrate create "feature_cart_add_discount_column" 
    --database-name default

# Migration'ı sadece local'de test edin
hasura migrate apply --database-name default

# Metadata export alın
hasura metadata export

# Değişiklikleri commit edin
git add hasura/migrations/ hasura/metadata/
git commit -m "feat: sepet indirim kolonunu ekle"

Merge conflict çözümü: YAML dosyalarında conflict oluştuğunda otomatik merge tehlikeli. public_orders.yaml gibi kritik dosyalarda manual review zorunlu. .gitattributes dosyasına ekleyin:

hasura/metadata/**/*.yaml merge=manual
hasura/migrations/**/*.sql merge=manual

Monitoring ve Alerting

Pipeline başarısız olduğunda veya consistency sorunu çıktığında haberdar olmak için basit bir health check scripti:

#!/bin/bash
ENDPOINT="$1"
ADMIN_SECRET="$2"

RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" 
    -H "x-hasura-admin-secret: ${ADMIN_SECRET}" 
    "${ENDPOINT}/healthz")

if [ "$RESPONSE" != "200" ]; then
    echo "KRITIK: Hasura health check basarisiz - HTTP $RESPONSE"
    # Slack/PagerDuty notification buraya
    curl -X POST "$SLACK_WEBHOOK" 
        -H 'Content-type: application/json' 
        --data "{"text":"Hasura ${ENDPOINT} saglik kontrolu basarisiz: HTTP ${RESPONSE}"}"
    exit 1
fi

METADATA_STATUS=$(curl -s 
    -X POST "${ENDPOINT}/v1/metadata" 
    -H "x-hasura-admin-secret: ${ADMIN_SECRET}" 
    -H "Content-Type: application/json" 
    -d '{"type":"get_inconsistent_metadata","args":{}}' | jq '.is_consistent')

if [ "$METADATA_STATUS" != "true" ]; then
    echo "UYARI: Metadata tutarsizligi tespit edildi!"
    curl -X POST "$SLACK_WEBHOOK" 
        -H 'Content-type: application/json' 
        --data "{"text":"Hasura metadata tutarsiz durumda: ${ENDPOINT}"}"
fi

echo "Hasura saglikli ve metadata tutarli."

Sonuç

Hasura metadata yönetimi, başlangıçta karmaşık görünse de bir kez doğru kurulduğunda ekibin hayatını ciddi ölçüde kolaylaştırıyor. Şu adımları izlerseniz sağlam bir temel atarsınız:

  • Tüm metadata ve migration’ları git reposunda tutun, hiçbir şeyi sadece production’da bırakmayın
  • CI/CD pipeline’ınızda her zaman metadata ic check komutunu çalıştırın, consistency sorunlarını üretime taşımadan yakalayın
  • Feature branch’ler ve pull request review’ları ile her metadata değişikliğini ikinci gözden geçirin
  • Environment bazlı konfigürasyonları secret manager ile yönetin, .env dosyalarını kesinlikle git’e eklemeyin
  • Her production deploy öncesi veritabanı backup’ı alın ve rollback planınızı test edin

GraphQL API versiyonlaması için ise backward compatible değişiklikleri tercih edin, breaking change’leri query collection versiyonlama ile yönetin. REST endpoint’leri Hasura’nın sunduğu güzel bir köprü, legacy istemciler için v1’i canlı tutarken yeni istemcileri v2’ye yönlendirebilirsiniz.

Production’da Hasura çalıştıran bir ekip olarak en büyük dersiniz şu olacak: metadata bir kez kaybolduğunda neyin ne olduğunu anlamak saatler alıyor. Ama her şey git’te kayıtlıysa, en kötü ihtimalle birkaç komutla eski haline dönebiliyorsunuz. Bu fark, gece 2’deki bir production paniğinde paha biçilmez.

Bir yanıt yazın

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