기존의 디스코드 봇은 주로 EC2 인스턴스를 통해 운영되었습니다.
하지만 명령어를 입력하여 동작하는 간단한 봇의 경우, EC2 인스턴스를 상시 실행하는 것은 서버 비용 부담이 클 수 있습니다.
이러한 문제를 해결하기 위해 AWS Lambda를 활용한 서버리스 방식을 이용하여 디스코드 봇을 배포하는 것을 고려하게 되었습니다.

Lambda는 서버를 직접 운영할 필요 없이, 특정 이벤트가 발생할 때만 실행되는 서버리스 컴퓨팅 서비스입니다.
Lambda를 사용하면 별도의 서버 운영 없이, 저렴한 비용으로 디스코드 봇을 24/7로 유지할 수 있습니다.
Node.js와 Python 등을 지원하는데, 저는 Python에 익숙해 Flask로 봇을 개발하였습니다.

이번 글에서는 Python과 Flask를 사용하여 Discord 봇을 개발하고, 이를 AWS CDK를 통해 Lambda에 배포하는 방법을 단계별로 설명하겠습니다.
깃허브에도 업로드 완료했습니다.
https://github.com/esc-beep/serverless-discord-bot
GitHub - esc-beep/serverless-discord-bot: DEV 서버 비용을 줄이고자 도입된 DEV 관리자 디스코드 봇
DEV 서버 비용을 줄이고자 도입된 DEV 관리자 디스코드 봇. Contribute to esc-beep/serverless-discord-bot development by creating an account on GitHub.
github.com
1. Discord 봇 만들기
Discord 개발자 포털에 디스코드 계정을 이용해 로그인합니다.

로그인에 성공했다면 "New Application"을 클릭하여 새 애플리케이션을 생성합니다.

좌측 메뉴에서 "Bot"을 클릭하고, MESSAGE CONTENT INTENT를 활성화합니다.


"OAuth2"를 선택하여 OAuth2 URL Generator로 봇을 서버에 초대합니다.
scope는 bot, bot permission은 원하는 권한을 선택합니다.
Administrator(관리자 권한)을 선택해도 되지만, 저는 최소한의 권한만 부여하고자 Send Messages를 선택했습니다.

Generate URL을 복사하고 입력하면 위의 사진과 같이 봇을 추가할 서버를 선택할 수 있습니다.
서버 관리자 권한이 있다면 봇을 추가할 수 있습니다.

봇이 서버에 추가되었습니다.
실행을 위한 코드를 작성하지 않았기 때문에 오프라인 상태입니다.
2. 봇 명령어 추가하기
봇이 지원할 Discord 명령어를 등록해야 합니다.
어떤 명령어를 인식하고, 해당 명령에 대해 어떤 입력을 받고 무엇을 수행해야 하는지 등록하는 과정입니다.
- name: hello
description: Say hello!
- name: echo
description: Echo message back to sender.
options:
- name: message
description: The message to echo back.
type: 3 # string
required: true
다음과 같이 명령어를 모두 포함한 discord_commands.yaml 파일을 생성했습니다.
hello 명령어와 echo 명령어를 설정했습니다.
echo 명령어는 message라는 string 타입 매개변수를 필수적으로 입력하도록 작성하였습니다.
파라미터에 관한 것은 아래 링크를 참고해보면 좋을 것 같습니다.
https://buddy.works/docs/yaml/yaml-actions/discord
YAML for Discord | Docs
List of YAML parameters and examples for the "Discord" action.
buddy.works
이어서 YAML 파일의 내용을 읽어 Discord 봇에게 명령으로 인식하도록 하는 register_commands.py을 작성했습니다.
import requests
import yaml
TOKEN = "본인 토큰 입력"
APPLICATION_ID = "본인 ID 입력"
URL = f"https://discord.com/api/v9/applications/{APPLICATION_ID}/commands"
with open("discord_commands.yaml", "r") as file:
yaml_content = file.read()
commands = yaml.safe_load(yaml_content)
headers = {"Authorization": f"Bot {TOKEN}", "Content-Type": "application/json"}
# Send the POST request for each command
for command in commands:
response = requests.post(URL, json=command, headers=headers)
command_name = command["name"]
print(f"Command {command_name} created: {response.status_code}")
해당 파일은 YAML 파일을 읽어온 뒤, yaml 라이브러리를 사용하여 yaml 파일의 내용을 python으로 변환합니다.
그 뒤 봇 인증 토큰을 포함한 헤더를 만들어 application에 url로 요청을 보내 명령을 등록합니다.
파일을 실행하기 위해서는 bot의 토큰과 application id가 필요합니다.


토큰은 Discord 개발자 포털의 bot에서, application id는 General Information에서 확인할 수 있습니다.
토큰을 제대로 입력했다면 로컬 cmd에서 python register_commands.py를 입력하여 명령어를 추가합니다.

제대로 실행이 되었다면 다음과 같이 추가된 명령어와 명령어에 대한 설명을 확인할 수 있습니다.
하지만 아직 명령만 추가되었을 뿐, 명령에 대한 동작은 추가되지 않았기 때문에 해당 명령을 입력하면 봇이 동작하지 않습니다.
3. 봇 엔드포인트 생성하기
디스코드 명령을 HTTP 엔드포인트를 통해 봇으로 전송할 수 있도록 flask를 통해 main.py 파일을 작성했습니다.
import os
from flask import Flask, jsonify, request
from mangum import Mangum
from asgiref.wsgi import WsgiToAsgi
from discord_interactions import verify_key_decorator
DISCORD_PUBLIC_KEY = os.environ.get("DISCORD_PUBLIC_KEY")
app = Flask(__name__)
asgi_app = WsgiToAsgi(app)
handler = Mangum(asgi_app)
@app.route("/", methods=["POST"])
async def interactions():
print(f"👉 Request: {request.json}")
raw_request = request.json
return interact(raw_request)
@verify_key_decorator(DISCORD_PUBLIC_KEY)
def interact(raw_request):
if raw_request["type"] == 1: # PING
response_data = {"type": 1} # PONG
else:
data = raw_request["data"]
command_name = data["name"]
if command_name == "hello":
message_content = "Hello there!"
elif command_name == "echo":
original_message = data["options"][0]["value"]
message_content = f"Echoing: {original_message}"
response_data = {
"type": 4,
"data": {"content": message_content},
}
return jsonify(response_data)
if __name__ == "__main__":
app.run(debug=True)
이제 interactions()를 통해 인덱스 경로와 POST 메소드를 사용하여 상호 작용 엔드포인트를 생성할 수 있습니다.
또한 interact() 함수를 통해 raw JSON을 처리하고, 응답에 따른 명령어가 작동하도록 코드를 입력했습니다.
파이썬으로 작성된 flask를 lambda가 요청을 전송할 수 있는 핸들러 함수로 변환하기 위해 mangum와 asgiref를 사용합니다.
로컬에서 main.py를 실행시킨 뒤 테스트를 진행할 수 있도록 main 부분도 추가했습니다.
또한 엔드포인트를 공개 엔드포인트로 게시하면 안전하지 않기 때문에 verify_key_decorator를 통해 요청을 보호하도록 작성했습니다.
이러한 종속성을 모두 패키징하고 람다에 호스팅하기 어렵기 때문에 Docker를 사용해 배포를 시도했습니다.
4. Docker 이미지 생성하기
배포를 위한 Dockerfile을 작성합니다.
FROM public.ecr.aws/lambda/python:3.13
# Copy requirements.txt
COPY requirements.txt ${LAMBDA_TASK_ROOT}
# Install the specified packages
RUN pip install -r requirements.txt
# Copy all files in ./app
COPY app/* ${LAMBDA_TASK_ROOT}
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "main.handler" ]
lambda의 파이썬 버전은 3.13으로 지정하고, 위의 파이썬 코드에서 사용한 flask, mangum, asgiref, discord-interactions를 requirement.txt로 작성해 필요한 요소들을 패키징하도록 작성했습니다.
실행 파일들은 람다가 실행되는 시작 위치로 복사하여 main.py에 작성된 handler이 실행됩니다.
5. Lambda에 배포하기
람다에 배포하기 위해 AWS CDK를 사용했습니다.
AWS CDK를 사용하기 위해서는 AWS CLI(명령줄 인터페이스) 설치 및 configure 작성이 필요합니다.
해당 부분에 대해서는 아래 링크를 참고하시면 좋을 것 같습니다.
[AWS] 📚 AWS CLI 설치 & 등록 방법 - 쉽고 빠르게 설명
AWS CLI (Command Line) AWS Command Line Interface는 쉘 커맨드를 사용하여 AWS 서비스와 상호 작용할 수 있는 도구이다. 우리가 브라우저로 아마존 웹 서비스 홈페이지(콘솔 홈)에 가서 서비스를 이용한 것
inpa.tistory.com
CDK로 배포하기 위해 lib 폴더에 봇을 위한 discord-bot-lambda-stack.ts를 작성합니다.
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
export class DiscordBotLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const dockerFunction = new lambda.DockerImageFunction(
this,
"DockerFunction",
{
code: lambda.DockerImageCode.fromImageAsset("./src"),
memorySize: 1024,
timeout: cdk.Duration.seconds(10),
architecture: lambda.Architecture.X86_64,
environment: {
DISCORD_PUBLIC_KEY: "본인 퍼블릭 키 입력",
},
}
);
const functionUrl = dockerFunction.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
cors: {
allowedOrigins: ["*"],
allowedMethods: [lambda.HttpMethod.ALL],
allowedHeaders: ["*"],
},
});
new cdk.CfnOutput(this, "FunctionUrl", {
value: functionUrl.url,
});
}
}
dockerFunction()에는 봇의 핵심 기능을 구현하는 lambda 함수에 대한 내용들이 들어가야 합니다.
lambda.DockerImageCode.fromImageAsset("./src")로 Dockerfile이 있는 소스 폴더를 이미지로 변환하고 이를 AWS에 업로드한 뒤 람다와 연관시킵니다.
람다 함수가 Docker 컨테이너 기반이기 때문에 실행 타임아웃을 10초로 증가시켰습니다.
저는 윈도우 환경에서 코드를 작업했기 때문에 architecture 부분에 X86_64를 작성했지만 맥이라면 해당 부분을 ARM64로 작성해야 합니다.
마지막으로 Discord 개발자 포털에서 공개 키를 가져와 입력합니다.
cdk bootstrap --region ap-northeast-2
cdk deploy
cdk 부트스트랩을 진행하고 배포하면 url이 출력된다.

해당 url을 개발자 포털의 상호작용 엔드포인트에 입력하고 저장하면 배포가 완료된다.

부록. Trouble Shooting
ERROR: Could not install packages due to an OSError: [WinError 2] 지정된 파일을 찾을 수 없습니다: 'C:\\Python312\\Scripts\\flask.exe' -> 'C:\\Python312\\Scripts\\flask.exe.deleteme'
pip 문제인 것 같아 관리자 권한으로 cmd를 열고 ``` python -m pip install --upgrade pip```를 통해 pip를 업그레이드한 뒤 다시 flask를 설치해 해결했다.
WARNING: Ignoring invalid distribution ~ip (C:\Python312\Lib\site-packages)
임시 폴더가 남아있어 cmd에서 거슬리게 나왔다.
해당 경로로 파일 관리자를 열어 직접 삭제해 해결했다.
https://seong6496.tistory.com/172
Ignoring invalid distribution -ip
cmd에서 pip으로 설치를 할 때 Ignoring invalid distribution -ip 이라는 메세지가 뜹니다. 이건 ~으로 이루어진 폴더 때문인데 임시폴더를 만들어놨는데 아직 지우지 않았거나 이름이 잘못 배정되어 있는
seong6496.tistory.com
cdk --version 'cdk' is not recognized as an internal or external command, operable program or batch file.
```npm install -g aws-cdk``` 명령어를 통해 AWS CDK를 전역으로 설치해 해결했다.
Environment aws://381491831353/ap-northeast-2 failed bootstrapping: AccessDenied: User: arn:aws:iam::381491831353:user/Infra_Choi_Eunso is not authorized to perform: cloudformation:CreateChangeSet on resource: arn:aws:cloudformation:ap-northeast-2:381491831353:stack/CDKToolkit/*
configure에 등록한 IAM이 cloudformation:CreateChangeSet 권한이 없어 에러가 발생했다. 해당 IAM에 권한을 추가하여 해결했다.
ERROR: error during connect: Head "http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/_ping": open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified. DiscordBotLambdaStack: fail: docker build --tag cdkasset-dc8aed837cb6b37b0af1671c36c5c5084812a9c6dfe15622784338f1ed962793 --platform linux/amd64 . exited with error code 1: ERROR: error during connect: Head "http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/_ping": open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.
CDK가 Docker를 사용하여 Lambda 함수를 빌드하는 과정에서 Docker가 실행되지 않아 문제가 발생했다.
Docker Desktop를 실행한 뒤 다시 ```cdk deploy```를 진행해 해결했다.
Server: ERROR: error during connect: Get "http://%2F%2F.%2Fpipe%2FdockerDesktopLinuxEngine/v1.46/info": open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified. errors pretty printing info
Docker 클라이언트는 정상적으로 실행되고 있지만, Docker 서버(데몬)가 정상적으로 실행되지 않아서 문제가 발생했다. Docker Desktop이 너무 구버전이어서 문제가 발생했는데, 업데이트를 진행한 뒤 다시 실행하니 해결됐다.