Развертывание приложения Django на AWS с помощью Terraform. Минимальная рабочая установка


Введение

Конечной целью данного руководства является создание масштабируемой и воспроизводимой установки веб-приложения Django на AWS с помощью Terraform.

Мы будем использовать эти службы:

  • VPC в качестве виртуальной сетевой среды.
  • ECS + Fargate для запуска бессерверных контейнеров docker.
  • Балансировщик нагрузки EC2 для маршрутизации трафика.
  • Route53 для управления DNS.
  • Certificate Manager для сертификата SSL.
  • Реестр контейнеров ECR.
  • IAM для управления разрешениями AWS.
  • RDS Postgresql в качестве СУБД.
  • S3 в качестве хранилища для медиафайлов.
  • SQS в качестве рабочего бэкенда Celery.
  • CloudWatch для журналов и метрик.
  • Namecheap для регистрации доменов.
  • GitLab для размещения исходного кода.
  • GitLab CI/CD для запуска тестов, создания образов докеров и непрерывного развертывания на AWS.

Локальные зависимости:

  • Terraform v1.2.1.
  • Python v3.10.
  • Docker v20.10.14.
  • Node v16.14.2.
  • AWS CLI v2.6.1.

О Terraform

Как говорится в официальной документации Terraform

Terraform — это инструмент инфраструктуры как кода (IaC), который позволяет безопасно и эффективно создавать, изменять и версионировать инфраструктуру. Сюда входят как низкоуровневые компоненты, такие как вычислительные экземпляры, хранилища и сети, так и высокоуровневые компоненты, такие как записи DNS и функции SaaS.

По сути, проект Terraform представляет текущее состояние вашей инфраструктуры в виде набора конфигурационных файлов. Вы можете легко создавать, обновлять и удалять части вашей облачной инфраструктуры, изменяя эти файлы. После описания нового состояния инфраструктуры вы можете просмотреть план изменений и применить их. Мы создадим отдельный проект для Terraform с помощью Git, чтобы сохранить историю изменений и совместной работы в будущем.

Если вы новичок в Terraform, ознакомьтесь со статьей Введение в Terraform и пройдите руководство по началу работы.

Создание минимальной рабочей установки

В этой части мы выполним базовую настройку учетной записи AWS, создадим проект Terraform и определим ресурсы для нашего веб-приложения. В результате мы развернем приложение Django на AWS ECS. Оно будет отвечать в браузере по URL балансировщика нагрузки.

Создание проекта Django

Давайте начнем с создания приложения Django. Создайте новую папку и инициализируйте проект Django по умолчанию.

$ mkdir django-aws && cd django-aws
$ mkdir django-aws-backend && cd django-aws-backend
$ git init --initial-branch=main
$ python3.10 -m venv venv
$ . ./venv/bin/activate
(venv) $ pip install Django==3.2.13
(venv) $ django-admin startproject django_aws .
(venv) $ ./manage.py migrate
(venv) $ ./manage.py runserver
Вход в полноэкранный режим Выйдите из полноэкранного режима

Проверьте страницу приветствия Django по адресу http://127.0.0.1:8000, убедитесь, что Django запущен, и убейте сервер разработки.

Теперь мы собираемся докеризировать наше приложение. Во-первых, добавьте файл requirements.txt в проект Django:

Django==3.2.13
Войти в полноэкранный режим Выйти из полноэкранного режима

Для тестирования включите режим отладки и разрешите все хосты в settings.py.

DEBUG = True

ALLOWED_HOSTS = ['*']
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем добавьте Dockerfile в текущий каталог:

FROM python:3.10-slim-buster

# Open http port
EXPOSE 8000

ENV PYTHONUNBUFFERED 1
ENV PYTHONDONTWRITEBYTECODE 1
ENV DEBIAN_FRONTEND noninteractive

# Install pip and gunicorn web server
RUN pip install --no-cache-dir --upgrade pip
RUN pip install gunicorn==20.1.0

# Install requirements.txt
COPY requirements.txt /
RUN pip install --no-cache-dir -r /requirements.txt

# Moving application files
WORKDIR /app
COPY . /app
Войти в полноэкранный режим Выйти из полноэкранного режима

Соберите и запустите контейнер docker локально.

$ docker build . -t django-aws-backend
$ docker run -p 8000:8000 django-aws-backend gunicorn -b 0.0.0.0:8000 django_aws.wsgi:application
Войти в полноэкранный режим Выйти из полноэкранного режима

Перейдите на страницу http://127.0.0.1:8000 и убедитесь, что мы успешно собрали и запустили образ docker с приложением Django. Вы должны увидеть точно такую же страницу приветствия, как и для команды runserver.

Давайте добавим файл .gitignore:

*.sqlite3
.idea
.env
venv
.DS_Store
__pycache__
static
media
Вход в полноэкранный режим Выйти из полноэкранного режима

и зафиксируем наши изменения:

$ git add .
$ git commit -m "initial commit"
Войти в полноэкранный режим Выход из полноэкранного режима

На данный момент мы закончили с частью Django. В следующих шагах мы развернем это приложение на AWS. Но сначала нам нужно создать учетную запись AWS.

Создание учетной записи AWS

Перейдите на aws.amazon.com и создайте учетную запись. Для этого процесса потребуется ваша личная контактная информация и кредитная карта. После завершения работы войдите в AWS Console.

AWS Console — это веб-приложение для управления пользователями AWS, политиками доступа и другими ресурсами. Здесь вы можете увидеть состояние своей инфраструктуры, просмотреть журналы приложений и увидеть фактические изменения, зафиксированные Terraform.

Все, что нужно для доступа и управления облаком AWS — в одном веб-интерфейсе

Теперь нам нужно создать учетные данные для AWS CLI и Terraform. Мы создадим нового пользователя с административным доступом к учетной записи AWS. Этот пользователь сможет создавать и изменять ресурсы на вашей учетной записи AWS.

Перейдите в службу IAM, выберите вкладку «Пользователи» и нажмите «Добавить пользователя».

Введите имя пользователя и выберите опцию «Ключ доступа — программный доступ». Эта опция означает, что у вашего пользователя будет «Ключ доступа» для использования AWS API. Также этот пользователь не сможет петь в веб-консоли AWS.

Выберите вкладку «Прикрепить существующие политики напрямую» и выберите «AdministratorAccess». Затем нажмите «Далее» и пропустите шаг «Добавить метки».

Просмотрите данные пользователя и нажмите «Создать пользователя».

Вы успешно создали пользователя! Теперь вам нужно сохранить ID ключа доступа и секретный ключ доступа в безопасном месте. Будьте осторожны, сохраняя эти ключи в публичных репозиториях или других общедоступных местах. Любой, кто владеет этими ключами, может управлять вашей учетной записью AWS.

Теперь мы можем настроить AWS CLI и проверить наши учетные данные. В этом руководстве мы будем использовать регион us-east-2. Не стесняйтесь изменить его.

$ aws configure
AWS Access Key ID [None]: AKU832EUBFEFWICT
AWS Secret Access Key [None]: 5HZMEFi4ff4F4DEi24HYEsOPDNE8DYWTzCx
Default region name [us-east-2]: us-east-2
Default output format [table]: table
$ aws sts get-caller-identity
-----------------------------------------------------
|                 GetCallerIdentity                 |
+---------+-----------------------------------------+
|  Account|  947134793474                           |  <- AWS_ACCOUNT_ID
|  Arn    |  arn:aws:iam::947134793474:user/admin   |
|  UserId |  AIDJEFFEIUFBFUR245EPV                  |
+---------+-----------------------------------------+

Вход в полноэкранный режим Выйдите из полноэкранного режима

Запомните свой AWS_ACCOUNT_ID. Мы будем использовать его в следующих шагах.

Теперь мы готовы к созданию проекта Terraform!

Создание проекта Terraform

Давайте создадим новую папку django-aws/django-aws-infrastructure для нашего проекта Terraform.

cd ..
mkdir django-aws-infrastructure && cd django-aws-infrastructure
git init --initial-branch=main

Войдите в полноэкранный режим Выйдите из полноэкранного режима

Добавьте файл provider.tf:

provider "aws" {
  region = var.region
}

Войдите в полноэкранный режим Выйти из полноэкранного режима

Здесь мы определили провайдера AWS. Мы используем переменную Terraform для указания региона AWS. Давайте определим переменные region и project_name в файле variables.tf:

variable "region" {
  description = "The AWS region to create resources in."
  default     = "us-east-2"
}

variable "project_name" {
  description = "Project name to use in resource names"
  default     = "django-aws"
}

Вход в полноэкранный режим Выход из полноэкранного режима

Запустите terraform init для создания нового рабочего каталога Terraform и загрузки провайдера AWS.

Теперь мы готовы к созданию ресурсов для нашей инфраструктуры.

Ресурсы AWS

Вот план, что мы будем конфигурировать.

  • ECR
  • Сетевое взаимодействие:
    • VPC
    • Публичные и частные подсети
    • Таблицы маршрутизации
    • Интернет и NAT шлюзы
  • Балансировщик нагрузки:
    • Слушатель
    • Целевые группы
    • Группы безопасности
  • ECS:
    • Кластер
    • Определение задачи
    • Служба
    • Роли и политики IAM

Чтобы сохранить чистоту кода, в данном руководстве мы будем придерживаться этого соглашения об именовании.

ECR

Сначала мы создадим реестр Docker и разместим в нем наш образ. Создайте файл ecr.tf:

resource "aws_ecr_repository" "backend" {
  name                 = "${var.project_name}-backend"
  image_tag_mutability = "MUTABLE"
}

Вход в полноэкранный режим Выйти из полноэкранного режима

Затем запустите terraform plan. Вы увидите, что Terraform собирается создать репозиторий ECR.

Terraform will perform the following actions:

  # aws_ecr_repository.backend will be created
  + resource "aws_ecr_repository" "backend" {
      ...
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Войдите в полноэкранный режим Выйдите из полноэкранного режима

Запустите terraform apply. Вы должны снова увидеть тот же план. Введите yes, чтобы подтвердить изменения.

aws_ecr_repository.backend: Creating...
aws_ecr_repository.backend: Creation complete after 1s [id=django-aws-backend]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Вход в полноэкранный режим Выход из полноэкранного режима

Репозиторий создан. Теперь давайте запустим наш образ Django в этот новый репозиторий. Вам нужно создать образ с тегом ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/django-aws-backend:latest, авторизоваться в ECR и протолкнуть образ:

$ cd ../django-aws-backend
$ docker build . -t 947134793474.dkr.ecr.us-east-2.amazonaws.com/django-aws-backend:latest
$ aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin 947134793474.dkr.ecr.us-east-2.amazonaws.com
$ docker push 947134793474.dkr.ecr.us-east-2.amazonaws.com/django-aws-backend:latest

Вход в полноэкранный режим Выход из полноэкранного режима

Сеть

Теперь давайте создадим сеть для нашего приложения. Добавьте этот блок в файл variables.tf:

variable "availability_zones" {
  description = "Availability zones"
  default     = ["us-east-2a", "us-east-2c"]
}

Войти в полноэкранный режим Выйти из полноэкранного режима

И создайте файл network.tf со следующим содержимым:

# Production VPC
resource "aws_vpc" "prod" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
}

# Public subnets
resource "aws_subnet" "prod_public_1" {
  cidr_block        = "10.0.1.0/24"
  vpc_id            = aws_vpc.prod.id
  availability_zone = var.availability_zones[0]
  tags = {
    Name = "prod-public-1"
  }
}
resource "aws_subnet" "prod_public_2" {
  cidr_block        = "10.0.2.0/24"
  vpc_id            = aws_vpc.prod.id
  availability_zone = var.availability_zones[1]
  tags = {
    Name = "prod-public-2"
  }
}

# Private subnets
resource "aws_subnet" "prod_private_1" {
  cidr_block        = "10.0.3.0/24"
  vpc_id            = aws_vpc.prod.id
  availability_zone = var.availability_zones[0]
  tags = {
    Name = "prod-private-1"
  }
}
resource "aws_subnet" "prod_private_2" {
  cidr_block        = "10.0.4.0/24"
  vpc_id            = aws_vpc.prod.id
  availability_zone = var.availability_zones[1]
  tags = {
    Name = "prod-private-2"
  }
}

# Route tables and association with the subnets
resource "aws_route_table" "prod_public" {
  vpc_id = aws_vpc.prod.id
}
resource "aws_route_table_association" "prod_public_1" {
  route_table_id = aws_route_table.prod_public.id
  subnet_id      = aws_subnet.prod_public_1.id
}
resource "aws_route_table_association" "prod_public_2" {
  route_table_id = aws_route_table.prod_public.id
  subnet_id      = aws_subnet.prod_public_2.id
}

resource "aws_route_table" "prod_private" {
  vpc_id = aws_vpc.prod.id
}
resource "aws_route_table_association" "private_1" {
  route_table_id = aws_route_table.prod_private.id
  subnet_id      = aws_subnet.prod_private_1.id
}
resource "aws_route_table_association" "private_2" {
  route_table_id = aws_route_table.prod_private.id
  subnet_id      = aws_subnet.prod_private_2.id
}

# Internet Gateway for the public subnet
resource "aws_internet_gateway" "prod" {
  vpc_id = aws_vpc.prod.id
}
resource "aws_route" "prod_internet_gateway" {
  route_table_id         = aws_route_table.prod_public.id
  gateway_id             = aws_internet_gateway.prod.id
  destination_cidr_block = "0.0.0.0/0"
}

# NAT gateway
resource "aws_eip" "prod_nat_gateway" {
  vpc                       = true
  associate_with_private_ip = "10.0.0.5"
  depends_on                = [aws_internet_gateway.prod]
}
resource "aws_nat_gateway" "prod" {
  allocation_id = aws_eip.prod_nat_gateway.id
  subnet_id     = aws_subnet.prod_public_1.id
}
resource "aws_route" "prod_nat_gateway" {
  route_table_id         = aws_route_table.prod_private.id
  nat_gateway_id         = aws_nat_gateway.prod.id
  destination_cidr_block = "0.0.0.0/0"
}

Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь мы определили следующие ресурсы:

  • Виртуальное частное облако.
  • Публичные и частные подсети в разных зонах доступности
  • Интернет-шлюз для доступа в интернет для публичных подсетей.
  • NAT-шлюз для доступа в интернет для частных подсетей.

Запустите terraform apply для применения изменений на AWS.

Балансировщик нагрузки

Далее создайте файл load_balancer.tf со следующим содержимым:

# Application Load Balancer for production
resource "aws_lb" "prod" {
  name               = "prod"
  load_balancer_type = "application"
  internal           = false
  security_groups    = [aws_security_group.prod_lb.id]
  subnets            = [aws_subnet.prod_public_1.id, aws_subnet.prod_public_2.id]
}

# Target group for backend web application
resource "aws_lb_target_group" "prod_backend" {
  name        = "prod-backend"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.prod.id
  target_type = "ip"

  health_check {
    path                = "/"
    port                = "traffic-port"
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 2
    interval            = 5
    matcher             = "200"
  }
}

# Target listener for http:80
resource "aws_lb_listener" "prod_http" {
  load_balancer_arn = aws_lb.prod.id
  port              = "80"
  protocol          = "HTTP"
  depends_on        = [aws_lb_target_group.prod_backend]

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.prod_backend.arn
  }
}

# Allow traffic from 80 and 443 ports only
resource "aws_security_group" "prod_lb" {
  name        = "prod-lb"
  description = "Controls access to the ALB"
  vpc_id      = aws_vpc.prod.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы собираемся создать:

  • Application Load Balancer
  • LB Слушатель для получения входящих HTTP запросов.
  • Целевая группа LB для маршрутизации запросов к приложению Django.
  • Группа безопасности для контроля входящего трафика на балансировщик нагрузки.

Также мы хотим знать URL балансировщика нагрузки. Добавьте файл outputs.tf со следующим кодом и запустите terraform apply, чтобы создать балансировщик нагрузки и узнать его имя хоста.

output "prod_lb_domain" {
  value = aws_lb.prod.dns_name
}

Вход в полноэкранный режим Выйдите из полноэкранного режима

Вы должны увидеть ваш домен ALB в выводе.

Outputs:

prod_lb_hostname = "prod-57218461274.us-east-2.elb.amazonaws.com"

Войдите в полноэкранный режим Выйдите из полноэкранного режима

Зайдите на этот домен в браузере. Он должен ответить ошибкой 503 Service Temporarily Unavailable, потому что пока нет целей, связанных с целевой группой. На следующем этапе мы развернем приложение Django, которое будет доступно по этому URL.

Приложение

Наконец, мы создадим приложение ECS Service. Добавьте файл ecs.tf со следующим содержимым:

# Production cluster
resource "aws_ecs_cluster" "prod" {
  name = "prod"
}

# Backend web task definition and service
resource "aws_ecs_task_definition" "prod_backend_web" {
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 256
  memory                   = 512

  family = "backend-web"
  container_definitions = templatefile(
    "templates/backend_container.json.tpl",
    {
      region     = var.region
      name       = "prod-backend-web"
      image      = aws_ecr_repository.backend.repository_url
      command    = ["gunicorn", "-w", "3", "-b", ":8000", "django_aws.wsgi:application"]
      log_group  = aws_cloudwatch_log_group.prod_backend.name
      log_stream = aws_cloudwatch_log_stream.prod_backend_web.name
    },
  )
  execution_role_arn = aws_iam_role.ecs_task_execution.arn
  task_role_arn      = aws_iam_role.prod_backend_task.arn
}

resource "aws_ecs_service" "prod_backend_web" {
  name                               = "prod-backend-web"
  cluster                            = aws_ecs_cluster.prod.id
  task_definition                    = aws_ecs_task_definition.prod_backend_web.arn
  desired_count                      = 1
  deployment_minimum_healthy_percent = 50
  deployment_maximum_percent         = 200
  launch_type                        = "FARGATE"
  scheduling_strategy                = "REPLICA"

  load_balancer {
    target_group_arn = aws_lb_target_group.prod_backend.arn
    container_name   = "prod-backend-web"
    container_port   = 8000
  }

  network_configuration {
    security_groups  = [aws_security_group.prod_ecs_backend.id]
    subnets          = [aws_subnet.prod_private_1.id, aws_subnet.prod_private_2.id]
    assign_public_ip = false
  }
}

# Security Group
resource "aws_security_group" "prod_ecs_backend" {
  name        = "prod-ecs-backend"
  vpc_id      = aws_vpc.prod.id

  ingress {
    from_port       = 0
    to_port         = 0
    protocol        = "-1"
    security_groups = [aws_security_group.prod_lb.id]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# IAM roles and policies
resource "aws_iam_role" "prod_backend_task" {
  name = "prod-backend-task"

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

resource "aws_iam_role" "ecs_task_execution" {
  name = "ecs-task-execution"

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

resource "aws_iam_role_policy_attachment" "ecs-task-execution-role-policy-attachment" {
  role       = aws_iam_role.ecs_task_execution.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Cloudwatch Logs
resource "aws_cloudwatch_log_group" "prod_backend" {
  name              = "prod-backend"
  retention_in_days = var.ecs_prod_backend_retention_days
}

resource "aws_cloudwatch_log_stream" "prod_backend_web" {
  name           = "prod-backend-web"
  log_group_name = aws_cloudwatch_log_group.prod_backend.name
}

Вход в полноэкранный режим Выйти из полноэкранного режима

Также добавьте переменную ecs_prod_backend_retention_days в файл variables.tf:

variable "ecs_prod_backend_retention_days" {
  description = "Retention period for backend logs"
  default     = 30
}

Вход в полноэкранный режим Выйти из полноэкранного режима

добавьте определение контейнера в новый файл templates/backend_container.json.tpl и запустите terraform apply.

[
  {
    "name": "${name}",
    "image": "${image}",
    "essential": true,
    "links": [],
    "portMappings": [
      {
        "containerPort": 8000,
        "hostPort": 8000,
        "protocol": "tcp"
      }
    ],
    "command": ${jsonencode(command)},
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${log_group}",
        "awslogs-region": "${region}",
        "awslogs-stream-prefix": "${log_stream}"
      }
    }
  }
]

Вход в полноэкранный режим Выход из полноэкранного режима

Здесь мы создали:

  • Кластер ECS
  • ECS Task Definition
  • Служба ECS для запуска задач с указанным определением в кластере ECS
  • Политики IAM для разрешения доступа задач к ресурсам.
  • Группа и поток журнала Cloudwatch для сбора журналов.

Теперь перейдите в AWS Console и посмотрите на запущенную службу и задачи.

Проверьте домен балансировщика нагрузки в браузере, чтобы убедиться, что наша настройка работает. Вы должны увидеть стартовую страницу Django.

Наша установка работает, поэтому пришло время зафиксировать наши изменения в репозитории django-aws-infrastructure. Добавьте файл .gitignore и зафиксируйте изменения:

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Include override files you do wish to add to version control using negated pattern
# !example_override.tf

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

# Ignore CLI configuration files
.terraformrc
terraform.rc

.idea/
.DS_Store
.env

Вход в полноэкранный режим Выход из полноэкранного режима
$ git add .
$ git commit -m "initialize infrastructure"

Войти в полноэкранный режим Выйти из полноэкранного режима

Итог

Поздравляем! Теперь мы развернули веб-приложение Django с помощью ECS Service + Fargate. Но теперь оно работает с файловой базой данных SQLite. Этот файл будет создаваться заново при каждом перезапуске сервиса. Таким образом, наше приложение пока не может сохранять данные. В следующей статье мы подключим Django к AWS RDS PostgreSQL.

Если вам нужен технический консалтинг по вашему проекту, загляните на наш сайт или свяжитесь со мной напрямую на LinkedIn.

Оцените статью
devanswers.ru
Добавить комментарий