Home » Как создать кастомный модуль Terraform
Как создать кастомный модуль Terraform

Как создать кастомный модуль 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 — это как хорошая функция в коде. Он скрывает сложность, предоставляет простой интерфейс и делает одну вещь, но делает ее идеально. Удачи в автоматизации!


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

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked