Discord бот UND_ERR_CONNECT_TIMEOUT при размещении на AWS EC2

Вопрос или проблема

Я создаю дискорд-бота, которого хочу разместить на экземплярах 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

Вот код для воспроизведения проблемы:

  1. Используйте Terraform для построения конфигурации AWS

  2. Найдите команды pull для вашего ECR

  3. Используйте эти команды, чтобы создать образ Docker и загрузить его в ECR

  4. Если задача не запускается автоматически для создания контейнера в вашем 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. Данная статья объясняет возможные причины этой ошибки и предлагает пути их устранения.

Возможные причины

  1. Настройки сети и безопасности:

    • Группы безопасности: Возможно, правила вашей группы безопасности не позволяют вашему контейнеру инициировать исходящие соединения к Discord API. Убедитесь, что исходящие правила разрешают трафик. В вашем коде группы безопасности указано разрешение на все порты и IP-адреса, что должно минимизировать эту проблему, но стоит проверить.
  2. Проблемы с маршрутами:

    • Если ваши EC2 экземпляры работают в частной подсети, убедитесь, что у вас настроены маршруты через NAT-Gateway или интернет-шлюз. Если EC2 не может получить доступ к интернету, это может привести к тайм-аутам при попытке соединиться с Discord API.
  3. Настройки прокси и брандмауэра:

    • В случаях, когда на вашем экземпляре EC2 настроен прокси-сервер или существуют ограничения брандмауэра, это может мешать установлению соединения с внешними API. Проверьте настройки прокси и убедитесь, что они не блокируют запросы к API Discord.
  4. Ресурсы и производительность:

    • Проверьте, достаточно ли ресурсов выделено для вашего контейнера ECS. Параметры cpu и memory в вашем определении задачи ECS могут быть недостаточными, что может вызывать задержки в обработке выходящих сетевых соединений.

Шаги по исправлению проблемы

  1. Проверка настроек групп безопасности:

    • Убедитесь, что у всех связанных групп безопасности разрешен исходящий трафик. Попробуйте временно разрешить весь выходящий трафик для диагностики.
  2. Настройка сети:

    • Если ваш EC2 экземпляр находится в частной подсети, добавьте NAT-Gateway и убедитесь, что все маршруты настроены правильно. Настройте маршрут, который перенаправляет трафик на NAT-Gateway, чтобы обеспечить доступ к интернету.
  3. Логгирование и мониторинг:

    • Подключите логгирование к вашему приложению и следите за логи, чтобы собрать больше информации о времени, когда возникает ошибка. Это может помочь в диагностике.
  4. Проверка подключения к интернету:

    • Создайте временный тестовый скрипт, который выполняет HTTP-запрос к публичному ресурсу (например, https://discord.com), чтобы убедиться в наличии интернет-соединения с экземпляра EC2.
  5. Оптимизация ресурсов задач ECS:

    • Попробуйте увеличить размеры ресурсов, выделенных для вашего контейнера, чтобы исключить случайные тайм-ауты, связанные с недостатком возможностей обработки.
  6. Тестирование на локальной машине:

    • Если ваши тесты на локальной машине проходят без проблем, попробуйте сравнить условия выполнения вашего кода в локальной среде и на EC2. Например, параметры сети, ОС, и любые оболочки, которые могут влиять на работу.

Заключение

Ошибки соединения при хостинге Discord-бота на AWS EC2 могут быть вызваны множеством факторы, начиная от сетевых настроек и заканчивая конфигурацией контейнеров. Используя описанные выше шаги, вы сможете диагностировать и устранить проблему UND_ERR_CONNECT_TIMEOUT. Убедитесь, что у вас есть стабильное интернет-соединение и правильно настроены все сетевые параметры, чтобы ваш бот работал эффективно и без сбоев.

Оцените материал
Добавить комментарий

Капча загружается...