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 checkkomutunu ç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,
.envdosyaları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.
