Ekim 25 2024

Terraform State Yönetimi: Remote Backend ve State Locking

Modern devops dünyasında, Altyapıyı Kod Olarak Yönetmek (iac) denince akla gelen ilk araç tartışmasız terraform. Ancak projeler büyüdükçe ve ekip arkadaşlarınızla aynı cloud kaynakları üzerinde çalışmaya başladığınızda, o meşhur local terraform.tfstate dosyası tam bir kabusa dönüşebilir. Bu yazımızda, state yönetimini local’den kurtarıp AWS üzerinde S3 ve DynamoDB kullanarak nasıl kurumsal seviyede güvenli, kilitlenebilir (state locking) ve ölçeklenebilir bir yapıya kavuşturacağımızı adım adım inceleyeceğiz.

Eğer production ortamınızda hala local state kullanıyorsanız, muhtemelen adrenaline aşırı bağımlı bir hayat yaşıyorsunuzdur. Gelin, o state dosyasını ait olduğu yere, buluta taşıyalım ve işleri profesyonelleştirelim.

Local State’in Vedası: Neden Remote Backend?

Terraform ile çalışırken oluşturduğunuz her kaynak, default olarak projenizin kök dizinindeki terraform.tfstate dosyasına kaydedilir. Tek başınıza çalışırken bu durum idare edilebilir gibi görünse de, ekibe ikinci bir kişi dahil olduğu anda şu sorunlar baş gösterir:

  • Race Condition (Yarış Durumu): Aynı anda iki mühendisin terraform apply çalıştırması, kaynakların çakışmasına ve state dosyasının bozulmasına neden olur.
  • Güvenlik ve Hassas Veriler: State dosyası, oluşturulan kaynakların şifrelerini, private key’lerini ve tüm meta datasını plain-text (açık metin) olarak tutar. Bu dosyayı Git deposuna göndermek, güvenlik ekiplerinin kapınıza dayanması için en kestirme yoldur.
  • Tek Nokta Hatası (Single Point of Failure): Bilgisayarınızın diski bozulduğunda veya kahve döküldüğünde tüm altyapı geçmişiniz yok olur.

Bu sorunların çözümü, state dosyasını merkezi ve güvenli bir yerde tutmaktır. AWS ekosisteminde bunun endüstri standardı karşılığı S3 (Storage) + DynamoDB (Locking) ikilisidir.

Adım Adım S3 ve DynamoDB Backend Kurulumu

Buradaki en büyük ironi şudur: Remote backend’i oluşturmak için de Terraform kullanmak isteriz. Ancak backend henüz var olmadığı için, bootstrap aşamasında local state kullanıp, kaynakları oluşturduktan sonra state’i yeni oluşturduğumuz bu remote backend’e migrate ederiz (taşırız).

1. S3 Bucket ve DynamoDB Kaynaklarının Tanımlanması

Öncelikle backend kaynaklarımızı tanımlayacağımız bir main.tf dosyası oluşturalım. S3 bucket’ımızın versiyonlamaya (versioning) açık olması hayati önem taşır. Bu sayede olası bir state bozulmasında geçmişe dönebiliriz.

provider "aws" {
  region = "eu-west-1"
}

# State dosyasını saklayacağımız S3 Bucket
resource "aws_s3_bucket" "terraform_state" {
  bucket        = "kertenkerem-tf-state-bucket"
  force_destroy = false

  lifecycle {
    prevent_destroy = true
  }
}

# S3 Bucket versiyonlamasını aktif ediyoruz
resource "aws_s3_bucket_versioning" "state_versioning" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

# State dosyasının diskte şifrelenmesi (Encryption)
resource "aws_s3_bucket_server_side_encryption_configuration" "state_encryption" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# State locking mekanizması için DynamoDB Tablosu
resource "aws_s3_bucket_public_access_block" "block_public" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "kertenkerem-tf-state-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Kritik Detay: DynamoDB tablosunun partition key (hash_key) değeri tam olarak LockID (büyük-küçük harf duyarlı) olmalıdır. Terraform, kilit bilgisi yazarken bu spesifik anahtarı arar.

Bu aşamada projemizi initialize edip kaynakları AWS üzerinde oluşturalım:

terraform init
terraform apply

2. Backend Konfigürasyonunun Eklenmesi

Kaynaklarımız AWS üzerinde başarıyla oluşturulduktan sonra, projemize bu backend’i kullanmasını söyleme zamanı geldi. Proje dizininde bir backend.tf dosyası oluşturalım:

terraform {
  backend "s3" {
    bucket         = "kertenkerem-tf-state-bucket"
    key            = "global/s3/terraform.tfstate"
    region         = "eu-west-1"
    dynamodb_table = "kertenkerem-tf-state-locks"
    encrypt        = true
  }
}

Büyük Göç: State Dosyasını Local’den S3’e Taşımak (Migration)

Backend tanımını ekledikten sonra Terraform’a bu değişikliği bildirmemiz gerekiyor. Terminale dönüp şu sihirli komutu çalıştırıyoruz:

terraform init

Terraform, local’de mevcut bir state dosyası olduğunu ve konfigürasyonda yeni bir S3 backend tanımlandığını algılayacaktır. Karşınıza şu şekilde bir uyarı gelecektir:

Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the "s3" backend.
  Do you want to copy this state to the new "s3" backend? Enter "yes" to copy and "no"
  to start with an empty state.

  Enter a value: yes

Bu soruya yes yanıtını verdiğinizde, Terraform yerel diskinizdeki tüm state geçmişini güvenli bir şekilde AWS S3 bucket’ına yükler. Artık projenizin kök dizinindeki local terraform.tfstate dosyasını güvenle silebilirsiniz (yine de silmeden önce yedeklemek her zaman iyi bir refleksdir).

Kriz Anı: State Lock Çakışması ve “force-unlock” Sanatı

Remote backend’imizin en güzel yanı, birisi terraform apply veya terraform plan çalıştırdığında DynamoDB’ye bir lock (kilit) kaydı yazmasıdır. Bu esnada başka biri aynı işlemi yapmaya çalışırsa, Terraform işlemi durdurur ve şu hatayı fırlatır:

Error: Error acquiring the state lock
Error info:
   ID:     e9b72a44-dfc1-4b11-9a73-519280d8f0f0
   Path:   kertenkerem-tf-state-bucket/global/s3/terraform.tfstate
   Who:    ahmet@kertenkerem-macbook
   Version: 1.5.0
   Created: 2023-10-27 12:00:00 +0000 UTC
   Info:    org: kertenkerem-devops

Peki ya bu kilitlenme, Jenkins/GitLab pipeline’ınızın yarıda kesilmesi, internetinizin kopması veya Docker container’ının crash olması nedeniyle havada asılı kaldıysa? Kimse işlem yapamadığı için deployment’lar kilitlenir.

Çözüm: force-unlock

Böyle bir durumda, hatanın size verdiği Lock ID‘yi kullanarak kilidi manuel olarak kırmak zorundasınız. Ancak dikkatli olun: Bu komutu çalıştırmadan önce, kilidin gerçekten “sahipsiz” kaldığından ve arka planda başka bir mühendisin veya pipeline’ın canlı bir apply işlemi yürütmediğinden emin olmalısınız.

terraform force-unlock e9b72a44-dfc1-4b11-9a73-519280d8f0f0

Komut başarıyla çalıştığında DynamoDB üzerindeki kilit kaydı silinecek ve pipeline’larınız tekrar çalışabilir hale gelecektir.

Workspace Stratejileri: Hangisini Seçmeliyiz?

Birden fazla ortamı (dev, stage, prod) yönetirken state dosyalarını nasıl izole edeceğiz? Burada iki temel yaklaşım var: Terraform Workspace CLI ve Directory-based (Klasör Tabanlı) İzolasyon.

Yöntem A: Terraform Workspace CLI

Terraform’un built-in workspace özelliğidir. Tek bir konfigürasyon kodunu kullanarak arka planda farklı state dosyaları oluşturur.

terraform workspace new dev
terraform workspace new prod

# Geçiş yapmak için:
terraform workspace select dev

S3 backend tarafında Terraform, state dosyalarınızı otomatik olarak env:/dev/global/s3/terraform.tfstate ve env:/prod/global/s3/terraform.tfstate yollarına kaydeder.

Neden Dikkatli Olunmalı? Workspace’ler CLI üzerinden kolayca değiştirilebildiği için, yanlışlıkla prod ortamındayken dev zannedip yıkıcı bir apply komutu çalıştırmak çok kolaydır. Ayrıca her iki ortam da aynı AWS account’unu ve aynı yetki sınırlarını paylaşıyorsa, güvenlik zafiyeti doğurabilir.

Yöntem B: Klasör Tabanlı İzolasyon (Önerilen)

Büyük ölçekli ve multi-account (çoklu AWS hesabı) yapılarında en güvenli yol, ortamları dizin bazında tamamen ayırmaktır.

├── environments
│   ├── dev
│   │   ├── backend.tf  # Dev AWS Account S3 kovasını gösterir
│   │   ├── main.tf
│   │   └── variables.tf
│   └── prod
│       ├── backend.tf  # Prod AWS Account S3 kovasını gösterir
│       ├── main.tf
│       └── variables.tf

Bu yaklaşımda, Dev ve Prod ortamlarının state dosyaları tamamen farklı AWS hesaplarındaki S3 bucket’larında durur. Prod ortamına dokunmak için Prod AWS credentials’ına ihtiyacınız vardır. Dolayısıyla, local’deki bir kaza eseri tüm prod altyapısını silme ihtimaliniz sıfıra iner.

Son Sözler ve Altın Kurallar

Terraform ile profesyonel bir altyapı yönetmek istiyorsanız, state dosyanızı gözünüz gibi korumalısınız. Özetlemek gerekirse:

  • Asla ama asla local state ile production yönetmeyin.
  • S3 bucket’ınızda Versioning ve Encryption özelliklerini her zaman açık tutun.
  • State lock için mutlaka DynamoDB entegrasyonunu yapın.
  • Büyük ve kritik projelerde CLI workspace’leri yerine, dizin tabanlı (ve tercihen Terragrunt destekli) izole yapılar kurun.
  • .gitignore dosyanıza .terraform/, *.tfstate ve *.tfstate.backup maskelerini eklemeyi unutmayın.

State’iniz kilitli, pipeline’larınız yeşil kalsın!

Category: Genel | LEAVE A COMMENT
Ekim 18 2024

GitOps ile Infrastructure as Code: ArgoCD ve Flux Karşılaştırması

Modern yazılım geliştirme süreçlerinde “lokalde çalışıyordu, production’da neden patladı?” sendromunu aşmanın en asil yolu altyapımızı da uygulama kodlarımız gibi sürüm kontrol sistemlerinde tutmaktan geçiyor. İşte bu noktada, Infrastructure as Code (IaC) felsefesinin uygulama dağıtım süreçleriyle evlenmesinden doğan GitOps yaklaşımı devreye giriyor. Günümüzde Kubernetes cluster’larını yönetmenin fiili standardı haline gelen GitOps modelinde, Git repomuz sistemimizin “Single Source of Truth” (Tek Doğruluk Kaynağı) haline geliyor. Peki, bu ekosistemin iki dev oyuncusu olan ArgoCD ve Flux arasından hangisini seçmeliyiz? Bu makalede, iki aracı derinlemesine karşılaştıracak, Helm chart deployment pratiklerine bakacak ve işler ters gittiğinde can simidimiz olacak rollback stratejilerini inceleyeceğiz.

Push-Based vs. Pull-Based: Neden GitOps?

Geleneksel CI/CD pipeline süreçlerinde genellikle “Push-Based” (İtme Odaklı) bir yaklaşım benimseriz. Jenkins, GitLab CI veya GitHub Actions gibi bir araç, uygulamanın imajını build eder ve ardından kubectl apply veya helm upgrade komutlarıyla hedef Kubernetes cluster’ına bağlanıp değişikliği push eder. Kulağa kolay geliyor değil mi? Ancak bu yaklaşımın iki büyük karanlık noktası vardır:

  1. Güvenlik Açığı: CI/CD aracınıza cluster üzerinde yüksek yetkili (admin) erişim anahtarları (kubeconfig) vermeniz gerekir. Eğer CI sunucunuz hacklenirse, tüm cluster anahtarlarını teslim etmiş olursunuz.
  2. State Drift (Durum Sapması): Cluster üzerinde birisi manuel olarak (örneğin gece yarısı hotfix’i ile) bir deployment’ı değiştirirse, Git repomuzdaki kod ile canlıdaki durum birbirinden sapar. CI aracının bir sonraki tetiklenmesine kadar bu durumdan haberimiz olmaz.

Pull-Based GitOps ise bu problemi tersine çevirir. Cluster içerisine kurduğumuz bir operator (ArgoCD veya Flux), Git repomuzu sürekli izler (polling veya webhook ile). Repoda yeni bir commit gördüğünde, aradaki farkı algılar ve cluster’ı Git’teki durumla eşitlemek için “pull” eder. Dışarıya hiçbir credential sızmaz, cluster kendi kendini yönetir.

ArgoCD vs. Flux: Mimari ve Felsefe Farkları

İki araç da CNCF (Cloud Native Computing Foundation) bünyesinde mezun (graduated) projelerdir ve production ortamlarında rüştünü fazlasıyla ispatlamıştır. Ancak felsefeleri oldukça farklıdır.

1. ArgoCD: Görsel Güç ve Merkezi Yönetim

ArgoCD, kullanıcı dostu web arayüzü (UI) ve merkezi yönetim kabiliyetleriyle öne çıkar. Eğer organizasyonunuzda çok sayıda developer varsa ve onların Kubernetes kaynaklarını görsel olarak izlemesini, loglara erişmesini, senkronizasyon durumunu canlı grafiklerle görmesini istiyorsanız ArgoCD tam size göredir.

  • CRD Yapısı: Temel olarak Application ve AppProject Custom Resource Definition (CRD) yapılarını kullanır.
  • SSO Entegrasyonu: Dex aracılığıyla Keycloak, Okta, GitHub gibi kimlik sağlayıcılarla harika entegre olur. Multi-tenancy desteği mükemmeldir.
  • Yönetim Tarzı: Web UI üzerinden manuel tetikleme (Sync) yapılmasına izin verir (Opsiyonel olarak kapatılabilir).

2. Flux (Flux v2): Kubernetes Native ve Minimalist

Flux, Kubernetes felsefesine sıkı sıkıya bağlıdır. Bir web arayüzü barındırmaz (üçüncü parti CLI veya UI araçları hariç). Tamamen “Git-first” ve “declarative” bir yaklaşımla çalışır. Mikrofonu Kubernetes’in kendi controller mimarisine bırakır.

  • Modüler Mimari: Flux, GitOps Toolkit adı verilen bağımsız controller’lardan oluşur (Source Controller, Kustomize Controller, Helm Controller, Notification Controller).
  • Çoklu Cluster (Multi-cluster) Kolaylığı: Git repolarını hiyerarşik olarak tarayıp düzinelerce cluster’ı tek bir Flux instance’ı ile yönetmek oldukça esnektir.
  • CLI Gücü: fluxctl (artık sadece flux) CLI’ı, terminal bağımlısı mühendislerin rüyalarını süsleyecek seviyededir.

Hızlı Karşılaştırma Tablosu

  • Multi-Tenancy
  • Özellik ArgoCD Flux v2
    Kullanıcı Arayüzü (UI) Var (Çok güçlü ve detaylı) Yok (CLI odaklı / Üçüncü parti eklentiler var)
    Mimari Monolitik Operator / API Server Mikroservis / Modüler Controller’lar
    Helm Desteği Var (Client-side render eder) Var (Dedicated Helm Controller kullanır)
    Yerleşik (AppProject nesneleri ile) Kubernetes RBAC ve Namespace izolasyonu ile

    GitOps Dünyasında Helm Yönetimi

    GitOps yaparken Kubernetes manifestlerini doğrudan çiğ YAML olarak tutmak yerine genellikle Helm chart paketlerini kullanırız. Hem ArgoCD hem de Flux, Helm entegrasyonuna sahiptir ancak işleyişleri farklıdır.

    ArgoCD ile Helm Dağıtımı

    ArgoCD, Helm chart’ı Git’ten veya harici bir Helm repo’sundan okur. Arka planda helm template komutunu çalıştırarak saf manifestleri üretir ve bunları cluster’a uygular. Yani cluster’da gerçek bir helm list komutu çalıştırdığınızda bu release’i göremezsiniz; her şey standart Kubernetes kaynakları olarak görünür.

    İşte ArgoCD için örnek bir declarative Application manifesti:

    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: kertenkerem-api
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: 'https://charts.bitnami.com/bitnami'
        chart: nginx
        targetRevision: 15.0.0
        helm:
          valueFiles:
            - values.yaml
          parameters:
            - name: service.type
              value: ClusterIP
      destination:
        server: 'https://kubernetes.default.svc'
        namespace: production
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
    

    Buradaki selfHeal: true parametresi kritik önem taşır. Eğer birisi cluster’da gidip kubectl edit ile Nginx deployment’ının replika sayısını değiştirirse, ArgoCD bunu anında yakalar ve saniyeler içinde Git’teki duruma (örneğin 3 replikaya) geri döndürür.

    Flux ile Helm Dağıtımı

    Flux ise işi Kubernetes native controller’lar aracılığıyla çözer. İlk olarak kaynağı (HelmRepository) tanımlarız, ardından bu kaynaktan beslenen bir HelmRelease kaynağı oluştururuz. Flux, Kubernetes içerisinde gerçek Helm release’leri oluşturur.

    İlk adım: Helm Repository Tanımlama

    apiVersion: source.toolkit.fluxcd.io/v1beta2
    kind: HelmRepository
    metadata:
      name: bitnami
      namespace: flux-system
    spec:
      interval: 1h
      url: https://charts.bitnami.com/bitnami
    

    İkinci adım: HelmRelease Kaynağı

    apiVersion: helm.toolkit.fluxcd.io/v2beta1
    kind: HelmRelease
    metadata:
      name: kertenkerem-api
      namespace: production
    spec:
      interval: 15m
      chart:
        spec:
          chart: nginx
          version: '15.x'
          sourceRef:
            kind: HelmRepository
            name: bitnami
            namespace: flux-system
          interval: 1h
      install:
        remediation:
          retries: 3
      upgrade:
        remediation:
          retries: 3
      values:
        service:
          type: ClusterIP
    

    Rollback Stratejileri: İşler Sarpasarınca Ne Yapıyoruz?

    Canlıya yeni bir sürüm çıktık ve loglar bir anda kırmızıya boyandı. Geleneksel dünyada panikle helm rollback yazarız. Ancak GitOps dünyasında bu bir antipattern’dir! Neden mi? Çünkü cluster üzerinde manuel yapacağınız her rollback, Git reposundaki durumla çelişecektir. GitOps operator’ınız canlı durumu tekrar Git’tekine eşitlemek için (eğer self-heal açıksa) uygulamanızı tekrar hatalı sürüme yükseltecektir. Kısır döngüye hoş geldiniz!

    Peki doğru rollback stratejileri nelerdir?

    1. “Git Revert” Stratejisi (The Pure GitOps Way)

    En temiz, en denetlenebilir rollback yöntemi Git geçmişini geriye almaktır.
    Hatalı deployment’a sebep olan commit belirlenir ve lokalde şu komut koşturulur:

    git revert <commit-hash>
    git push origin main
    

    GitOps operator’ı değişikliği algılar, yeni “eski” versiyonu cluster’a uygular. Bu sayede audit log’larımızda kimin, ne zaman, neden rollback yaptığının kaydı kalır.

    2. Otomatik Helm Rollback (Automated Remediation)

    Bazen uygulamanın ayağa kalkamaması (CrashLoopBackOff) durumunda Git commit’ini bekleyecek vaktimiz olmayabilir. Flux ve ArgoCD bu durumlar için “Auto-Rollback” mekanizmaları sunar.

    Flux’ta Rollback Yapılandırması:
    Yukarıda paylaştığımız Flux HelmRelease manifestindeki remediation bloğu tam olarak bu işe yarar. Eğer Helm upgrade işlemi başarısız olursa (örneğin readiness probe’lar geçemezse), Flux otomatik olarak bir önceki çalışan Helm release sürümüne geri döner (rollback).

    ArgoCD’de Rollback Yapılandırması:
    ArgoCD’de eğer senkronizasyon başarısız olursa, otomatik rollback doğrudan ArgoCD Application seviyesinde yönetilebilir. Ancak dikkat edin; eğer Git’teki kod hala hatalı sürümü işaret ediyorsa ve ArgoCD “Auto-Sync” modundaysa, sürekli bir loop oluşabilir. Bu yüzden ArgoCD’de genellikle Progressive Delivery araçları (Argo Rollouts) ile birlikte Canary veya Blue/Green deployment stratejileri tercih edilir.

    Hangisini Seçmelisiniz?

    Yol ayrımındaysanız şu basit kuralları göz önünde bulundurabilirsiniz:

    • Şu durumlarda ArgoCD seçin: Yazılım geliştirme ekipleriniz cluster durumunu izlemek için terminale mahkum kalmak istemiyorsa, SSO entegreli şık bir dashboard şartsa ve uygulama gruplarını mantıksal projeler halinde (multi-tenancy) izole etmek istiyorsanız.
    • Şu durumlarda Flux seçin: Kubernetes felsefesine sıkı sıkıya bağlı kalmak istiyorsanız, kaynak tüketimi (resource footprint) sizin için kritikse, cluster içinde ekstra bir web server/UI çalıştırmak istemiyor ve tamamen Git entegrasyonuna güveniyorsanız.

    Her iki araç da günün sonunda sizi manuel müdahalelerin getirdiği güvensiz hislerden kurtaracak ve altyapınızı gerçek anlamda kod olarak yönetmenizi sağlayacaktır. Cluster’ınız kararlı, pipeline’larınız yeşil kalsın!

    Category: Genel | LEAVE A COMMENT
    Ekim 11 2024

    Linux Namespace ve Cgroup: Container’ların Altındaki Mekanizma

    Eğer bir teknik mülakatta ya da kahve molasında “Container nedir?” sorusuna “Hizmetleri izole eden hafifletilmiş sanal makinelerdir (VM)” yanıtını verdiyseniz, bugün bu ezberi bozuyoruz. Çünkü aslında işletim sistemi seviyesinde “container” diye bir nesne veya teknoloji bulunmuyor. Bizim docker, containerd ya da podman adını verdiğimiz araçların yaptığı her şey, linux çekirdeğinin (kernel) sunduğu iki temel özelliğin etrafına örülmüş şık birer kullanıcı arayüzü (wrapper) olmaktan ibaret: namespace ve cgroup.

    Bu makalede, işin kolayına kaçıp hazır CLI araçları kullanmak yerine, modern bir container runtime’ın (örneğin containerd) yaptığı işi tamamen elle (manual) yapacağız. Sıfırdan izole bir root filesystem (rootfs) hazırlayacak, kendi ağ geçidimizi kuracak, prosesleri izole edecek ve cgroup v2 ile kaynak sınırları koyacağız. Arkanıza yaslanın, terminalinizi açın ve container dünyasının arka bahçesine hoş geldiniz.

    1. Hazırlık: Minimal Bir Root Filesystem (rootfs) Oluşturmak

    Bir container’ın kendi dünyasında yaşayabilmesi için öncelikle bağımsız bir dosya sistemine ihtiyacı vardır. Docker imajları aslında katmanlaştırılmış (layered) tar dosyalarından başka bir şey değildir. Biz de işe minimal bir Alpine Linux rootfs indirerek başlayacağız.

    # Çalışma dizinimizi oluşturalım
    mkdir -p /tmp/kerten-container/rootfs
    cd /tmp/kerten-container
    
    # Alpine rootfs indiriyoruz
    curl -sSL https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.4-x86_64.tar.gz -o alpine.tar.gz
    tar -xzf alpine.tar.gz -C rootfs/
    rm alpine.tar.gz

    Artık elimizde izole bir işletim sisteminin sahip olması gereken tüm temel dizin yapısı (bin, sbin, etc, lib, proc…) mevcut. Sıradaki adım, bu dizini yeni dünyamızın kök dizini (root) haline getirmek.

    2. Chroot Değil, Pivot Root: Güvenli Mount Namespace

    Dosya sistemi izolasyonu denince akla gelen ilk syscall (sistem çağrısı) genellikle chroot olur. Ancak chroot esnektir ve root yetkilerine sahip bir proses kolayca bu hapishaneden kaçabilir (jailbreak). Modern container dünyası bunun yerine çok daha güvenli olan pivot_root sistem çağrısını kullanır.

    pivot_root, mevcut mount namespace’in root mount noktasını yeni bir dizine taşır ve eski root’u başka bir dizine bind eder. Bunu elle simüle etmek için öncelikle yeni bir mount namespace oluşturmamız gerekir.

    İşte sihirli komutumuz: unshare. Bu komut, belirtilen namespace türlerini sıfırdan oluşturarak yeni bir proses başlatır.

    # Mount, UTS (hostname) ve IPC namespace'lerini izole ederek yeni bir bash oturumu açıyoruz
    unshare --mount --uts --ipc --fork /bin/bash

    Şu andan itibaren açılan bu yeni shell oturumunda yaptığımız mount işlemleri host sistemimizi etkilemeyecek. Şimdi rootfs dizinimizi bir mount noktasına dönüştürelim ve pivot_root için hazırlayalım:

    # rootfs dizinimizi bir bind mount olarak işaretliyoruz (pivot_root bunu şart koşar)
    mount --bind /tmp/kerten-container/rootfs /tmp/kerten-container/rootfs
    
    # Eski root dizinini koyacağımız geçici bir klasör oluşturuyoruz
    mkdir -p /tmp/kerten-container/rootfs/put_old
    
    # pivot_root komutunu çalıştırıyoruz: [yeni_root] [eski_root_un_duracagi_yer]
    cd /tmp/kerten-container/rootfs
    pivot_root . put_old
    
    # Artık yeni root içerisindeyiz. Eski sisteme ait mount noktalarını temizleyelim
    cd /
    umount -l /put_old
    rmdir /put_old
    
    # Hostname'i değiştirelim (UTS namespace sayesinde host etkilenmez)
    hostname kerten-container

    Harika! Şu an sadece kendi hazırladığımız rootfs içerisindeki ikili dosyaları (binaries) görebilen, izole edilmiş bir dosya sistemindeyiz. Ancak eksik bir şeyler var: Prosesler nerede?

    3. PID Namespace: “Ben Kimim?” Sorusu

    Eğer az önce oluşturduğumuz ortamda ps aux çalıştırmayı denerseniz, hata alırsınız. Çünkü proseslerin yönetildiği ve kernel bilgilerinin okunduğu sanal dosya sistemi olan /proc henüz mount edilmedi. Daha da önemlisi, hala host sistemin PID (Process ID) uzayını paylaşıyoruz.

    Gelin, yeni bir PID namespace oluşturalım. Bunun için yeni bir terminal penceresi açıp host üzerinde çalışmaya devam edeceğiz. Bu sayede hem host hem de container arasındaki ilişkiyi daha net görebiliriz.

    Bu sefer PID namespace’i de işin içine katarak unshare ile yeni bir container başlatalım (dosya sistemi adımlarını bu yeni namespace içinde hızlıca tekrarladığımızı varsayalım veya doğrudan aşağıdaki komutla temiz bir başlangıç yapalım):

    # PID ve Mount namespace izole edilmiş şekilde başlatıyoruz
    unshare --mount --pid --fork --mount-proc /bin/bash

    --mount-proc parametresi, bizim için otomatik olarak yepyeni ve izole bir /proc dosya sistemi mount eder. Şimdi bu oturumda prosesleri listeleyelim:

    ps aux

    Çıktıya dikkat edin:

    PID   USER     TIME  COMMAND
        1 root      0:00 /bin/bash
        2 root      0:00 ps aux

    İşte container dünyasının kutsal kasesi! Host üzerinde binlerce çalışan proses varken, bizim container’ımız kendisini dünyadaki tek proses (PID 1) olarak görüyor. PID 1 olmak büyük bir sorumluluktur; eğer bu proses ölürse, kernel tüm namespace’i sonlandırır.

    4. Network Namespace: Kablolama İşlemleri

    Şu ana kadar dosya sistemini ve prosesleri izole ettik ancak container’ımızın dış dünya ile bağlantısı yok (loopback arayüzü bile kapalı). Docker arkada bu işi sanal bir switch (docker0 bridge) ve veth pair (sanal ethernet kablosu) kullanarak çözer.

    Gelin bu kablolamayı host üzerinde elle yapalım. Bu senaryo için host üzerinde root yetkileriyle yeni bir terminal açın.

    Öncelikle container’ımızın namespace’ini kalıcı hale getirmemiz gerekir ki host üzerinden oraya erişebilelim. Linux’ta her namespace /proc/[PID]/ns/ altında bir dosya olarak temsil edilir.

    # Container'ımızın PID'sini host üzerinde bulalım (örneğin 12345 olsun)
    # Host üzerinde:
    ip netns attach kerten-netns 12345

    Şimdi sanal ethernet çiftimizi oluşturalım. Bu işlem, bir ucu hostta, diğer ucu container içinde olan sanal bir kablo yaratacaktır:

    # veth çiftini oluştur
    ip link add veth-host type veth peer name veth-container
    
    # Kablonun container ucunu container'ın network namespace'ine taşıyalım
    ip link set veth-container netns kerten-netns
    
    # Host tarafındaki uca IP verelim ve ayağa kaldıralım
    ip addr add 10.200.0.1/24 dev veth-host
    ip link set veth-host up
    
    # Container tarafındaki uca IP verelim ve ayağa kaldıralım
    ip netns exec kerten-netns ip addr add 10.200.0.2/24 dev veth-container
    ip netns exec kerten-netns ip link set veth-container up
    ip netns exec kerten-netns ip link set lo up # Loopback'i unutmayalım
    
    # Container için default gateway tanımlayalım
    ip netns exec kerten-netns ip route add default via 10.200.0.1

    Artık container içinden host tarafındaki 10.200.0.1 IP adresine ping atabilirsiniz. Container runtime’ların her container için saniyeler içinde yaptığı o karmaşık ağ konfigürasyonunun temel mekanizması tam olarak budur.

    5. Cgroup v2 Sihri: Kaynakları Sınırlandırmak

    Güzel, izole bir ortamımız ve ağımız var. Peki ya bu container içerisindeki bir proses çıldırır ve host sistemin tüm CPU ve RAM kaynaklarını tüketmeye çalışırsa? İşte burada devreye cgroup (Control Groups) giriyor.

    Modern Linux dağıtımları artık varsayılan olarak cgroup v2 kullanıyor. Cgroup v2, v1’deki dağınık yapıyı tek bir hiyerarşik ağaç altında birleştirerek işleri inanılmaz derecede kolaylaştırdı. Cgroup v2 hiyerarşisi varsayılan olarak /sys/fs/cgroup dizininde yaşar.

    Şimdi elle “kerten-limit” adında bir kontrol grubu oluşturalım ve container’ımızın bellek kullanımını 100 MB ile sınırlayalım:

    # Host üzerinde cgroup dizinine gidelim
    cd /sys/fs/cgroup
    
    # Yeni bir grup oluşturmak sadece bir dizin oluşturmaktan ibarettir!
    mkdir kerten-limit
    cd kerten-limit
    
    # Kernel, bu dizini oluşturduğumuz an içine kontrol dosyalarını otomatik olarak yerleştirir.
    ls -la

    Şimdi bellek sınırımızı (memory limit) 100MB (104857600 bytes) olarak ayarlayalım:

    echo "104857600" > memory.max

    Peki bu sınırı container prosesimize nasıl uygulayacağız? Çok basit: Container prosesimizin host üzerindeki PID’sini (örneğin 12345) cgroup altındaki cgroup.procs dosyasına yazmamız yeterli:

    echo "12345" > cgroup.procs

    Artık bu proses veya bu prosesten türeyecek (fork) olan tüm alt süreçler toplamda 100 MB bellek sınırını aşamazlar. Sınırı aşmaya çalıştıkları anda kernel’ın meşhur OOM-Killer (Out of Memory Killer) mekanizması devreye girecek ve o prosesi acımasızca sonlandıracaktır. Tıpkı Kubernetes ortamında aldığınız o meşhur OOMKilled hatası gibi!

    Özet: Container Aslında Bir İllüzyondur

    Gördüğünüz gibi, arka planda çalışan gizemli hipervizörler, sanal donanımlar veya ağır sanallaştırma katmanları yok. Yaptığımız her şey, Linux kernel’ına “Bu prosese sadece şu dizini göster (mount namespace), sadece şu prosesleri görmesine izin ver (PID namespace), ağ trafiğini şu sanal kabloya yönlendir (net namespace) ve şu kadar kaynak tüketmesine izin ver (cgroups)” demekten ibaretti.

    Docker ve containerd gibi araçlar, bu karmaşık syscall ve CLI yönetimini otomatize ederek bize pratik birer imaj paketleme ve dağıtım standardı sunar. Bu temel mekanizmayı kavramak, Kubernetes ortamlarında veya büyük ölçekli altyapılarda karşılaştığınız ağ, performans ve izolasyon sorunlarını (troubleshooting) çok daha hızlı ve profesyonelce çözmenizi sağlayacaktır.

    Bir sonraki derin dalış makalemizde görüşmek üzere, sistemleriniz ayakta, container’larınız hafif kalsın!

    Category: Genel | LEAVE A COMMENT
    Ekim 4 2024

    Cron İşlerini Bash ile Monitör Etme ve Alerting

    Modern altyapılarda Kubernetes cronjob’ları, serverless scheduler’lar havada uçuşsa da, günün sonunda hepimizin bir yerlerde tıkır tıkır çalışan (ya da çalıştığını umduğu) emektar bir linux sunucusu ve içinde barınan klasik cron işleri vardır. Ancak cron’un en büyük problemi “sessiz ölüm” (silent failure) dediğimiz durumdur. Bir backup script’i patladığında, disk dolduğunda veya API key’in süresi geçtiğinde cron bunu size söylemez. Bu yazıda, kendi yazacağımız esnek bir bash wrapper script ile tüm cron işlerinizi merkezi olarak monitoring dünyasına entegre edecek, hata durumlarında anında alert üretecek ve üçüncü parti servislerle nasıl haberleştireceğimizi göreceğiz.

    Klasik Cron Yaklaşımlarının Sorunu Nedir?

    Hepimiz o yollardan geçtik. Bir cron işinin çıktısını almak için satırın sonuna efsanevi >> /var/log/myjob.log 2>&1 yönlendirmesini yazarız. Sonra ne mi olur? O log dosyası gigabaytlarca büyür, diski doldurur ve bir gün script hata verdiğinde kimsenin haberi olmaz. Çünkü logları aktif olarak okuyan kimse yoktur.

    Eski usul Linux yöneticileri size “MTA (Mail Transfer Agent) kurun, cron hata aldığında lokal mail atsın” diyecektir. Yıl 2024+. ephemeral VM’lerin, cloud instance’larının cirit attığı bu çağda localhost mail kuyruklarıyla uğraşmak, postfix yapılandırmak ve spame düşen lokal mailleri takip etmek tam bir operasyonel işkencedir. Bizim daha proaktif, modern ve DevOps pratiklerine uygun bir çözüme ihtiyacımız var.

    Wrapper Script Pattern Neden En İyi Çözümdür?

    Elinizde 20 farklı cron script’i olduğunu düşünün. Her birinin içine tek tek Slack webhook entegrasyonu, hata yakalama (error handling) ve çalışma süresi ölçümü eklemek tam bir anti-pattern’dir. Kod tekrarı yaratır ve yarın öbür gün Slack yerine Teams’e geçildiğinde 20 farklı script’i update etmeniz gerekir.

    Wrapper Script Pattern, çalıştırılacak asıl komutu sarmalayan, onun exit code’unu yakalayan, standart output (stdout) ve standart error (stderr) akışlarını yöneten merkezi bir aracıdır. Crontab içerisindeki tanımımız şuna benzer:

    0 4 * * * /usr/local/bin/cron-wrapper.sh --job "db-backup" --cmd "/opt/scripts/backup.sh --force"

    Bu yaklaşım sayesinde, asıl işi yapan script’in monitoring mantığından tamamen bağımsız (decoupled) kalmasını sağlarız. Script sadece işini yapar ve exit code döner; wrapper ise raporlamayı üstlenir.

    Adım 1: Savaşçı Ruhlu Bash Wrapper Script

    Lafı uzatmadan production ortamında güvenle kullanabileceğiniz, hata durumunda exit code’u ve logları yakalayan, zaman aşımı (timeout) kontrolü yapabilen wrapper script’imizi yazalım. Bu script’i /usr/local/bin/cron-wrapper.sh olarak kaydedebilirsiniz.

    #!/usr/bin/env bash
    
    # Robust Bash Ayarları
    set -o nounset
    set -o pipefail
    
    # Değişken Tanımları
    JOB_NAME=""
    COMMAND=""
    SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-""}"
    HEALTHCHECK_URL="${HEALTHCHECK_URL:-""}"
    
    # Kullanım Yardımcısı
    usage() {
        echo "Kullanım: $0 --job [JOB_NAME] --cmd [COMMAND]"
        exit 1
    }
    
    # Parametreleri Parse Etme
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --job)
                JOB_NAME="$2"
                shift 2
                ;;
            --cmd)
                COMMAND="$2"
                shift 2
                ;;
            *)
                usage
                ;;
        esac
    done
    
    if [[ -z "$JOB_NAME" || -z "$COMMAND" ]]; then
        usage
    fi
    
    # Geçici Log Dosyası Oluşturma (Güvenli yöntem)
    LOG_FILE=$(mktemp /tmp/cron_wrapper_${JOB_NAME}.XXXXXX)
    
    # Her durumda temizlik yapılması için trap kullanımı
    cleanup() {
        rm -f "$LOG_FILE"
    }
    trap cleanup EXIT
    
    # Zamanı Ölçmeye Başla
    START_TIME=$(date +%s)
    
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job '$JOB_NAME' başlatılıyor..." >> "$LOG_FILE"
    echo "Komut: $COMMAND" >> "$LOG_FILE"
    echo "--------------------------------------------------" >> "$LOG_FILE"
    
    # Komutu çalıştır ve tüm çıktıyı log dosyasına yönlendir
    # set +e kullanıyoruz çünkü komut hata verirse wrapper script'in hemen çökmesini istemiyoruz,
    # hatayı yakalayıp alert üreteceğiz.
    set +e
    eval "$COMMAND" >> "$LOG_FILE" 2>&1
    EXIT_CODE=$?
    set -e
    
    # Zamanı Hesapla
    END_TIME=$(date +%s)
    DURATION=$((END_TIME - START_TIME))
    
    echo "--------------------------------------------------" >> "$LOG_FILE"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Job bitti. Süre: ${DURATION}sn, Exit Code: $EXIT_CODE" >> "$LOG_FILE"
    
    # Sonuçları Değerlendir
    if [ $EXIT_CODE -eq 0 ]; then
        echo "Başarılı: $JOB_NAME sorunsuz tamamlandı."
        # Eğer tanımlıysa Healthcheck ping'i gönder (Mavi Tik)
        if [ -n "$HEALTHCHECK_URL" ]; then
            curl -fsS --retry 3 "$HEALTHCHECK_URL" > /dev/null 2>&1 || true
        fi
    else
        echo "HATA: $JOB_NAME başarısız oldu! Exit code: $EXIT_CODE" >&2
        
        # Slack Alert Tetikleme fonksiyonunu çağır
        if [ -n "$SLACK_WEBHOOK_URL" ]; then
            # Log dosyasının son 20 satırını alalım ki Slack mesajı devasa olmasın
            LAST_LOGS=$(tail -n 20 "$LOG_FILE" | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
            
            PAYLOAD=$(cat <<EOF
    {
      "text": "🚨 *Cron Job Hatası!*",
      "attachments": [
        {
          "color": "danger",
          "fields": [
            { "title": "Job Adı", "value": "${JOB_NAME}", "short": true },
            { "title": "Exit Code", "value": "${EXIT_CODE}", "short": true },
            { "title": "Süre", "value": "${DURATION} saniye", "short": true },
            { "title": "Sunucu", "value": "$(hostname)", "short": true }
          ],
          "text": "*Son Loglar:*\n\`\`\`${LAST_LOGS}\`\`\`"
        }
      ]
    }
    EOF
    )
            curl -X POST -H 'Content-type: application/json' --data "$PAYLOAD" "$SLACK_WEBHOOK_URL" > /dev/null 2>&1 || true
        fi
        
        # Wrapper kendisi de hata koduyla çıksın ki pipeline'lar veya üst sistemler anlasın
        exit $EXIT_CODE
    fi
    

    Neden “set +e” ve “set -o pipefail” Kullandık?

    Bash’te hata yönetimi hassas bir konudur. set -o pipefail komutu, bir pipe zincirindeki herhangi bir komut hata verirse tüm satırın hata kodu dönmesini sağlar. Wrapper script’in başında bunu aktif ettik. Ancak, sarmaladığımız asıl komut hata verdiğinde script’imizin patlayıp Slack alert’ini gönderemeden sonlanmasını engellemek için, çalıştırma aşamasında geçici olarak set +e ile esneklik sağladık, exit code’u güvenli bir şekilde $? ile lokal değişkene aldık ve ardından set -e ile güvenli modumuza geri döndük.

    Ayrıca mktemp kullanımı, paralel çalışan cron işlerinin aynı log dosyası üzerine yazarak yarış durumuna (race condition) girmesini engeller.

    Adım 2: “Dead Man’s Snitch” Mantığı ile Healthcheck Entegrasyonu

    Peki ya sunucunun kendisi çökerse? Ya da cron daemon durursa? Bu durumlarda “hata aldığımda Slack’e yaz” mantığı tamamen devre dışı kalır. Çünkü çalışmayan bir sistem hata mesajı da gönderemez. Buna monitoring dünyasında silent failure denir.

    Çözüm: Inverted Monitoring (Ters İzleme) ya da bilinen adıyla Heartbeat / Dead Man’s Snitch pattern. Sistem çalışmayı durdurduğunda alarm çalmasını istiyorsak, sistemin düzenli aralıklarla “Ben hayattayım!” sinyali göndermesi gerekir. Eğer sinyal gelmezse, monitoring sunucumuz bizi uyarır.

    Bu iş için healthchecks.io veya kendi host ettiğiniz open-source alternatiflerini (örneğin Uptime Kuma) kullanabilirsiniz. Script’imizdeki HEALTHCHECK_URL tam olarak bunu yapar. Cron başarılı biterse ilgili adrese bir curl isteği atar. Eğer o istek zamanında gitmezse, dış dünya sunucunun patladığını anlar.

    Adım 3: Slack Webhook ile Gerçek Zamanlı Alerting

    Yukarıdaki script’imizde Slack için JSON payload’unu dinamik olarak oluşturduk. Bash içinde JSON oluştururken kaçış karakterleri (escaping) her zaman baş ağrıtır. Bu yüzden log dosyasından okuduğumuz son 20 satırı (LAST_LOGS) temizlemek için sed ile yeni satır karakterlerini (\n) ve çift tırnakları escape ettik.

    Slack’te görünecek mesaj, hata anında debug yapmanızı inanılmaz kolaylaştıracaktır. Hangi sunucuda, ne kadar sürede patladığını ve hatanın son satırlarını terminale girmeden telefonunuzdan görebilirsiniz:

    Slack Notification Alerting

    Adım 4: Crontab Yapılandırması

    Şimdi yazdığımız bu sistemi gerçek bir senaryoda test edelim. Sunucumuzdaki env variable’ları crontab ortamına geçirmek bazen can sıkıcı olabilir. En temiz yöntem webhook url gibi hassas verileri wrapper script içine hardcode etmek yerine wrapper’ı çağırırken bir konfigürasyondan beslemek veya crontab’ın üst kısmında tanımlamaktır.

    # /etc/cron.d/my-monitored-crons veya crontab -e
    
    SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00/B00/X00"
    HEALTHCHECK_URL="https://hc-ping.com/your-uuid-here"
    
    # Her gece 03:00'te yedek al, hata olursa Slack'e bildir, başarılı olursa ping at
    0 3 * * * root /usr/local/bin/cron-wrapper.sh --job "database-backup" --cmd "/opt/backups/pg_dump.sh"
    

    Dikkat Edilmesi Gereken İpuçları

    • Mutlak Yol (Absolute Paths) Kullanın: Cron ortamında $PATH değişkeni kısıtlıdır. Hem wrapper script’in içinde çağırdığınız tool’ların (curl, sed vb.) hem de crontab içindeki komutların tam yollarını yazmaya özen gösterin.
    • Yetkilendirme: Wrapper script’in çalıştırılabilir olduğundan emin olun: chmod +x /usr/local/bin/cron-wrapper.sh
    • Log Rotasyonu: Biz script içinde trap kullanarak geçici logları işlem sonunda siliyoruz. Ancak işlem yarıda kesilirse (örneğin kill -9) loglar kalabilir. Periyodik olarak /tmp dizininin temizlendiğinden emin olun (çoğu modern Linux dağıtımı bunu otomatik yapar).

    Özet

    Cron işlerini kendi haline bırakmak, prod ortamında saatli bir bomba üzerinde oturmak gibidir. Bu makalede yazdığımız esnek bash wrapper script sayesinde, mevcut hiçbir altyapınızı bozmadan, tek bir merkezi mekanizma ile tüm linux cron işlerinizi modern birer microservice gibi monitoring ve alert süreçlerine dahil ettik. Unutmayın, en iyi sistem tıkır tıkır çalışan değil, bozulduğunda neresinin bozulduğunu size ilk söyleyen sistemdir.

    Category: Genel | LEAVE A COMMENT