AWS AppSync & GraphQL: Production-Ready Real-time API'ler Geliştirmek
AWS AppSync ile ölçeklenebilir real-time API'ler: JavaScript resolver'lar, subscription filtering, caching stratejileri ve infrastructure as code pattern'leri.
AWS AppSync; WebSocket subscription’ları, conflict resolution ve AWS data source’larına doğrudan bağlantı sunan yönetilen bir GraphQL endpoint veriyor. Çoğu ekip için geçerli kalan varsayılan dar olanı: resolver’ları JavaScript (APPSYNC_JS) runtime’ıyla doğrudan DynamoDB’ye bağla, Lambda’ya ancak bir field async I/O ya da runtime’ın ifade edemediği bir mantık istediğinde uzan.
Bu tek karar, latency ve maliyeti herhangi bir şema kararından daha fazla oynatıyor. Subscription filtering, caching katmanları, single-table ile multi-table modelleme ve bunların etrafındaki CDK bağlantıları hep bu kararın üzerine yapılan ince ayar.
Real-time API’lerin Zor Tarafları#
Real-time özellikler içeren modern uygulamalar geliştirmek, basit REST API development’ın ötesinde birkaç teknik zorluk sunuyor:
Infrastructure karmaşıklığı: WebSocket server’larını yönetmek, connection state’ini handle etmek, bidirectional communication’ı scale etmek ve high availability sağlamak gerekiyor. Geleneksel yaklaşımlar socket.io server’ları deploy etmeyi veya Redis pub/sub altyapısını maintain etmeyi içeriyor.
Data senkronizasyonu: Kullanıcılar offline olup pending değişikliklerle geri döndüğünde, birden çok client arasında veri tutarlılığını korumak katlanarak karmaşıklaşıyor. N-client problemi, her yeni kullanıcıyla potential conflict’lerin katlanarak artması demek.
Fine-grained authorization: REST API’ler genellikle endpoint seviyesinde authorize ederken, GraphQL field-level access control gerektiriyor. Tek bir query, nested field’lar boyunca farklı permission gereksinimleri olan veri isteyebiliyor.
Performance vs maliyet trade-off’ları: Real-time özellikler, long-lived WebSocket connection’lar, high-frequency subscription update’ler ve inefficient resolver implementation’lar yoluyla beklenmedik maliyetlere yol açabiliyor.
AppSync’de tipik bir request flow’u şöyle görünüyor:
Teknik Gereksinimler#
Production-ready bir real-time GraphQL API, şu teknik gereksinimleri karşılamalı:
Resolver performansı: JavaScript resolver’lar, VTL (Velocity Template Language), pipeline resolver’lar ve direct Lambda integration arasında seçim yapılmalı. Her yaklaşımın farklı latency karakteristikleri ve geliştirme karmaşıklığı var.
Subscription mimarisi: Client bandwidth ve processing overhead’i azaltmak için server-side filtering implement edilmeli. Traditional mutation-based subscription’larla yeni AppSync Events channel-based yaklaşım arasındaki farklar anlaşılmalı.
Caching layer’ları: AppSync’in built-in ElastiCache integration’ı, DynamoDB’nin long-term cache olarak kullanımı ve farklı access pattern’ler ve TTL gereksinimleri için DAX (DynamoDB Accelerator) değerlendirilmeli.
Data modeling stratejisi: Access pattern’lere göre single-table ve multi-table DynamoDB tasarımları arasında karar verilmeli. GraphQL schema yapısının database yapısını yansıtması gerekmiyor; bu esneklik hem güçlü hem de potansiyel olarak sorunlu olabiliyor.
Authorization konfigürasyonu: Granular access control için field-level directive’lerle multi-auth mode’ları (API Key, Cognito User Pools, IAM, OIDC, Lambda authorizer’lar) kurulmalı.
Uygulama#
AppSync Mimarisi#
AppSync, client’lar ile data source’lar arasında durarak, subscription’lar için entegre WebSocket desteği olan yönetilen bir GraphQL endpoint sağlıyor. Araya Lambda girmeden doğrudan AWS data source’larına bağlanabiliyor; latency ve maliyet tartışmasının çoğu bu tek özelliğe dayanıyor:
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import { Construct } from 'constructs';
export class AppSyncApiStack extends Construct {
public readonly api: appsync.GraphqlApi;
constructor(scope: Construct, id: string) {
super(scope, id);
// Multi-auth konfigürasyonuyla GraphQL API oluştur
this.api = new appsync.GraphqlApi(this, 'Api', {
name: 'production-api',
definition: appsync.Definition.fromFile('schema.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: {
userPool: userPool,
},
},
additionalAuthorizationModes: [
{ authorizationType: appsync.AuthorizationType.IAM },
{ authorizationType: appsync.AuthorizationType.API_KEY },
],
},
xrayEnabled: true,
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ALL,
excludeVerboseContent: false,
},
});
// Real-time update'ler için stream'li DynamoDB table
const table = new dynamodb.Table(this, 'DataTable', {
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
pointInTimeRecovery: true,
});
// Direct DynamoDB data source (Lambda yok)
const dataSource = this.api.addDynamoDbDataSource('MainDataSource', table);
}
}
Direct data source connection, Lambda invocation maliyetlerini ve cold start latency’sini ortadan kaldırıyor. Basit CRUD operasyonlarında istek AppSync runtime’ından hiç çıkmıyor; ödenecek bir init fazı ve ayarlanacak ikinci bir faturalandırma yüzeyi kalmıyor.
Modern JavaScript Resolver’lar#
AppSync artık VTL yerine JavaScript resolver’ları önerilen yaklaşım olarak destekliyor. İşte yaygın bir DynamoDB query operasyonunu kullanan pratik bir karşılaştırma:
Legacy VTL yaklaşımı (maintain etmesi daha zor):
{
"version": "2018-05-29",
"operation": "Query",
"query": {
"expression": "PK = :pk AND begins_with(SK, :sk)",
"expressionValues": {
":pk": $util.dynamodb.toDynamoDBJson($ctx.args.userId),
":sk": $util.dynamodb.toDynamoDBJson("ORDER#")
}
},
"index": "GSI1",
"limit": $util.defaultIfNull($ctx.args.limit, 20),
"nextToken": $util.toJson($ctx.args.nextToken)
}
Modern JavaScript yaklaşımı (daha iyi developer experience):
// resolvers/getUserOrders.js
import * as ddb from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
const { userId, limit = 20, nextToken } = ctx.args;
return ddb.query({
query: {
PK: { eq: userId },
SK: { beginsWith: 'ORDER#' },
},
index: 'GSI1',
limit,
nextToken,
});
}
export function response(ctx) {
if (ctx.error) {
util.error(ctx.error.message, ctx.error.type);
}
return {
items: ctx.result.items,
nextToken: ctx.result.nextToken,
};
}
JavaScript resolver’ların önemli kısıtlamaları:
- Async/await desteği yok (APPSYNC_JS runtime kısıtlaması)
- Geleneksel for loop’lar yok (for-in, for-of veya array method’ları kullan)
- try/catch block’ları yok (early return’ler ve explicit error handling kullan)
- Sadece ECMAScript 6 subset’i
Kompleks async operasyonlar için Lambda function step’li pipeline resolver’lar veya direct Lambda resolver’lar kullan.
Multi-Step Operasyonlar için Pipeline Resolver’lar#
Pipeline resolver’lar, ek Lambda invocation’ları olmadan birden çok operasyonu compose etmeye izin veriyor. Bu pattern, authorization check’leri, quota enforcement ve data transformation’lar için iyi çalışıyor:
// Function 1: User quota kontrolü
export function request(ctx) {
return {
operation: 'GetItem',
key: util.dynamodb.toMapValues({ userId: ctx.identity.sub }),
};
}
export function response(ctx) {
const quota = ctx.result?.quota ?? 0;
if (quota <= 0) {
util.error('API kotası aşıldı', 'QuotaExceeded');
}
// Quota bilgisini stash ile sonraki function'a aktar
ctx.stash.currentQuota = quota;
return ctx.result;
}
// Function 2: İstenen veriyi getir
export function request(ctx) {
return {
operation: 'Query',
query: {
expression: 'PK = :pk',
expressionValues: {
':pk': util.dynamodb.toDynamoDB(ctx.args.id),
},
},
};
}
export function response(ctx) {
// Veriyi sonraki function'a aktar
ctx.stash.data = ctx.result.items;
return ctx.result;
}
// Function 3: Quota counter'ı güncelle
export function request(ctx) {
return {
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ userId: ctx.identity.sub }),
update: {
expression: 'SET quota = quota - :decrement',
expressionValues: {
':decrement': { N: 1 },
},
},
};
}
export function response(ctx) {
// Function 2'den gelen veriyi döndür
return ctx.stash.data;
}
ctx.stash objesi, final function’a kadar gerçek response’u değiştirmeden pipeline function’lar arasında veri geçişine izin veriyor.
Enhanced Filtering ile Real-time Subscription’lar#
Traditional GraphQL subscription’lar mutation’larda trigger olur ama client’lar genellikle hangi update’leri alacaklarını filtrelemek ister. AppSync’in enhanced filtering’i bunu server-side yapıyor:
GraphQL schema:
type Subscription {
onMessagePosted(roomId: ID!): Message
@aws_subscribe(mutations: ["postMessage"])
}
type Mutation {
postMessage(roomId: ID!, content: String!, userId: ID!): Message
}
type Message {
id: ID!
roomId: ID!
userId: ID!
content: String!
timestamp: AWSDateTime!
}
Enhanced filtering ile subscription resolver:
// resolvers/onMessagePosted.js
export function request(ctx) {
return { payload: null };
}
export function response(ctx) {
// Server-side subscription filter ayarla
const filter = {
filterGroup: [
{
filters: [
// Sadece bu room'un mesajları
{
fieldName: 'roomId',
operator: 'eq',
value: ctx.args.roomId,
},
// Mesaj yazana gönderme
{
fieldName: 'userId',
operator: 'ne',
value: ctx.identity.sub,
},
],
},
],
};
extensions.setSubscriptionFilter(util.transform.toSubscriptionFilter(filter));
return null;
}
Mevcut filter operator’ler: eq, ne, in, notIn, gt, ge, lt, le, between, contains, notContains, beginsWith, containsAny. Bir grup içindeki filter’lar AND mantığı, birden çok grup OR mantığı kullanıyor.
Server-side filtreleme en çok, tek bir client’ın birden fazla room’a subscribe olduğu multi-tenant uygulamalarda fark yaratıyor. Filtreleme olmadan her client, mutation’ın ürettiği her mesajı alıyor ve payload ağı geçtikten sonra çoğunu atıyor.
AppSync Events: Channel-Based Real-time#
AppSync Events, GraphQL mutation’lardan decoupled, daha esnek bir real-time update yaklaşımı sağlıyor:
Traditional subscription’lardan temel farklar:
| Özellik | Traditional Subscriptions | AppSync Events |
|---|---|---|
| Trigger | GraphQL mutation’lar | HTTP/WebSocket publish |
| Schema coupling | Tight (mutation-based) | Loose (channel-based) |
| Filtering | Field-based filter’lar | Custom handler’lar |
| Wildcard’lar | Desteklenmiyor | namespace/channel/* |
| Authorization | GraphQL directive’ler | OnPublish/OnSubscribe handler’lar |
Use case örneği: Cihazların HTTP ile publish ettiği ama client’ların WebSocket ile subscribe olduğu IoT sensor verisi:
// Lambda function HTTP ile AppSync Events channel'a publish ediyor
import { SignatureV4 } from '@aws-sdk/signature-v4';
import { Sha256 } from '@aws-crypto/sha256-js';
export async function handler(event) {
// IoT sensor veri gönderiyor
const sensorData = JSON.parse(event.body);
const endpoint = `https://${process.env.APPSYNC_API_ID}.appsync-api.${process.env.AWS_REGION}.amazonaws.com`;
const payload = JSON.stringify({
channel: `device/${sensorData.deviceId}`,
events: [JSON.stringify(sensorData)],
});
// SigV4 ile request'i imzala
const signer = new SignatureV4({
credentials: await import('@aws-sdk/credential-provider-node').then(m => m.defaultProvider()()),
region: process.env.AWS_REGION,
service: 'appsync',
sha256: Sha256,
});
const signedRequest = await signer.sign({
method: 'POST',
hostname: `${process.env.APPSYNC_API_ID}.appsync-api.${process.env.AWS_REGION}.amazonaws.com`,
path: `/event`,
protocol: 'https:',
headers: {
'Content-Type': 'application/json',
host: `${process.env.APPSYNC_API_ID}.appsync-api.${process.env.AWS_REGION}.amazonaws.com`,
},
body: payload,
});
const response = await fetch(`${endpoint}/event`, {
method: 'POST',
headers: signedRequest.headers,
body: payload,
});
return { statusCode: response.status };
}
Client belirli device’a veya tüm device’lara subscribe oluyor:
subscription OnSensorData {
subscribe(namespace: "sensors", channel: "device/sensor-123") {
id
data
}
}
subscription OnAllSensors {
subscribe(namespace: "sensors", channel: "device/*") {
id
data
}
}
Caching Stratejileri#
AppSync, ElastiCache üzerinden built-in caching sağlıyor ama doğru caching stratejisini seçmek data freshness gereksinimleri ve maliyet kısıtlamalarına bağlı.
AppSync built-in cache konfigürasyonu:
// CDK konfigürasyonu
const resolver = dataSource.createResolver('GetProduct', {
typeName: 'Query',
fieldName: 'getProduct',
code: appsync.Code.fromAsset('resolvers/getProduct.js'),
runtime: appsync.FunctionRuntime.JS_1_0_0,
cachingConfig: {
ttl: Duration.minutes(5),
cachingKeys: ['$context.identity.sub', '$context.arguments.id'],
},
});
cachingKeys listesi cache’in nasıl bölüneceğini belirliyor. $context.identity.sub eklemek her kullanıcıya ayrı bir entry veriyor; kişiselleştirilmiş veri için doğru ama entry sayısını çoğaltıp hit rate’i aşağı çekiyor. Product catalog gibi paylaşılan reference data’da bu anahtarı listeye koyma.
Long-term cache olarak DynamoDB (pipeline resolver pattern):
// Function 1: Cache table'ı kontrol et
export function request(ctx) {
return {
operation: 'GetItem',
key: util.dynamodb.toMapValues({ cacheKey: ctx.args.id }),
};
}
export function response(ctx) {
const cached = ctx.result;
const now = util.time.nowEpochSeconds();
// Cache'in valid olup olmadığını kontrol et
if (cached && cached.ttl > now) {
// Cache'lenmiş veriyi döndür, kalan function'ları skip et
return JSON.parse(cached.data);
}
// Cache miss, sonraki function'a devam et
return null;
}
// Function 2: Pahalı source'dan getir (external API, kompleks query)
// Function 3: Sonucu TTL attribute ile cache table'a kaydet
export function request(ctx) {
const ttl = util.time.nowEpochSeconds() + 3600; // 1 saat
return {
operation: 'PutItem',
key: util.dynamodb.toMapValues({ cacheKey: ctx.args.id }),
attributeValues: util.dynamodb.toMapValues({
data: JSON.stringify(ctx.prev.result),
ttl: ttl,
}),
};
}
export function response(ctx) {
return ctx.prev.result; // Function 2'den gelen veriyi döndür
}
Expired cache entry’lerini otomatik silmek için ttl attribute’unda DynamoDB TTL’i aktifleştir.
Şema Tasarımı: Single-table vs Multi-table#
Single-table ve multi-table DynamoDB tasarımı arasındaki seçim, resolver karmaşıklığını ve query performansını önemli ölçüde etkiliyor.
Multi-table design (daha basit resolver’lar, daha fazla esneklik):
UsersTable: PK=userId
ProductsTable: PK=productId
OrdersTable: PK=orderId, GSI: userId-timestamp
Order’larıyla birlikte user için GraphQL resolver iki query gerektiriyor:
// getUser resolver
export function request(ctx) {
return { operation: 'GetItem', key: { id: ctx.args.userId } };
}
// user.orders resolver (ayrı resolver)
export function request(ctx) {
return {
operation: 'Query',
index: 'userIdIndex',
query: {
userId: { eq: ctx.source.id },
},
};
}
Single-table design (kompleks resolver’lar, optimize edilmiş query’ler):
MainTable:
PK=USER#123, SK=PROFILE
PK=USER#123, SK=ORDER#2024-12-01#001
PK=USER#123, SK=ORDER#2024-11-30#002
PK=PRODUCT#789, SK=METADATA
Tek query user ve order’ları getiriyor:
export function request(ctx) {
return {
operation: 'Query',
query: {
PK: { eq: `USER#${ctx.args.userId}` },
},
};
}
export function response(ctx) {
const items = ctx.result.items;
// Profile'ı order'lardan ayır
const profile = items.find(item => item.SK === 'PROFILE');
const orders = items.filter(item => item.SK.startsWith('ORDER#'));
return {
...profile,
orders: orders,
};
}
Her yaklaşımı ne zaman kullanmalı:
- Multi-table: Prototyping, evolving schema’lar, bilinmeyen access pattern’ler, küçük-orta ölçek
- Single-table: Bilinen access pattern’ler, high scale gereksinimleri, latency-critical uygulamalar, maliyet optimizasyonu
Authorization Mode’ları#
AppSync, tek bir API’de kombine edilebilen beş authorization mode destekliyor:
type Query {
# API key ile erişilebilir public veri
publicPosts: [Post] @aws_api_key
# Sadece authenticated user'lar
myPosts: [Post] @aws_cognito_user_pools
# Sadece admin user'lar
allUsers: [User] @aws_cognito_user_pools(cognito_groups: ["Admin"])
# IAM ile service-to-service
internalData: [Data] @aws_iam
# Custom authorization logic
partnerData: [Data] @aws_lambda
}
Custom logic için Lambda authorizer (örn: DynamoDB’de saklanan API key’leri validate etmek):
export async function handler(event: AppSyncAuthorizerEvent) {
const apiKey = event.authorizationToken;
// DynamoDB'de API key'i ara
const result = await dynamodb.get({
TableName: 'ApiKeys',
Key: { apiKey },
});
if (!result.Item || result.Item.expiresAt < Date.now()) {
return {
isAuthorized: false,
deniedFields: ['Query.*'],
};
}
return {
isAuthorized: true,
resolverContext: {
customerId: result.Item.customerId,
tier: result.Item.tier,
},
ttlOverride: 300, // Authorization sonucunu 5 dakika cache'le
};
}
resolverContext, resolver’larda ctx.identity.resolverContext ile erişilebilir ve custom authorization verisinin request boyunca akmasını sağlıyor.
Offline Support için Conflict Resolution#
Offline-first uygulamalar geliştirirken, concurrent update’leri handle etmek bir conflict resolution stratejisi gerektiriyor. AppSync üç yaklaşımı destekliyor:
1. Optimistic Concurrency (version kontrolü):
// Version check ile mutation resolver
export function request(ctx) {
return {
operation: 'UpdateItem',
key: util.dynamodb.toMapValues({ id: ctx.args.id }),
update: {
expression: 'SET #content = :content, #version = :newVersion',
expressionNames: {
'#content': 'content',
'#version': 'version',
},
expressionValues: {
':content': util.dynamodb.toDynamoDB(ctx.args.content),
':newVersion': util.dynamodb.toDynamoDB(ctx.args.version + 1),
':expectedVersion': util.dynamodb.toDynamoDB(ctx.args.version),
},
},
condition: {
expression: '#version = :expectedVersion',
expressionNames: { '#version': 'version' },
},
};
}
export function response(ctx) {
if (ctx.error) {
// Version mismatch - conflict tespit edildi
if (ctx.error.type === 'DynamoDB:ConditionalCheckFailedException') {
util.error('Conflict: Item başka bir kullanıcı tarafından değiştirildi', 'ConflictError', ctx.result);
}
util.error(ctx.error.message, ctx.error.type);
}
return ctx.result;
}
2. Automerge (Amplify DataStore için default):
- Conflicting olmayan field değişikliklerini otomatik merge eder
- Collection’lar için set union kullanır
- Scalar’lar için last-writer-wins kullanır
3. Custom Lambda resolver:
export async function handler(event: ConflictEvent) {
const { base, local, remote } = event;
// Custom merge logic
const resolved = {
...base,
// Content için local edit'leri tercih et
content: local.content,
// Numeric değerleri topla
viewCount: (local.viewCount || 0) + (remote.viewCount || 0) - (base.viewCount || 0),
// Array'leri merge et
tags: [...new Set([...local.tags, ...remote.tags])],
};
return resolved;
}
Efficient synchronization için Delta Sync:
AppSync, değişiklikleri ayrı bir Delta Sync table’da track edebiliyor ve client’ların sadece son sync’lerinden itibaren değişen item’ları request etmelerini sağlıyor:
query SyncPosts($lastSync: AWSTimestamp!) {
syncPosts(lastSync: $lastSync, limit: 100) {
items {
id
content
updatedAt
_deleted
}
nextToken
}
}
Uçtan Uca CDK Altyapı Örneği#
TypeScript resolver bundling ile production-ready bir AppSync API:
import * as cdk from 'aws-cdk-lib';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as logs from 'aws-cdk-lib/aws-logs';
import { Construct } from 'constructs';
import { execSync } from 'child_process';
export class ProductionAppSyncStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// TypeScript resolver'ları JavaScript'e build et
execSync('npm run build:resolvers', {
cwd: './resolvers',
stdio: 'inherit',
});
// Authentication için Cognito User Pool
const userPool = new cognito.UserPool(this, 'UserPool', {
selfSignUpEnabled: true,
userVerification: {
emailSubject: 'E-posta adresini doğrula',
emailBody: 'Doğrulama kodu: {####}',
},
signInAliases: { email: true },
passwordPolicy: {
minLength: 8,
requireLowercase: true,
requireUppercase: true,
requireDigits: true,
},
});
// Single-table design ile DynamoDB table
const table = new dynamodb.Table(this, 'MainTable', {
partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
pointInTimeRecovery: true,
removalPolicy: cdk.RemovalPolicy.RETAIN,
// Cache entry'ler için TTL aktifleştir
timeToLiveAttribute: 'ttl',
});
// User-specific query'ler için GSI
table.addGlobalSecondaryIndex({
indexName: 'GSI1',
partitionKey: { name: 'GSI1PK', type: dynamodb.AttributeType.STRING },
sortKey: { name: 'GSI1SK', type: dynamodb.AttributeType.STRING },
projectionType: dynamodb.ProjectionType.ALL,
});
// API log'ları için CloudWatch log group
const logGroup = new logs.LogGroup(this, 'ApiLogs', {
retention: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// AppSync GraphQL API
const api = new appsync.GraphqlApi(this, 'Api', {
name: `${id}-api`,
definition: appsync.Definition.fromFile('schema.graphql'),
authorizationConfig: {
defaultAuthorization: {
authorizationType: appsync.AuthorizationType.USER_POOL,
userPoolConfig: { userPool },
},
additionalAuthorizationModes: [
{ authorizationType: appsync.AuthorizationType.IAM },
{
authorizationType: appsync.AuthorizationType.API_KEY,
apiKeyConfig: {
expires: cdk.Expiration.after(cdk.Duration.days(365)),
},
},
],
},
xrayEnabled: true,
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ALL,
excludeVerboseContent: false,
cloudWatchLogsLogGroup: logGroup,
},
});
// DynamoDB data source
const dataSource = api.addDynamoDbDataSource('MainDataSource', table);
// Bundled JavaScript file'lardan resolver'lar oluştur
const resolvers = [
{ typeName: 'Query', fieldName: 'getUser', file: 'getUser.js' },
{ typeName: 'Query', fieldName: 'listPosts', file: 'listPosts.js' },
{ typeName: 'Mutation', fieldName: 'createPost', file: 'createPost.js' },
{ typeName: 'Mutation', fieldName: 'updatePost', file: 'updatePost.js' },
];
resolvers.forEach(({ typeName, fieldName, file }) => {
dataSource.createResolver(`${typeName}${fieldName}Resolver`, {
typeName,
fieldName,
code: appsync.Code.fromAsset(`resolvers/dist/${file}`),
runtime: appsync.FunctionRuntime.JS_1_0_0,
});
});
// Output'lar
new cdk.CfnOutput(this, 'GraphQLApiUrl', {
value: api.graphqlUrl,
});
new cdk.CfnOutput(this, 'ApiKey', {
value: api.apiKey || 'N/A',
});
new cdk.CfnOutput(this, 'UserPoolId', {
value: userPool.userPoolId,
});
}
}
Resolver build script (resolvers/package.json):
{
"scripts": {
"build:resolvers": "esbuild src/*.ts --bundle --platform=node --target=es2020 --outdir=dist --format=esm"
},
"devDependencies": {
"esbuild": "^0.19.0",
"@aws-appsync/utils": "^1.3.0"
}
}
Monitoring ve Observability#
Production AppSync API’leri, birden çok boyutta kapsamlı monitoring gerektiriyor:
CloudWatch Metrics (otomatik):
4XXErrorve5XXError: Client ve server error rate’leriLatency: Request processing süresi (P50, P95, P99)ConnectedSubscriptions: Aktif WebSocket connection’larSubscriptionPublishErrors: Başarısız subscription delivery’ler
X-Ray tracing detaylı request flow görselleştirmesi sağlıyor:
// X-Ray gösteriyor:
// 1. AppSync API entry
// 2. Resolver execution süresi
// 3. DynamoDB query latency
// 4. Total request duration
Specific resolver sorunlarını debug etmek için field-level logging aktifleştir:
logConfig: {
fieldLogLevel: appsync.FieldLogLevel.ALL, // Her resolver execution'ı logla
excludeVerboseContent: false, // Request/response body'lerini dahil et
}
Custom CloudWatch dashboard:
const dashboard = new cloudwatch.Dashboard(this, 'ApiDashboard', {
dashboardName: 'AppSync-Production',
});
dashboard.addWidgets(
new cloudwatch.GraphWidget({
title: 'Request Latency',
left: [
api.metricLatency({ statistic: 'p50' }),
api.metricLatency({ statistic: 'p95' }),
api.metricLatency({ statistic: 'p99' }),
],
}),
new cloudwatch.GraphWidget({
title: 'Error Rate',
left: [
api.metric4XXError(),
api.metric5XXError(),
],
}),
);
Pattern’ler Arasında Seçim#
Yukarıdaki parçalar, şema birkaç düzine field’ı geçmeden önce netleştirilmesi gereken birkaç karara iniyor.
İlk karar resolver tipi. JavaScript resolver’lar basit CRUD’u, pipeline resolver’lar authorization check’in ardından gelen fetch gibi çok adımlı işleri, Lambda da APPSYNC_JS runtime’ının ifade edemediklerini karşılıyor: async I/O, üçüncü parti SDK’lar, ağır business logic. Direct DynamoDB resolver’ın yeteceği bir yola Lambda koymak, faturası ödenecek fazladan bir invocation ve açıklanacak bir cold start getiriyor.
Geri dönmesi en zor karar table layout’u. Access pattern’ler zaten belliyse ve tek query bir ekranı doldurabiliyorsa single-table kazandırıyor. Şema hâlâ oynuyorsa her resolver bağımsız kaldığı için multi-table kazandırıyor. Proje ortasında multi-table’dan single-table’a geçmek her resolver’ı yeniden yazmak ve her item’ı backfill etmek demek; bu yüzden kararı iyi vermekten çok erken vermek gerekiyor.
Subscription filtering’in kuralı daha basit: birden fazla consumer’ı olan her subscription server-side filtrelenmeli. Client-side filtreleme aynı payload’ı her subscriber’a gönderip cihazın atmasını bekliyor; bunun bedeli önce mobil şebekede ve batarya tüketiminde görülüyor.
Caching kararı verinin şekline göre ayrışıyor. Okuma frekansı yüksek, güncellemesi seyrek olan reference data; isabet alsın almasın saatlik faturalanan built-in AppSync cache’ine uyuyor. Saatlerce geçerli kalan user-specific data, TTL attribute’lu ve request başına faturalanan bir DynamoDB cache table’ına uyuyor. DAX ise erişimin zaten bir DynamoDB okuması olduğu ve hedefin mikrosaniye olduğu durumu karşılıyor.
İki maliyet kalemi genelde sürpriz oluyor. Subscription’lar mesaj başına olduğu kadar connection-minute başına da faturalanıyor; arka planda WebSocket’ini açık tutan mobil client, kimse bakmazken ücret biriktiriyor. Uygulama arka plana düştüğünde bağlantıyı kapat, öne döndüğünde yeniden subscribe ol. İkincisi, optimistic concurrency çakışan yazmayı kaybetmiyor, reddediyor; collaborative editing yayına çıkmadan önce client’ın ConflictError için bir retry yolu olmalı.
Çoğu API için geçerli kalan varsayılan dar olanı: direct data source’lar, JavaScript resolver’lar ve runtime gerçekten önünü kestiğinde Lambda. Bir field async çağrı, üçüncü parti SDK ya da early return’lerle ifade edilemeyecek bir hata yönetimi istiyorsa bu varsayılandan sap. Fazladan invocation maliyetini orada hak ediyor.
Kaynaklar#
- AWS AppSync Nedir? (yeni sekmede açılır) - AWS AppSync’in yönetilen GraphQL yeteneklerine resmi genel bakış
- AWS AppSync JavaScript Resolver’larına Genel Bakış (yeni sekmede açılır) - APPSYNC_JS çalışma zamanı kullanılarak birim ve pipeline resolver yazımı
- AWS AppSync’te Pipeline Resolver Yapılandırması (JavaScript) (yeni sekmede açılır) - Çok adımlı resolver bileşimi ve ctx.stash kullanımı
- AWS AppSync’te Real-Time Veriler için Subscription Kullanımı (yeni sekmede açılır) - WebSocket tabanlı subscription implementasyonu ve gelişmiş filtreleme
- AWS AppSync’te Gelişmiş Subscription Filtreleri Tanımlama (yeni sekmede açılır) - İstemci bant genişliğini azaltmak için sunucu taraflı filtreleme
- GraphQL Spesifikasyonu (yeni sekmede açılır) - Yetkili GraphQL dili ve tip sistemi spesifikasyonu
- GraphQL API Verilerini İzlemek için CloudWatch Kullanımı (yeni sekmede açılır) - AppSync API’leri için metrik, loglama ve gösterge paneli yapılandırması
İlgili yazılar
AppSync subscription'ları yalnızca mutation ile tetiklenir. Downstream BFF olaylarını NONE veri kaynaklı bir mutation'a EventBridge ve CDK ile köprülemeyi inceliyorum.
aws · graphql · serverless +4
Gelişmiş Amazon Cognito teknik rehberi: özel auth akışları, federation, multi-tenancy, migration stratejileri ve CDK ile production-grade güvenlik.
aws · authentication · serverless +6
Amazon SNS ve SQS ile güvenli cross-account event dağıtımı: IAM policy'leri, KMS şifreleme, AWS CDK kurulumu ve production'da karşılaşılan yaygın sorunlar.
aws · sns · sqs +6
Step Functions ile production serverless workflow kur: Standard ve Express, Distributed Map, error handling ve CDK örnekleriyle maliyet optimizasyonu.
step-functions · aws-cdk · serverless +4
Builder pattern'in TypeScript tip sistemiyle serverless, veri katmanı ve testte güvenli, keşfedilebilir API'leri nasıl kurduğu, çalışan örneklerle.
typescript · design-patterns · aws-cdk +2