# 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](https://github.com/acantril/learn-cantrill-io-labs/tree/master/aws-lex-lambda-rds), 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

```yaml
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

```yaml
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

```yaml
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
}
```

```bash
#!/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`](http://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`](http://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`](http://dkr.ecr.us-east-1.amazonaws.com/animal-grooming-repo:latest)
    

Create the SSM Parameter store entries

```bash
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`](http://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

```yaml
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

```yaml
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.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736627236902/a9adf7f3-8ac2-4e23-a14a-2a3bb01544ac.png align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736627294643/90f0e120-c287-431b-9e5b-0c8f300a8ad9.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1736627315440/25eb047c-1ea9-47aa-8981-02561d6d6558.png align="center")

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