Serverless architecture offers a great way to build and deploy applications without managing servers. In this post, we’ll walk through the process of creating a Telegram bot using AWS Lambda for serverless execution, Terraform for infrastructure as code, and Python for the bot’s logic. We’ll also use a Lambda Layer to manage our dependencies efficiently.

Project Structure

Let’s start with our project structure:

Initial Project Structure

telegram-bot/
├── terraform/
│   ├── main.tf
│   └── variables.tf
├── src/
│   └── bot.py
├── requirements.txt
├── build_layer.sh
└── README.md

Project Structure After Building

After running our build scripts, the structure will look like this:

telegram-bot/
├── terraform/
│   ├── main.tf
│   └── variables.tf
├── src/
│   └── bot.py
├── requirements.txt
├── build_layer.sh
├── layer.zip
├── bot.zip
└── README.md

Implementing the Bot Logic

First, let’s implement the bot logic in Python. Create a bot.py file in the src folder:

import asyncio
import json
import os
import traceback

from telegram import Update
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, ContextTypes, filters

# Initialize the bot application
bot_app = ApplicationBuilder().token(os.getenv('TELEGRAM_BOT_TOKEN')).build()

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    await update.message.reply_text("Hello! I'm your friendly neighborhood AWS Lambda bot. 🤖")

async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    text = update.message.text
    response = f"You said: {text}\nI'm just a simple echo bot for now, but I have big dreams! 🌟"
    await update.message.reply_text(response)

# Add handlers
bot_app.add_handler(CommandHandler("start", start))
bot_app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))

async def tg_bot_main(bot_app, event):
    async with bot_app:
        await bot_app.process_update(
            Update.de_json(json.loads(event["body"]), bot_app.bot)
        )

def lambda_handler(event, context):
    try:
        asyncio.run(tg_bot_main(bot_app, event))
    except Exception as e:
        traceback.print_exc()
        print(e)
        return {"statusCode": 500, "body": json.dumps("Oops! Something went wrong 😅")}

    return {"statusCode": 200, "body": json.dumps("Message processed successfully")}

Creating a Lambda Layer for Dependencies

To manage our dependencies efficiently, we’ll use a Lambda Layer. First, create a requirements.txt file in your project root:

python-telegram-bot==13.7

Next, create a script named build_layer.sh in your project root to build the layer:

#!/bin/bash

# Create a directory for the layer
mkdir -p layer/python

# Install dependencies into the layer directory
pip install -r requirements.txt -t layer/python

# Create a zip file of the layer
cd layer
zip -r ../layer.zip .
cd ..

# Clean up
rm -rf layer

echo "Layer package created: layer.zip"

Make the script executable and run it:

chmod +x build_layer.sh
./build_layer.sh

Setting Up Infrastructure with Terraform

Now, let’s set up our AWS infrastructure using Terraform. Create a main.tf file in the terraform folder:

provider "aws" {
  region = "us-east-1"  
}

resource "aws_lambda_layer_version" "dependencies_layer" {
  filename   = "layer.zip"
  layer_name = "telegram-bot-dependencies"

  compatible_runtimes = ["python3.12"]
}

resource "aws_lambda_function" "telegram_bot" {
  filename      = "src/bot.zip"
  function_name = "telegram-bot"
  role          = aws_iam_role.lambda_exec.arn
  handler       = "bot.lambda_handler"
  runtime       = "python3.12"
  
  layers = [aws_lambda_layer_version.dependencies_layer.arn]

  environment {
    variables = {
      TELEGRAM_BOT_TOKEN = var.telegram_bot_token
    }
  }
}

resource "aws_lambda_function_url" "telegram_bot_url" {
  function_name      = aws_lambda_function.telegram_bot.function_name
  authorization_type = "NONE"

  cors {
    allow_credentials = true
    allow_origins     = ["*"]
    allow_methods     = ["POST"]
    allow_headers     = ["*"]
    expose_headers    = ["keep-alive", "date"]
  }
}

resource "aws_iam_role" "lambda_exec" {
  name = "telegram_bot_lambda"

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

resource "aws_iam_role_policy_attachment" "lambda_policy" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  role       = aws_iam_role.lambda_exec.name
}

output "function_url" {
  description = "Function URL for the Lambda"
  value       = aws_lambda_function_url.telegram_bot_url.function_url
}

variable "telegram_bot_token" {
  type        = string
  description = "Telegram Bot API Token"
}

Create a variables.tf file to define the Telegram bot token variable:

variable "telegram_bot_token" {
  type        = string
  description = "Telegram Bot API Token"
}

Deploying the Bot

To deploy the bot, follow these steps:

  1. Set up the Telegram bot:

    • Open Telegram and search for the BotFather
    • Start a chat and use the /newbot command to create a new bot
    • Save the API token provided by BotFather
  2. Build the Lambda Layer:

    ./build_layer.sh
    
  3. Prepare the Lambda function:

    cd src
    zip bot.zip bot.py
    cd ..
    
  4. Initialize Terraform:

    cd terraform
    terraform init
    
  5. Deploy the infrastructure:

    terraform apply -var="telegram_bot_token=YOUR_BOT_TOKEN"
    

    Replace YOUR_BOT_TOKEN with the token you got from BotFather.

  6. After the deployment is complete, use the output URL to set the webhook for your Telegram bot:

    https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook?url=<YOUR_API_GATEWAY_URL>
    

Testing the Bot

To test your bot, simply start a chat with it on Telegram and send a message. The bot should respond according to the logic implemented in bot.py.

Updating the Bot

When you need to update your bot’s code or dependencies:

  1. For code changes, update src/bot.py and rezip it:

    cd src
    zip -u bot.zip bot.py
    cd ..
    
  2. For dependency changes, update requirements.txt and rebuild the layer:

    ./build_layer.sh
    
  3. Apply the changes with Terraform:

    cd terraform
    terraform apply -var="telegram_bot_token=YOUR_BOT_TOKEN"
    

Conclusion

We’ve successfully created a serverless Telegram bot using AWS Lambda, Lambda Layers, and Terraform. This approach allows for easy scaling, maintenance, and dependency management of your bot. You can extend the bot’s functionality by modifying the Python code in the Lambda function and updating the deployment as needed.

Remember to clean up your AWS resources if you no longer need them to avoid unnecessary charges:

terraform destroy -var="telegram_bot_token=YOUR_BOT_TOKEN"