- Home »

Как создать кастомный модуль Terraform
Каждый раз, когда копируешь одинаковые блоки Terraform кода из проекта в проект, частичка твоей души отмирает. Да, я знаю это чувство. Сегодня мы разберем, как создать кастомный модуль Terraform, который сделает твою жизнь проще, код — чище, а инфраструктуру — более предсказуемой. Это не просто способ избавиться от copy-paste, это целая философия построения инфраструктуры как кода.
Если ты уже устал от бесконечного дублирования ресурсов, запутанных зависимостей и сложности поддержки растущей инфраструктуры, то эта статья точно для тебя. Кастомные модули — это как написание собственных функций в программировании, только для инфраструктуры. Они позволяют инкапсулировать логику, стандартизировать deployments и значительно упростить совместную работу в команде.
Анатомия Terraform модуля: что под капотом?
Terraform модуль — это просто директория с .tf файлами. Звучит банально, но дьявол кроется в деталях. Каждый модуль состоит из трех основных компонентов:
- Входные переменные (variables) — что модуль принимает на вход
- Ресурсы (resources) — что модуль создает
- Выходные значения (outputs) — что модуль возвращает
Структура типичного модуля выглядит так:
my-module/
├── main.tf # Основные ресурсы
├── variables.tf # Входные переменные
├── outputs.tf # Выходные значения
├── versions.tf # Версии провайдеров
└── README.md # Документация
Важный момент: модуль не должен содержать конфигурацию провайдеров с жестко заданными параметрами. Это антипаттерн, который сломает переиспользование.
Пошаговое создание модуля для веб-сервера
Давай создадим практический модуль для развертывания веб-сервера. Это классический пример, который покажет все основные принципы.
Шаг 1: Создание структуры
mkdir terraform-webserver-module
cd terraform-webserver-module
touch main.tf variables.tf outputs.tf versions.tf
Шаг 2: Определяем переменные (variables.tf)
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "environment" {
description = "Environment name"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "vpc_id" {
description = "VPC ID where resources will be created"
type = string
}
variable "subnet_id" {
description = "Subnet ID for the instance"
type = string
}
variable "allowed_cidr_blocks" {
description = "CIDR blocks allowed to access the web server"
type = list(string)
default = ["0.0.0.0/0"]
}
variable "tags" {
description = "Additional tags"
type = map(string)
default = {}
}
Шаг 3: Основные ресурсы (main.tf)
locals {
common_tags = merge(var.tags, {
Environment = var.environment
ManagedBy = "Terraform"
Module = "webserver"
})
}
# Security Group
resource "aws_security_group" "webserver" {
name_prefix = "${var.environment}-webserver-"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = var.allowed_cidr_blocks
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = merge(local.common_tags, {
Name = "${var.environment}-webserver-sg"
})
}
# User Data Script
locals {
user_data = base64encode(<<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Hello from ${var.environment} environment!
" > /var/www/html/index.html
EOF
)
}
# EC2 Instance
resource "aws_instance" "webserver" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = var.subnet_id
vpc_security_group_ids = [aws_security_group.webserver.id]
user_data = local.user_data
tags = merge(local.common_tags, {
Name = "${var.environment}-webserver"
})
}
# Data source для получения AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
Шаг 4: Выходные значения (outputs.tf)
output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.webserver.id
}
output "instance_public_ip" {
description = "Public IP address of the instance"
value = aws_instance.webserver.public_ip
}
output "instance_private_ip" {
description = "Private IP address of the instance"
value = aws_instance.webserver.private_ip
}
output "security_group_id" {
description = "ID of the security group"
value = aws_security_group.webserver.id
}
output "website_url" {
description = "URL of the deployed website"
value = "http://${aws_instance.webserver.public_ip}"
}
Шаг 5: Версии провайдеров (versions.tf)
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
Использование модуля в проекте
Теперь создадим проект, который использует наш модуль:
mkdir webserver-deployment
cd webserver-deployment
Создаем main.tf:
provider "aws" {
region = "us-west-2"
}
module "dev_webserver" {
source = "../terraform-webserver-module"
environment = "dev"
instance_type = "t3.micro"
vpc_id = "vpc-12345678"
subnet_id = "subnet-12345678"
allowed_cidr_blocks = ["10.0.0.0/8"]
tags = {
Project = "MyWebApp"
Owner = "DevOps Team"
}
}
module "prod_webserver" {
source = "../terraform-webserver-module"
environment = "prod"
instance_type = "t3.small"
vpc_id = "vpc-87654321"
subnet_id = "subnet-87654321"
allowed_cidr_blocks = ["0.0.0.0/0"]
tags = {
Project = "MyWebApp"
Owner = "DevOps Team"
}
}
output "dev_website_url" {
value = module.dev_webserver.website_url
}
output "prod_website_url" {
value = module.prod_webserver.website_url
}
Запускаем:
terraform init
terraform plan
terraform apply
Продвинутые техники и лучшие практики
Условная логика в модулях
Иногда нужно создавать ресурсы условно. Вот как это делается:
variable "create_load_balancer" {
description = "Whether to create a load balancer"
type = bool
default = false
}
resource "aws_lb" "webserver" {
count = var.create_load_balancer ? 1 : 0
name = "${var.environment}-webserver-lb"
internal = false
load_balancer_type = "application"
subnets = var.subnet_ids
tags = local.common_tags
}
Использование for_each для множественных ресурсов
variable "servers" {
description = "Map of server configurations"
type = map(object({
instance_type = string
subnet_id = string
}))
default = {}
}
resource "aws_instance" "servers" {
for_each = var.servers
ami = data.aws_ami.amazon_linux.id
instance_type = each.value.instance_type
subnet_id = each.value.subnet_id
tags = merge(local.common_tags, {
Name = "${var.environment}-${each.key}"
})
}
Паттерны и антипаттерны
Паттерн | ✅ Хорошо | ❌ Плохо |
---|---|---|
Именование | Используй префиксы для избежания конфликтов | Хардкодить имена ресурсов |
Переменные | Добавляй validation и description | Создавать переменные для всего подряд |
Outputs | Возвращай только нужные значения | Выводить чувствительную информацию |
Зависимости | Используй depends_on явно | Полагаться только на implicit зависимости |
Версионирование и публикация модулей
Для серьезных проектов модули нужно версионировать. Если у тебя есть собственный Git-репозиторий, можно использовать теги:
module "webserver" {
source = "git::https://github.com/yourorg/terraform-webserver-module.git?ref=v1.0.0"
environment = "prod"
# другие переменные
}
Для публикации в Terraform Registry нужно следовать конвенциям именования и структуры:
- Репозиторий должен называться terraform-{provider}-{name}
- Использовать семантическое версионирование
- Добавить examples/ директорию с примерами
- Написать подробный README.md
Тестирование модулей
Да, модули Terraform тоже нужно тестировать! Для этого отлично подходит Terratest:
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestWebserverModule(t *testing.T) {
terraformOptions := &terraform.Options{
TerraformDir: "../examples/simple",
Vars: map[string]interface{}{
"environment": "test",
},
}
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
instanceId := terraform.Output(t, terraformOptions, "instance_id")
assert.NotEmpty(t, instanceId)
}
Альтернативы и сравнение
Стоит упомянуть альтернативные подходы к созданию переиспользуемой инфраструктуры:
- Pulumi — IaC на настоящих языках программирования
- AWS CDK — для тех, кто живет в экосистеме AWS
- Ansible — больше для конфигурации, но может и инфраструктуру
- Helm charts — для Kubernetes окружения
Но если твоя команда уже использует Terraform, кастомные модули — это самый естественный способ переиспользования кода.
Интеграция с CI/CD и автоматизация
Модули отлично интегрируются с CI/CD пайплайнами. Вот пример для GitHub Actions:
name: 'Terraform Module Test'
on:
pull_request:
branches: [ main ]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Terraform Init
run: terraform init
working-directory: ./examples/simple
- name: Terraform Validate
run: terraform validate
working-directory: ./examples/simple
- name: Terraform Plan
run: terraform plan
working-directory: ./examples/simple
Мониторинг и логирование через модули
Крутая фишка — встроить мониторинг прямо в модуль. Добавим CloudWatch алармы:
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
count = var.enable_monitoring ? 1 : 0
alarm_name = "${var.environment}-webserver-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "300"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors ec2 cpu utilization"
dimensions = {
InstanceId = aws_instance.webserver.id
}
}
Масштабирование и композиция модулей
Модули можно комбинировать для создания сложных архитектур. Например, модуль “приложение” может использовать модули “database”, “cache”, “load-balancer”. Это называется композицией модулей.
Для больших проектов рекомендую создать mono-repo с модулями или использовать Terragrunt для управления зависимостями.
Дебаг и траблшутинг модулей
Несколько полезных команд для отладки:
# Посмотреть план только для конкретного модуля
terraform plan -target=module.webserver
# Подробный лог
TF_LOG=DEBUG terraform apply
# Граф зависимостей
terraform graph | dot -Tpng > graph.png
Заключение и рекомендации
Кастомные модули Terraform — это не просто способ избежать дублирования кода, это фундамент для построения масштабируемой, поддерживаемой инфраструктуры. Начни с простых модулей для часто используемых паттернов, постепенно усложняя их функциональность.
Основные принципы, которые стоит запомнить:
- Модуль должен решать одну задачу, но решать ее хорошо
- Делай модули настраиваемыми, но не переусложняй
- Всегда добавляй валидацию входных параметров
- Документируй все — будущий ты скажет спасибо
- Тестируй модули перед использованием в продакшене
Если ты планируешь разворачивать инфраструктуру в облаке, обрати внимание на качественные VPS-решения для тестирования модулей на https://arenda-server.cloud/vps или мощные выделенные серверы для продакшена на https://arenda-server.cloud/dedicated.
Помни: хороший модуль Terraform — это как хорошая функция в коде. Он скрывает сложность, предоставляет простой интерфейс и делает одну вещь, но делает ее идеально. Удачи в автоматизации!
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.