Skip to content

AWS Bedrock ve CDK ile RAG Agent Kurmak

AWS Bedrock + Knowledge Bases + OpenSearch Serverless üstüne CDK ile TypeScript kullanarak RAG agent kurmak — mimari, IAM bağlantısı, otomatik ingestion ve chat UI.

AWS, Bedrock Agent ile Knowledge Bases ve OpenSearch Serverless üçlüsünü yönetilen RAG yolu olarak konumlandırıyor: retrieval kodunu kendin yazmadan, belgeye dayalı sohbet uygulaması çıkarıyorsun. DigitalOcean rag-assistant şablonunun şeklini AWS-native servislere TypeScript ile CDK üzerinde taşıdım, uçtan uca dağıttım ve ilk koşunun düştüğünü gördüm. AWS Bedrock yönetilen RAG yığını CDK ile çalışıyor, ancak belge boşluğundan doğan iki açık ilk dağıtımını düşürecek: bölgeler arası inference profile için IAM açığı ve dağıtım anında otomatik ingestion job'un olmaması.

Mimari eşleme

Şekil, DO şablonu ile bire bir aynı. DO tarafında tek bir GenAI Agent yönetilen inference kümesi üzerinde, KBaaS de yönetilen bir vektör deposu ile birlikte geliyor. AWS tarafında aynı parçalar isimli servislere bölünmüş ve aralarını IaC ile sen kuruyorsun. Yedi servis, tek CDK yığını, 51 kaynak.

DigitalOcean ŞablonuAWS Karşılığı (bu repo)
GenAI Agent (Nemotron)Bedrock Agent + Claude Sonnet 4.5
Knowledge Base (Qwen3 embed)Bedrock KB + Titan Embed v2
KBaaS yönetilen vektör depoOpenSearch Serverless VECTORSEARCH
Guardrails (jailbreak/içerik/PII)Bedrock Guardrails (içerik + PII)
App Platform FastAPI sohbet UICloudFront + S3 SPA, Lambda Function URL
Yalnızca tor1 bölgesiHerhangi bir Bedrock bölgesi (us-east-1 kullandım)
TerraformAWS CDK (TypeScript)

Tüm yığın us-east-1'de temiz şekilde yaklaşık 757 saniyede dağıtıldı. Aşağıdaki iki açık ilk koşuda sırasıyla yüzeye çıktı.

Yığın nasıl bağlanıyor

CDK kodu üç küçük özel construct'tan oluşuyor — VectorStore, KnowledgeBase, Agent — her biri aws-cdk-lib/aws-bedrock L1 primitive'leri (CfnKnowledgeBase, CfnDataSource, CfnAgent, CfnGuardrail) ile standart S3 ve IAM üzerine kurulu. Üçüncü taraf bir CDK kütüphanesi yok. Trap'lerden önce üç somut şekil görmeye değer.

S3 bucket + seed corpus. Belge bucket'i standart bir s3.Bucket; stack destroy edilince auto-delete oluyor. BucketDeployment yerel docs-sample/ dizinindeki seed corpus'u synth anında bucket'a yüklüyor, böylece ilk dağıtım elle yükleme adımı olmadan uçtan uca çalışıyor.

ts
const documents = new s3.Bucket(this, 'Documents', {  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,  encryption: s3.BucketEncryption.S3_MANAGED,  removalPolicy: cdk.RemovalPolicy.DESTROY,  autoDeleteObjects: true,});
new s3deploy.BucketDeployment(this, 'SeedDocs', {  sources: [s3deploy.Source.asset(path.join(__dirname, '..', '..', 'docs-sample'))],  destinationBucket: documents,});

Knowledge Base + S3 data source. CfnKnowledgeBase, OpenSearch Serverless collection'a işaret ediyor ve Bedrock'un beklediği field mapping'i veriyor. CfnDataSource S3 bucket'ı bağlıyor ve sabit boyutta chunking stratejisi kuruyor — %20 örtüşmeli, 512 token'lık parçalar yoğun referans materyali için uygun.

ts
const kb = new bedrock.CfnKnowledgeBase(this, 'Kb', {  name: `${basename}-kb`,  roleArn: kbRole.roleArn,  knowledgeBaseConfiguration: {    type: 'VECTOR',    vectorKnowledgeBaseConfiguration: {      embeddingModelArn: `arn:aws:bedrock:${region}::foundation-model/amazon.titan-embed-text-v2:0`,    },  },  storageConfiguration: {    type: 'OPENSEARCH_SERVERLESS',    opensearchServerlessConfiguration: {      collectionArn: vectorStore.collectionArn,      vectorIndexName: vectorStore.indexName,      fieldMapping: {        vectorField: 'bedrock-knowledge-base-default-vector',        textField: 'AMAZON_BEDROCK_TEXT_CHUNK',        metadataField: 'AMAZON_BEDROCK_METADATA',      },    },  },});
new bedrock.CfnDataSource(this, 'S3DataSource', {  knowledgeBaseId: kb.attrKnowledgeBaseId,  name: `${basename}-s3-source`,  dataSourceConfiguration: {    type: 'S3',    s3Configuration: { bucketArn: documents.bucketArn },  },  vectorIngestionConfiguration: {    chunkingConfiguration: {      chunkingStrategy: 'FIXED_SIZE',      fixedSizeChunkingConfiguration: { maxTokens: 512, overlapPercentage: 20 },    },  },});

Guardrail. CfnGuardrail bir içerik politikası (altı filtre tipi, input/output strength) ve hassas-bilgi politikası (PII varlıkları için anonymise-veya-block aksiyonları) alıyor. Agent'a bağlamak CfnAgent üzerinde bu kaynağın attrGuardrailId'sini referans alan tek bir guardrailConfiguration bloğu.

ts
new bedrock.CfnGuardrail(this, 'Guardrail', {  name: `${basename}-guardrail`,  blockedInputMessaging: 'I cannot process that request.',  blockedOutputsMessaging: 'I cannot share that response.',  contentPolicyConfig: {    filtersConfig: [      { type: 'SEXUAL',        inputStrength: 'HIGH',   outputStrength: 'HIGH' },      { type: 'VIOLENCE',      inputStrength: 'HIGH',   outputStrength: 'HIGH' },      { type: 'HATE',          inputStrength: 'HIGH',   outputStrength: 'HIGH' },      { type: 'INSULTS',       inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' },      { type: 'MISCONDUCT',    inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' },      { type: 'PROMPT_ATTACK', inputStrength: 'HIGH',   outputStrength: 'NONE' },    ],  },  sensitiveInformationPolicyConfig: {    piiEntitiesConfig: [      { type: 'EMAIL',                       action: 'ANONYMIZE' },      { type: 'PHONE',                       action: 'ANONYMIZE' },      { type: 'CREDIT_DEBIT_CARD_NUMBER',    action: 'BLOCK' },      { type: 'US_SOCIAL_SECURITY_NUMBER',   action: 'BLOCK' },    ],  },});

Her construct'ın tam kaynağı repo'nun lib/constructs/ dizininde. Aşağıdaki trap'lerin hepsi bu şeklin içinde bir yerde duruyor.

Bölgeler arası inference profile için IAM açığı

Yola çıktığım örnek snippet'lar bedrock:InvokeModel iznini arn:aws:bedrock:${region}::foundation-model/* üzerine veriyor. Bu ARN, yığının dağıtıldığı bölgeye sabitlenmiş durumda. Agent'a verdiğim Claude Sonnet 4.5 model kimliği ise bölgeler arası inference profile formunda: us.anthropic.claude-sonnet-4-5-20250929-v1:0. us.* ailesindeki profile'lar, altta yatan çağrıyı kapasitesi olan herhangi bir bölgeye yönlendiriyor — us-east-1'den çağrıldığında havuz us-east-1, us-east-2 ve us-west-2; yönlendirilen hedef küme başka kaynak bölgelerden farklı olabilir, bu yüzden kendi senaryon için Bedrock'un inference-profile belgelerine bak. IAM kontrolünü ise çağıranın bölgesi değil, yönlendirilen bölge belirliyor.

Sonuçta dağıtım başarılı oluyor. Agent Prepare başarılı. Ingestion başarılı (Titan Embed v2 bölgesel olduğu için bu da hatayı maskeliyor). Sonra SPA'dan ilk InvokeAgent çağrısı şu yanıtla dönüyor:

"Access denied when calling Bedrock. Check your request permissions and retry the request."

Servis izi yok, hata mesajında foundation-model ARN'i yok, bölge sabitlemesine işaret eden hiçbir şey yok. AgentRole üzerinde aws iam get-role-policy çalıştırdım, foundation-model satırını taradım ve ${region} değişkeninin tek bir değere pişmiş olduğunu gördüm. Çözüm: foundation model için bölge joker karakteri ve profile'ın kendisi için açık inference-profile/* kaynağı.

ts
agentRole.addToPolicy(  new iam.PolicyStatement({    actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],    resources: [      `arn:aws:bedrock:*::foundation-model/*`,      `arn:aws:bedrock:*:${account}:inference-profile/*`,    ],  }),);

Foundation-model ARN'leri tasarım gereği hesap segmenti taşımıyor; bölge yuvasındaki *, politikayı us.* profile'ın yönlendirebileceği tüm bölgelere genişletiyor. Profile ARN'inin kendisi ise hesap segmenti içeriyor.

Bir tur fazladan kaybettiren küçük bir not: cdk deploy --hotswap yalnızca Lambda ve Step Functions kodu için iş gördüğünden, IAM-only diff'lerde "no changes" raporluyor. Politikayı gerçekten itmek için sade cdk deploy gerekiyor.

Dağıtım anında ingestion job yok

CfnKnowledgeBase ile CfnDataSource deklaratif kaynaklar. KB'yi kuruyor, S3 veri kaynağını bağlıyor. Ingestion job başlatmıyor. S3'e seed edilen korpus, biri bedrock-agent start-ingestion-job çağrısı yapana kadar OpenSearch'te sıfır vektör olarak duruyor.

Bu engel IAM açığından daha sinsi, çünkü hata bir retrieval kalitesi sorunu gibi görünüyor. Sohbet her soruya genel bir "bilgim yok" yanıtı veriyor. İlk refleks chunking'i, embedding'i ya da korpusu suçlamak oluyor. Asıl neden yığında belgeleri embed eden hiçbir şeyin tetiklenmemiş olması.

Üç tetikleyiciyi tarttım:

  • Dağıtım sonrası aws bedrock-agent start-ingestion-job koşan bir kabuk adımı. Çalışıyor, ama IaC dışında kalıyor ve sonraki dağıtımda unutuluyor.
  • EventBridge kuralı ve küçük bir Lambda. Her dağıtımda tam olarak bir kez çalışması gereken bir davranış için iki ekstra kaynak ve bir izin sıçraması demek.
  • Aynı API'yi doğrudan CloudFormation'dan çağıran bir AwsCustomResource.

AwsCustomResource'u seçtim. CloudFormation içinde tetikleniyor, izlenecek ayrı bir runtime'ı yok ve PhysicalResourceId'yi her synth'te değiştirince her dağıtımda yeniden tetikleniyor. Politika tek aksiyon ve tek ARN ile dar tutuluyor.

ts
const ingestParams = {  knowledgeBaseId: kb.attrKnowledgeBaseId,  dataSourceId: dataSource.attrDataSourceId,};const physicalIdBase = `${cdk.Stack.of(this).stackName}-ingest`;const ingestionTrigger = new cr.AwsCustomResource(this, 'IngestionTrigger', {  onCreate: {    service: 'BedrockAgent',    action: 'StartIngestionJob',    parameters: ingestParams,    physicalResourceId: cr.PhysicalResourceId.of(`${physicalIdBase}-create`),  },  onUpdate: {    service: 'BedrockAgent',    action: 'StartIngestionJob',    parameters: ingestParams,    physicalResourceId: cr.PhysicalResourceId.of(`${physicalIdBase}-${Date.now()}`),  },  policy: cr.AwsCustomResourcePolicy.fromStatements([    new iam.PolicyStatement({      actions: ['bedrock:StartIngestionJob'],      resources: [kb.attrKnowledgeBaseArn],    }),  ]),  installLatestAwsSdk: false,});ingestionTrigger.node.addDependency(seedDeployment);ingestionTrigger.node.addDependency(dataSource);

İki ayrıntı önemli. Update tarafındaki PhysicalResourceId içindeki Date.now(), her synth'i CloudFormation'a gerçek bir değişiklik gibi gösterip her dağıtımda yeniden ingestion'u zorluyor. Sonraki koşular incremental: Bedrock yalnızca yeni veya değişen S3 nesnelerini yeniden işliyor. Custom resource ayrıca korpusu seed eden BucketDeployment'a bağımlı, böylece job hiçbir zaman boş bir bucket üzerinde sıraya girmiyor.

Çağrı asenkron. CDK ingestion'un bitmesini beklemiyor; custom resource job sıraya girer girmez dönüyor. Bu testte üç küçük markdown belgesi, cdk deploy döndükten sonra yaklaşık 40 saniyede indekslendi. Bu davranış genellenebilir değil: ingestion süresi nesne sayısına, chunk boyutuna ve Bedrock'un yoğunluğuna bağlı. 40 saniyeyi tahmin değil, sağlık kontrolü olarak kullan.

Bedel

Faturayı OpenSearch Serverless domine ediyor. Collection 2 OCU minimumla çalışıyor ve indeksleme ya da sorgu olmasa bile saatlik ücret işliyor; us-east-1'de bırakılırsa ayda yaklaşık 350 USD'ye geliyor (2 OCU × 0,24 USD/OCU-saat × 730 saat). Bedrock çağrıları, Titan embedding'leri, S3, Lambda ve CloudWatch istek başına ücretlendirildiği için demo ölçekte ihmal edilebilir. Öğrenme projeleri için anlamlı tek akış: dağıt, test et, cdk destroy çalıştır, hepsi aynı saat içinde.

Kapanış

Bedrock yönetilen RAG'i değerlendiriyorsan repoyu klonla, README'yi izle ve iki açığın yukarıdaki sırayla yüzeye çıkmasını bekle: önce IAM, sonra ingestion. Bedrock yönetilen RAG, retrieval'i sıfırdan kurmaya kıyasla gerçek bir kısayol; ancak belgeler her kaynağı izole anlatıyor ve aralarındaki kabloyu sen döşüyorsun.

Bu küçük korpus üzerinde bir öğrenme raporu. Üretim ölçeğinde OCU boyutlandırma, çok kiracılı erişim desenleri ve rerank modeli entegrasyonu kapsam dışı.

Kaynaklar

İlgili Yazılar