Mikro Servis Mimarisi için Apollo Federation Kurulumu ve Kullanımı

Mikro servis mimarisine geçiş yapan ekiplerin önünde duran en büyük engellerden biri, her servise ait veriyi tek bir tutarlı API üzerinden sunmaktır. “Her servis kendi GraphQL endpoint’ini açsın, frontend bunları ayrı ayrı sorgulasın” diyebilirsiniz, ama bu yaklaşım frontend ekibinin işini ciddi ölçüde zorlaştırır. Apollo Federation tam da bu sorunu çözmek için tasarlanmış bir mimaridir. Farklı ekiplerin bağımsız olarak geliştirip deploy ettiği servisleri tek bir birleşik GraphQL şemasında toplayan bu yaklaşım, hem operasyonel özgürlüğü hem de geliştirici deneyimini bir arada sunar.

Apollo Federation Nedir ve Neden Gerekli?

Klasik bir mikro servis senaryosunu düşünelim. E-ticaret platformunuzda users, products, orders ve inventory servisleri var. Her birinin kendi veritabanı ve iş mantığı mevcut. Frontend bir sipariş detay sayfası göstermek istediğinde kullanıcı bilgisi, ürün detayı, stok durumu ve sipariş verilerini ayrı ayrı sorgulayıp birleştirmek zorunda kalır. Bu hem performans kaybına yol açar hem de frontend kodunu gereksiz yere karmaşıklaştırır.

Apollo Federation bu problemi subgraph ve gateway kavramlarıyla çözer.

  • Subgraph: Her mikro servis kendi GraphQL şemasını tanımlar. Bu şema, diğer subgraphlardaki tiplere referans verebilir.
  • Gateway (Router): Tüm subgraphları birleştirip tek bir GraphQL endpoint’i sunan merkezi bileşendir.
  • Supergraph: Tüm subgraphların birleştirilmiş halidir.

Federation v2, v1’e kıyasla çok daha esnek bir mimari sunar. Özellikle @shareable, @override ve @inaccessible direktifleri ekip bağımsızlığını önemli ölçüde artırmıştır.

Proje Yapısını Oluşturmak

Gerçek dünya senaryomuzda basit bir e-ticaret platformu kuracağız. Üç subgraph olacak: users-service, products-service ve orders-service. Ayrıca hepsini yönetecek bir gateway uygulaması.

mkdir ecommerce-federation
cd ecommerce-federation
mkdir users-service products-service orders-service gateway

Her servis için bağımlılıkları yükleyelim:

# Users Service
cd users-service
npm init -y
npm install @apollo/subgraph graphql @apollo/server express
npm install -D typescript ts-node @types/node @types/express

# Products Service
cd ../products-service
npm init -y
npm install @apollo/subgraph graphql @apollo/server express

# Orders Service
cd ../orders-service
npm init -y
npm install @apollo/subgraph graphql @apollo/server express

# Gateway
cd ../gateway
npm init -y
npm install @apollo/gateway @apollo/server express graphql

Users Subgraph’ı Oluşturmak

Kullanıcı servisi, Federation mimarisinin temel taşıdır. Burada tanımladığımız User tipi diğer servisler tarafından genişletilebilir olacak.

// users-service/src/index.js
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { expressMiddleware } = require('@apollo/server/express4');
const { gql } = require('graphql-tag');
const express = require('express');

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0",
          import: ["@key", "@shareable"])

  type User @key(fields: "id") {
    id: ID!
    username: String!
    email: String!
    createdAt: String!
    profile: UserProfile
  }

  type UserProfile @shareable {
    firstName: String
    lastName: String
    avatarUrl: String
  }

  type Query {
    user(id: ID!): User
    users: [User!]!
    me: User
  }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
  }

  input CreateUserInput {
    username: String!
    email: String!
    password: String!
  }

  input UpdateUserInput {
    username: String
    email: String
  }
`;

// Simule edilmis veritabani
const users = [
  {
    id: '1',
    username: 'ahmet_kaya',
    email: '[email protected]',
    createdAt: '2024-01-15',
    profile: { firstName: 'Ahmet', lastName: 'Kaya', avatarUrl: null }
  },
  {
    id: '2',
    username: 'zeynep_demir',
    email: '[email protected]',
    createdAt: '2024-02-20',
    profile: { firstName: 'Zeynep', lastName: 'Demir', avatarUrl: null }
  }
];

const resolvers = {
  Query: {
    user: (_, { id }) => users.find(u => u.id === id),
    users: () => users,
    me: (_, __, context) => users.find(u => u.id === context.userId)
  },
  Mutation: {
    createUser: (_, { input }) => {
      const newUser = {
        id: String(users.length + 1),
        ...input,
        createdAt: new Date().toISOString(),
        profile: null
      };
      users.push(newUser);
      return newUser;
    }
  },
  User: {
    __resolveReference(reference) {
      return users.find(u => u.id === reference.id);
    }
  }
};

async function startServer() {
  const app = express();
  const server = new ApolloServer({
    schema: buildSubgraphSchema({ typeDefs, resolvers })
  });

  await server.start();
  app.use(express.json());
  app.use('/graphql', expressMiddleware(server));
  app.listen(4001, () => console.log('Users service calisiyor: port 4001'));
}

startServer();

Buradaki kritik nokta __resolveReference resolver’ıdır. Gateway başka bir subgraphtaki User tipine referans verdiğinde, bu resolver devreye girerek ilgili kullanıcıyı getirir. Bu Federation’ın temel çalışma prensibidir.

Products Subgraph’ı Oluşturmak

Ürün servisi hem kendi tiplerini tanımlar hem de User tipini genişleterek ürün sahipliği bilgisi ekler.

// products-service/src/index.js
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { expressMiddleware } = require('@apollo/server/express4');
const { gql } = require('graphql-tag');
const express = require('express');

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0",
          import: ["@key", "@external", "@requires", "@provides"])

  type Product @key(fields: "id") {
    id: ID!
    name: String!
    description: String
    price: Float!
    category: String!
    sellerId: ID!
    seller: User @provides(fields: "username email")
    stockCount: Int!
  }

  type User @key(fields: "id", resolvable: false) {
    id: ID!
    username: String! @external
    email: String! @external
  }

  type Query {
    product(id: ID!): Product
    products(category: String): [Product!]!
    productsBySeller(sellerId: ID!): [Product!]!
  }

  type Mutation {
    createProduct(input: CreateProductInput!): Product!
    updateStock(productId: ID!, quantity: Int!): Product!
  }

  input CreateProductInput {
    name: String!
    description: String
    price: Float!
    category: String!
    sellerId: ID!
    stockCount: Int!
  }
`;

const products = [
  {
    id: 'p1',
    name: 'Laptop Pro 15',
    description: 'Yuksek performansli laptop',
    price: 45000,
    category: 'Elektronik',
    sellerId: '1',
    stockCount: 15
  },
  {
    id: 'p2',
    name: 'Mekanik Klavye',
    description: 'RGB aydinlatmali mekanik klavye',
    price: 2800,
    category: 'Elektronik',
    sellerId: '2',
    stockCount: 50
  }
];

const resolvers = {
  Query: {
    product: (_, { id }) => products.find(p => p.id === id),
    products: (_, { category }) =>
      category ? products.filter(p => p.category === category) : products,
    productsBySeller: (_, { sellerId }) =>
      products.filter(p => p.sellerId === sellerId)
  },
  Mutation: {
    createProduct: (_, { input }) => {
      const product = { id: `p${products.length + 1}`, ...input };
      products.push(product);
      return product;
    },
    updateStock: (_, { productId, quantity }) => {
      const product = products.find(p => p.id === productId);
      if (!product) throw new Error('Urun bulunamadi');
      product.stockCount = quantity;
      return product;
    }
  },
  Product: {
    seller: (product) => ({ __typename: 'User', id: product.sellerId }),
    __resolveReference(reference) {
      return products.find(p => p.id === reference.id);
    }
  }
};

async function startServer() {
  const app = express();
  const server = new ApolloServer({
    schema: buildSubgraphSchema({ typeDefs, resolvers })
  });

  await server.start();
  app.use(express.json());
  app.use('/graphql', expressMiddleware(server));
  app.listen(4002, () => console.log('Products service calisiyor: port 4002'));
}

startServer();

@provides direktifine dikkat edin. Bu direktif, gateway’e şunu söyler: “Bu resolver’ı çözdüğümde, User tipinin username ve email alanlarını da zaten getireceğim, ayrıca users servisine sorgu atmana gerek yok.” Bu yaklaşım performansı ciddi ölçüde artırır.

Orders Subgraph’ı Oluşturmak

Sipariş servisi en karmaşık olanıdır çünkü hem User hem de Product tiplerine bağımlıdır.

// orders-service/src/index.js
const { ApolloServer } = require('@apollo/server');
const { buildSubgraphSchema } = require('@apollo/subgraph');
const { expressMiddleware } = require('@apollo/server/express4');
const { gql } = require('graphql-tag');
const express = require('express');

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.0",
          import: ["@key", "@external"])

  type Order @key(fields: "id") {
    id: ID!
    status: OrderStatus!
    totalAmount: Float!
    createdAt: String!
    userId: ID!
    user: User
    items: [OrderItem!]!
  }

  type OrderItem {
    productId: ID!
    product: Product
    quantity: Int!
    unitPrice: Float!
  }

  enum OrderStatus {
    PENDING
    CONFIRMED
    SHIPPED
    DELIVERED
    CANCELLED
  }

  type User @key(fields: "id", resolvable: false) {
    id: ID!
    orders: [Order!]!
  }

  type Product @key(fields: "id", resolvable: false) {
    id: ID!
  }

  type Query {
    order(id: ID!): Order
    orders: [Order!]!
    ordersByUser(userId: ID!): [Order!]!
  }

  type Mutation {
    createOrder(input: CreateOrderInput!): Order!
    updateOrderStatus(orderId: ID!, status: OrderStatus!): Order!
  }

  input CreateOrderInput {
    userId: ID!
    items: [OrderItemInput!]!
  }

  input OrderItemInput {
    productId: ID!
    quantity: Int!
    unitPrice: Float!
  }
`;

const orders = [
  {
    id: 'o1',
    status: 'DELIVERED',
    totalAmount: 45000,
    createdAt: '2024-03-01',
    userId: '1',
    items: [{ productId: 'p1', quantity: 1, unitPrice: 45000 }]
  },
  {
    id: 'o2',
    status: 'PENDING',
    totalAmount: 5600,
    createdAt: '2024-03-15',
    userId: '2',
    items: [{ productId: 'p2', quantity: 2, unitPrice: 2800 }]
  }
];

const resolvers = {
  Query: {
    order: (_, { id }) => orders.find(o => o.id === id),
    orders: () => orders,
    ordersByUser: (_, { userId }) => orders.filter(o => o.userId === userId)
  },
  Mutation: {
    createOrder: (_, { input }) => {
      const total = input.items.reduce(
        (sum, item) => sum + item.quantity * item.unitPrice, 0
      );
      const order = {
        id: `o${orders.length + 1}`,
        status: 'PENDING',
        totalAmount: total,
        createdAt: new Date().toISOString(),
        userId: input.userId,
        items: input.items
      };
      orders.push(order);
      return order;
    },
    updateOrderStatus: (_, { orderId, status }) => {
      const order = orders.find(o => o.id === orderId);
      if (!order) throw new Error('Siparis bulunamadi');
      order.status = status;
      return order;
    }
  },
  Order: {
    user: (order) => ({ __typename: 'User', id: order.userId }),
    __resolveReference(reference) {
      return orders.find(o => o.id === reference.id);
    }
  },
  OrderItem: {
    product: (item) => ({ __typename: 'Product', id: item.productId })
  },
  User: {
    orders: (user) => orders.filter(o => o.userId === user.id)
  }
};

async function startServer() {
  const app = express();
  const server = new ApolloServer({
    schema: buildSubgraphSchema({ typeDefs, resolvers })
  });

  await server.start();
  app.use(express.json());
  app.use('/graphql', expressMiddleware(server));
  app.listen(4003, () => console.log('Orders service calisiyor: port 4003'));
}

startServer();

Gateway’i Yapılandırmak

Gateway, tüm subgraphları bir araya getiren merkezi noktadır. Managed Federation kullanmak yerine burada IntrospectAndCompose ile yerel geliştirme ortamı kuruyoruz.

// gateway/src/index.js
const { ApolloServer } = require('@apollo/server');
const { ApolloGateway, IntrospectAndCompose, RemoteGraphQLDataSource } = require('@apollo/gateway');
const { expressMiddleware } = require('@apollo/server/express4');
const express = require('express');

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({ request, context }) {
    // Her subgrapha kullanici bilgisi gonder
    if (context.userId) {
      request.http.headers.set('x-user-id', context.userId);
    }
    if (context.userRole) {
      request.http.headers.set('x-user-role', context.userRole);
    }
  }
}

const gateway = new ApolloGateway({
  supergraphSdl: new IntrospectAndCompose({
    subgraphs: [
      { name: 'users', url: 'http://localhost:4001/graphql' },
      { name: 'products', url: 'http://localhost:4002/graphql' },
      { name: 'orders', url: 'http://localhost:4003/graphql' }
    ]
  }),
  buildService({ url }) {
    return new AuthenticatedDataSource({ url });
  }
});

async function startGateway() {
  const app = express();
  const server = new ApolloServer({
    gateway,
    plugins: [
      {
        async requestDidStart() {
          return {
            async didResolveOperation({ request, document }) {
              console.log(`Gelen sorgu: ${request.operationName || 'Anonim'}`);
            }
          };
        }
      }
    ]
  });

  await server.start();
  app.use(express.json());
  app.use(
    '/graphql',
    expressMiddleware(server, {
      context: async ({ req }) => {
        // JWT token parse edilip context'e eklenir
        const userId = req.headers['x-user-id'];
        const userRole = req.headers['x-user-role'];
        return { userId, userRole };
      }
    })
  );

  app.listen(4000, () => {
    console.log('Gateway calisiyor: http://localhost:4000/graphql');
    console.log('Tum subgraphlar birlestirildi');
  });
}

startGateway().catch(console.error);

Tüm Servisleri Ayağa Kaldırmak

Tüm servisleri aynı anda çalıştırmak için proje kökünde bir docker-compose.yml veya basit bir script kullanabiliriz:

# package.json'a ekle veya direkt calistir
# Her terminal sekmesinde ayri calistir:

# Terminal 1 - Users Service
cd users-service && node src/index.js

# Terminal 2 - Products Service
cd products-service && node src/index.js

# Terminal 3 - Orders Service
cd orders-service && node src/index.js

# Terminal 4 - Gateway (diger servisler ayakta olunca)
cd gateway && node src/index.js

# Veya concurrently paketi ile tek seferde:
npm install -g concurrently
concurrently 
  "cd users-service && node src/index.js" 
  "cd products-service && node src/index.js" 
  "cd orders-service && node src/index.js" 
  "cd gateway && node src/index.js"

Gateway başarıyla ayağa kalktıktan sonra http://localhost:4000/graphql adresine giderek tüm subgraphların birleştirilmiş şemasını görebilirsiniz. Artık tek bir sorguda birden fazla servisten veri çekebiliriz:

# curl ile test sorgusu
curl -X POST http://localhost:4000/graphql 
  -H "Content-Type: application/json" 
  -d '{
    "query": "{ orders { id status totalAmount user { username email } items { quantity product { name price } } } }"
  }'

Üretim Ortamında Dikkat Edilecekler

Schema Registry Kullanımı: Üretim ortamında Apollo Studio ile managed federation kullanmak çok daha güvenlidir. Schema değişikliklerini rover CLI aracıyla yayınlayabilirsiniz:

  • rover subgraph publish: Subgraph şemasını registry’e gönderir
  • rover subgraph check: Mevcut şemaya uyumluluğu kontrol eder
  • rover subgraph delete: Registry’den subgraph siler

Query Planning ve Performans: Gateway her sorgu için bir query plan oluşturur. Karmaşık sorgular için bu plan birden fazla servise paralel istek atabilir. @requires ve @provides direktiflerini doğru kullanmak, gereksiz ağ isteklerini engeller.

Circuit Breaker Pattern: Subgraphlardan biri çöktüğünde gateway’in tüm sorguları reddetmemesi için her RemoteGraphQLDataSource‘a timeout ve retry mekanizması eklenmelidir.

Versiyonlama Stratejisi: Federation’da breaking change yapmak tehlikelidir. Bir alana @deprecated eklemek, eski istemcilerin yine de çalışmasını sağlar. Alan kaldırma işlemi her zaman aşamalı yapılmalıdır.

Auth ve Context Yayılımı: Gateway context’ten subgraphlara bilgi taşımak için willSendRequest hook’unu kullanın. JWT token’ı her subgraphta ayrıca doğrulamak yerine gateway’de bir kez doğrulayıp parsed bilgileri header aracılığıyla servislere iletmek hem güvenli hem de performanslıdır.

Health Check Endpoint’leri: Her subgraph /health endpoint’i sunmalıdır. Gateway başlarken veya periyodik olarak bu endpoint’leri kontrol edebilirsiniz. Kubernetes ortamında liveness ve readiness probe’ları bu endpoint’lere bağlanır.

Observability: Her subgraph kendi trace’ini gateway’e bildirmelidir. Apollo Studio ile distributed tracing özelliğini aktif ederseniz, hangi sorgunun hangi serviste ne kadar süre harcadığını görsel olarak takip edebilirsiniz.

Sonuç

Apollo Federation, mikro servis mimarisiyle GraphQL’i birlikte kullanmak isteyenler için oldukça güçlü bir çözümdür. Her ekip kendi subgraph’ını bağımsız olarak geliştirir, test eder ve deploy eder. Frontend ekibi ise sanki tek bir monolitik GraphQL API varmış gibi çalışır.

Gerçek dünya senaryolarında dikkat edilmesi gereken en kritik nokta, subgraphlar arası bağımlılıkları minimum tutmaktır. @key direktifleri doğru planlanmazsa N+1 problemleri ortaya çıkabilir. DataLoader pattern’i her subgraphta uygulamak, özellikle ilişkisel sorgu yükünü önemli ölçüde azaltır.

Küçük ekipler için Federation bazen overkill olabilir. Eğer tüm veri aynı ekip tarafından yönetiliyorsa basit bir monolitik Apollo Server yeterli olacaktır. Ancak farklı ekiplerin farklı hızlarda çalıştığı, bağımsız deployment gerektiren büyük organizasyonlarda Apollo Federation gerçekten dönüştürücü bir etki yaratır.

Bir yanıt yazın

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