Вопрос или проблема
Я создаю дискорд-бота, которого хочу разместить на экземплярах AWS EC2. Я в основном следую этому посту для настройки моего AWS: medium/aws-ecs-cluster-on-ec2-with-terraform.
Что не работает:
- Вызов client.login({CLIENT TOKEN}), похоже, завершает работу после 10 секунд, поэтому мой бот не запускается.
ConnectTimeoutError: Connect Timeout Error
at onConnectTimeout (/app/node_modules/undici/lib/core/connect.js:190:24)
at /app/node_modules/undici/lib/core/connect.js:133:46
at Immediate._onImmediate (/app/node_modules/undici/lib/core/connect.js:174:9)
at process.processImmediate (node:internal/timers:476:21) {
code: 'UND_ERR_CONNECT_TIMEOUT'
}
Что работает:
- Экземпляры EC2 запускаются из моей группы автошкалирования
- Кластер успешно выполняет задачу для запуска моего образа Docker на экземпляре EC2
- Поскольку задачи терпят неудачу, новые задачи создаются, чтобы занять их место <– это действительно поведение, которое я хочу сохранить, чтобы мой бот всегда был в сети
Попытки исправить:
- Отправил axios.get(“https://discord.com”): я видел на других постах, что я мог не подключиться, но это, похоже, сработало нормально.
- Открыл трафик со всех портов на мой экземпляр EC2, балансировщик нагрузки, сервис ECS (только чтобы увидеть, является ли это проблемой): я подумал, что возможно, я не получал разрешение через какой-то канал
- Проверил, что дискорд-бот может работать локально и на Docker
Вот код для воспроизведения проблемы:
-
Используйте Terraform для построения конфигурации AWS
-
Найдите команды pull для вашего ECR
-
Используйте эти команды, чтобы создать образ Docker и загрузить его в ECR
-
Если задача не запускается автоматически для создания контейнера в вашем EC2, обновите сервис ECS с принудительным развертыванием
#main.tf
terraform {
required_version = "1.9.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_default_vpc" "main" {
tags = {
Name = "main"
}
}
data "aws_region" "current" {}
data "aws_availability_zones" "available_zones" {}
resource "aws_default_subnet" "public_a" {
availability_zone = data.aws_availability_zones.available_zones.names[0]
tags = {
Name = "public subnet a"
}
}
resource "aws_default_subnet" "public_b" {
availability_zone = data.aws_availability_zones.available_zones.names[1]
tags = {
Name = "public subnet b"
}
}
resource "aws_default_subnet" "public_c" {
availability_zone = data.aws_availability_zones.available_zones.names[2]
tags = {
Name = "public subnet c"
}
}
resource "aws_subnet" "private_a" {
vpc_id = aws_default_vpc.main.id
cidr_block = "172.31.128.0/20"
map_public_ip_on_launch = "false"
availability_zone = data.aws_availability_zones.available_zones.names[0]
tags = {
Name = "private subnet a"
}
}
resource "aws_subnet" "private_b" {
vpc_id = aws_default_vpc.main.id
cidr_block = "172.31.144.0/20"
map_public_ip_on_launch = "false"
availability_zone = data.aws_availability_zones.available_zones.names[1]
tags = {
Name = "private subnet b"
}
}
resource "aws_route_table" "main_private" {
vpc_id = aws_default_vpc.main.id
tags = {
Name = "main private"
}
}
resource "aws_route_table_association" "private_a" {
subnet_id = aws_subnet.private_a.id
route_table_id = aws_route_table.main_private.id
}
resource "aws_route_table_association" "private_b" {
subnet_id = aws_subnet.private_b.id
route_table_id = aws_route_table.main_private.id
}
resource "aws_iam_role" "ecs_exec_role" {
name = "ecs-exec-role"
assume_role_policy = data.aws_iam_policy_document.assume_task_role_policy.json
}
data "aws_iam_policy_document" "assume_task_role_policy" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy_attachment" "ecs_exec_role_policy" {
role = aws_iam_role.ecs_exec_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_ecs_cluster" "mre" {
name = "mre"
}
resource "aws_security_group" "mre_sg" {
name_prefix = "mre-sg-"
vpc_id = aws_default_vpc.main.id
egress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
data "aws_ssm_parameter" "ecs_ami" {
name = "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id"
}
resource "aws_iam_role" "ecs_role" {
name_prefix = "ecs-role"
assume_role_policy = data.aws_iam_policy_document.assume_ec2_role_policy.json
}
data "aws_iam_policy_document" "assume_ec2_role_policy" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy_attachment" "ecs_role_policy" {
role = aws_iam_role.ecs_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role"
}
resource "aws_iam_instance_profile" "ecs_profile" {
name_prefix = "ecs-profile"
path = "/ecs/instance/"
role = aws_iam_role.ecs_role.name
}
resource "aws_launch_template" "mre_ec2" {
name_prefix = "mre-ec2-"
image_id = data.aws_ssm_parameter.ecs_ami.value
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.mre_sg.id]
iam_instance_profile { arn = aws_iam_instance_profile.ecs_profile.arn }
monitoring { enabled = true }
user_data = base64encode(<<-EOF
#!/bin/bash
echo ECS_CLUSTER=${aws_ecs_cluster.mre.name} >> /etc/ecs/ecs.config;
EOF
)
}
resource "aws_autoscaling_group" "mre_asg" {
name_prefix = "mre-asg-"
vpc_zone_identifier = [
aws_default_subnet.public_a.id,
aws_default_subnet.public_b.id,
aws_default_subnet.public_c.id
]
min_size = 1
max_size = 1
health_check_grace_period = 0
health_check_type = "EC2"
protect_from_scale_in = false
launch_template {
id = aws_launch_template.mre_ec2.id
version = "$Latest"
}
tag {
key = "Name"
value = "mre"
propagate_at_launch = true
}
tag {
key = "AmazonECSManaged"
value = ""
propagate_at_launch = true
}
}
resource "aws_ecs_capacity_provider" "mre" {
name = "mre-ec2"
auto_scaling_group_provider {
auto_scaling_group_arn = aws_autoscaling_group.mre_asg.arn
managed_termination_protection = "DISABLED"
managed_scaling {
maximum_scaling_step_size = 2
minimum_scaling_step_size = 1
status = "ENABLED"
target_capacity = 100
}
}
}
resource "aws_ecs_cluster_capacity_providers" "mre" {
cluster_name = aws_ecs_cluster.mre.name
capacity_providers = [aws_ecs_capacity_provider.mre.name]
default_capacity_provider_strategy {
capacity_provider = aws_ecs_capacity_provider.mre.name
base = 1
weight = 100
}
}
resource "aws_ecr_repository" "mre" {
name = "mre"
force_delete = true
}
resource "aws_iam_role" "ecs_task_role" {
name_prefix = "ecs-task-role"
assume_role_policy = data.aws_iam_policy_document.assume_task_role_policy.json
}
resource "aws_cloudwatch_log_group" "mre_lg" {
name = "/mre/mre-task"
retention_in_days = 7
}
resource "aws_ecs_task_definition" "mre" {
family = "mre-task"
task_role_arn = aws_iam_role.ecs_task_role.arn
execution_role_arn = aws_iam_role.ecs_exec_role.arn
network_mode = "awsvpc"
cpu = 256
memory = 256
container_definitions = jsonencode([{
name = "mre-task",
image = "${aws_ecr_repository.mre.repository_url}:latest",
essential = true,
portMappings = [
{
containerPort = 80
hostPort = 80
}
],
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = "${aws_cloudwatch_log_group.mre_lg.name}"
awslogs-region = "${data.aws_region.current.name}"
awslogs-stream-prefix = "mre"
}
}
}])
}
resource "aws_security_group" "mre" {
name_prefix = "mre-sg-"
description = "Allow all traffic within the VPC"
vpc_id = aws_default_vpc.main.id
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_default_vpc.main.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_ecs_service" "mre" {
name = "mre"
cluster = aws_ecs_cluster.mre.id
task_definition = aws_ecs_task_definition.mre.arn
desired_count = 1
network_configuration {
security_groups = [aws_security_group.mre.id]
subnets = [
aws_default_subnet.public_a.id,
aws_default_subnet.public_b.id,
aws_default_subnet.public_c.id
]
}
capacity_provider_strategy {
capacity_provider = aws_ecs_capacity_provider.mre.name
base = 1
weight = 100
}
load_balancer {
target_group_arn = aws_lb_target_group.mre_lb_tg.arn
container_name = "mre-task"
container_port = 80
}
lifecycle {
ignore_changes = [desired_count]
}
depends_on = [aws_lb_target_group.mre_lb_tg]
}
resource "aws_security_group" "load_balancer" {
name_prefix = "lb-sg-"
description = "security group for load balancer that allows HTTP/HTTPS port"
vpc_id = aws_default_vpc.main.id
dynamic "ingress" {
for_each = [80, 443]
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_lb" "mre_lb" {
name = "mre-lb"
load_balancer_type = "application"
subnets = [
aws_default_subnet.public_a.id,
aws_default_subnet.public_b.id,
aws_default_subnet.public_c.id
]
security_groups = [aws_security_group.load_balancer.id]
tags = {
Name = "mre-lb"
}
}
resource "aws_lb_target_group" "mre_lb_tg" {
name_prefix = "mre-tg"
vpc_id = aws_default_vpc.main.id
protocol = "HTTP"
port = 80
target_type = "ip"
health_check {
path = "/"
}
}
resource "aws_lb_listener" "mre_lb_listener" {
load_balancer_arn = aws_lb.mre_lb.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.mre_lb_tg.arn
}
}
//package.json
{
"name": "mre",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node .",
"dev": "nodemon app.js",
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"discord.js": "^14.15.2"
}
}
//app.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const discord_js_1 = require("discord.js");
// создаем клиент Discord с правильными намерениями
const client = new discord_js_1.Client({
intents: [
discord_js_1.IntentsBitField.Flags.Guilds
]
});
client.once(discord_js_1.Events.ClientReady, readyClient => {
console.log(`Готов! Вошел как ${readyClient.user.tag}`);
});
client.login({CLIENT TOKEN});
#Dockerfile
#Предварительная стадия сборки для установки зависимостей
FROM node:18-alpine AS pre-build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
#Стадия сборки
FROM node:18-alpine AS build
WORKDIR /app
COPY --from=pre-build /app/node_modules ./node_modules
COPY . .
RUN npm run build
#Стадия производства
FROM node:18-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/app.js ./app.js
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "app.js"]
Ответ или решение
Ошибка UND_ERR_CONNECT_TIMEOUT при хостинге Discord-бота на AWS EC2
Создание и развертывание Discord-бота на AWS EC2 — это сложная задача, которая иногда может сопровождаться различными ошибками. Одной из наиболее часто встречающихся проблем является тайм-аут соединения, выражающийся в ошибке UND_ERR_CONNECT_TIMEOUT
. Данная статья объясняет возможные причины этой ошибки и предлагает пути их устранения.
Возможные причины
-
Настройки сети и безопасности:
- Группы безопасности: Возможно, правила вашей группы безопасности не позволяют вашему контейнеру инициировать исходящие соединения к Discord API. Убедитесь, что исходящие правила разрешают трафик. В вашем коде группы безопасности указано разрешение на все порты и IP-адреса, что должно минимизировать эту проблему, но стоит проверить.
-
Проблемы с маршрутами:
- Если ваши EC2 экземпляры работают в частной подсети, убедитесь, что у вас настроены маршруты через NAT-Gateway или интернет-шлюз. Если EC2 не может получить доступ к интернету, это может привести к тайм-аутам при попытке соединиться с Discord API.
-
Настройки прокси и брандмауэра:
- В случаях, когда на вашем экземпляре EC2 настроен прокси-сервер или существуют ограничения брандмауэра, это может мешать установлению соединения с внешними API. Проверьте настройки прокси и убедитесь, что они не блокируют запросы к API Discord.
-
Ресурсы и производительность:
- Проверьте, достаточно ли ресурсов выделено для вашего контейнера ECS. Параметры
cpu
иmemory
в вашем определении задачи ECS могут быть недостаточными, что может вызывать задержки в обработке выходящих сетевых соединений.
- Проверьте, достаточно ли ресурсов выделено для вашего контейнера ECS. Параметры
Шаги по исправлению проблемы
-
Проверка настроек групп безопасности:
- Убедитесь, что у всех связанных групп безопасности разрешен исходящий трафик. Попробуйте временно разрешить весь выходящий трафик для диагностики.
-
Настройка сети:
- Если ваш EC2 экземпляр находится в частной подсети, добавьте NAT-Gateway и убедитесь, что все маршруты настроены правильно. Настройте маршрут, который перенаправляет трафик на NAT-Gateway, чтобы обеспечить доступ к интернету.
-
Логгирование и мониторинг:
- Подключите логгирование к вашему приложению и следите за логи, чтобы собрать больше информации о времени, когда возникает ошибка. Это может помочь в диагностике.
-
Проверка подключения к интернету:
- Создайте временный тестовый скрипт, который выполняет HTTP-запрос к публичному ресурсу (например,
https://discord.com
), чтобы убедиться в наличии интернет-соединения с экземпляра EC2.
- Создайте временный тестовый скрипт, который выполняет HTTP-запрос к публичному ресурсу (например,
-
Оптимизация ресурсов задач ECS:
- Попробуйте увеличить размеры ресурсов, выделенных для вашего контейнера, чтобы исключить случайные тайм-ауты, связанные с недостатком возможностей обработки.
-
Тестирование на локальной машине:
- Если ваши тесты на локальной машине проходят без проблем, попробуйте сравнить условия выполнения вашего кода в локальной среде и на EC2. Например, параметры сети, ОС, и любые оболочки, которые могут влиять на работу.
Заключение
Ошибки соединения при хостинге Discord-бота на AWS EC2 могут быть вызваны множеством факторы, начиная от сетевых настроек и заканчивая конфигурацией контейнеров. Используя описанные выше шаги, вы сможете диагностировать и устранить проблему UND_ERR_CONNECT_TIMEOUT
. Убедитесь, что у вас есть стабильное интернет-соединение и правильно настроены все сетевые параметры, чтобы ваш бот работал эффективно и без сбоев.