AWS CDK ve Serverless ile Geçici Preview Ortamları Oluşturma
AWS CDK, Lambda ve GitHub Actions kullanarak otomatik preview ortamları oluşturmayı öğrenin - sorunsuz PR test ve inceleme süreçleri için
Paylaşılan Staging Ortamlarının Problemi#
Paylaşılan tek bir staging ortamı, bir ekip günde birden fazla pull request açmaya başladığı an darboğaza dönüşür. Eşzamanlı PR’lar birbirlerinin veritabanı durumunu üzerine yazar, aynı feature flag’lerde çakışır ve aynı DNS kaydı için yarışır; test sinyali düşer çünkü bir hata, gerçek bir regresyon kadar “başka birinin PR’ı” anlamına da gelebilir.
PR başına bir tane olan geçici (ephemeral) preview ortamları kaynak çakışmasını ortadan kaldırır ama kendi kısıtlarını getirir: ortam başına maliyet, incelemeci için cold-start sürtünmesi ve temizlik güvenilirliği. Ekip ölçeğinde ayakta kalan kompozisyon şudur: PR başına bir CDK stack’i, deterministik bir preview URL’i, PR kapandığında tetiklenen teardown ve teardown’ın kaçırdığı stack’ler için arkada çalışan bir TTL temizleyicisi. Seeding stratejisi ile maliyet kontrolleri de desenin ilk aydan sonra yaşayıp yaşamayacağını belirler.
Mimari Genel Bakış#
Çözüm, AWS serverless servislerini GitHub Actions ile birleştirerek tamamen otomatik preview ortamları oluşturur. Her PR kendi subdomain’ini ve production’u taklit eden ama uygun şekilde ölçeklenen altyapı stack’ini alır.
Temel Implementasyon#
CDK Stack Mimarisi#
Temel, her PR için özdeş altyapı oluşturan parametreli bir CDK stack’i:
// lib/preview-environment-stack.ts
import { Stack, StackProps, Duration, RemovalPolicy, Aspects, Tags, IAspect, CfnResource } from 'aws-cdk-lib';
import { Construct, IConstruct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
export interface PreviewStackProps extends StackProps {
prNumber: string;
commitSha: string;
domain: string;
certificateArn: string;
}
export class PreviewEnvironmentStack extends Stack {
constructor(scope: Construct, id: string, props: PreviewStackProps) {
super(scope, id, props);
const { prNumber, commitSha, domain, certificateArn } = props;
// Kaynak yönetimi için ortak tag'ler
const commonTags = {
Environment: 'preview',
PRNumber: prNumber,
CommitSha: commitSha.substring(0, 8),
CreatedBy: 'github-actions',
TTL: this.calculateTTL(72), // 72 saat
};
// Uygulama için Lambda function
const appFunction = new lambda.Function(this, 'AppFunction', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('dist'),
timeout: Duration.seconds(30),
memorySize: 256,
environment: {
STAGE: 'preview',
PR_NUMBER: prNumber,
COMMIT_SHA: commitSha,
},
});
// Özel domain ile API Gateway
const api = new apigateway.RestApi(this, 'PreviewApi', {
restApiName: `preview-api-pr-${prNumber}`,
description: `PR ${prNumber} için preview ortamı`,
binaryMediaTypes: ['*/*'],
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
},
});
// Lambda integration
const lambdaIntegration = new apigateway.LambdaIntegration(appFunction, {
requestTemplates: { 'application/json': '{ "statusCode": "200" }' },
});
api.root.addMethod('ANY', lambdaIntegration);
api.root.addProxy({
defaultIntegration: lambdaIntegration,
});
// Preview subdomain'i için hosted zone lookup
const previewDomain = `pr-${prNumber}.${domain}`;
const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
domainName: domain,
});
// Caching için CloudFront distribution. Preview subdomain'i distribution'da
// alternate domain name olarak tanımlı olmalı ve us-east-1'de üretilmiş bir
// wildcard sertifika onu kapsamalı; varsayılan *.cloudfront.net sertifikası
// bu adı kapsamaz, bu tanım olmadan pr-N URL'i TLS doğrulamasında düşer
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'Distribution', {
originConfigs: [{
customOriginSource: {
domainName: api.restApiId + '.execute-api.' + this.region + '.amazonaws.com',
originPath: '/prod',
},
behaviors: [{ isDefaultBehavior: true }],
}],
viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(
acm.Certificate.fromCertificateArn(this, 'PreviewCertificate', certificateArn),
{
aliases: [previewDomain],
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
sslMethod: cloudfront.SSLMethod.SNI,
}
),
comment: `PR ${prNumber} için preview distribution`,
});
// Preview subdomain'ini distribution'a yönlendiren alias kaydı
new route53.ARecord(this, 'PreviewRecord', {
zone: hostedZone,
recordName: previewDomain,
target: route53.RecordTarget.fromAlias(
new route53Targets.CloudFrontTarget(distribution)
),
});
// Tüm kaynaklara ortak tag'leri uygula
Object.entries(commonTags).forEach(([key, value]) => {
Tags.of(this).add(key, value);
});
// Temizlik aspect'ini uygula
Aspects.of(this).add(new AutoCleanupAspect());
}
private calculateTTL(hours: number): string {
const expiryDate = new Date(Date.now() + hours * 60 * 60 * 1000);
return expiryDate.toISOString();
}
}
// Cleanup aspect: preview stack'inde silinmeden ayakta kalan kaynak olmamalı
class AutoCleanupAspect implements IAspect {
visit(node: IConstruct): void {
if (node instanceof CfnResource) {
node.applyRemovalPolicy(RemovalPolicy.DESTROY);
}
}
}
GitHub Actions Workflow#
Otomasyon, PR olaylarına yanıt veren bir GitHub Actions workflow’u ile başlar:
# .github/workflows/preview-environment.yml
name: Preview Environment
on:
pull_request:
types: [opened, synchronize, closed]
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
deploy-preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{ github.event.number }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
PREVIEW_DOMAIN: pr-${{ github.event.number }}.preview.company.com
steps:
- name: Kodu checkout et
uses: actions/checkout@v4
- name: Node.js setup
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
- name: AWS credentials yapılandır
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Dependencies'leri yükle
run: |
npm ci
npm run build
- name: CDK stack'i deploy et
run: |
npx cdk deploy preview-pr-${{ env.PR_NUMBER }} \
--parameters prNumber=${{ env.PR_NUMBER }} \
--parameters commitSha=${{ env.COMMIT_SHA }} \
--require-approval never \
--outputs-file cdk-outputs.json
- name: Deployment URL'ini çıkar
id: extract-url
run: |
PREVIEW_URL=$(jq -r '.["preview-pr-${{ env.PR_NUMBER }}"].PreviewURL' cdk-outputs.json)
echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT
- name: Deployment hazır olmasını bekle
run: |
for i in {1..30}; do
if curl -f -s "${{ steps.extract-url.outputs.url }}/health" > /dev/null; then
echo "Deployment hazır!"
break
fi
echo "Deployment bekleniyor... ($i/30)"
sleep 10
done
- name: E2E testleri çalıştır
env:
CYPRESS_BASE_URL: ${{ steps.extract-url.outputs.url }}
run: |
npm run test:e2e
- name: PR yorumunu güncelle
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Preview Environment')
);
const body = `## Preview Environment
**URL:** ${{ steps.extract-url.outputs.url }}
**Durum:** Hazır
**Commit:** \`${{ env.COMMIT_SHA }}\`
E2E testler: Başarılı
`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
}
cleanup-preview:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Kodu checkout et
uses: actions/checkout@v4
- name: AWS credentials yapılandır
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: CDK stack'i yok et
run: |
npx cdk destroy preview-pr-${{ github.event.number }} --force
- name: PR yorumunu güncelle
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Preview Environment')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: botComment.body + '\n\n**Durum:** Temizlendi'
});
}
OIDC Authentication Kurulumu#
Uzun süreli AWS credential’ları saklamak yerine, güvenli, geçici erişim için GitHub’ın OIDC provider’ını kullan:
// iam/github-oidc-role.ts
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
export class GitHubOIDCStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// GitHub Actions için OIDC provider oluştur
const provider = new iam.OpenIdConnectProvider(this, 'GitHubProvider', {
url: 'https://token.actions.githubusercontent.com',
clientIds: ['sts.amazonaws.com'],
thumbprints: ['6938fd4d98bab03faadb97b34396831e3780aea1'],
});
// GitHub Actions için IAM role
const role = new iam.Role(this, 'GitHubActionsRole', {
assumedBy: new iam.WebIdentityPrincipal(
provider.openIdConnectProviderArn,
{
StringEquals: {
'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
},
StringLike: {
'token.actions.githubusercontent.com:sub': 'repo:your-org/your-repo:*',
},
}
),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess'),
],
});
// CDK operasyonları için ek policy'ler
role.addToPolicy(new iam.PolicyStatement({
effect: iam.Effect.Allow,
actions: [
'iam:CreateRole',
'iam:DeleteRole',
'iam:AttachRolePolicy',
'iam:DetachRolePolicy',
'iam:PassRole',
],
resources: ['*'],
}));
}
}
Maliyet Optimizasyonu ve İzleme#
Kaynak Boyutlandırma#
Preview ortamlarının maliyet ile işlevsellik arasında denge kurması gerekir. İnceleme trafiğinde faturada görünen şey istek başına ücretlendirilen servisler değildir; saat başına ücretlendirilen her kaynak, incelemeci URL’i açsa da açmasa da 72 saatin tamamı için ücret işletir.
// Maliyet optimizasyon konfigürasyonu
const previewConfig = {
lambda: {
memorySize: 256, // MB - çoğu workload için yeterli
timeout: Duration.seconds(30),
reservedConcurrency: 5, // Eşzamanlı execution'ları sınırla
},
apiGateway: {
throttling: {
rateLimit: 100,
burstLimit: 200,
},
},
cloudfront: {
priceClass: cloudfront.PriceClass.PRICE_CLASS_100, // Sadece ABD ve Avrupa edge lokasyonları
defaultTtl: Duration.hours(1), // Geliştirme için kısa TTL
},
};
// 72 saatlik ortamda maliyeti belirleyen kalemler:
// - API Gateway, Lambda, Route 53 sorguları: istek/invocation başına ücretlendirilir,
// birkaç yüz inceleme isteği kuruş mertebesinde kalır
// - CloudFront: dışarı aktarılan GB başına ücretlendirilir, istek bazlı en büyük kalem
// - Sürekli açık kalan her şey (NAT gateway, provisioned concurrency, RDS instance):
// 72 saatin tamamı için faturalanır ve istek bazlı tüm kalemlerin toplamından büyüktür
// Sürekli açık kaynakları preview stack'inin dışında tut ya da sabit saatlik maliyeti kabul et
Otomatik Temizlik#
Temizlik otomasyonu maliyetin kontrolden çıkmasını önler ve ortamların birikmemesini sağlar:
// lib/cleanup-function.ts
import { CloudFormationClient, DeleteStackCommand, ListStacksCommand } from '@aws-sdk/client-cloudformation';
export const handler = async (event: any) => {
const cfn = new CloudFormationClient({});
try {
// Süresi dolmuş TTL tag'leriyle stack'leri bul
const { StackSummaries } = await cfn.send(new ListStacksCommand({
StackStatusFilter: ['CREATE_COMPLETE', 'UPDATE_COMPLETE'],
}));
const expiredStacks = StackSummaries?.filter(stack => {
if (!stack.StackName?.startsWith('preview-pr-')) return false;
const ttlTag = stack.Tags?.find(tag => tag.Key === 'TTL');
if (!ttlTag?.Value) return false;
const expiryDate = new Date(ttlTag.Value);
return expiryDate < new Date();
}) || [];
// Süresi dolmuş stack'leri sil
for (const stack of expiredStacks) {
console.log(`Süresi dolmuş stack siliniyor: ${stack.StackName}`);
await cfn.send(new DeleteStackCommand({
StackName: stack.StackName,
}));
}
return {
statusCode: 200,
body: JSON.stringify({
message: `${expiredStacks.length} süresi dolmuş stack temizlendi`,
deletedStacks: expiredStacks.map(s => s.StackName),
}),
};
} catch (error) {
console.error('Temizlik başarısız:', error);
throw error;
}
};
// Temizlik Lambda'sını EventBridge ile zamanla
import { Duration } from 'aws-cdk-lib';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const cleanupRule = new events.Rule(this, 'CleanupRule', {
schedule: events.Schedule.rate(Duration.hours(6)),
description: 'Clean up expired preview environments',
});
cleanupRule.addTarget(new targets.LambdaFunction(cleanupFunction));
E2E Testing Entegrasyonu#
Cypress Konfigürasyonu#
E2E testing’i preview ortamlarıyla entegre etmek için aşağıdaki pattern kullanılabilir:
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: process.env.CYPRESS_BASE_URL || 'http://localhost:3000',
video: true,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
requestTimeout: 15000,
responseTimeout: 15000,
setupNodeEvents(on, config) {
// Başarısızlıkta screenshot al
on('after:screenshot', (details) => {
console.log('Screenshot alındı:', details.path);
});
// Preview ortam testing için özel komutlar
on('task', {
waitForDeployment() {
// Deployment hazır olmasını beklemek için özel logic
return null;
},
});
},
},
});
// cypress/e2e/preview-environment.cy.js
describe('Preview Environment Tests', () => {
beforeEach(() => {
// Doğru ortamı test ettiğimizden emin ol
cy.visit('/');
cy.get('[data-testid="environment-indicator"]')
.should('contain', 'Preview');
});
it('should load the application correctly', () => {
cy.get('[data-testid="app-header"]').should('be.visible');
cy.get('[data-testid="main-content"]').should('be.visible');
});
it('should handle API requests', () => {
cy.intercept('GET', '/api/health').as('healthCheck');
cy.visit('/dashboard');
cy.wait('@healthCheck').then((interception) => {
expect(interception.response?.statusCode).to.equal(200);
});
});
it('should display correct environment information', () => {
cy.visit('/debug');
cy.get('[data-testid="pr-number"]')
.should('contain', Cypress.env('PR_NUMBER'));
cy.get('[data-testid="commit-sha"]')
.should('contain', Cypress.env('COMMIT_SHA'));
});
});
Güvenlik Önerileri#
Network Güvenliği#
İnternete açık preview ortamları için bu güvenlik pattern’leri iyi çalışır:
// Lambda için VPC ve security group'lar
import * as ec2 from 'aws-cdk-lib/aws-ec2';
const vpc = new ec2.Vpc(this, 'PreviewVPC', {
maxAzs: 2,
natGateways: 1, // Maliyet optimizasyonu
subnetConfiguration: [
{
cidrMask: 24,
name: 'Private',
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
{
cidrMask: 24,
name: 'Public',
subnetType: ec2.SubnetType.PUBLIC,
},
],
});
const securityGroup = new ec2.SecurityGroup(this, 'LambdaSecurityGroup', {
vpc,
description: 'Preview Lambda function'ları için security group',
allowAllOutbound: true,
});
// Inbound traffic'i kısıtla
securityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Sadece HTTPS traffic'
);
Secrets Yönetimi#
// Gizli bilgiler için Systems Manager Parameter Store
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as lambda from 'aws-cdk-lib/aws-lambda';
const dbPassword = new ssm.StringParameter(this, 'DatabasePassword', {
parameterName: `/preview/${prNumber}/database/password`,
stringValue: 'generated-secure-password',
tier: ssm.ParameterTier.STANDARD,
});
// Lambda environment variable'ları parametrelere referans verir
const appFunction = new lambda.Function(this, 'AppFunction', {
// ... diğer konfigürasyon
environment: {
DATABASE_PASSWORD_PARAM: dbPassword.parameterName,
},
});
// Lambda'ya parametreyi okuma izni ver
dbPassword.grantRead(appFunction);
Pratik Notlar#
Yaygın Tuzaklar#
DNS Propagation Gecikmeleri: Route53 değişikliklerinin yayılması 30-60 saniye sürebilir. Deployment’ları hazır olarak işaretlemeden önce health check ekleyin:
// Health check implementasyonu
const healthCheck = new route53.CfnHealthCheck(this, 'HealthCheck', {
healthCheckConfig: {
type: 'HTTPS',
resourcePath: '/health',
fullyQualifiedDomainName: previewDomain,
requestInterval: 30,
failureThreshold: 3,
},
});
Kaynak Temizlik Başarısızlıkları: CDK destroy operasyonları bazen kaynak bağımlılıkları yüzünden başarısız olur. İşe yarayan retry mekanizması:
# Gelişmiş temizlik script'i
#!/bin/bash
STACK_NAME="preview-pr-$1"
MAX_RETRIES=3
for i in $(seq 1 $MAX_RETRIES); do
echo "$STACK_NAME stack'ini yok etme denemesi $i"
if npx cdk destroy $STACK_NAME --force; then
echo "Stack başarıyla yok edildi"
exit 0
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "30 saniye sonra tekrar denenecek..."
sleep 30
fi
done
echo "$MAX_RETRIES denemeden sonra stack yok edilemedi"
exit 1
Cold Start Performance: Lambda cold start’lar ilk testlerin başarısız olmasına neden olabilir. Pre-warming bu riski azaltır:
// Lambda warmer function
const warmerFunction = new lambda.Function(this, 'Warmer', {
runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler', // fromInline her zaman index.js üretir
code: lambda.Code.fromInline(`
const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda');
const client = new LambdaClient({});
exports.handler = async () => {
await client.send(new InvokeCommand({
FunctionName: process.env.TARGET_FUNCTION,
InvocationType: 'Event',
Payload: Buffer.from(JSON.stringify({ warmer: true })),
}));
};
`),
environment: {
TARGET_FUNCTION: appFunction.functionName,
},
});
appFunction.grantInvoke(warmerFunction);
// Pre-warming'i zamanla
const warmupRule = new events.Rule(this, 'WarmupRule', {
schedule: events.Schedule.rate(Duration.minutes(5)),
});
warmupRule.addTarget(new targets.LambdaFunction(warmerFunction));
Performans Optimizasyonları#
Paralel CDK Deployment’ları: Çok sayıda eş zamanlı PR’ı olan ekipler için, birden fazla stack’i paralel deploy edin:
# Paralel deployment'lar için matrix stratejisi
strategy:
matrix:
include:
- stack: frontend
directory: packages/frontend
- stack: backend
directory: packages/backend
- stack: infrastructure
directory: infrastructure
steps:
- name: Deploy ${{ matrix.stack }}
working-directory: ${{ matrix.directory }}
run: |
npx cdk deploy preview-pr-${{ env.PR_NUMBER }}-${{ matrix.stack }} \
--require-approval never
CloudFront Caching Stratejisi: Tazelik ile performansı dengele:
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'Distribution', {
originConfigs: [{
customOriginSource: {
domainName: api.restApiId + '.execute-api.' + this.region + '.amazonaws.com',
originPath: '/prod',
},
behaviors: [
{
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL,
cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD_OPTIONS,
defaultTtl: Duration.minutes(5), // Geliştirme için kısa TTL
maxTtl: Duration.hours(1),
minTtl: Duration.seconds(0),
},
],
}],
});
İzleme ve Alarm#
Maliyet İzleme#
Bütçe sürprizlerini önlemek için PR başına harcamaları takip et:
// Preview ortamları için CloudWatch dashboard
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
const dashboard = new cloudwatch.Dashboard(this, 'PreviewDashboard', {
dashboardName: 'Preview-Environments',
widgets: [
[
new cloudwatch.GraphWidget({
title: 'Preview Ortam Maliyetleri',
left: [
new cloudwatch.Metric({
namespace: 'AWS/Billing',
metricName: 'EstimatedCharges',
dimensionsMap: {
Currency: 'USD',
},
}),
],
}),
],
[
new cloudwatch.SingleValueWidget({
title: 'Aktif Preview Ortamları',
metrics: [
new cloudwatch.Metric({
namespace: 'Custom/Preview',
metricName: 'ActiveEnvironments',
}),
],
}),
],
],
});
// Maliyet alarmı
const costAlarm = new cloudwatch.Alarm(this, 'PreviewCostAlarm', {
metric: new cloudwatch.Metric({
namespace: 'AWS/Billing',
metricName: 'EstimatedCharges',
dimensionsMap: {
Currency: 'USD',
},
}),
threshold: 50, // Aylık maliyetler $50'ı aşarsa alarm ver
evaluationPeriods: 1,
});
Deployment Başarı Takibi#
// Başarılı deploy'dan sonra çalışan workflow adımından gönderilir,
// CDK uygulamasından değil: metrik stack'i değil deployment'ı kaydeder
import { CloudWatchClient, PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';
const cloudwatchClient = new CloudWatchClient({});
await cloudwatchClient.send(new PutMetricDataCommand({
Namespace: 'Custom/Preview',
MetricData: [{
MetricName: 'DeploymentSuccess',
Value: 1,
Unit: 'Count',
Dimensions: [
{ Name: 'Environment', Value: 'preview' },
{ Name: 'PRNumber', Value: prNumber },
],
}],
}));
Bu Deseni Ne Zaman Kullanmalı#
PR başına ortam, aynı anda birden fazla pull request açıkken ve incelemeciler yerel kurulum yerine bir URL beklerken maliyetini çıkarır. Bu hacmin altında paylaşılan tek bir staging ortamı hem daha ucuz hem de hata ayıklaması daha kolaydır; yukarıdaki otomasyon da boşuna bakımı yapılan bir düzenek olur. Desenin ilk ayı atlatıp atlatamayacağını iki şey belirler. Teardown idempotent olmalı ve yeniden denenmeli, çünkü silinemeyen bir stack faturayı işletmeye devam eder ve aynı isimle yapılacak sonraki deploy’u bloklar. Sürekli açık kalan kaynaklar (NAT gateway, provisioned concurrency, veritabanı instance’ları) istek başına maliyet modelini sessizce saat başına modele çevirir; bunları PR başına stack’te değil paylaşılan bir stack’te tutmak gerekir.
Uygulamanın VPC içinde bir veritabanına ihtiyacı varsa, ortam başına veritabanı yerine tek bir paylaşılan preview veritabanı ve PR başına şema ile başlamak daha sağlıklıdır. Seeding yolu basit kalır, sabit maliyet de tek yerde toplanır.
Kaynaklar#
- AWS CDK v2 Geliştirici Kılavuzu (yeni sekmede açılır) - CDK construct’ları, stack’ler ve ortam tabanlı deployment kalıplarına yönelik resmi belgeleme.
- GitHub Actions Belgeleri (yeni sekmede açılır) - İş akışı otomasyonu, CI/CD pipeline’ları ve olay tetiklemeli deployment’lara ilişkin resmi kılavuz.
- Amazon Route 53 Geliştirici Kılavuzu (yeni sekmede açılır) - PR başına alt alan adı sağlamak için kullanılan programatik DNS kaydı yönetimi referansı.
- Amazon CloudFront Geliştirici Kılavuzu (yeni sekmede açılır) - Önizleme ortamlarında CDN dağıtım yapılandırması ve statik varlık sunumu belgeleri.
- AWS Lambda En İyi Uygulamaları (yeni sekmede açılır) - Geçici iş yükleriyle ilgili fonksiyon yapılandırması, zaman aşımları ve kaynak yönetimi için resmi rehber.
- CDK Pipelines: AWS CDK Uygulamaları için CI/CD (yeni sekmede açılır) - GitHub pull request iş akışlarıyla entegre otomatik deployment pipeline’ları oluşturma kılavuzu.
İlgili yazılar
İç servis katmanı kurmadan önce kurup kurmayacağınıza karar verin. Katmanın çağrı başına maliyeti, VPC Lattice'in kazandığı hacim ve direct invoke'un hâlâ kazandığı an.
aws · aws-cdk · lambda +4
Private REST API gRPC’yi yapısal olarak taşıyamaz ve gRPC konuşan her AWS yüzeyi Lambda hedeflerini dışlar. gRPC’den neyi tutmalı, neyi bırakmalı.
aws · aws-cdk · lambda +4
Private REST API, onu açan resource policy, route bazlı AWS_IAM yetkileri, iki CDK stack'i ve Node 22 Lambda'dan çağrıyı imzalamak.
aws · aws-cdk · lambda +4
SigV4 hangi servisin çağırdığını kanıtlar, kimin adına çağırdığını değil. Doğrulanmış özneyi nasıl taşırsınız ve taşıma katmanı gerçekte neyi şifreler.
aws · aws-cdk · lambda +4
Aynı hesapta resource policy ile çağıranın identity policy'si bir OR'dur. Hesaplar arası çağrıda AND'e döner ve sessizlik reddeder. Veri sınırında bunun sonuçları.
aws · aws-cdk · lambda +4