Building Intelligent Chatbots with AWS: A Hands-On Guide with Terraform

MorolakeMorolake
8 min read

Amazon Lex is a fully managed artificial intelligence (AI) service with advanced natural language models to design, build, test, and deploy AI chatbots and voice bots in applications. In this post, I’ll walk you through my experience implementing the AWS Lex-Lambda-RDS project by Adrian Cantrill, leveraging Terraform to automate infrastructure provisioning.

In this project, we’ll create an AWS Lex bot designed to help users book grooming appointments for their pets. The bot will interact with users to schedule appointments seamlessly. To support this functionality, we’ll build a Lambda function that acts as the backend for the Lex bot. This function will handle appointment logic and store the information in an Amazon RDS database. Finally, we’ll deploy a straightforward web application using the AWS App Runner service, allowing users to review and cancel their appointments with ease.

There are six stages involved:

  • Create the ECR repository and create the Lambda function

  • Create the RDS database

  • Build the Docker image, create Parameter Store entries and set up the RDS database

  • Create and configure the Lex bot

  • Deploy the application using App Runner

  • Test the bot and review the appointments

Stage 1: ECR and Lambda function

resource "aws_ecr_repository" "appointment" {
  name = "appointment"
}

resource "aws_lambda_layer_version" "appointment" {
  layer_name               = "appointment"
  filename                 = "pymysql_layer.zip"
  compatible_architectures = ["x86_64"]
  compatible_runtimes      = ["python3.8"]
}

resource "aws_lambda_function" "appointment" {
  filename      = "lambda_function.zip"
  function_name = "appointment"
  runtime       = "python3.8"
  handler       = "lambda_function.lambda_handler"
  role          = aws_iam_role.lambda-role.arn
  layers        = [aws_lambda_layer_version.appointment.arn]
}

resource "aws_iam_role" "lambda-role" {
  name = "lambda-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })

  inline_policy {
    name = "LambdaRolePolicy"
    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["ssm:GetParameters"]
          Effect   = "Allow"
          Resource = "*"
        }
      ]
    })
  }

  tags = {
    tag-key = "lambda-iam-role"
  }
}

resource "aws_lambda_permission" "lex_lambda" {
  statement_id  = "lex-bot-resource-policy"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.appointment.function_name
  principal     = "lexv2.amazonaws.com"
  source_arn = "arn:aws:lex:us-east-1:<account-id>:bot-alias/DQPPTSKBG3/TSTALIASID"
}

Stage 2: Create an RDS Database

resource "aws_db_instance" "appointment" {
  db_name             = "appointment"
  engine              = "mysql"
  engine_version      = "8.0.32"
  username            = "admin"
  password            = "qU21OXk8"
  instance_class      = "db.t3.micro"
  allocated_storage   = 20
  publicly_accessible = true
  skip_final_snapshot = true
  storage_encrypted   = true
}

Stage 3: Build the Docker image, create Parameter Store entries and set up the RDS database

Create an EC2 instance

resource "aws_instance" "appointment" {
  instance_type        = "t3.micro"
  ami                  = local.instance_ami
  user_data            = file("script.sh")
  iam_instance_profile = aws_iam_instance_profile.ec2.name
}

resource "aws_iam_role" "ec2" {
  name               = "ec2"
  assume_role_policy = data.aws_iam_policy_document.ec2_assume_role.json

  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  ]

  inline_policy {
    name   = "EC2RolePolicy"
    policy = data.aws_iam_policy_document.ec2_inline_policy.json
  }
}

data "aws_iam_policy_document" "ec2_assume_role" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
  }
}

data "aws_iam_policy_document" "ec2_inline_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ecr:GetDownloadUrlForLayer",
      "ecr:CompleteLayerUpload",
      "ecr:UploadLayerPart",
      "ecr:InitiateLayerUpload",
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:PutImage"
    ]
    resources = ["*"]
  }
}

resource "aws_iam_instance_profile" "ec2" {
  name = "ec2_instance_profile"
  role = aws_iam_role.ec2.name
}
#!/bin/bash -xe
yum install wget -y
cd /home/ec2-user/
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
pip3 install pymysql boto3
wget https://learn-cantrill-labs.s3.amazonaws.com/aws-lex-lambda-rds/app.zip
wget https://learn-cantrill-labs.s3.amazonaws.com/aws-lex-lambda-rds/db_init.py

Create the SSM Parameter store entries

resource "aws_ssm_parameter" "db-url" {
  name  = "/appointment-app/prod/db-url"
  type  = "String"
  value = "terraform-20250102200105680300000001.clacmsizwwve.us-east-1.rds.amazonaws.com"
  tier  = "Standard"
}

resource "aws_ssm_parameter" "db-user" {
  name  = "/appointment-app/prod/db-user"
  type  = "String"
  value = "admin"
  tier  = "Standard"
}

resource "aws_ssm_parameter" "db-password" {
  name  = "/appointment-app/prod/db-password"
  type  = "SecureString"
  value = "qU21OXk8"
  tier  = "Standard"
}

resource "aws_ssm_parameter" "db-database" {
  name      = "/appointment-app/prod/db-database"
  type      = "String"
  value     = "pets"
  data_type = "text"
  tier      = "Standard"
}

Set up the RDS database

Navigate to the EC2 console and select the instance created above. Right-click and select connect, select Session Manager and click Connect. In the terminal window, enter the following commands:

  • sudo su - ec2-user

  • python3 db_init.py

If the script runs successfully, you should see the following output:

  • Connected to MySQL database

  • Database created successfully

  • Table created successfully

Stage 4: Create and Configure the Lex bot

resource "aws_lexv2models_bot" "AppointmentBot" {
  name                        = "AppointmentBot"
  role_arn                    = aws_iam_role.lexv2-role.arn
  type                        = "Bot"
  idle_session_ttl_in_seconds = 60

  description = "Bot to book appointments for pet grooming"

  data_privacy {
    child_directed = false
  }
}

resource "aws_lexv2models_bot_locale" "locale" {
  bot_id                           = aws_lexv2models_bot.AppointmentBot.id
  locale_id                        = "en_US"
  n_lu_intent_confidence_threshold = 0.7
  bot_version                      = "DRAFT"

#   voice_settings {
#     voice_id = "Danielle"
#     engine   = "standard"
#   }
}

resource "aws_lexv2models_bot_version" "bot-version" {
  bot_id = aws_lexv2models_bot.AppointmentBot.id
  locale_specification = {
    (aws_lexv2models_bot_locale.locale.locale_id) = {
      source_bot_version = "DRAFT"
    }
  }
}

resource "aws_lexv2models_intent" "Book" {
  name        = "Book"
  bot_id      = aws_lexv2models_bot.AppointmentBot.id
  locale_id   = aws_lexv2models_bot_locale.locale.locale_id
  bot_version = "DRAFT"

  fulfillment_code_hook {
    enabled = true
  }

  dialog_code_hook {
    enabled = true
  }

  sample_utterance {
    utterance = "Book an appointment"
  }

  sample_utterance {
    utterance = "I want to make a animal grooming reservation"
  }

  sample_utterance {
    utterance = "Book an appointment in the animal grooming salon"
  }

  sample_utterance {
    utterance = "I want to make an appointment for {AnimalName}"
  }

  sample_utterance {
    utterance = "Schedule a grooming session for my pet"
  }

  sample_utterance {
    utterance = "I need to make an appointment for pet grooming on {ReservationDate}"
  }

  sample_utterance {
    utterance = "I want to book a pet grooming appointment for {AnimalName} on {ReservationDate} at {ReservationTime}"
  }

  confirmation_setting {
    prompt_specification {
      message_group {
        message {
          plain_text_message {
            value = "Ok, I will book an appointment for {AnimalName} on {ReservationDate} at {ReservationTime}. Does this sound good?"
          }
        }
      }
      max_retries = 3
    }

    declination_response {
      message_group {
        message {
          plain_text_message {
            value = "No worries, I will not book the appointment."
          }
        }
      }
    }
  }
}

resource "aws_lexv2models_slot" "AnimalName" {
  bot_id       = aws_lexv2models_bot.AppointmentBot.id
  bot_version  = "DRAFT"
  intent_id    = aws_lexv2models_intent.Book.intent_id
  locale_id    = aws_lexv2models_bot_locale.locale.locale_id
  name         = "AnimalName"
  slot_type_id = "AMAZON.AlphaNumeric"

  value_elicitation_setting {
    slot_constraint = "Required"
    prompt_specification {
      max_retries = 3
      message_group {
        message {
          plain_text_message {
            value = "What is the name of your pet?"
          }
        }
      }
    }
  }
}

resource "aws_lexv2models_slot" "AnimalType" {
  bot_id       = aws_lexv2models_bot.AppointmentBot.id
  bot_version  = "DRAFT"
  intent_id    = aws_lexv2models_intent.Book.intent_id
  locale_id    = aws_lexv2models_bot_locale.locale.locale_id
  name         = "AnimalType"
  slot_type_id = "AMAZON.AlphaNumeric"


  value_elicitation_setting {
    slot_constraint = "Required"
    prompt_specification {
      max_retries = 3
      message_group {
        message {
          plain_text_message {
            value = "What type of animal are you booking for?"
          }
        }
      }
    }
  }
}

resource "aws_lexv2models_slot" "ReservationDate" {
  bot_id       = aws_lexv2models_bot.AppointmentBot.id
  bot_version  = "DRAFT"
  intent_id    = aws_lexv2models_intent.Book.intent_id
  locale_id    = aws_lexv2models_bot_locale.locale.locale_id
  name         = "ReservationDate"
  slot_type_id = "AMAZON.Date"


  value_elicitation_setting {
    slot_constraint = "Required"
    prompt_specification {
      max_retries = 3
      message_group {
        message {
          plain_text_message {
            value = "What date?"
          }
        }
      }
    }
  }
}

resource "aws_lexv2models_slot" "ReservationTime" {
  bot_id       = aws_lexv2models_bot.AppointmentBot.id
  bot_version  = "DRAFT"
  intent_id    = aws_lexv2models_intent.Book.intent_id
  locale_id    = aws_lexv2models_bot_locale.locale.locale_id
  name         = "ReservationTime"
  slot_type_id = "AMAZON.Time"


  value_elicitation_setting {
    slot_constraint = "Required"
    prompt_specification {
      max_retries = 3
      message_group {
        message {
          plain_text_message {
            value = "What time?"
          }
        }
      }
    }
  }
}

resource "aws_iam_role" "lexv2-role" {
  name = "lexv2-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lexv2.amazonaws.com"
        }
      }
    ]
  })

  inline_policy {
    name = "LexV2RolePolicy"
    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["polly:SynthesizeSpeech"]
          Effect   = "Allow"
          Resource = "*"
        }
      ]
    })
  }

  tags = {
    tag-key = "lexv2-iam-role"
  }
}

resource "aws_lexv2models_slot_type" "animal_type" {
  name        = "AnimalType"
  bot_id      = aws_lexv2models_bot.AppointmentBot.id
  locale_id   = aws_lexv2models_bot_locale.locale.locale_id
  bot_version = "DRAFT"

  value_selection_setting {
    resolution_strategy = "OriginalValue"
  }

  slot_type_values {
    sample_value {
      value = "dog"
    }
  }

  slot_type_values {
    sample_value {
      value = "cat"
    }
  }

  slot_type_values {
    sample_value {
      value = "bird"
    }
  }

  slot_type_values {
    sample_value {
      value = "fish"
    }
  }
}

output "intent_id" {
  value = aws_lexv2models_intent.Book.id
}

Stage 5: Deploy the application using App Runner

resource "aws_iam_service_linked_role" "apprunner" {
  aws_service_name = "apprunner.amazonaws.com"
  description      = "Service Linked Role for App Runner"
}

resource "aws_apprunner_service" "animal-grooming" {
    depends_on = [aws_iam_service_linked_role.apprunner]

  service_name = "animal-grooming"
  source_configuration {
    # authentication_configuration {
    #   access_role_arn = aws_iam_role.apprunner-build-role.arn
    # }

    image_repository {
      image_configuration {
        port = 80
      }
      image_identifier      = "556298987240.dkr.ecr.us-east-1.amazonaws.com/appointment:latest"
      image_repository_type = "ECR_PUBLIC"
    }
    auto_deployments_enabled = false
  }

  instance_configuration {
    cpu               = 1024
    memory            = 2048
    instance_role_arn = aws_iam_role.apprunner-task-role.arn
  }

  network_configuration {
    ingress_configuration {
      is_publicly_accessible = true
    }
    egress_configuration {
      egress_type = "DEFAULT"
    }
  }
}

resource "aws_iam_role" "apprunner-task-role" {
  name = "apprunner-task-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "tasks.apprunner.amazonaws.com"
        }
      },
    ]
  })

  inline_policy {
    name = "ECSRolePolicy"
    policy = jsonencode({
      Version = "2012-10-17"
      Statement = [
        {
          Action   = ["ssm:GetParameters"]
          Effect   = "Allow"
          Resource = "*"
        }
      ]
    })
  }

  tags = {
    tag-key = "apprunner-task-iam-role"
  }
}

resource "aws_iam_role" "apprunner-build-role" {
  name = "apprunner-build-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Sid    = ""
        Principal = {
          Service = "build.apprunner.amazonaws.com"
        }
      },
    ]
  })

  managed_policy_arns = [
    "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess",
    "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
  ]

  tags = {
    tag-key = "apprunner-build-iam-role"
  }
}

Stage 6: Test the Bot and review the appointments

Click on the "Default domain" link in the "Service overview" section to open the web application. You should see a web application with no appointments.

Navigate to the Lex v2 console, select the bot, click on Intents and test.

The project repository can be found here: https://github.com/RolakeAnifowose/Lex-Lambda-RDS-Demo

0
Subscribe to my newsletter

Read articles from Morolake directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Morolake
Morolake

Quality Assurance Analyst turned DevOps Engineer. Interested in all things Cloud Computing.