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

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
Build the Docker image:
docker build -t animal-grooming-app .
Tag the Docker image:
docker tag animal-grooming-app:latest <account-id>.
dkr.ecr.us-east-1.amazonaws.com/animal-grooming-repo:latest
Login to the ECR repository:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account-id>.
dkr.ecr.us-east-1.amazonaws.com
Push the Docker image to the ECR repository:
docker push <account-id>.
dkr.ecr.us-east-1.amazonaws.com/animal-grooming-repo:latest
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
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.