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.
