Terraform ile Monitoring Altyapısı Kurulumu
Monitoring altyapısı kurmak, her sysadmin’in en çok zaman harcadığı ve en çok baş ağrıtan konulardan biridir. Prometheus kuracaksın, Grafana ekleyeceksin, Alertmanager’ı yapılandıracaksın, Node Exporter’ları tüm sunuculara dağıtacaksın… Ve bunu her yeni ortam için tekrar tekrar yapacaksın. Terraform ile bu süreci kodla tanımlayıp, tekrar edilebilir ve tutarlı hale getirmek hem zamanından hem de sinirinden tasarruf ettirir.
Neden Terraform ile Monitoring?
Manuel kurulumların en büyük sorunu tutarsızlıktır. Staging ortamında bir şekilde, production’da başka bir şekilde kurulmuş Prometheus instance’ları, bir süre sonra kim neyi yapılandırdı belli olmayan bir kaosa dönüşür. Terraform ile altyapını kod olarak tanımlayınca şu avantajları elde edersin:
- Tekrar üretilebilirlik: Aynı konfigürasyonu dev, staging ve production’da birebir uygularsın
- Versiyon kontrolü: Altyapı değişiklikleri Git history’sinde görünür
- Takım çalışması: Pull request ile altyapı değişikliklerini review edebilirsin
- Hızlı disaster recovery: Bir şeyler patlarsa
terraform applyile sıfırdan ayağa kaldırırsın
Bu yazıda AWS üzerinde Prometheus, Grafana ve Alertmanager’dan oluşan tam bir monitoring stack’ini Terraform ile kuracağız. Gerçek üretim senaryolarına yakın bir yapı olacak, yani sadece “hello world” değil.
Proje Yapısı
Terraform projelerinde dosya organizasyonu kritik. Tek bir main.tf içine her şeyi doldurmak, proje büyüdüğünde bakımını imkansız hale getirir. Şu yapıyı kullanacağız:
monitoring-infra/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── modules/
│ ├── prometheus/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── grafana/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── alertmanager/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── templates/
│ ├── prometheus.yml.tpl
│ ├── alertmanager.yml.tpl
│ └── grafana.ini.tpl
└── environments/
├── dev.tfvars
├── staging.tfvars
└── prod.tfvars
Bu yapı sayesinde her bileşeni bağımsız olarak yönetebilir, modülleri farklı projelerde yeniden kullanabilirsin.
Provider ve Backend Yapılandırması
Önce providers.tf dosyasını hazırlayalım. State dosyasını S3’te tutmak ekip çalışması için şart, yoksa her terraform apply çalıştırdığında çakışmalar yaşarsın:
# providers.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "sirket-terraform-state"
key = "monitoring/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Project = "monitoring"
ManagedBy = "terraform"
Environment = var.environment
}
}
}
DynamoDB tablosunu state locking için önceden oluşturman gerekiyor. İki kişi aynı anda terraform apply yaparsa state dosyası bozulabilir, bu tablo bunu önler.
Variables Tanımlamaları
variables.tf dosyasına ortam değişkenlerini tanımlayalım:
# variables.tf
variable "aws_region" {
description = "AWS region"
type = string
default = "eu-west-1"
}
variable "environment" {
description = "Ortam adi (dev/staging/prod)"
type = string
}
variable "vpc_id" {
description = "Mevcut VPC ID"
type = string
}
variable "private_subnet_ids" {
description = "Private subnet ID listesi"
type = list(string)
}
variable "public_subnet_ids" {
description = "Public subnet ID listesi"
type = list(string)
}
variable "prometheus_instance_type" {
description = "Prometheus EC2 instance tipi"
type = string
default = "t3.medium"
}
variable "grafana_instance_type" {
description = "Grafana EC2 instance tipi"
type = string
default = "t3.small"
}
variable "retention_days" {
description = "Prometheus veri saklama suresi (gun)"
type = number
default = 30
}
variable "grafana_admin_password" {
description = "Grafana admin sifresi"
type = string
sensitive = true
}
variable "alert_email" {
description = "Alert email adresi"
type = string
}
sensitive = true ile işaretlenen değişkenler Terraform output’larında ve log dosyalarında gizlenir. Şifreleri asla default değeri olarak tanımlama, bunları Terraform Cloud, AWS Secrets Manager veya ortam değişkenleri üzerinden geçir.
Prometheus Modülü
Prometheus için ayrı bir modül oluşturalım. Bu modül EC2 instance, Security Group ve gerekli IAM rollerini yönetecek:
# modules/prometheus/main.tf
# IAM rol - Prometheus'un EC2 metadata okuyabilmesi ve
# diğer AWS servislerini kesfedebilmesi icin
resource "aws_iam_role" "prometheus" {
name = "${var.environment}-prometheus-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy" "prometheus_discovery" {
name = "prometheus-ec2-discovery"
role = aws_iam_role.prometheus.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus",
"cloudwatch:GetMetricStatistics",
"cloudwatch:ListMetrics"
]
Resource = "*"
}
]
})
}
resource "aws_iam_instance_profile" "prometheus" {
name = "${var.environment}-prometheus-profile"
role = aws_iam_role.prometheus.name
}
# Security Group
resource "aws_security_group" "prometheus" {
name = "${var.environment}-prometheus-sg"
description = "Prometheus monitoring server security group"
vpc_id = var.vpc_id
ingress {
from_port = 9090
to_port = 9090
protocol = "tcp"
security_groups = [var.grafana_sg_id]
description = "Grafana'dan Prometheus erisimi"
}
ingress {
from_port = 9100
to_port = 9100
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
description = "Node exporter metrikleri"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-prometheus"
}
}
# Prometheus konfigurasyon dosyasini template'ten olustur
data "template_file" "prometheus_config" {
template = file("${path.module}/../../templates/prometheus.yml.tpl")
vars = {
environment = var.environment
retention_days = var.retention_days
aws_region = var.aws_region
}
}
# EC2 Instance
resource "aws_instance" "prometheus" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.private_subnet_ids[0]
vpc_security_group_ids = [aws_security_group.prometheus.id]
iam_instance_profile = aws_iam_instance_profile.prometheus.name
root_block_device {
volume_type = "gp3"
volume_size = 100
encrypted = true
}
# Prometheus data volume
ebs_block_device {
device_name = "/dev/xvdf"
volume_type = "gp3"
volume_size = var.data_volume_size
encrypted = true
tags = {
Name = "${var.environment}-prometheus-data"
}
}
user_data = base64encode(templatefile("${path.module}/userdata.sh.tpl", {
prometheus_config = data.template_file.prometheus_config.rendered
retention_days = var.retention_days
environment = var.environment
}))
tags = {
Name = "${var.environment}-prometheus"
Role = "monitoring"
}
lifecycle {
ignore_changes = [ami]
}
}
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-22.04-amd64-server-*"]
}
}
Prometheus Konfigürasyon Template’i
Template dosyaları Terraform’un en güçlü özelliklerinden biri. Ortama göre dinamik konfigürasyonlar üretebilirsin:
# templates/prometheus.yml.tpl
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
environment: '${environment}'
region: '${aws_region}'
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files:
- "/etc/prometheus/rules/*.yml"
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# EC2 service discovery - tum tagged instance'lari otomatik bulur
- job_name: 'node-exporter'
ec2_sd_configs:
- region: ${aws_region}
port: 9100
filters:
- name: tag:Environment
values:
- ${environment}
- name: instance-state-name
values:
- running
relabel_configs:
- source_labels: [__meta_ec2_tag_Name]
target_label: instance
- source_labels: [__meta_ec2_instance_id]
target_label: instance_id
- source_labels: [__meta_ec2_tag_Role]
target_label: role
- job_name: 'blackbox'
metrics_path: /probe
params:
module: [http_2xx]
static_configs:
- targets:
- https://app.sirket.com
- https://api.sirket.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox-exporter:9115
EC2 service discovery burada çok işe yarıyor. Her yeni sunucu eklediğinde Prometheus konfigürasyonunu güncellemene gerek yok. Sunucuya Environment tag’ini eklersen Prometheus onu otomatik olarak keşfeder.
Grafana Modülü
Grafana için ayrı modül ve ALB arkasında çalışacak şekilde yapılandıralım:
# modules/grafana/main.tf
resource "aws_security_group" "grafana" {
name = "${var.environment}-grafana-sg"
description = "Grafana security group"
vpc_id = var.vpc_id
ingress {
from_port = 3000
to_port = 3000
protocol = "tcp"
security_groups = [aws_security_group.grafana_alb.id]
description = "ALB'den Grafana erisimi"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "grafana_alb" {
name = "${var.environment}-grafana-alb-sg"
description = "Grafana ALB security group"
vpc_id = var.vpc_id
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"]
}
}
resource "aws_lb" "grafana" {
name = "${var.environment}-grafana-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.grafana_alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = var.environment == "prod" ? true : false
}
resource "aws_lb_listener" "grafana_https" {
load_balancer_arn = aws_lb.grafana.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.grafana.arn
}
}
resource "aws_lb_target_group" "grafana" {
name = "${var.environment}-grafana-tg"
port = 3000
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
path = "/api/health"
matcher = "200"
}
}
resource "aws_lb_target_group_attachment" "grafana" {
target_group_arn = aws_lb_target_group.grafana.arn
target_id = aws_instance.grafana.id
port = 3000
}
resource "aws_instance" "grafana" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = var.private_subnet_ids[0]
vpc_security_group_ids = [aws_security_group.grafana.id]
root_block_device {
volume_type = "gp3"
volume_size = 30
encrypted = true
}
user_data = base64encode(templatefile("${path.module}/userdata.sh.tpl", {
admin_password = var.admin_password
prometheus_url = var.prometheus_url
grafana_ini = templatefile("${path.module}/../../templates/grafana.ini.tpl", {
domain = var.grafana_domain
environment = var.environment
})
}))
tags = {
Name = "${var.environment}-grafana"
Role = "monitoring"
}
}
enable_deletion_protection = var.environment == "prod" ? true : false satırına dikkat et. Production’da yanlışlıkla ALB’yi silmemek için bu ayarı conditional olarak aktifleştiriyoruz.
Environment Bazlı Değişkenler
Farklı ortamlar için farklı boyutlar tanımlayalım:
# environments/prod.tfvars
environment = "prod"
aws_region = "eu-west-1"
vpc_id = "vpc-0abc123def456"
private_subnet_ids = ["subnet-0123", "subnet-0456"]
public_subnet_ids = ["subnet-0789", "subnet-0abc"]
prometheus_instance_type = "t3.xlarge"
grafana_instance_type = "t3.medium"
retention_days = 90
alert_email = "[email protected]"
# environments/dev.tfvars
environment = "dev"
aws_region = "eu-west-1"
vpc_id = "vpc-0def456abc123"
private_subnet_ids = ["subnet-0def", "subnet-0ghi"]
public_subnet_ids = ["subnet-0jkl", "subnet-0mno"]
prometheus_instance_type = "t3.small"
grafana_instance_type = "t3.micro"
retention_days = 7
alert_email = "[email protected]"
CI/CD Pipeline Entegrasyonu
GitHub Actions ile Terraform’u otomatize edelim. Bu workflow, her PR’da plan çalıştırır, main branch’e merge olunca apply eder:
# .github/workflows/terraform-monitoring.yml
name: Terraform Monitoring Infrastructure
on:
push:
branches:
- main
paths:
- 'monitoring-infra/**'
pull_request:
paths:
- 'monitoring-infra/**'
env:
TF_VERSION: "1.6.0"
AWS_REGION: "eu-west-1"
jobs:
terraform-plan:
name: Terraform Plan
runs-on: ubuntu-latest
defaults:
run:
working-directory: monitoring-infra
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan (Staging)
run: |
terraform plan
-var-file=environments/staging.tfvars
-var="grafana_admin_password=${{ secrets.GRAFANA_ADMIN_PASSWORD }}"
-out=tfplan
- name: Upload Plan
uses: actions/upload-artifact@v3
with:
name: tfplan
path: monitoring-infra/tfplan
terraform-apply:
name: Terraform Apply
runs-on: ubuntu-latest
needs: terraform-plan
if: github.ref == 'refs/heads/main'
environment: staging
defaults:
run:
working-directory: monitoring-infra
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Download Plan
uses: actions/download-artifact@v3
with:
name: tfplan
path: monitoring-infra
- name: Terraform Init
run: terraform init
- name: Terraform Apply
run: terraform apply -auto-approve tfplan
Alertmanager Konfigürasyonu
Alertmanager için de bir template hazırlayalım. Slack ve email bildirimleri için:
# templates/alertmanager.yml.tpl
global:
smtp_smarthost: 'smtp.sirket.com:587'
smtp_from: '[email protected]'
smtp_auth_username: '${smtp_username}'
smtp_auth_password: '${smtp_password}'
slack_api_url: '${slack_webhook_url}'
route:
group_by: ['alertname', 'environment']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default'
routes:
- match:
severity: critical
receiver: 'pagerduty-critical'
continue: true
- match:
severity: warning
receiver: 'slack-warnings'
- match:
environment: prod
receiver: 'email-ops'
receivers:
- name: 'default'
slack_configs:
- channel: '#alerts-${environment}'
title: '{{ .GroupLabels.alertname }}'
text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: '${pagerduty_key}'
- name: 'slack-warnings'
slack_configs:
- channel: '#alerts-warning'
send_resolved: true
- name: 'email-ops'
email_configs:
- to: '${alert_email}'
send_resolved: true
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'instance']
Sık Karşılaşılan Sorunlar ve Çözümleri
Gerçek hayatta bu altyapıyı kurarken şu sorunlarla karşılaşırsın:
State dosyası kilitleme sorunu: İki pipeline aynı anda çalışırsa Error locking state alırsın. DynamoDB tablosunda kilidi manuel olarak silmen gerekebilir:
# State kilidin manuel kaldirilmasi (dikkatli kullan)
aws dynamodb delete-item
--table-name terraform-state-lock
--key '{"LockID": {"S": "sirket-terraform-state/monitoring/terraform.tfstate"}}'
--region eu-west-1
Drift tespiti: Birisi konsol üzerinden manuel değişiklik yaparsa Terraform state ile gerçek altyapı arasında fark oluşur. Bunu tespit etmek için düzenli terraform plan çalıştır:
# Drift kontrolu - degisiklik olmamali
terraform plan
-var-file=environments/prod.tfvars
-var="grafana_admin_password=${GRAFANA_PASS}"
-detailed-exitcode
# Exit code 0 = degisiklik yok, 2 = degisiklik var
echo "Exit code: $?"
Modül versiyonlama: Production ortamında modülleri versiyon sabitlemeden kullanma. Git tag’i referans ver:
# main.tf icinde modul cagirma - versiyonlu
module "prometheus" {
source = "git::https://github.com/sirket/terraform-modules.git//prometheus?ref=v2.1.0"
environment = var.environment
instance_type = var.prometheus_instance_type
vpc_id = var.vpc_id
}
Outputs ve Bağımlılık Yönetimi
outputs.tf dosyası hem debugging için hem de modüller arası bağımlılıkları çözmek için kritik:
# outputs.tf
output "prometheus_private_ip" {
description = "Prometheus sunucu private IP"
value = module.prometheus.private_ip
}
output "grafana_url" {
description = "Grafana erisim URL'i"
value = "https://${module.grafana.alb_dns_name}"
}
output "alertmanager_endpoint" {
description = "Alertmanager endpoint"
value = module.alertmanager.endpoint
sensitive = false
}
output "prometheus_security_group_id" {
description = "Prometheus SG ID - diger sunucular icin Node Exporter erisimi"
value = module.prometheus.security_group_id
}
prometheus_security_group_id output’u özellikle önemli. Uygulama sunucularına Node Exporter eklerken bu SG ID’yi kullanarak sadece Prometheus’un 9100 portuna erişebildiğinden emin olursun.
Sonuç
Terraform ile monitoring altyapısı kurmak başlangıçta zahmetli görünse de uzun vadede kazanç çok büyük. Bir kere iyi kurulmuş modüler yapıyla yeni bir ortam için monitoring ayağa kaldırmak terraform apply -var-file=environments/prod.tfvars komutundan ibaret hale geliyor.
Özellikle dikkat etmeni istediğim noktaları özetleyeyim:
- State yönetimi: Remote state olmadan ekip çalışması mümkün değil, bunu ilk adımda hallettir
- Modüler yapı: Her bileşeni kendi modülüne koy, ileride büyüyen projelerde sağlığını korursun
- Sensitive değişkenler: Şifreler ve API anahtarları asla
.tfvarsdosyalarında committed olmamalı - Drift detection: CI/CD pipeline’ına düzenli
terraform planekle, altyapı sürprizleri sevmez - Environment ayrımı: Dev’de küçük instance ile test et, production’a geçmeden önce staging’de doğrula
Bu altyapıyı kurduğunda önüne gelecek ilk gerçek test genellikle disk dolması alarmıdır. Prometheus’un data volume’unu başlangıçta büyük tutmanı tavsiye ederim, retention_days değerini düşürmek her zaman terraform apply kadar kolay olmuyor fiziksel disk açısından.
