diff --git a/IaC/AmazonBedRock/README.md b/IaC/AmazonBedRock/README.md new file mode 100644 index 0000000..826f3dc --- /dev/null +++ b/IaC/AmazonBedRock/README.md @@ -0,0 +1,33 @@ +# AWS BedRock and LEX2 Agents + +# *BTW: I am actively looking for a DevOps/Clod Engineer job. Please considering me !* + + + +## Overview + +This project demonstrates the use of Terraform to create AWS BedRock and LEX2 agents and establish communication between them. +Also, Lambda function will be envoked everytime if the LLM will faile to get the answer from the existed Knowledge Base (located on S3) +The goal is to automate the provisioning and configuration of these resources to facilitate seamless interaction. + +## Prerequisites +Before you begin, ensure you have the following: +- An AWS account with appropriate permissions. +- Terraform installed on your local machine. +- AWS CLI configured with your credentials. +- Basic knowledge of Terraform and AWS services. +- The S3 bucket with some materials which Knowledge Base will use as a source (RAG) + + +## Project Structure +The project directory is organized as follows: + +├── ai_lambda.tf +├── genai-lambda-package.zip +├── lex2-bot.tf +├── lex2-intent.tf +├── main.tf +├── outputs.tf +├── provider.tf +├── README.md +└── variables.tf diff --git a/IaC/AmazonBedRock/ai_lambda.tf b/IaC/AmazonBedRock/ai_lambda.tf new file mode 100644 index 0000000..5ca3460 --- /dev/null +++ b/IaC/AmazonBedRock/ai_lambda.tf @@ -0,0 +1,61 @@ +locals { + ai_function_source = "./genai-lambda-package.zip" + src_bucket = "demo-kb-usw2-lex-web-ui" +} + +resource "aws_s3_object" "ai_function" { + bucket = local.src_bucket + key = "${filemd5(local.ai_function_source)}.zip" + source = local.ai_function_source +} + +module "ai_lambda_function" { + source = "terraform-aws-modules/lambda/aws" + + function_name = "genai-demo-lambda" + description = "lambda function to interact with AI" + handler = "index.lambda_handler" + runtime = "python3.12" + timeout = "240" + + publish = true + environment_variables = { + PROJECT = "vpatoka-poc", + FallbackIntent = "genai-demo-lambda" + } + + create_package = false + s3_existing_package = { + bucket = local.src_bucket + key = aws_s3_object.ai_function.id + } + + attach_policy_statements = true + policy_statements = { + cloud_watch = { + effect = "Allow", + actions = ["cloudwatch:PutMetricData"], + resources = ["*"] + }, + lambda = { + effect = "Allow", + actions = ["lambda:InvokeFunction"], + resources = ["*"] + }, + ai = { + effect = "Allow", + actions = ["bedrock:InvokeModel"], + resources = ["arn:aws:bedrock:*::foundation-model/*"] + } + } +} + +# Gives an external source Lex permission to access the Lambda function. +# We need our bot to be able to invoke a lambda function when +# we attempt to fulfill our intent. +resource "aws_lambda_permission" "allow_lex" { + statement_id = "AllowExecutionFromLex" + action = "lambda:InvokeFunction" + function_name = "${module.ai_lambda_function.lambda_function_name}" + principal = "lex.amazonaws.com" +} diff --git a/IaC/AmazonBedRock/genai-lambda-package.zip b/IaC/AmazonBedRock/genai-lambda-package.zip new file mode 100644 index 0000000..b0fd159 Binary files /dev/null and b/IaC/AmazonBedRock/genai-lambda-package.zip differ diff --git a/IaC/AmazonBedRock/lex2-bot.tf b/IaC/AmazonBedRock/lex2-bot.tf new file mode 100644 index 0000000..b09af23 --- /dev/null +++ b/IaC/AmazonBedRock/lex2-bot.tf @@ -0,0 +1,109 @@ +data "aws_partition" "current" {} + +resource "aws_iam_role" "VladsBot" { + name = "vpatoka-ai-poc" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "lexv2.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_iam_role_policy" "special_lex2_policy" { + name = "SpecialAmazoLex2Policy_${var.kb_name}" + role = aws_iam_role.VladsBot.name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ "iam:AttachRolePolicy", "iam:PutRolePolicy", "iam:GetRolePolicy" ] + Effect = "Allow" + Resource = "arn:${local.partition}:iam::*:role/aws-service-role/lexv2.amazonaws.com/AWSServiceRoleForLexBots*" + }, + { + Action = "iam:ListRoles" + Effect = "Allow" + Resource = "*" + }, + { + #Sid = "Permissions to invoke Lambda + Action = "lambda:InvokeFunction" + Effect = "Allow" + Resource = "*" + }, + { + #Sid = "Permissions to invoke Amazon Bedrock foundation models" + Action = "bedrock:InvokeModel" + Effect = "Allow" + #Resource = data.aws_bedrock_foundation_model.kb.model_arn + Resource = "arn:${local.partition}:bedrock:${local.region}::foundation-model/${var.response_foundation_model}" + }, + { + #Sid = "Permissions to access knowledge base in Amazon Bedrock" + Action = "bedrock:Retrieve" + Effect = "Allow" + Resource = "arn:${local.partition}:bedrock:*:${local.account_id}:knowledge-base/*" + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "VladsBot" { + role = aws_iam_role.VladsBot.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonLexFullAccess" +} + +resource "aws_lexv2models_bot" "VladsBot" { + name = "vpatoka-ai-poc" + idle_session_ttl_in_seconds = 300 + role_arn = aws_iam_role.VladsBot.arn + + data_privacy { + child_directed = false + } +} + +resource "aws_lexv2models_bot_locale" "VladsBot" { + locale_id = "en_US" + bot_id = aws_lexv2models_bot.VladsBot.id + bot_version = "DRAFT" + n_lu_intent_confidence_threshold = 0.4 + + voice_settings { + voice_id = "Danielle" + engine = "neural" + } +} + +resource "aws_lexv2models_bot_version" "VladsBot" { + bot_id = aws_lexv2models_bot.VladsBot.id + locale_specification = { + (aws_lexv2models_bot_locale.VladsBot.locale_id) = { + source_bot_version = "DRAFT" + } + } +} + + +/* Alternative way to create LEX v2 Bot +Another option is to use the AWS CLI in Terraform through local execution provisioners. +While this method can work effectively, it requires the AWS CLI to be installed on the host executing Terraform, +which might introduce additional dependencies. +*/ + +/* +resource "null_resource" "create-endpoint" { + provisioner "local-exec" { + command = " aws lexv2-models create-bot --bot-name "vpatoka-ai-poc" --role-arn --data-privacy --cli-input-json file://" + } +} + +*/ diff --git a/IaC/AmazonBedRock/lex2-intent.tf b/IaC/AmazonBedRock/lex2-intent.tf new file mode 100644 index 0000000..dc0475c --- /dev/null +++ b/IaC/AmazonBedRock/lex2-intent.tf @@ -0,0 +1,70 @@ +# Generic Custom Intent needs to be presented +resource "aws_lexv2models_intent" "Newintent" { + bot_id = aws_lexv2models_bot.VladsBot.id + bot_version = aws_lexv2models_bot_locale.VladsBot.bot_version + name = "Newintent" + locale_id = aws_lexv2models_bot_locale.VladsBot.locale_id + + #dialog_code_hook { + # enabled = true + #} + + sample_utterance { + utterance = "Hello" + } + sample_utterance { + utterance = "Howdy" + } + sample_utterance { + utterance = "Hi" + } + sample_utterance { + utterance = "Bonjour" + } +} + +/* +Amazon Lex V2 offers a built-in AMAZON.QnAIntent that you can add to your bot. +This intent harnesses generative AI capabilities from Amazon Bedrock by recognizing customer questions and +searching for an answer from the following knowledge stores +(for example, Can you provide me details on the baggage limits for my international flight?). +This feature reduces the need to configure questions and answers using task-oriented dialogue within Amazon Lex V2 intents. +This intent also recognizes follow-up questions +(for example, What about domestic flight?) based on the conversation history and provides the answer accordingly. +*/ +#resource "aws_lexv2models_intent" "QnAIntent" { +# bot_id = aws_lexv2models_bot.VladsBot.id +# bot_version = aws_lexv2models_bot_locale.VladsBot.bot_version +# name = "QnAIntent" +# locale_id = aws_lexv2models_bot_locale.VladsBot.locale_id +# +# parent_intent_signature = "AMAZON.QnAIntent" +# +# QnAIntentConfiguration { +# dataSourceConfiguration = { +# bedrockKnowledgeStoreConfiguration = { +# "bedrockKnowledgeBaseArn" = "${aws_bedrockagent_knowledge_base.demo_kb.arn}" +# } +# } +# } +# +#} + +# Update Bot with tweaked intents and alias to use Lambda +resource "null_resource" "VladsBot" { + depends_on = [aws_lexv2models_bot_version.VladsBot] + + triggers = { + bot_id = aws_lexv2models_bot.VladsBot.id + locale_id = aws_lexv2models_bot_locale.VladsBot.locale_id + latest_version = aws_lexv2models_bot_version.VladsBot.bot_version + lambda_arn = "${module.ai_lambda_function.lambda_function_arn}" + + always_run = timestamp() + } + + provisioner "local-exec" { + #command = "./bot_update.sh ${aws_lexv2models_bot.VladsBot.id} ${aws_lexv2models_bot_locale.VladsBot.locale_id} ${aws_lexv2models_bot_version.VladsBot.bot_version} '${module.ai_lambda_function.lambda_function_arn}'" + command = "./bot_update.sh ${aws_lexv2models_bot.VladsBot.id} ${aws_lexv2models_bot_locale.VladsBot.locale_id} 'DRAFT' '${module.ai_lambda_function.lambda_function_arn}' ${aws_bedrockagent_knowledge_base.demo_kb.arn}" + } +} diff --git a/IaC/AmazonBedRock/main.tf b/IaC/AmazonBedRock/main.tf new file mode 100644 index 0000000..63bc411 --- /dev/null +++ b/IaC/AmazonBedRock/main.tf @@ -0,0 +1,382 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.48" + } + opensearch = { + source = "opensearch-project/opensearch" + version = "= 2.2.0" + } + } + required_version = "~> 1.5" +} + +# Use data sources to get common information about the environment +data "aws_caller_identity" "this" {} +data "aws_partition" "this" {} +data "aws_region" "this" {} + +data "aws_bedrock_foundation_model" "agent" { + model_id = var.agent_model_id +} + +# With the service role in place, we can now proceed to define the corresponding IAM policy. +data "aws_bedrock_foundation_model" "kb" { + model_id = var.kb_model_id +} + +locals { + account_id = data.aws_caller_identity.this.account_id + partition = data.aws_partition.this.partition + region = data.aws_region.this.name + region_name_tokenized = split("-", local.region) + region_short = "${substr(local.region_name_tokenized[0], 0, 2)}${substr(local.region_name_tokenized[1], 0, 1)}${local.region_name_tokenized[2]}" +} + +locals { + lambda_ssm_param = "arn:aws:ssm:us-west-2:123456789012:parameter/app/vecdb/temp-creds" + + common_tags = { + Description = "Vlad's Retrieval-Augmented Generationi tests" + CreatedBy = "Vlad Patoka" + Terraform = "true" + TerraformStack = local.stack_name + } +} + +# Vlad's AWS BedRock + +# Knowledge base resource role +resource "aws_iam_role" "bedrock_kb_demo_kb" { + name = "AmazonBedrockExecutionRoleForKnowledgeBase_${var.kb_name}" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "bedrock.amazonaws.com" + } + Condition = { + StringEquals = { + "aws:SourceAccount" = local.account_id + } + ArnLike = { + "aws:SourceArn" = "arn:${local.partition}:bedrock:${local.region}:${local.account_id}:knowledge-base/*" + } + } + } + ] + }) +} + +resource "aws_iam_role_policy" "bedrock_kb_demo_kb_model" { + name = "AmazonBedrockFoundationModelPolicyForKnowledgeBase_${var.kb_name}" + role = aws_iam_role.bedrock_kb_demo_kb.name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + #Sid = "Permissions to invoke Amazon Bedrock foundation models" + Action = "bedrock:InvokeModel" + Effect = "Allow" + Resource = data.aws_bedrock_foundation_model.kb.model_arn + }, + { + #Sid = "Permissions to access knowledge base in Amazon Bedrock" + Action = "bedrock:Retrieve" + Effect = "Allow" + Resource = "arn:${local.partition}:bedrock:${local.region}:${local.account_id}:knowledge-base/*" + } + ] + }) +} + +/* +We create the Amazon S3 bucket that acts as the data source for the knowledge base +using the aws_s3_bucket resource. To adhere to security best practices, we also enable S3-SSE +*/ + +# S3 bucket for the knowledge base +resource "aws_s3_bucket" "demo_kb" { + bucket = "${var.kb_s3_bucket_name_prefix}-${local.region_short}-${local.account_id}" + force_destroy = true +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "demo_kb" { + bucket = aws_s3_bucket.demo_kb.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +resource "aws_s3_bucket_versioning" "demo_kb" { + bucket = aws_s3_bucket.demo_kb.id + versioning_configuration { + status = "Enabled" + } + depends_on = [aws_s3_bucket_server_side_encryption_configuration.demo_kb] +} + +# Now that the S3 bucket is available, we can create the IAM policy that gives +resource "aws_iam_role_policy" "bedrock_kb_demo_kb_s3" { + name = "AmazonBedrockS3PolicyForKnowledgeBase_${var.kb_name}" + role = aws_iam_role.bedrock_kb_demo_kb.name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "S3ListBucketStatement" + Action = "s3:ListBucket" + Effect = "Allow" + Resource = aws_s3_bucket.demo_kb.arn + Condition = { + StringEquals = { + "aws:PrincipalAccount" = local.account_id + } + } }, + { + Sid = "S3GetObjectStatement" + Action = "s3:GetObject" + Effect = "Allow" + Resource = "${aws_s3_bucket.demo_kb.arn}/*" + Condition = { + StringEquals = { + "aws:PrincipalAccount" = local.account_id + } + } + } + ] + }) +} + +/* +This data access policy provides read and write permissions to the vector +search collection and its indices to the knowledge base execution role and the creator of the policy. + +Note that aoss:DeleteIndex was added to the list because this is required for cleanup by Terraform via terraform destroy. +*/ +resource "aws_opensearchserverless_access_policy" "demo_kb" { + name = var.kb_oss_collection_name + type = "data" + policy = jsonencode([ + { + Rules = [ + { + ResourceType = "index" + Resource = [ + "index/${var.kb_oss_collection_name}/*" + ] + Permission = [ + "aoss:CreateIndex", + "aoss:DeleteIndex", # Required for Terraform + "aoss:DescribeIndex", + "aoss:ReadDocument", + "aoss:UpdateIndex", + "aoss:WriteDocument" + ] + }, + { + ResourceType = "collection" + Resource = [ + "collection/${var.kb_oss_collection_name}" + ] + Permission = [ + "aoss:CreateCollectionItems", + "aoss:DescribeCollectionItems", + "aoss:UpdateCollectionItems" + ] + } + ], + Principal = [ + aws_iam_role.bedrock_kb_demo_kb.arn, + data.aws_caller_identity.this.arn + ] + } + ]) +} + +# This encryption policy simply assigns an AWS-owned key to the vector search collection +resource "aws_opensearchserverless_security_policy" "demo_kb_encryption" { + name = var.kb_oss_collection_name + type = "encryption" + policy = jsonencode({ + Rules = [ + { + Resource = [ + "collection/${var.kb_oss_collection_name}" + ] + ResourceType = "collection" + } + ], + AWSOwnedKey = true + }) +} + +/* +We need a network policy which defines whether a collection is accessible publicly or privately + +This network policy allows public access to the vector search collection's API endpoint +and dashboard over the internet +*/ +resource "aws_opensearchserverless_security_policy" "demo_kb_network" { + name = var.kb_oss_collection_name + type = "network" + policy = jsonencode([ + { + Rules = [ + { + ResourceType = "collection" + Resource = [ + "collection/${var.kb_oss_collection_name}" + ] + }, + { + ResourceType = "dashboard" + Resource = [ + "collection/${var.kb_oss_collection_name}" + ] + } + ] + AllowFromPublic = true + } + ]) +} + +# Creating the collection +resource "aws_opensearchserverless_collection" "demo_kb" { + name = var.kb_oss_collection_name + type = "VECTORSEARCH" + depends_on = [ + aws_opensearchserverless_access_policy.demo_kb, + aws_opensearchserverless_security_policy.demo_kb_encryption, + aws_opensearchserverless_security_policy.demo_kb_network + ] +} + +# The knowledge base service role also needs access to the collection +resource "aws_iam_role_policy" "bedrock_kb_demo_kb_oss" { + name = "AmazonBedrockOSSPolicyForKnowledgeBase_${var.kb_name}" + role = aws_iam_role.bedrock_kb_demo_kb.name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "aoss:APIAccessAll" + Effect = "Allow" + Resource = aws_opensearchserverless_collection.demo_kb.arn + } + ] + }) +} + +/* +Creating the index in Terraform is however more complex, since it is not an AWS resource but an OpenSearch construct +*/ +provider "opensearch" { + url = aws_opensearchserverless_collection.demo_kb.collection_endpoint + healthcheck = false +} + +/* +We can create the index using the opensearch_index resource +Note that the dimension is set to 1536, which is the value required for the Titan G1 Embeddings - Text model. +*/ +resource "opensearch_index" "demo_kb" { + name = "bedrock-knowledge-base-default-index" + number_of_shards = "2" + number_of_replicas = "0" + index_knn = true + index_knn_algo_param_ef_search = "512" + mappings = <<-EOF + { + "properties": { + "bedrock-knowledge-base-default-vector": { + "type": "knn_vector", + "dimension": 1536, + "method": { + "name": "hnsw", + "engine": "faiss", + "parameters": { + "m": 16, + "ef_construction": 512 + }, + "space_type": "l2" + } + }, + "AMAZON_BEDROCK_METADATA": { + "type": "text", + "index": "false" + }, + "AMAZON_BEDROCK_TEXT_CHUNK": { + "type": "text", + "index": "true" + } + } + } + EOF + force_destroy = true + depends_on = [aws_opensearchserverless_collection.demo_kb] +} + +/* +Since Terraform creates resources in quick succession, there is a chance +that the configuration of the knowledge base service role is not propagated +across AWS endpoints before it is used by the knowledge base during its creation, +resulting in temporary permission issues. +*/ +resource "time_sleep" "aws_iam_role_policy_bedrock_kb_demo_kb_oss" { + create_duration = "20s" + depends_on = [aws_iam_role_policy.bedrock_kb_demo_kb_oss] +} + +/* +Creating the knowledge base +*/ +resource "aws_bedrockagent_knowledge_base" "demo_kb" { + name = var.kb_name + role_arn = aws_iam_role.bedrock_kb_demo_kb.arn + knowledge_base_configuration { + vector_knowledge_base_configuration { + embedding_model_arn = data.aws_bedrock_foundation_model.kb.model_arn + } + type = "VECTOR" + } + storage_configuration { + type = "OPENSEARCH_SERVERLESS" + opensearch_serverless_configuration { + collection_arn = aws_opensearchserverless_collection.demo_kb.arn + vector_index_name = "bedrock-knowledge-base-default-index" + field_mapping { + vector_field = "bedrock-knowledge-base-default-vector" + text_field = "AMAZON_BEDROCK_TEXT_CHUNK" + metadata_field = "AMAZON_BEDROCK_METADATA" + } + } + } + depends_on = [ + aws_iam_role_policy.bedrock_kb_demo_kb_model, + aws_iam_role_policy.bedrock_kb_demo_kb_s3, + opensearch_index.demo_kb, + time_sleep.aws_iam_role_policy_bedrock_kb_demo_kb_oss + ] +} + +/* +We also need to add the data source to the knowledge base +*/ +resource "aws_bedrockagent_data_source" "demo_kb" { + knowledge_base_id = aws_bedrockagent_knowledge_base.demo_kb.id + name = "${var.kb_name}DataSource" + data_source_configuration { + type = "S3" + s3_configuration { + bucket_arn = aws_s3_bucket.demo_kb.arn + } + } +} + diff --git a/IaC/AmazonBedRock/outputs.tf b/IaC/AmazonBedRock/outputs.tf new file mode 100644 index 0000000..52276cc --- /dev/null +++ b/IaC/AmazonBedRock/outputs.tf @@ -0,0 +1,4 @@ +output "kb_id" { + value = aws_bedrockagent_knowledge_base.demo_kb.id +} + diff --git a/IaC/AmazonBedRock/provider.tf b/IaC/AmazonBedRock/provider.tf new file mode 100644 index 0000000..ac662d3 --- /dev/null +++ b/IaC/AmazonBedRock/provider.tf @@ -0,0 +1,21 @@ +# Define provider and region +provider "aws" { + region = "us-west-2" + + default_tags { + tags = { + Terraform = "true" + CreatedBy = "Vlad Patoka" + Project = "vpatoka-ai-poc" + } + } +} + +terraform { + backend "s3" { + bucket = "genai-tf-state" + key = "terraform.tfstate" + region = "us-west-2" + } + required_version = "~> 1.5" +} diff --git a/IaC/AmazonBedRock/variables.tf b/IaC/AmazonBedRock/variables.tf new file mode 100644 index 0000000..6e419e5 --- /dev/null +++ b/IaC/AmazonBedRock/variables.tf @@ -0,0 +1,43 @@ +variable "kb_s3_bucket_name_prefix" { + description = "The name prefix of the S3 bucket for the data source of the knowledge base." + type = string + default = "demo-kb" +} + +variable "kb_oss_collection_name" { + description = "The name of the OSS collection for the knowledge base." + type = string + default = "bedrock-knowledge-base-demo-kb" +} + +variable "kb_model_id" { + description = "The ID of the foundational model used by the knowledge base." + type = string + default = "amazon.titan-embed-text-v1" +} + +variable "kb_name" { + description = "The knowledge base name." + type = string + default = "DemoKB" +} + +variable "agent_model_id" { + description = "The ID of the foundational model used by the agent." + type = string + default = "anthropic.claude-3-haiku-20240307-v1:0" +} + +variable "action_group_desc" { + description = "The action group description." + type = string + default = "The action group description" +} + +variable "response_foundation_model" { + description = "The response foundation model." + type = string + default = "anthropic.claude-3-haiku-20240307-v1:0" +} + +