Kubernetes Uygulama Deployment: Ansible ile Tam Rehber

Kubernetes cluster’ınız var, uygulamalarınız hazır ama her deployment’ta aynı kubectl apply komutlarını tekrar tekrar yazmaktan bıktınız mı? Ya da staging’de çalışan bir şeyi production’a taşırken “acaba bir şeyi unuttu muyum” diye endişe duyuyor musunuz? İşte bu noktada Ansible devreye giriyor. Ansible’ın idempotent yapısı ve Kubernetes modülleriyle birlikte, deployment süreçlerinizi tekrarlanabilir, güvenilir ve otomatik hale getirebilirsiniz.

Bu yazıda gerçek dünya senaryoları üzerinden Ansible ile Kubernetes deployment’ı nasıl yönetirsiniz, adım adım anlatacağım.

Neden Ansible + Kubernetes?

Kubectl direkt kullanmak başlangıçta yeterli görünüyor. Ama ekip büyüdükçe, ortam sayısı arttıkça ve deployment sıklığı yükseldikçe manuel süreçler kaos yaratmaya başlıyor. Ansible bu noktada birkaç kritik avantaj sağlıyor.

İdempotency en büyük kazanım. Aynı playbook’u 10 kez çalıştırsanız da sonuç aynı. Deployment’ın yarıda kesilmesi durumunda tekrar başlatabilirsiniz, sistem tutarlı kalır.

Secret yönetimi bir diğer önemli nokta. Ansible Vault ile hassas bilgileri şifreleyip playbook’larınıza dahil edebilirsiniz. Kubernetes secret’larını düz metin olarak Git’e push etme günleri geride kalır.

Multi-environment yönetimi de çok kolaylaşıyor. Tek bir playbook ile dev, staging ve production ortamlarına farklı parametrelerle deployment yapabilirsiniz.

Ön Gereksinimler ve Kurulum

Ansible kontrol makinenizde şunların kurulu olması gerekiyor:

# Ansible kurulumu (Python 3.8+ gerekli)
pip3 install ansible kubernetes

# Kubernetes koleksiyonunu kur
ansible-galaxy collection install kubernetes.core

# Kurulumu doğrula
ansible --version
python3 -c "import kubernetes; print(kubernetes.__version__)"

Ansible’ın Kubernetes cluster’ınıza erişebilmesi için kubeconfig dosyasının doğru konumda olması gerekiyor:

# Kubeconfig konumunu test et
kubectl cluster-info

# Ansible'ın hangi kubeconfig'i kullanacağını belirt
export KUBECONFIG=/home/your_user/.kube/config

# Ya da inventory dosyasında belirtin
# Bu yaklaşımı aşağıda göreceğiz

Proje Dizin Yapısı

Düzenli bir Ansible projesi için şu yapıyı öneriyorum. Gerçek dünyada bu yapı hem okunabilirliği artırıyor hem de CI/CD pipeline’ına entegrasyonu kolaylaştırıyor:

k8s-ansible/
├── inventory/
│   ├── dev/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   │       └── all.yml
│   ├── staging/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   │       └── all.yml
│   └── production/
│       ├── hosts.yml
│       └── group_vars/
│           └── all.yml
├── roles/
│   ├── k8s-namespace/
│   ├── k8s-deployment/
│   ├── k8s-service/
│   └── k8s-ingress/
├── playbooks/
│   ├── deploy-app.yml
│   ├── rollback.yml
│   └── cleanup.yml
├── vault/
│   └── secrets.yml
└── ansible.cfg

İlk Playbook: Namespace ve Temel Kaynaklar

Bir e-ticaret uygulamasını deploy ettiğimizi düşünelim. Önce namespace ve temel kaynakları oluşturalım:

# playbooks/setup-namespace.yml
---
- name: Kubernetes Namespace ve Temel Kaynaklar
  hosts: localhost
  connection: local
  gather_facts: false

  vars:
    app_namespace: "ecommerce"
    environment: "production"
    kubeconfig_path: "{{ lookup('env', 'KUBECONFIG') }}"

  tasks:
    - name: Namespace olustur
      kubernetes.core.k8s:
        kubeconfig: "{{ kubeconfig_path }}"
        state: present
        definition:
          apiVersion: v1
          kind: Namespace
          metadata:
            name: "{{ app_namespace }}"
            labels:
              environment: "{{ environment }}"
              managed-by: ansible

    - name: Resource Quota tanimla
      kubernetes.core.k8s:
        kubeconfig: "{{ kubeconfig_path }}"
        state: present
        definition:
          apiVersion: v1
          kind: ResourceQuota
          metadata:
            name: "{{ app_namespace }}-quota"
            namespace: "{{ app_namespace }}"
          spec:
            hard:
              requests.cpu: "4"
              requests.memory: 8Gi
              limits.cpu: "8"
              limits.memory: 16Gi
              pods: "20"

    - name: Namespace durumunu dogrula
      kubernetes.core.k8s_info:
        kubeconfig: "{{ kubeconfig_path }}"
        kind: Namespace
        name: "{{ app_namespace }}"
      register: namespace_info

    - name: Sonucu goster
      debug:
        msg: "Namespace {{ namespace_info.resources[0].metadata.name }} durumu: {{ namespace_info.resources[0].status.phase }}"

Secret ve ConfigMap Yönetimi

Bu kısım genellikle en çok hata yapılan yer. Production secret’larını Ansible Vault ile yönetmek hem güvenli hem de pratik:

# Vault dosyası oluştur
ansible-vault create vault/secrets.yml

# İçeriği şu şekilde olacak (vault editor'da):
# db_password: "super_secret_password_123"
# redis_password: "redis_secret_456"
# jwt_secret: "jwt_very_long_secret_key"
# docker_registry_password: "registry_token_xyz"

Şimdi bu secret’ları Kubernetes’e aktaran playbook:

# playbooks/deploy-secrets.yml
---
- name: Kubernetes Secrets ve ConfigMap Deploy
  hosts: localhost
  connection: local
  gather_facts: false

  vars_files:
    - ../vault/secrets.yml

  vars:
    app_namespace: "ecommerce"
    app_name: "web-store"
    db_host: "postgres-service.ecommerce.svc.cluster.local"
    redis_host: "redis-service.ecommerce.svc.cluster.local"
    app_replicas: 3

  tasks:
    - name: Database secret olustur
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Secret
          metadata:
            name: "{{ app_name }}-db-secret"
            namespace: "{{ app_namespace }}"
          type: Opaque
          stringData:
            DB_PASSWORD: "{{ db_password }}"
            REDIS_PASSWORD: "{{ redis_password }}"
            JWT_SECRET: "{{ jwt_secret }}"

    - name: ConfigMap olustur
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: v1
          kind: ConfigMap
          metadata:
            name: "{{ app_name }}-config"
            namespace: "{{ app_namespace }}"
          data:
            DB_HOST: "{{ db_host }}"
            REDIS_HOST: "{{ redis_host }}"
            APP_ENV: "production"
            LOG_LEVEL: "warn"
            MAX_CONNECTIONS: "100"

    - name: Docker registry secret olustur
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Secret
          metadata:
            name: registry-credentials
            namespace: "{{ app_namespace }}"
          type: kubernetes.io/dockerconfigjson
          data:
            .dockerconfigjson: "{{ {'auths': {'registry.example.com': {'username': 'deploy_user', 'password': docker_registry_password}}} | to_json | b64encode }}"

Ana Deployment Playbook’u

İşte asıl iş burada başlıyor. Gerçek bir uygulama deployment’ı için kapsamlı bir örnek:

# playbooks/deploy-app.yml
---
- name: Web Store Uygulama Deployment
  hosts: localhost
  connection: local
  gather_facts: false

  vars_files:
    - ../vault/secrets.yml

  vars:
    app_name: "web-store"
    app_namespace: "ecommerce"
    image_tag: "{{ lookup('env', 'IMAGE_TAG') | default('latest') }}"
    image_repo: "registry.example.com/ecommerce/web-store"
    app_replicas: "{{ lookup('env', 'APP_REPLICAS') | default(3) | int }}"
    min_ready_seconds: 30
    deployment_timeout: 300

  pre_tasks:
    - name: Image tag kontrolu
      fail:
        msg: "IMAGE_TAG environment variable tanimlanmamis! Ornek: export IMAGE_TAG=v1.2.3"
      when: image_tag == "latest" and ansible_env.CI is defined

    - name: Mevcut deployment durumunu kaydet
      kubernetes.core.k8s_info:
        kind: Deployment
        name: "{{ app_name }}"
        namespace: "{{ app_namespace }}"
      register: current_deployment
      ignore_errors: true

    - name: Rollback icin mevcut image'i kaydet
      set_fact:
        previous_image: "{{ current_deployment.resources[0].spec.template.spec.containers[0].image | default('none') }}"
      when: current_deployment.resources | length > 0

  tasks:
    - name: Deployment manifest uygula
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: "{{ app_name }}"
            namespace: "{{ app_namespace }}"
            labels:
              app: "{{ app_name }}"
              version: "{{ image_tag }}"
            annotations:
              deployment.kubernetes.io/revision: "1"
              ansible/last-deployed: "{{ ansible_date_time.iso8601 }}"
          spec:
            replicas: "{{ app_replicas }}"
            minReadySeconds: "{{ min_ready_seconds }}"
            strategy:
              type: RollingUpdate
              rollingUpdate:
                maxUnavailable: 1
                maxSurge: 1
            selector:
              matchLabels:
                app: "{{ app_name }}"
            template:
              metadata:
                labels:
                  app: "{{ app_name }}"
                  version: "{{ image_tag }}"
              spec:
                imagePullSecrets:
                  - name: registry-credentials
                containers:
                  - name: "{{ app_name }}"
                    image: "{{ image_repo }}:{{ image_tag }}"
                    ports:
                      - containerPort: 8080
                    envFrom:
                      - configMapRef:
                          name: "{{ app_name }}-config"
                      - secretRef:
                          name: "{{ app_name }}-db-secret"
                    resources:
                      requests:
                        memory: "256Mi"
                        cpu: "250m"
                      limits:
                        memory: "512Mi"
                        cpu: "500m"
                    readinessProbe:
                      httpGet:
                        path: /health/ready
                        port: 8080
                      initialDelaySeconds: 15
                      periodSeconds: 10
                      failureThreshold: 3
                    livenessProbe:
                      httpGet:
                        path: /health/live
                        port: 8080
                      initialDelaySeconds: 30
                      periodSeconds: 20
                      failureThreshold: 3
                affinity:
                  podAntiAffinity:
                    preferredDuringSchedulingIgnoredDuringExecution:
                      - weight: 100
                        podAffinityTerm:
                          labelSelector:
                            matchExpressions:
                              - key: app
                                operator: In
                                values:
                                  - "{{ app_name }}"
                          topologyKey: kubernetes.io/hostname

    - name: Deployment'in tamamlanmasini bekle
      kubernetes.core.k8s_rollout_status:
        name: "{{ app_name }}"
        namespace: "{{ app_namespace }}"
        kind: Deployment
        timeout: "{{ deployment_timeout }}"
      register: rollout_status

    - name: Deployment basarili mesaji
      debug:
        msg: "Deployment basarili! {{ app_name }}:{{ image_tag }} {{ app_replicas }} replica ile calisıyor."
      when: rollout_status is succeeded

Service ve Ingress Yapılandırması

Deployment sonrasında servis ve ingress kaynaklarını da aynı playbook zincirinde yönetebilirsiniz:

# playbooks/deploy-networking.yml
---
- name: Service ve Ingress Yapilandirma
  hosts: localhost
  connection: local
  gather_facts: false

  vars:
    app_name: "web-store"
    app_namespace: "ecommerce"
    domain_name: "store.example.com"
    tls_secret_name: "store-tls-cert"

  tasks:
    - name: ClusterIP Service olustur
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Service
          metadata:
            name: "{{ app_name }}-service"
            namespace: "{{ app_namespace }}"
            labels:
              app: "{{ app_name }}"
          spec:
            selector:
              app: "{{ app_name }}"
            ports:
              - name: http
                protocol: TCP
                port: 80
                targetPort: 8080
            type: ClusterIP

    - name: HorizontalPodAutoscaler tanimla
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: autoscaling/v2
          kind: HorizontalPodAutoscaler
          metadata:
            name: "{{ app_name }}-hpa"
            namespace: "{{ app_namespace }}"
          spec:
            scaleTargetRef:
              apiVersion: apps/v1
              kind: Deployment
              name: "{{ app_name }}"
            minReplicas: 2
            maxReplicas: 10
            metrics:
              - type: Resource
                resource:
                  name: cpu
                  target:
                    type: Utilization
                    averageUtilization: 70
              - type: Resource
                resource:
                  name: memory
                  target:
                    type: Utilization
                    averageUtilization: 80

    - name: Ingress olustur
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: networking.k8s.io/v1
          kind: Ingress
          metadata:
            name: "{{ app_name }}-ingress"
            namespace: "{{ app_namespace }}"
            annotations:
              nginx.ingress.kubernetes.io/rewrite-target: /
              nginx.ingress.kubernetes.io/ssl-redirect: "true"
              cert-manager.io/cluster-issuer: letsencrypt-prod
          spec:
            ingressClassName: nginx
            tls:
              - hosts:
                  - "{{ domain_name }}"
                secretName: "{{ tls_secret_name }}"
            rules:
              - host: "{{ domain_name }}"
                http:
                  paths:
                    - path: /
                      pathType: Prefix
                      backend:
                        service:
                          name: "{{ app_name }}-service"
                          port:
                            number: 80

    - name: Ingress IP adresini al
      kubernetes.core.k8s_info:
        kind: Ingress
        name: "{{ app_name }}-ingress"
        namespace: "{{ app_namespace }}"
      register: ingress_info
      retries: 10
      delay: 15
      until: ingress_info.resources[0].status.loadBalancer.ingress is defined

    - name: Erisim bilgisini goster
      debug:
        msg: "Uygulama erisim adresi: https://{{ domain_name }} (IP: {{ ingress_info.resources[0].status.loadBalancer.ingress[0].ip | default('pending') }})"

Rollback Mekanizması

Production’da bir şeyler ters gittiğinde hızlı rollback yapabilmek hayat kurtarır. İşte bunun için hazır bir playbook:

# playbooks/rollback.yml
---
- name: Uygulama Rollback
  hosts: localhost
  connection: local
  gather_facts: false

  vars:
    app_name: "web-store"
    app_namespace: "ecommerce"
    rollback_timeout: 180

  tasks:
    - name: Mevcut deployment revision bilgisini al
      kubernetes.core.k8s_info:
        kind: Deployment
        name: "{{ app_name }}"
        namespace: "{{ app_namespace }}"
      register: current_deploy

    - name: Revision bilgisini goster
      debug:
        msg: |
          Mevcut Image: {{ current_deploy.resources[0].spec.template.spec.containers[0].image }}
          Mevcut Revision: {{ current_deploy.resources[0].metadata.annotations['deployment.kubernetes.io/revision'] | default('bilinmiyor') }}

    - name: Rollback onayini iste
      pause:
        prompt: "Rollback yapilacak. Devam etmek icin ENTER, iptal icin Ctrl+C"
      when: not ansible_check_mode

    - name: Kubectl rollout undo calistir
      kubernetes.core.k8s_json_patch:
        kind: Deployment
        name: "{{ app_name }}"
        namespace: "{{ app_namespace }}"
        patch:
          - op: replace
            path: /spec/template/metadata/annotations
            value:
              rollback-triggered: "{{ ansible_date_time.iso8601 }}"
      when: rollback_to_image is not defined

    - name: Belirli bir image'a rollback
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: "{{ app_name }}"
            namespace: "{{ app_namespace }}"
          spec:
            template:
              spec:
                containers:
                  - name: "{{ app_name }}"
                    image: "{{ rollback_to_image }}"
      when: rollback_to_image is defined

    - name: Rollback tamamlanmasini bekle
      kubernetes.core.k8s_rollout_status:
        name: "{{ app_name }}"
        namespace: "{{ app_namespace }}"
        kind: Deployment
        timeout: "{{ rollback_timeout }}"

    - name: Pod durumlarini kontrol et
      kubernetes.core.k8s_info:
        kind: Pod
        namespace: "{{ app_namespace }}"
        label_selectors:
          - "app={{ app_name }}"
      register: pods_after_rollback

    - name: Rollback sonrasi pod ozeti
      debug:
        msg: "Pod {{ item.metadata.name }}: {{ item.status.phase }}"
      loop: "{{ pods_after_rollback.resources }}"
      loop_control:
        label: "{{ item.metadata.name }}"

Multi-Environment Yönetimi

Aynı kodu birden fazla ortamda kullanmak için inventory yapısından faydalanabilirsiniz:

# inventory/production/group_vars/all.yml
---
app_replicas: 5
environment: production
image_tag: "{{ lookup('env', 'IMAGE_TAG') }}"
domain_name: store.example.com
db_host: postgres-prod.ecommerce.svc.cluster.local
resource_cpu_request: "500m"
resource_memory_request: "512Mi"
resource_cpu_limit: "1000m"
resource_memory_limit: "1Gi"
kubeconfig_path: /home/deploy/.kube/production-config
# inventory/staging/group_vars/all.yml
---
app_replicas: 2
environment: staging
image_tag: "{{ lookup('env', 'IMAGE_TAG') | default('develop') }}"
domain_name: store-staging.example.com
db_host: postgres-staging.ecommerce.svc.cluster.local
resource_cpu_request: "250m"
resource_memory_request: "256Mi"
resource_cpu_limit: "500m"
resource_memory_limit: "512Mi"
kubeconfig_path: /home/deploy/.kube/staging-config

Bu yapıyla playbook’u ortama göre çalıştırmak son derece basit:

# Staging deploy
ansible-playbook -i inventory/staging playbooks/deploy-app.yml --ask-vault-pass

# Production deploy (image tag ile)
IMAGE_TAG=v2.1.5 ansible-playbook -i inventory/production playbooks/deploy-app.yml --ask-vault-pass

# Dry run (ne yapacagini gormek icin)
ansible-playbook -i inventory/production playbooks/deploy-app.yml --check --diff

# Sadece belirli task'lari calistir
ansible-playbook -i inventory/production playbooks/deploy-app.yml --tags "deployment,service"

CI/CD Pipeline Entegrasyonu

GitLab CI ile bu playbook’ları birleştirmek için basit ama etkili bir yaklaşım:

# .gitlab-ci.yml (ilgili kisimlar)
stages:
  - build
  - test
  - deploy-staging
  - deploy-production

variables:
  ANSIBLE_HOST_KEY_CHECKING: "False"
  ANSIBLE_FORCE_COLOR: "True"

.ansible-base:
  image: python:3.11-slim
  before_script:
    - pip install ansible kubernetes
    - ansible-galaxy collection install kubernetes.core
    - echo "$VAULT_PASSWORD" > .vault_pass
    - chmod 600 .vault_pass
    - mkdir -p ~/.kube

deploy-staging:
  extends: .ansible-base
  stage: deploy-staging
  environment: staging
  script:
    - echo "$KUBECONFIG_STAGING" | base64 -d > ~/.kube/config
    - chmod 600 ~/.kube/config
    - IMAGE_TAG=$CI_COMMIT_SHORT_SHA
      ansible-playbook
      -i inventory/staging
      playbooks/deploy-app.yml
      --vault-password-file .vault_pass
      -e "image_tag=$CI_COMMIT_SHORT_SHA"
  only:
    - develop

deploy-production:
  extends: .ansible-base
  stage: deploy-production
  environment: production
  script:
    - echo "$KUBECONFIG_PRODUCTION" | base64 -d > ~/.kube/config
    - chmod 600 ~/.kube/config
    - ansible-playbook
      -i inventory/production
      playbooks/deploy-app.yml
      --vault-password-file .vault_pass
      -e "image_tag=$CI_COMMIT_TAG"
  only:
    - tags
  when: manual

Sık Yapılan Hatalar ve Çözümleri

kubeconfig yolu problemi: Ansible farklı bir kullanıcı olarak çalışıyorsa kubeconfig bulunamıyor. Bunu playbook’ta açıkça kubeconfig parametresiyle belirtmek çözüyor.

Vault şifresi CI/CD’de: --ask-vault-pass yerine --vault-password-file kullanın ve şifre dosyasını environment variable’dan oluşturun. Yukarıdaki GitLab CI örneğinde bu yaklaşımı görebilirsiniz.

Rollout timeout: Büyük cluster’larda default timeout yetersiz kalabiliyor. deployment_timeout değişkenini ortama göre artırın.

Image pull hatası: Registry credential’larını deployment’tan önce oluşturduğunuzdan emin olun. Bu yüzden pre_tasks bloğuna taşımanızı öneririm.

İdempotency sorunu: kubernetes.core.k8s modülü genellikle idempotent çalışır ama bazı annotation güncellemelerinde gereksiz restart’a neden olabilir. Sadece değişmesi gereken alanları patch ile güncelleyin.

Performans İpuçları

  • Büyük cluster’larda async ve poll kullanarak paralel deployment yapabilirsiniz.
  • kubernetes.core.k8s_info ile sık durum kontrolü yapmak API server’a yük bindiriyor. retries ve delay değerlerini makul tutun.
  • Prod’da --diff flag’ini kullanarak tam olarak neyin değiştiğini loglayın. Audit trail için çok değerli.
  • Playbook’larınızı rollere bölün. Uzun tek dosya playbook’lar zaman içinde yönetilemez hale geliyor.

Sonuç

Ansible ile Kubernetes deployment yönetimi başlangıçta biraz kurulum gerektiriyor ama uzun vadede ciddi zaman ve hata tasarrufu sağlıyor. Özellikle birden fazla ortamı yöneten küçük ve orta ölçekli ekipler için Helm kadar karmaşık olmadan aynı işi yapıyor.

Bu yazıda anlattığım yapıyı kendi ortamınıza adapte ederken en önemli şu noktalara dikkat edin: secret’larınızı her zaman Vault’ta tutun, rollback mekanizmasını production’a geçmeden test edin ve --check modunu CI pipeline’ınıza dahil edin.

Bir sonraki adım olarak Ansible rollerini Ansible Galaxy’ye yükleyip ekipler arasında paylaşmayı veya AWX/Ansible Tower ile web tabanlı deployment yönetimine geçişi düşünebilirsiniz. Ama önce temellerinizi sağlam atın; karmaşık araçlar basit temelin üstüne inşa edilir.

Bir yanıt yazın

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