Terraform external data source ile Python scriptleri
Selamlar, bu yazımda Terraform tarafında zaman zaman ihtiyaç duyduğumuz bir konuya, external data source ile Python scriptlerini birlikte kullanmaya bakacağız. Konu basit görünüyor ama işin içine girince ufak ufak detaylar çıkıyor; o yüzden lafı çok uzatmadan başlayalım.
Terraform genelde HCL ile her şeyi halletmeye çalışır. Ama bazen öyle bir hesap yapmanız gerekir ki, HCL'in cidrsubnet fonksiyonu yetmez, locals bloğuna sığmaz, ya da bir API'den dinamik veri çekmeniz gerekir. İşte tam o anda external data source devreye giriyor. Bash ile de yapılabilir, doğrudur, ama benim tecrübeme göre JSON ile uğraşıyorsanız Python işi çok daha temiz çözüyor.
external data source nedir?
external data source aslında çok sade bir kontrat: Terraform sizin scriptinize stdin üzerinden bir JSON gönderir, scriptiniz işini yapar, stdout'a yine bir JSON yazar. Hata varsa stderr'e yazıp non-zero exit code dönersiniz. Tek bir kural var, o da önemli: çıktıdaki bütün değerler string olmalı. Integer, boolean, liste dönerseniz Terraform plan aşamasında yüzünüze patlatır.
Bunun güzel tarafı, scriptiniz herhangi bir dilde olabilir. Bash, Python, Go, hatta PHP. Ama Python'ın standart kütüphanesi (json, ipaddress, urllib, secrets gibi) bu iş için biraz fazla uygun, o yüzden ekibimde de varsayılan tercih bu.
Provider tarafı
Önce Terraform tarafını hazırlayalım. external provider'ı required_providers içine ekliyoruz:
terraform {
required_version = '>= 1.5.0'
required_providers {
external = {
source = 'hashicorp/external'
version = '~> 2.3'
}
}
}
variable 'environment' {
type = string
default = 'production'
}
Ekstra bir provider config'i gerekmiyor; external'ın kendi iç ayarı yok. Çağrı yerinde program ile scripti, query ile de göndermek istediğiniz parametreleri belirliyorsunuz.
Pratik örnek: subnet hesabı
Diyelim ki bir VPC altında 6 tane /24 subnet ayırmamız lazım ve cidrsubnet ile 6 satır tekrar yazmaktansa hesabı tek seferde yapıp döndürmek istiyoruz. HCL tarafı şöyle:
data 'external' 'subnet_calc' {
program = ['python3', '${path.module}/scripts/cidr_calculator.py']
query = {
vpc_cidr = '10.0.0.0/16'
num_subnets = '6'
subnet_mask = '24'
}
}
output 'subnets' {
value = data.external.subnet_calc.result
}
query içindeki tüm değerlerin string olduğunu fark etmişsinizdir, bu da kontratın parçası. Sayıları Python tarafında int() ile parse ediyoruz. Script de böyle görünüyor:
#!/usr/bin/env python3
import ipaddress
import json
import sys
def main() -> None:
try:
payload = json.load(sys.stdin)
except json.JSONDecodeError as exc:
print(f'Gecersiz JSON girdi: {exc}', file=sys.stderr)
sys.exit(1)
vpc_cidr = payload['vpc_cidr']
num_subnets = int(payload['num_subnets'])
new_prefix = int(payload['subnet_mask'])
network = ipaddress.ip_network(vpc_cidr)
subnets = list(network.subnets(new_prefix=new_prefix))
if len(subnets) < num_subnets:
print(f'{vpc_cidr} icinden {num_subnets} adet /{new_prefix} cikmiyor.',
file=sys.stderr)
sys.exit(1)
out = {f'subnet_{idx}': str(net) for idx, net in enumerate(subnets[:num_subnets])}
out['hosts_per_subnet'] = str(subnets[0].num_addresses - 2)
json.dump(out, sys.stdout)
if __name__ == '__main__':
main()
Burada üç tane önemli detay var. Birincisi, stdin'i doğrudan json.load ile okuyoruz, satır satır parse etmiyoruz. İkincisi, hata olursa stderr'e mesaj yazıp sys.exit(1) çağırıyoruz; Terraform bu durumda plan'i kesiyor. Üçüncüsü, çıktının tüm değerlerini stringe çeviriyoruz; integer döndürseydik plan aşamasında cannot use object of type number as string diye bir hata alırdık.
Sık karşılaşılan tuzaklar
- Tüm çıktıyı string yapmamak: Bool veya int dönerseniz Terraform şikayet eder. Çıkıştan önce
{k: str(v) for k, v in out.items()}ile güvene almak iyi bir alışkanlık. stdout'a yanlışlıklaprintyapmak: Debug için yazdırdığınız bir satır Terraform'un JSON parser'ını bozar. Loglarınızıstderr'e yazın (print(..., file=sys.stderr)),stdoutsadece JSON çıktısı içindir.- Scriptin her plan'da yeniden çağrılması:
externaldata sourceterraform planher çağrıldığında çalışır. Yavaş bir API uca bağlarsanız plan'i yavaşlatırsınız; cache'lemeyi script içinde düşünün veyaterraform_dataile birleştirip lifecycle yönetin. - Hassas veri döndürmek:
resultmap'iterraform.tfstateiçine düz metin olarak yazılır. Şifre, token gibi şeyleri buradan çıkarmayın; Vault provider'ı veyasensitiveworkaround'ları daha doğru tercih. - Python yorumlayıcısının yokluğunu varsaymak: CI runner'ında
python3olmayabilir.program = ['python3', ...]yerine bazen['/usr/bin/env', 'python3', ...]daha taşınabilir oluyor.
Sağlam bir iskelet
Ben her yeni external scriptine şöyle bir iskelet ile başlıyorum, sonra ortasını doldurup gidiyorum. Hata yönetimini peşinen koymak, sonradan eklemekten çok daha kolay:
#!/usr/bin/env python3
import json
import sys
REQUIRED = ('vpc_cidr', 'num_subnets')
def run(payload: dict) -> dict:
missing = [k for k in REQUIRED if k not in payload]
if missing:
raise ValueError(f'Eksik parametre: {", ".join(missing)}')
# ... asil is burada
return {'ok': 'true'}
def main() -> None:
try:
payload = json.load(sys.stdin)
result = run(payload)
json.dump({k: str(v) for k, v in result.items()}, sys.stdout)
except Exception as exc:
print(f'Hata: {exc}', file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
main()
Kapanış
Bu yazıda external data source ile Python scriptlerini nasıl bağladığımıza, kontratın sade kurallarına ve sık takıldığımız noktalara baktık. Bence external aceleci bir çözüm değil, son çare olarak düşünülmesi gereken bir araç; HCL ile yapılabilen bir şeyi dışarıya çıkarmak ileride teknik borç yaratıyor. Ama gerçekten karmaşık bir hesap veya dış sistem entegrasyonu varsa, Python burada hâlâ en temiz seçim. Umarım faydalı olur, görüşmek üzere.
