- Обзор
- Простой сервер
- Настройка окружения
- Создайте учетную запись AWS
- Создайте роль для terraform с правами доступа
- Создайте ~/.aws/config и ~/.aws/credentials для использования профилей
- Установите terraform
- Docker
- Настройка инфраструктуры
- Настройка сети
- Создание образа docker
- Настройка приложения в EC2
- Предоставление ресурсов
- Послесловие
Обзор
В этой статье мы развернем простой сервис Golang на эластичном облачном вычислении (EC2) в AWS. Поскольку мы развертываем приложение в виде образа docker, мы можем аналогичным образом запустить практически любое приложение внутри контейнера docker.
Мы не будем развертывать высокодоступный и надежный проект. Цель статьи — познакомиться с terraform как инструментом инфраструктуры как кода (IaaC) и с тем, как развернуть простое приложение на AWS.
Весь код можно найти в репозитории GitHub. Мы будем использовать его для изучения кода terraform в других разделах статьи.
Простой сервер
Мы не будем глубоко погружаться в приложение Для создания мы будем использовать простейший сервер gin API.
router.GET("/", server.CounterHandler)
router.GET("/health", server.HealthHandler)
// HealthHandler returns a success message with code 200 when the server is running
func (s *Server) HealthHandler(ctx *gin.Context) {
ctx.JSON(200, gin.H{"success": true})
}
// CounterHandler calculates number of requests and return a json with counter number
func (s *Server) CounterHandler(ctx *gin.Context) {
counter := atomic.AddInt64(&s.counter, 1)
ctx.JSON(200, gin.H{"counter": counter})
}The implementation of handlers
Настройка окружения
Прежде чем приступить к созданию инфраструктуры, нам необходимо настроить все инструменты, которые мы будем использовать: AWS аккаунт, terraform и docker.
Создайте учетную запись AWS
Если у вас нет учетной записи на AWS, вам необходимо сначала создать ее. Инструкцию можно найти в официальном руководстве AWS.
Создайте роль для terraform с правами доступа
Использовать корневого пользователя AWS для чего-либо, кроме критически важных задач управления учетной записью, крайне нежелательно, поэтому мы создадим нового пользователя внутри учетной записи AWS, которую terraform будет использовать для создания инфраструктуры. Это можно сделать с помощью службы управления идентификацией и доступом (IAM).
После этого мы можем создать нового пользователя.
Укажите имя пользователя и выберите для него только программный доступ, так как он будет использоваться только в terraform.
Добавьте пользователю группу admin
с помощью поиска, чтобы terraform мог предоставлять все ресурсы.
Примечание: Одним из лучших методов обеспечения безопасности является принцип наименьших привилегий, когда каждому сервису и пользователю должны быть предоставлены только необходимые разрешения. В данном примере для простоты мы предоставляем пользователю полный контроль администратора.
Для шага 3, где мы устанавливаем теги, мы можем установить project: go-example
или что-то подобное. Теги в AWS в основном используются для фильтрации и группировки ресурсов и сбора аналитики, например, затрат.
Шаг 4 — это валидация, где мы можем проверить все данные.
На шаге 5 нам нужно сохранить ID ключа доступа и секретный ключ доступа. Они предоставят нам программный доступ к учетной записи AWS.
Примечание: Секретный ключ доступа мы увидим только один раз, поэтому его следует хранить в надежном и безопасном месте.
Создайте ~/.aws/config и ~/.aws/credentials для использования профилей
Чтобы использовать профиль AWS, нам необходимо его настроить. Конфигурация AWS хранится в каталоге ~/.aws
и состоит из двух важных файлов:
~/.aws/config
— хранит конфигурацию AWS. Мы будем хранить профиль с именем go-example
в файле config, и он должен выглядеть следующим образом:
[profile go-example]
region=eu-central-1
output=json
~/.aws/credentials
— хранит учетные данные для профилей. Мы должны создать этот файл и скопировать туда ключи доступа и секретные ключи.
[go-example]
aws_access_key_id=<ACCESS_KEY>
aws_secret_access_key=<SECRET_KEY>
Установите terraform
Для развертывания мы будем использовать Terraform. Для установки terraform следуйте официальной инструкции.
Совет: Для более реального использования проекта удобнее использовать https://github.com/tfutils/tfenv, так как он может управлять несколькими версиями Terraform для многих проектов.
Docker
Для создания образов docker нам необходимо установить docker, используя официальную инструкцию.
Настройка инфраструктуры
Существует несколько возможностей развернуть приложение на AWS: с помощью AWS Elastic Beanstalk, AWS Elastic container service (ECS) или непосредственно на экземпляре EC2. Мы будем использовать последний подход и развернем приложение на системе, а для простоты обернем его докером.
Мы собираемся
- Настроим виртуальное частное облако (VPC), интернет-шлюз (IG), список контроля доступа к сети (NACL) и группы безопасности для защиты сетевого доступа к нашему экземпляру.
- Создадим образ docker для нашего приложения и загрузим его в elastic container registry (ECR), который является реестром AWS для образов (аналогично хабу Docker).
- Создайте экземпляр EC2 и запустите в нем наше приложение как docker.
В этом случае мы можем управлять версиями приложения. С ECR проще использовать ECS, как систему управления контейнерами. Но мы собираемся настроить экземпляр EC2 как виртуальную машину и запустить на нем приложение как контейнер docker.
Примечание: ECR не входит в бесплатный уровень, поэтому вы будете платить несколько центов в месяц за использование.
Настройка сети
Мы создадим новый VPC с интернет-шлюзом и одной подсетью, базовой группой безопасности и NACL. Сеть будет простой, поскольку мы пытаемся избежать сложностей. Группа безопасности разрешит подключение только к порту приложения (80), а NACL разрешит весь трафик для подсети.
Примечание: Для повышения безопасности и управления доступом сетевой трафик лучше хранить внутри AWS. Это можно сделать с помощью конечной точки VPC — частного соединения, которое позволяет трафику между сервисами AWS не покидать сеть AWS. Мы не будем создавать конечную точку VPC, чтобы разрешить внутреннее подключение к ECR, поэтому мы будем загружать образ докера через интернет. Для производственной системы лучше сохранить соединение частным для повышения безопасности.
Все настройки сети можно найти в файле [network.tf](http://network.tf)
в репозитории Github. Во-первых, нам нужен новый ресурс VPC с определенным блоком CIDR.
resource aws_vpc main {
cidr_block = "10.0.0.0/20"
}
VPC — это логически изолированная виртуальная сеть. Здесь мы можем создавать различные подсети, которые могут быть публичными или частными, и управлять трафиком так, как нам нужно. VPC не имеет доступа к Интернету. Поскольку мы ожидаем входящий трафик из Интернета, нам нужен интернет-шлюз (IG).
resource aws_internet_gateway gateway {
vpc_id = aws_vpc.main.id
}
Изначально в VPC есть только таблица маршрутов по умолчанию. Чтобы обеспечить доступ к интернету через IG, нам нужно добавить таблицу маршрутизации и связать ее с VPC. В этом случае весь трафик, который не является локальным (в нашем случае, не 10.0.0.0/20), мы должны направлять на IG.
resource aws_route_table public {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gateway.id
}
}
resource aws_main_route_table_association main {
route_table_id = aws_route_table.public.id
vpc_id = aws_vpc.main.id
}
В примере у нас будет только один экземпляр EC2, поэтому мы создадим одну подсеть в одной зоне доступности (AZ) — для AWS это похоже на один большой центр обработки данных в определенном регионе. Для обеспечения сетевой маршрутизации в подсети нам также необходимо создать ассоциацию с маршрутизатором main
, который мы создали ранее.
resource aws_subnet main {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.0.0/24"
map_public_ip_on_launch = true
availability_zone = "us-east-1a"
}
resource aws_route_table_association main {
subnet_id = aws_subnet.main.id
route_table_id = aws_route_table.public.id
}
По умолчанию весь трафик внутри созданной подсети будет запрещен (terraform обновляет VPC по умолчанию во время создания), и нам нужно обновить NACL по умолчанию, чтобы разрешить входящий и исходящий трафик и связать его с подсетью.
resource aws_default_network_acl main {
default_network_acl_id = aws_vpc.main.default_network_acl_id
ingress {
protocol = -1
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
egress {
protocol = -1
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
}
resource aws_network_acl_association main {
network_acl_id = aws_default_network_acl.main.id
subnet_id = aws_subnet.main.id
}
Группа безопасности разрешает государственное подключение к сервисам AWS, в нашем случае к будущему экземпляру. Правило ingress (для входящих соединений) должно разрешать HTTP (TCP) трафик на порт 80 и egress трафик (так как мы собираемся тянуть образ докера).
resource aws_security_group public {
name = "public-sg"
vpc_id = aws_vpc.main.id
ingress {
from_port = local.application_port
to_port = local.application_port
protocol = "tcp" // allows only tcp trafic for the provided port
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1" // allows all outgoing traffic from the instance
cidr_blocks = ["0.0.0.0/0"]
}
}
Примечание: Группы безопасности могут разрешить только некоторый трафик, и они являются государственными (например, если запрос был сделан, то ответ пройдет без дополнительных правил). NACL может запретить трафик, и они не зависят от состояния (например, если запрос был сделан, нам нужны отдельные правила для входящего и исходящего трафика).
Создание образа docker
Мы собираемся обернуть наше приложение в docker и создать образ docker. Для хранения этого образа нам понадобится хранилище образов. В AWS для этого есть служба ECR, которую мы создадим и будем использовать для приложения. Нам нужно разрешить действия ECR, описанные в aws_iam_policy_document
, для управления образами docker.
resource aws_ecr_repository go_server {
name = local.service_name
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
data aws_iam_policy_document ecr_policy {
statement {
effect = "Allow"
principals {
identifiers = ["*"]
type = "*"
}
actions = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:DescribeRepositories",
"ecr:GetRepositoryPolicy",
"ecr:ListImages",
"ecr:DeleteRepository",
"ecr:BatchDeleteImage",
"ecr:SetRepositoryPolicy",
"ecr:DeleteRepositoryPolicy"
]
}
}
resource aws_ecr_repository_policy go_server {
repository = aws_ecr_repository.go_server.name
policy = data.aws_iam_policy_document.ecr_policy.json
}
Для управления докерами мы будем использовать модуль terraform kreuzwerker/docker. Он предоставляет нам возможность создавать и загружать докер-образы в докер-репозиторий.
Мы можем создать провайдера terraform, который будет управлять аутентификацией в ECR
data aws_ecr_authorization_token go_server {
registry_id = aws_ecr_repository.go_server.registry_id
}
provider docker {
registry_auth {
address = split("/", local.ecr_url)[0]
username = data.aws_ecr_authorization_token.go_server.user_name
password = data.aws_ecr_authorization_token.go_server.password
}
}
Для создания образа из файла docker нам понадобится отдельный ресурс docker_registry_image
, который будет создавать образ и загружать его в ECR.
resource docker_registry_image go_example {
name = "${local.ecr_url}:v1"
build {
context = "${path.module}/../app/."
dockerfile = "app/Dockerfile"
no_cache = true
}
depends_on = [aws_ecr_repository.go_server]
}
Настройка приложения в EC2
Прежде чем создавать экземпляр и запускать в нем контейнер docker, нам нужно разрешить экземпляру EC2 получать образ из репозитория. AWS имеет службу управления идентификацией и доступом (IAM) для контроля доступа между службами AWS. Чтобы позволить EC2 извлекать образы из ECR, нам нужно создать отдельную роль IAM — идентификатор, обладающий определенными правами.
data aws_iam_policy_document assume_role_ec2_policy_document {
statement {
effect = "Allow"
principals {
identifiers = ["ec2.amazonaws.com"]
type = "Service"
}
actions = [
"sts:AssumeRole"
]
}
}
resource aws_iam_role role {
name = "application-role"
assume_role_policy = data.aws_iam_policy_document.assume_role_ec2_policy_document.json
}
Чтобы предоставить разрешение на извлечение образа докера, нам нужно создать политику IAM и прикрепить ее к созданной нами роли.
// policy allows only ecr:ecr:BatchGetImage action for all resources that have such permission.
data aws_iam_policy_document ecr_access_policy_document {
statement {
effect = "Allow"
actions = [
"ecr:BatchGetImage",
]
resources = ["*"]
}
}
resource aws_iam_policy ecr_access_policy {
name = "ecr-access-policy"
policy = data.aws_iam_policy_document.ecr_access_policy_document.json
}
resource aws_iam_policy_attachment ecr_access {
name = "ecr-access"
roles = [aws_iam_role.role.name]
policy_arn = aws_iam_policy.ecr_access_policy.arn
}
Для экземпляра EC2 нам нужно создать профиль с этой ролью.
resource aws_iam_instance_profile profile {
name = "application-ec2-profile"
role = aws_iam_role.role.name
}
Для создания экземпляра EC2 нам нужно знать идентификатор образа машины amazon (AMI), который представляет собой виртуальную машину, на которой будет запущено приложение. Для этой цели мы будем использовать образ ubuntu-20.04
.
data aws_ami ubuntu {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
После этого мы готовы к созданию экземпляра EC2. Экземпляр определяет ресурсы для виртуальной машины. Тип t2.micro
предоставляет 1 vCPU и 1 ГБ оперативной памяти, доступные на бесплатном уровне и достаточные для простого приложения.
Начальный скрипт предоставляется в user_data
— скрипт, который запускается во время инициализации экземпляра EC2, где мы устанавливаем docker, извлекаем образ приложения и запускаем его.
resource aws_instance application {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.public.id]
subnet_id = aws_subnet.main.id
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.profile.name
user_data = <<EOF
#!/bin/bash
sudo apt-get update
sudo apt-get install -y pt-transport-https ca-certificates curl gnupg lsb-release software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu `lsb_release -cs` test"
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
sudo gpasswd -a $USER docker
newgrp docker
echo ${data.aws_ecr_authorization_token.go_server.password} | docker login --username=${data.aws_ecr_authorization_token.go_server.user_name} --password-stdin ${aws_ecr_repository.go_server.repository_url}
docker run -p ${local.application_port}:${local.application_internal_port} -d --restart always ${docker_registry_image.go_example.name}
EOF
depends_on = [aws_internet_gateway.gateway]
}
Чтобы получить IP-адрес созданного сервера, мы можем создать выход terraform.
output application_ip {
value = aws_instance.application.public_ip
description = "Application public IP"
}
Предоставление ресурсов
Перед созданием ресурсов, которые мы описали в файле terraform, нам необходимо сначала инициализировать terraform, чтобы установить необходимые модули. Из корневого каталога репозитория запустите:
terraform -chdir=iaac init
Для создания ресурсов нам нужно запустить apply
и terraform будет управлять всеми ресурсами.
terraform -chdir=iaac apply
Перед применением terraform предоставляет план изменений (diff или ресурсы, которые он собирается сделать). Мы можем просмотреть план и увидеть все изменения, после чего набрать yes
для их применения. В выводе мы увидим информацию о созданных ресурсах и вывод с IP приложения, например:
Apply complete! Resources: 17 added, 0 changed, 0 destroyed.
Outputs:
application_ip = "44.204.255.51"
Через несколько минут приложение будет доступно по адресу application_ip
.
Для уничтожения всех ресурсов необходимо выполнить команду destroy:
terraform -chdir=iaac destroy
Примечание: Terraform создаст все ресурсы и сохранит состояние terraform.tfstate
и terraform.tfstate.backup
. Для производственных систем файлы состояния обычно хранятся удаленно в долговечном месте, например, в S3. В этом случае несколько разработчиков могут обновлять окружение и поддерживать состояние terraform в неизменном виде.
Послесловие
Я написал эту статью, чтобы поделиться опытом управления инфраструктурой через IaaC и лучшими практиками создания и развертывания приложений. Чтобы помочь с выбором тем, о которых было бы ценнее написать, поделитесь тем, что было бы интересным улучшением текущего развертывания приложения:
- Добавление постоянных уровней в приложение, таких как база данных и хранилище объектов.
- Масштабирование сервера для обеспечения высокой доступности.
- Добавление развертывания фронтенда.
- Больше основных деталей об использованных технологиях.
Все советы по улучшению контента ценны и полезны 😇