Terraform ile Otomatik SSL Sertifika Yönetimi

SSL sertifika yönetimi, sistem yöneticilerinin en sık baş ağrısı yaşadığı konuların başında geliyor. “Sertifika süresi doldu” alarmları, gece yarısı acil müdahaleler, manuel yenileme süreçleri… Bunların hepsini otomatize etmek artık hem mümkün hem de zorunlu hale geldi. Terraform ile bu süreci tamamen kod olarak yönetmek, hem zaman kazandırıyor hem de insan hatalarını minimize ediyor.

Neden Terraform ile SSL Yönetimi?

Geleneksel yaklaşımda SSL sertifikaları genellikle şu şekilde yönetilir: birisi Certbot çalıştırır, sertifika dosyaları bir yere kopyalanır, cron job kurulur ve “umarım çalışır” diye beklenir. Bu yaklaşımın sorunları çok açık: hangi sunucuda hangi sertifika var, ne zaman dolacak, kim yeniledi, nerede saklanıyor? Hiçbirini bilmiyorsunuz.

Terraform ile SSL yönetimi şu avantajları getiriyor:

  • Infrastructure as Code: Sertifika yapılandırması Git’te, versiyon kontrolünde
  • Tekrarlanabilirlik: Aynı konfigürasyonu farklı ortamlarda uygulayabilirsiniz
  • State yönetimi: Terraform hangi sertifikanın nerede olduğunu biliyor
  • Entegrasyon: DNS, load balancer, CDN hepsi aynı iş akışında

Bu yazıda Let’s Encrypt, AWS ACM ve Cloudflare kombinasyonlarını gerçek dünya senaryolarıyla ele alacağız.

Temel Yapı ve Provider Kurulumu

Önce çalışma ortamını kuralım. Terraform’da SSL yönetimi için genellikle birden fazla provider bir arada kullanılır.

# versions.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    acme = {
      source  = "vancluever/acme"
      version = "~> 2.0"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 4.0"
    }
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

provider "acme" {
  server_url = "https://acme-v02.api.letsencrypt.org/directory"
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}

Staging ortamı için ACME provider’ın test URL’ini kullanmak önemli. Let’s Encrypt’in rate limit’leri var ve test aşamasında bunlara takılmak can sıkıcı.

# Staging ortamı için ayrı provider tanımı
provider "acme" {
  alias      = "staging"
  server_url = "https://acme-staging-v02.api.letsencrypt.org/directory"
}

Let’s Encrypt ile Otomatik Sertifika Oluşturma

Let’s Encrypt sertifikalarını Terraform ile yönetmenin en temiz yolu ACME provider kullanmak. Önce bir private key oluşturuyoruz, ardından ACME registration yapıp sertifikayı talep ediyoruz.

# certificates.tf - Let's Encrypt sertifika yönetimi

# ACME hesabı için private key
resource "tls_private_key" "acme_account_key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

# ACME hesap kaydı
resource "acme_registration" "main" {
  account_key_pem = tls_private_key.acme_account_key.private_key_pem
  email_address   = var.acme_email
}

# Sertifika için private key
resource "tls_private_key" "cert_key" {
  algorithm = "RSA"
  rsa_bits  = 2048
}

# Sertifika talebi (CSR)
resource "tls_cert_request" "main" {
  private_key_pem = tls_private_key.cert_key.private_key_pem

  subject {
    common_name  = var.domain_name
    organization = var.organization_name
  }

  dns_names = concat(
    [var.domain_name],
    var.san_domains
  )
}

# ACME sertifika - DNS challenge ile
resource "acme_certificate" "main" {
  account_key_pem         = acme_registration.main.account_key_pem
  certificate_request_pem = tls_cert_request.main.cert_request_pem

  dns_challenge {
    provider = "cloudflare"
    config = {
      CF_DNS_API_TOKEN = var.cloudflare_api_token
    }
  }

  # Sertifika süre dolmadan 30 gün önce yenile
  min_days_remaining = 30

  depends_on = [acme_registration.main]
}

Burada dikkat edilmesi gereken nokta min_days_remaining parametresi. Bu değer, Terraform her çalıştığında sertifikanın kaç gün kaldığını kontrol eder. 30 günden az kaldıysa otomatik yenileme başlatır.

Wildcard Sertifika Yapılandırması

Birden fazla subdomain kullanan ortamlarda wildcard sertifikalar hayat kurtarıcı. DNS challenge zorunlu olduğundan Cloudflare entegrasyonu burada kritik.

# wildcard_cert.tf

locals {
  domains = {
    "example.com" = {
      wildcard    = true
      san_domains = ["example.com", "*.example.com", "*.staging.example.com"]
    }
    "api.example.com" = {
      wildcard    = false
      san_domains = ["api.example.com", "api-v2.example.com"]
    }
  }
}

# Her domain için sertifika oluştur
resource "acme_certificate" "domains" {
  for_each = local.domains

  account_key_pem         = acme_registration.main.account_key_pem
  certificate_request_pem = tls_cert_request.domains[each.key].cert_request_pem

  dns_challenge {
    provider = "cloudflare"
    config = {
      CF_DNS_API_TOKEN        = var.cloudflare_api_token
      CF_PROPAGATION_TIMEOUT  = "120"
      CF_POLLING_INTERVAL     = "10"
    }
  }

  min_days_remaining = 30
}

# Sertifikaları AWS Secrets Manager'a kaydet
resource "aws_secretsmanager_secret" "certificates" {
  for_each = local.domains

  name        = "/ssl/certificates/${replace(each.key, ".", "-")}"
  description = "SSL certificate for ${each.key}"

  tags = {
    Domain      = each.key
    ManagedBy   = "terraform"
    Environment = var.environment
  }
}

resource "aws_secretsmanager_secret_version" "certificates" {
  for_each = local.domains

  secret_id = aws_secretsmanager_secret.certificates[each.key].id
  secret_string = jsonencode({
    certificate       = acme_certificate.domains[each.key].certificate_pem
    private_key       = acme_certificate.domains[each.key].private_key_pem
    issuer_ca         = acme_certificate.domains[each.key].issuer_pem
    full_chain        = "${acme_certificate.domains[each.key].certificate_pem}${acme_certificate.domains[each.key].issuer_pem}"
    expiration_date   = acme_certificate.domains[each.key].certificate_not_after
  })
}

AWS Certificate Manager (ACM) ile Entegrasyon

Eğer AWS altyapısı kullanıyorsanız, ACM muhtemelen en temiz çözüm. ACM sertifikaları otomatik yeniliyor ve AWS servisleriyle doğrudan entegre çalışıyor.

# acm.tf - AWS Certificate Manager entegrasyonu

# ACM sertifika talebi
resource "aws_acm_certificate" "main" {
  domain_name               = var.domain_name
  subject_alternative_names = var.san_domains
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Name        = "${var.project_name}-ssl-cert"
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

# Cloudflare'de DNS doğrulama kayıtları oluştur
resource "cloudflare_record" "acm_validation" {
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
  }

  zone_id = var.cloudflare_zone_id
  name    = each.value.name
  value   = each.value.record
  type    = each.value.type
  ttl     = 60
  proxied = false
}

# Sertifika doğrulama tamamlanana kadar bekle
resource "aws_acm_certificate_validation" "main" {
  certificate_arn         = aws_acm_certificate.main.arn
  validation_record_fqdns = [for record in cloudflare_record.acm_validation : record.hostname]

  timeouts {
    create = "10m"
  }
}

# ALB için HTTPS listener
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate_validation.main.certificate_arn

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

# HTTP'den HTTPS'e yönlendirme
resource "aws_lb_listener" "http_redirect" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"
    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

create_before_destroy lifecycle kuralına dikkat edin. Sertifika yenileme sırasında önce yeni sertifika oluşturulur, sonra eskisi silinir. Bu sayede downtime yaşanmaz.

Sertifika Depolama ve Gizli Bilgi Yönetimi

Private key’leri nerede saklayacağınız kritik bir güvenlik sorusu. Terraform state dosyasında düz metin olarak bulunur, bu yüzden remote state ve şifreleme zorunlu.

# backend.tf - Şifreli remote state

terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "ssl/terraform.tfstate"
    region         = "eu-west-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:eu-west-1:123456789:key/xxxxx"
    dynamodb_table = "terraform-state-lock"
  }
}

# HashiCorp Vault ile private key yönetimi
resource "vault_generic_secret" "ssl_private_keys" {
  for_each = local.domains

  path = "secret/ssl/${replace(each.key, ".", "-")}/private_key"

  data_json = jsonencode({
    private_key = acme_certificate.domains[each.key].private_key_pem
    certificate = acme_certificate.domains[each.key].certificate_pem
    issuer      = acme_certificate.domains[each.key].issuer_pem
    expires_at  = acme_certificate.domains[each.key].certificate_not_after
  })
}

Vault kullanmıyorsanız en azından AWS Secrets Manager veya Azure Key Vault kullanın. Private key’i Git’e commit etmek veya S3’te şifresiz bırakmak ciddi güvenlik açığı demek.

Nginx ile Otomatik Sertifika Dağıtımı

Sertifikayı oluşturdunuz, Secrets Manager’a kaydettiniz, şimdi bunu Nginx’e uygulamanız gerekiyor. Bu adımı da Terraform’a bağlayabiliriz.

# nginx_ssl.tf - Nginx sunucularına sertifika dağıtımı

# User data script ile EC2 instance'lara sertifika yükleme
data "template_file" "nginx_userdata" {
  template = file("${path.module}/templates/nginx_setup.sh.tpl")

  vars = {
    secret_arn   = aws_secretsmanager_secret.certificates["example.com"].arn
    domain_name  = "example.com"
    aws_region   = var.aws_region
  }
}

# EC2 launch template
resource "aws_launch_template" "web" {
  name_prefix   = "${var.project_name}-web-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = var.instance_type

  iam_instance_profile {
    name = aws_iam_instance_profile.web.name
  }

  user_data = base64encode(data.template_file.nginx_userdata.rendered)

  lifecycle {
    create_before_destroy = true
  }
}

User data scriptini de yönetmek gerekiyor:

#!/bin/bash
# templates/nginx_setup.sh.tpl

set -e

# AWS CLI ve jq kur
apt-get update -q
apt-get install -y nginx awscli jq

# Sertifikayı Secrets Manager'dan çek
SECRET=$(aws secretsmanager get-secret-value 
  --secret-id "${secret_arn}" 
  --region "${aws_region}" 
  --query SecretString 
  --output text)

# Sertifika dosyalarını oluştur
mkdir -p /etc/nginx/ssl/${domain_name}

echo "$SECRET" | jq -r '.certificate' > /etc/nginx/ssl/${domain_name}/cert.pem
echo "$SECRET" | jq -r '.private_key' > /etc/nginx/ssl/${domain_name}/key.pem
echo "$SECRET" | jq -r '.full_chain' > /etc/nginx/ssl/${domain_name}/fullchain.pem

# Dosya izinlerini ayarla
chmod 600 /etc/nginx/ssl/${domain_name}/key.pem
chmod 644 /etc/nginx/ssl/${domain_name}/cert.pem
chown -R nginx:nginx /etc/nginx/ssl/

# Otomatik yenileme için cron job ekle
cat > /etc/cron.daily/refresh-ssl-cert << 'EOF'
#!/bin/bash
SECRET=$(aws secretsmanager get-secret-value 
  --secret-id "${secret_arn}" 
  --region "${aws_region}" 
  --query SecretString --output text)

NEW_CERT=$(echo "$SECRET" | jq -r '.certificate')
CURRENT_CERT=$(cat /etc/nginx/ssl/${domain_name}/cert.pem)

if [ "$NEW_CERT" != "$CURRENT_CERT" ]; then
  echo "$SECRET" | jq -r '.certificate' > /etc/nginx/ssl/${domain_name}/cert.pem
  echo "$SECRET" | jq -r '.private_key' > /etc/nginx/ssl/${domain_name}/key.pem
  echo "$SECRET" | jq -r '.full_chain' > /etc/nginx/ssl/${domain_name}/fullchain.pem
  nginx -t && systemctl reload nginx
fi
EOF
chmod +x /etc/cron.daily/refresh-ssl-cert

CI/CD Pipeline Entegrasyonu

Bu yapının gerçekten işe yaraması için CI/CD pipeline’a entegre etmek gerekiyor. GitHub Actions örneği:

# .github/workflows/ssl-renewal.yml
name: SSL Certificate Renewal

on:
  schedule:
    # Her gün sabah 03:00'te çalış
    - cron: '0 3 * * *'
  workflow_dispatch:

jobs:
  check-and-renew:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout
        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: eu-west-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.6.0

      - name: Terraform Init
        run: terraform init
        working-directory: ./infrastructure/ssl
        env:
          TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          TF_VAR_acme_email: ${{ secrets.ACME_EMAIL }}

      - name: Terraform Plan
        id: plan
        run: terraform plan -out=tfplan
        working-directory: ./infrastructure/ssl
        env:
          TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          TF_VAR_acme_email: ${{ secrets.ACME_EMAIL }}

      - name: Terraform Apply
        if: steps.plan.outcome == 'success'
        run: terraform apply -auto-approve tfplan
        working-directory: ./infrastructure/ssl

      - name: Notify on Failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "SSL sertifika yenileme basarisiz! Kontrol edin: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Sertifika İzleme ve Uyarı Sistemi

Terraform sertifika yönetimini otomatize etse bile izleme şart. Bir şeyler ters gidebilir ve bunu önceden haber almak önemli.

# monitoring.tf - Sertifika izleme

# CloudWatch alarm - sertifika süresi dolmadan önce uyar
resource "aws_cloudwatch_metric_alarm" "acm_cert_expiry" {
  alarm_name          = "${var.project_name}-acm-cert-expiry"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "DaysToExpiry"
  namespace           = "AWS/CertificateManager"
  period              = "86400"
  statistic           = "Minimum"
  threshold           = "30"
  alarm_description   = "ACM sertifikasinin suresine 30 gun kaldi"
  alarm_actions       = [aws_sns_topic.ssl_alerts.arn]

  dimensions = {
    CertificateArn = aws_acm_certificate.main.arn
  }
}

# SNS topic ve email bildirimi
resource "aws_sns_topic" "ssl_alerts" {
  name = "${var.project_name}-ssl-alerts"
}

resource "aws_sns_topic_subscription" "ssl_email" {
  topic_arn = aws_sns_topic.ssl_alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email
}

# Lambda fonksiyonu ile özel sertifika kontrolü
resource "aws_lambda_function" "cert_checker" {
  filename         = "cert_checker.zip"
  function_name    = "${var.project_name}-cert-checker"
  role             = aws_iam_role.lambda_cert_checker.arn
  handler          = "index.handler"
  runtime          = "python3.11"
  timeout          = 60

  environment {
    variables = {
      SECRET_ARNS    = join(",", [for s in aws_secretsmanager_secret.certificates : s.arn])
      SNS_TOPIC_ARN  = aws_sns_topic.ssl_alerts.arn
      WARNING_DAYS   = "30"
    }
  }
}

# EventBridge rule - her gün Lambda'yi tetikle
resource "aws_cloudwatch_event_rule" "daily_cert_check" {
  name                = "${var.project_name}-daily-cert-check"
  description         = "Her gun sertifika suresi kontrolu"
  schedule_expression = "cron(0 6 * * ? *)"
}

resource "aws_cloudwatch_event_target" "cert_checker" {
  rule = aws_cloudwatch_event_rule.daily_cert_check.name
  arn  = aws_lambda_function.cert_checker.arn
}

Yaygın Sorunlar ve Çözümleri

Gerçek dünya deneyiminden bazı önemli noktalar:

Rate limiting sorunu: Let’s Encrypt’in haftalık 50 sertifika limiti var. Wildcard sertifika kullanarak bu sorunu aşabilirsiniz. Ayrıca staging provider’da test yapın.

DNS propagation gecikmesi: DNS challenge sırasında kayıtların yayılması zaman alabilir. CF_PROPAGATION_TIMEOUT değerini artırın veya CF_POLLING_INTERVAL ayarını düzenleyin.

State dosyası güvenliği: Private key’ler Terraform state’inde düz metin olarak saklanır. Mutlaka şifreli remote backend kullanın ve state dosyasına erişimi kısıtlayın.

Sertifika dönüşü sırasında downtime: create_before_destroy = true lifecycle kuralını kullanmak bu sorunu önler. ALB ve CloudFront bu geçişi kesintisiz yapar.

Multi-region deployment: ACM sertifikalarını CloudFront için us-east-1 bölgesinde oluşturmanız gerekir. Diğer servisler için ilgili bölgelerde ayrı sertifika oluşturun.

# Multi-region sertifika yönetimi
provider "aws" {
  alias  = "us_east_1"
  region = "us-east-1"
}

# CloudFront için us-east-1'de sertifika
resource "aws_acm_certificate" "cloudfront" {
  provider          = aws.us_east_1
  domain_name       = var.domain_name
  validation_method = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

Değişken Yapısı ve Modüler Tasarım

Büyük ortamlarda bu yapıyı modüler hale getirmek şart. Yeniden kullanılabilir bir SSL modülü:

  • domain_name: Ana domain adı, zorunlu
  • san_domains: Subject Alternative Names listesi, varsayılan boş liste
  • environment: Ortam adı (prod, staging, dev)
  • acme_email: Let’s Encrypt kayıt e-postası
  • cloudflare_zone_id: Cloudflare zone kimliği
  • min_days_remaining: Yenileme öncesi minimum gün sayısı, varsayılan 30
  • store_in_secrets_manager: Secrets Manager’a kaydet, varsayılan true
  • alert_email: Uyarı bildirimleri için e-posta adresi

Modül çıktıları olarak sertifika ARN’i, Secrets Manager secret ARN’i, sertifika son kullanma tarihi ve private key ARN’i döndürülmelidir.

Sonuç

Terraform ile SSL sertifika yönetimi başlangıçta karmaşık görünse de bir kez doğru kurulduğunda inanılmaz zaman kazandırıyor. Gece yarısı “sertifika doldu” paniği yaşamak yerine her şeyin otomatik işlediğini bilmek büyük rahatlık.

Özetlemek gerekirse yapmanız gerekenler şunlar: ACME provider ile Let’s Encrypt veya ACM ile sertifika oluşturun, private key’leri şifreli remote state ve Secrets Manager ile güvenli saklayın, CI/CD pipeline’a günlük kontrol ekleyin, CloudWatch ve Lambda ile izleme yapın, mutlaka staging ortamında test edin.

Bu yapının en büyük avantajı her şeyin kod olarak tanımlı olması. Yeni bir domain eklemeniz gerektiğinde local.domains map’ine bir satır ekliyor ve terraform apply çalıştırıyorsunuz. Sertifika yenileme, DNS kayıt oluşturma, Secrets Manager güncellemesi hepsi otomatik gerçekleşiyor. İnfrastructure as Code felsefesinin SSL yönetimindeki pratik karşılığı bu.

Son olarak, bu sistemin düzgün çalışması için Terraform state’ini düzenli backup alın ve lock mekanizmasını kullanın. DynamoDB ile state locking aktif olmadan iki farklı pipeline aynı anda apply çalıştırırsa ciddi sorunlarla karşılaşabilirsiniz.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir