Teknoloji

Terraform ve Ansible ile VPS Otomasyonu

Neden Terraform ve Ansible ile VPS Otomasyonu Kurmalısınız?

Yeni bir proje için VPS altyapısını planlarken genellikle aynı sorular etrafında dönüyoruz: Hangi boyutta VPS alacağız, hangi portlar açılacak, hangi paketler kurulacak, SSH anahtarları nasıl dağıtılacak, güvenlik duvarı kuralları nasıl standart olacak? Bir projede elle yaptığınız adımları birkaç ay sonra başka bir ortamda tekrarlarken küçük farklar büyük sorunlara dönüşebilir. Bir yerde ufw açık, diğerinde kapalı; birinde swap var, diğerinde yok; PHP ayarları iki ortamda da farklı…

Terraform, desteklenen API üzerinden altyapıyı; Ansible ise sunucudaki paket ve yapılandırmaları yönetir. İki aracı birleştirmek staging ve üretim farklarını azaltabilir. Tek komutluk akış ancak erişim, değişiklik incelemesi ve hata kontrolleri tamamlandığında anlamlıdır; uygulama verileri için ayrıca doğrulanmış yedek ve geri yükleme gerekir.

Bu rehberde Terraform ve Ansible ile VPS otomasyonu için gerçek bir OpenStack sağlayıcı örneğini, Ubuntu 24.04 üzerinde Ansible rollerini ve envanter aktarımını kuruyoruz. OpenStack bölümü yalnız yetkili olduğunuz, erişilebilir bir OpenStack API için geçerlidir; DCHost’un böyle bir API veya Terraform sağlayıcısı sunduğu varsayılmaz. Önceden açılmış bir DCHost VPS’yi doğrudan Ansible envanterine ekleyebilirsiniz. Örnek HTTP başlangıç sayfası ve PHP-FPM bağlantısını hazırlar; gerçek uygulama, TLS, izleme ve geri yükleme ayrı kabul kontrolleri ister.

Genel Mimarinin Resmi: Terraform Nerede, Ansible Nerede?

Önce rolleri netleştirelim. Terraform ve Ansible aynı problemi çözmez, birbirini tamamlar:

  • Terraform: Desteklenen bir provider ve API üzerinden altyapıyı yönetir. VPS, ek disk, ağ, IP ve DNS kaydı gibi hangi kaynakların yönetilebileceği seçilen entegrasyonun kapsamına bağlıdır.
  • Ansible: Kurulan VPS’in içini hazırlar. Paketleri kurar, ayar dosyalarını yerleştirir, servisleri başlatır, güvenlik sertleştirmesini yapar.

Bu ikili sayesinde üç katmanınız olur:

  • Katman 1 – Altyapı: VPS, ağ, IP, DNS (Terraform)
  • Katman 2 – İşletim sistemi ve temel servisler: SSH, firewall, log, izleme (Ansible rolleri)
  • Katman 3 – Uygulama: PHP/Node.js/Java, veritabanı, cache, kodun deploy’u (Yine Ansible rolleri + CI/CD)

Terraform tarafındaki akışın daha derinine girmek isterseniz, blogda yer alan Terraform ile VPS ve DNS otomasyonu yazısında DNS ve çok bölgeli yapılarla birleştirilmiş daha gelişmiş bir senaryoyu da inceleyebilirsiniz.

Terraform ile VPS Altyapısını Kodlamak

Temel Kavramlar: Provider, Resource, State

Terraform’un üç temel kavramını netleştirelim:

  • Provider: API ile konuşan eklenti. Burada terraform-provider-openstack/openstack 3.4.0 kullanılır; kaynak şeması bu sürüme aittir.
  • Resource: Sağlayıcının yönettiği nesne; örnekte SSH public key kaydı ve sanal makine. Ek disk ve DNS gibi kaynaklar ilgili API yetkisini de gerektirir.
  • State: Kod ile yönetilen gerçek kaynaklar arasındaki eşleştirmeleri tutar. Plan; yapılandırma, state ve sağlayıcıdan okunan bilgilerle oluşur. State gerçek sistemin değişmez veya eksiksiz kopyası değildir.

Basit bir dizin yapısıyla başlayalım:

infra/
  main.tf
  variables.tf
  outputs.tf
  cloud-init.yml
  terraform.tfvars
  .terraform.lock.hcl

OpenStack ile Sürümü Sabitlenmiş Terraform Örneği

Örnek Terraform 1.17.1 ve OpenStack provider 3.4.0 için hazırlanmıştır. Yetkili clouds.yaml profilinizin adını ve bölgesini kullanın; kimlik bilgilerini bu depoya taşımayın. Ubuntu 24.04 cloud image, flavor, ağ ve güvenlik grubu sizin bulutunuzda mevcut olmalıdır. Ağın sabit IPv4 adresine Ansible çalıştırıcısından yol bulunmalıdır; bu örnek floating IP veya genel internet erişimi oluşturmaz. SSH portu 22 ve ubuntu kullanıcısı seçilen imajla ayrıca doğrulanmalıdır.

infra/main.tf

terraform {
  required_version = "= 1.17.1"
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "= 3.4.0"
    }
  }
}
provider "openstack" {
  cloud  = var.cloud
  region = var.region
}
resource "openstack_compute_keypair_v2" "runner" {
  name       = "${var.name_prefix}-runner"
  public_key = file(pathexpand(var.ssh_public_key_file))
}
resource "openstack_compute_instance_v2" "app" {
  count           = var.vps_count
  name            = "${var.name_prefix}-${count.index + 1}"
  image_id        = var.image_id
  flavor_id       = var.flavor_id
  key_pair        = openstack_compute_keypair_v2.runner.name
  security_groups = var.security_group_names
  user_data       = file("${path.module}/cloud-init.yml")
  metadata        = { environment = var.name_prefix, role = "app" }
  network {
    uuid           = var.network_id
    access_network = true
  }
}

Değişkenleri variables.tf içinde tanımlayalım:

infra/variables.tf

variable "cloud" { type = string }
variable "region" { type = string }
variable "image_id" { type = string }
variable "flavor_id" { type = string }
variable "network_id" { type = string }
variable "security_group_names" { type = list(string) }
variable "ssh_public_key_file" { type = string }
variable "name_prefix" {
  type = string
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{0,30}$", var.name_prefix))
    error_message = "Use a short lowercase host prefix."
  }
}
variable "vps_count" {
  type    = number
  default = 1
  validation {
    condition     = var.vps_count >= 1 && var.vps_count <= 20 && floor(var.vps_count) == var.vps_count
    error_message = "Use an integer between 1 and 20."
  }
}

terraform.tfvars.example içeriğini infra/terraform.tfvars olarak kaydedip REPLACE_… değerlerinin tamamını kendi yetkili ortamınızdan doldurun. SSH dosyası yalnız public key içermelidir; kısaltılmış bir anahtar veya private key kullanmayın. Sağlayıcı kimlik bilgileri erişimi kısıtlanmış profilinizde kalsın. Her ortam için ayrı çalışma dizini ve ayrı state/erişim sınırı kullanın.

infra/terraform.tfvars.example

cloud                = "REPLACE_AUTHORIZED_CLOUD_NAME"
region               = "REPLACE_REGION"
image_id             = "REPLACE_UBUNTU_24_04_IMAGE_UUID"
flavor_id            = "REPLACE_FLAVOR_ID"
network_id           = "REPLACE_RUNNER_REACHABLE_NETWORK_UUID"
security_group_names = ["REPLACE_REVIEWED_SECURITY_GROUP_NAME"]
ssh_public_key_file  = "~/.ssh/id_ed25519.pub"
name_prefix          = "app-staging"
vps_count            = 1

Aşağıdaki komutları proje kökünden çalıştırın. cloud-init.yml dosyasını da bir sonraki bölümdeki içerikle oluşturmuş olun. Kaydedilen planı inceleyin; doğru hesap, bölge, kaynak sayısı, değiştirme ve silme işlemlerini doğruladıktan sonra apply komutunu çalıştırın. Kaydedilmiş planla apply ayrıca onay sormaz. Yeni bir değişiklikte yeni plan üretin; başarısız veya kısmen uygulanmış işlemden sonra eski planı tekrar kullanmayın.

umask 077
terraform -chdir=infra init
terraform -chdir=infra validate
terraform -chdir=infra plan -out=reviewed.tfplan
# Planı inceleyip uygulama kararını verdikten sonra:
terraform -chdir=infra apply reviewed.tfplan

Burada kritik nokta: Sır içermeyen Terraform tanımlarını Git deposunda tutarak kodda yapılan değişiklikleri takip edebilirsiniz. Ortama özel terraform.tfvars dosyasını ve hassas veri içerebilen state/plan dosyalarını depoya eklemeyin; kimlik bilgileri bu örnekte ayrı cloud profilinde tutulur. Commit geçmişi amaçlanan değişiklikleri gösterir; hangi değişikliğin gerçekten uygulandığını izlemek için ayrıca uygulama kayıtları gerekir.

cloud-init ile İlk Nefes: Ansible’a Hazırlık

Bu örnek, OpenStack metadata/config-drive desteğiyle uyumlu Ubuntu cloud image’in varsayılan kullanıcısını ve public key aktarımını kullanır. user_data dosyası Python ve sudo paketlerini hazırlar. Bir kullanıcı oluşturma, farklı anahtar dağıtımı veya başka bir imaj gerekiyorsa cloud-init’in users ve ssh_authorized_keys alanlarını o imajda test edin. User data içine sır koymayın; bu içerik state ve sağlayıcı tarafında görülebilir. Başarılı sanal makine oluşturma, cloud-init’in tamamlandığını göstermez.

infra/cloud-init.yml

#cloud-config
package_update: true
packages:
  - python3
  - python3-apt
  - sudo
# Ubuntu cloud image's default user and OpenStack-injected public key are used.
# Verify the image's username, SSH policy and sudo access before using it.

Bu mimariyi daha derinlemesine anlatan cloud-init ve Ansible ile tekrar üretilebilir VPS kurulumu yazısına mutlaka göz atmanızı öneririm; burada ise daha çok Terraform + Ansible entegrasyonuna odaklanacağız.

Ansible ile VPS Konfigürasyonunu Kodlamak

Envanter, Playbook ve Roller

Ansible tarafında temel yapı taşlarımız:

  • Inventory: Hangi sunucular var, IP adresleri, gruplar, değişkenler.
  • Playbook: Hangi gruplara hangi rollerin uygulanacağını söyleyen orkestrasyon dosyası.
  • Role: Belirli bir işi üstlenen, yeniden kullanılabilir görev paketleri (örneğin nginx, php, security_hardening rolü).

Aşağıdaki dizin yapısını proje boyunca koruyun. Yönetim makinesinde Python 3.13, Terraform 1.17.1, Bash, GNU Make ve SSH istemcisi bulunmalıdır. Ansible core 2.20.11 ile community.general 14.0.0 birlikte sabitlenir. Paket güncellemesinde destek matrisi ve porting notlarını inceleyip testleri tekrarlayın; bu sabitleme sürümleri süresiz güncel yapmaz.

infra-project/
  Makefile
  .gitignore
  infra/
    main.tf
    variables.tf
    outputs.tf
    cloud-init.yml
    terraform.tfvars
    .terraform.lock.hcl
  config/
    ansible.cfg
    requirements.yml
    collections/
    inventory/
      hosts.json
      group_vars/app.yml
    playbooks/
      site.yml
      hardening.yml
    roles/
      common/tasks/main.yml
      webserver/
        tasks/main.yml
        files/validate-site
        templates/app.conf.j2
      hardening/tasks/main.yml
  scripts/generate_inventory.sh

config/ansible.cfg

[defaults]
inventory = ./inventory/hosts.json
roles_path = ./roles
collections_path = ./collections
host_key_checking = True
retry_files_enabled = False

config/requirements.yml

---
collections:
  - name: community.general
    version: 14.0.0
# From the repository root; Python 3.13 and Terraform 1.17.1 installed.
python3.13 -m venv .venv
. .venv/bin/activate
python -m pip install 'ansible-core==2.20.11'
(cd config && ansible-galaxy collection install -r requirements.yml -p collections)

ansible.cfg, config/ içinden okunduğunda roller için ./roles, koleksiyonlar için ./collections yolunu açıkça belirler. Depoyu dünya tarafından yazılabilir bir dizine koymayın. Komutlar bu nedenle cd config veya parantezli alt kabuk kullanır.

Basit Bir Inventory Örneği

Önceden açılmış bir DCHost VPS için Terraform adımlarını atlayıp bu envanteri doldurabilirsiniz. 192.0.2.10 yalnız dokümantasyon adresidir; gerçek, size ait VPS adresiyle değiştirin. SSH host key parmak izini güvenilir konsol veya sağlayıcı kaydıyla doğrulayın; host key kontrolünü kapatmayın. Non-root kullanıcı, anahtarlı SSH ve bu örneğin kullandığı parolasız sudo yetkisi çalışmalı; hedef Ubuntu 24.04 ve Python hazır olmalıdır. cloud_init_wait yalnız bu hizmeti kullanan imajlarda true olmalıdır.

config/inventory/hosts.json

{
  "app": {
    "hosts": {
      "app-staging-1": {
        "ansible_host": "192.0.2.10",
        "ansible_user": "ubuntu",
        "ansible_port": 22,
        "ansible_python_interpreter": "/usr/bin/python3",
        "cloud_init_wait": false
      }
    }
  }
}

config/inventory/group_vars/app.yml

---
app_server_name: app.example.com
app_timezone: Etc/UTC
# Set to true only after the fresh-host preflight described in the article.
fresh_host_reviewed: false

app_server_name değerini size ait, aynı Nginx üzerinde başka siteye atanmamış alan adıyla değiştirin. fresh_host_reviewed ancak yeni/ayrılmış hedefte paket, port, dosya yolu, SSH ve sudo kontrollerini yaptıktan sonra true olmalıdır. /var/www/iac-app, validator ve site dosyası yollarını bu örneğe ayırın; cPanel/Plesk gibi mevcut bir panelin yönettiği Nginx’e bu rolü uygulamayın.

Ardından basit bir playbook:

config/playbooks/site.yml

---
- name: Configure the reviewed Ubuntu web hosts
  hosts: app
  become: true
  gather_facts: false
  serial: 1
  any_errors_fatal: true
  pre_tasks:
    - name: Wait for SSH and a working Python module transport
      ansible.builtin.wait_for_connection:
        timeout: 300
    - name: Wait for cloud-init before using APT
      ansible.builtin.command: cloud-init status --wait
      changed_when: false
      when: cloud_init_wait | default(false) | bool
    - name: Gather facts
      ansible.builtin.setup:
    - name: Require the documented target and completed preflight
      ansible.builtin.assert:
        that:
          - ansible_facts.distribution == 'Ubuntu'
          - ansible_facts.distribution_version == '24.04'
          - ansible_user != 'root'
          - fresh_host_reviewed | bool
          - app_server_name is match('^[a-z0-9]+([.-][a-z0-9]+)*$')
        fail_msg: Review SSH/sudo, network and existing services before enabling this example.
  roles:
    - common
    - webserver

common rolü paket önbelleğini gerektiğinde yeniler, yardımcı araçları ve timezone bağımlılıklarını kurar. community.general.timezone ayrı koleksiyondandır; Ubuntu 24.04 için tzdata ve util-linux-extra eklenmiştir. Rol tam sistem yükseltmesi yapmaz. Güncelleme/reboot politikasını bakım penceresiyle planlayın; zaman dilimi değişiminden etkilenen cron görevlerini de kontrol edin.

config/roles/common/tasks/main.yml

---
- name: Install base tools and timezone dependencies
  ansible.builtin.apt:
    name: [curl, git, htop, tzdata, util-linux-extra]
    state: present
    update_cache: true
    cache_valid_time: 3600
- name: Set the system timezone
  community.general.timezone:
    name: "{{ app_timezone }}"
# Package upgrades, SSH and firewall changes use a separate reviewed rollout.

Web rolü Ubuntu paketleriyle Nginx ve php8.3-fpm kurar. APT’nin servis başlatmasını policy_rc_d: 101 ile engeller; mevcut çalışan servislerin varlığı ayrıca değerlendirilir. Varsayılan ve diğer siteleri silmez. Yalnız sahiplik işaretli iac-app.conf dosyasını yönetir; önce aday şablonu, sonra tüm kurulu Nginx ağacını kontrol eder. İkinci kontrolde hata veya uyarı varsa bu role ait site değişikliklerini geri almaya çalışır ve durur. Paket kurulumu veya dışarıdan eşzamanlı değişiklikler bu geri dönüşün kapsamı dışındadır.

config/roles/webserver/templates/app.conf.j2

# Managed by iac-app example
server {
    listen 80;
    server_name {{ app_server_name }};
    root /var/www/iac-app;
    index index.html index.php;
    location / {
        try_files $uri $uri/ =404;
    }
    location ~ /\. {
        deny all;
    }
    location ~ \.php$ {
        try_files $uri =404;
        include /etc/nginx/fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

config/roles/webserver/files/validate-site

#!/usr/bin/env bash
set -euo pipefail
candidate=${1:?candidate required}
work=$(mktemp -d)
trap 'rm -rf -- "$work"' EXIT
cp -- "$candidate" "$work/site.conf"
cat > "$work/nginx.conf" <<EOF
events {}
http {
    include /etc/nginx/mime.types;
    include $work/site.conf;
}
EOF
/usr/sbin/nginx -t -c "$work/nginx.conf"

config/roles/webserver/tasks/main.yml

---
- name: Install the Ubuntu packages without automatic service starts
  ansible.builtin.apt:
    name: [nginx, php8.3-fpm]
    state: present
    policy_rc_d: 101
- name: Check the existing complete Nginx configuration
  ansible.builtin.command: /usr/sbin/nginx -t
  changed_when: false
- name: Inspect the owned site file
  ansible.builtin.stat:
    path: /etc/nginx/sites-available/iac-app.conf
    follow: false
  register: old_site
- name: Inspect the owned enable link
  ansible.builtin.stat:
    path: /etc/nginx/sites-enabled/iac-app.conf
    follow: false
  register: old_link
- name: Reject path collisions
  ansible.builtin.assert:
    that:
      - not old_site.stat.exists or old_site.stat.isreg | default(false)
      - not old_link.stat.exists or (old_link.stat.islnk | default(false) and old_link.stat.lnk_source == '/etc/nginx/sites-available/iac-app.conf')
- name: Read the previous site if present
  ansible.builtin.slurp:
    src: /etc/nginx/sites-available/iac-app.conf
  register: old_bytes
  when: old_site.stat.exists
- name: Require the ownership marker before updating an existing file
  ansible.builtin.assert:
    that:
      - (old_bytes.content | b64decode).startswith('# Managed by iac-app example\n')
  when: old_site.stat.exists
- name: Install the candidate syntax validator
  ansible.builtin.copy:
    src: validate-site
    dest: /usr/local/sbin/validate-iac-site
    owner: root
    group: root
    mode: '0755'
- name: Create the example document root
  ansible.builtin.file:
    path: /var/www/iac-app
    state: directory
    owner: root
    group: root
    mode: '0755'
- name: Write the static bootstrap page
  ansible.builtin.copy:
    content: "IaC HTTP bootstrap ready\n"
    dest: /var/www/iac-app/index.html
    owner: root
    group: root
    mode: '0644'
- name: Install and validate the owned site
  block:
    - name: Validate the candidate before replacing the site
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/nginx/sites-available/iac-app.conf
        owner: root
        group: root
        mode: '0644'
        backup: true
        validate: /usr/local/sbin/validate-iac-site %s
      register: site_write
    - name: Enable only this owned site
      ansible.builtin.file:
        src: /etc/nginx/sites-available/iac-app.conf
        dest: /etc/nginx/sites-enabled/iac-app.conf
        state: link
      register: link_write
    - name: Validate all installed sites together
      ansible.builtin.command: /usr/sbin/nginx -t
      register: nginx_complete_check
      changed_when: false
      failed_when: nginx_complete_check.rc != 0 or '[warn]' in nginx_complete_check.stderr
  rescue:
    - name: Restore the previous owned site content
      ansible.builtin.copy:
        content: "{{ old_bytes.content | b64decode }}"
        dest: /etc/nginx/sites-available/iac-app.conf
        owner: "{{ old_site.stat.uid }}"
        group: "{{ old_site.stat.gid }}"
        mode: "{{ old_site.stat.mode }}"
      when: old_site.stat.exists
    - name: Remove only the newly created owned link
      ansible.builtin.file:
        path: /etc/nginx/sites-enabled/iac-app.conf
        state: absent
      when: not old_link.stat.exists
    - name: Remove only the newly created owned file
      ansible.builtin.file:
        path: /etc/nginx/sites-available/iac-app.conf
        state: absent
      when: not old_site.stat.exists
    - name: Stop after the failed deployment
      ansible.builtin.fail:
        msg: Validation failed; owned site restored where possible. Inspect the failure before retrying.
- name: Validate the installed PHP-FPM configuration
  ansible.builtin.command: /usr/sbin/php-fpm8.3 -t
  changed_when: false
- name: Ensure PHP-FPM is running
  ansible.builtin.service:
    name: php8.3-fpm
    state: started
    enabled: true
- name: Read service state before start or enable
  ansible.builtin.service_facts:
- name: Remember whether Nginx was already running
  ansible.builtin.set_fact:
    nginx_was_running: "{{ ansible_facts.services.get('nginx.service', {}).get('state', 'stopped') == 'running' }}"
- name: Ensure validated Nginx is running
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true
- name: Reload validated Nginx even after an earlier activation failure
  ansible.builtin.service:
    name: nginx
    state: reloaded
  when: nginx_was_running

Şablon statik başlangıç sayfasını sunar; mevcut gerçek PHP dosyalarını Ubuntu’nun /run/php/php8.3-fpm.sock soketine iletir. PHP uygulaması oluşturmaz. Validator geçici bir tam yapılandırma kurar; aday dosyanın tek başına geçmesi diğer sitelerle uyumu kanıtlamadığı için ikinci kontrol zorunludur. Nginx ve PHP-FPM doğrulamaları ile gerekli servis işlemleri başarılı olduktan sonra, önceden çalışan Nginx dosya değişmese de her çalıştırmada reload edilir. Böylece dosya kaydedildikten sonra PHP kontrolü, servis işlemi veya reload başarısız olmuşsa sonraki başarılı deneme yapılandırmayı devreye alır. Başlangıçta durmuş Nginx doğrulamadan sonra başlatılır; o çalıştırmada ek reload yapılmaz. Hatalı doğrulama veya servis işlemi akışı durdurur. Servis, dosya erişimi ve uygulama yanıtını dağıtımdan sonra ayrıca kontrol edin.

Proje kökünde sanal ortam etkin durumdayken envanteri, sözdizimini ve ardından gerçek hedefte sonucu kontrol edin. --syntax-check rol keşfini ve sözdizimini kontrol eder; SSH, sudo, APT, servis veya uygulama başarısını kanıtlamaz. --check de tam çalışma garantisi değildir. İlk kurulumdan sonra aynı hedefte ikinci çalıştırmanın değişikliklerini inceleyerek beklenen idempotence davranışını ölçün. Bu örnekte doğrulanmış, önceden çalışan Nginx ile doğrulanmış SSH için her başarılı çalıştırmada reload yapılması kasıtlıdır ve changed olarak raporlanır; tüm görevlerin ikinci çalıştırmada değişikliksiz kalması beklenmez.

# From the repository root, with .venv active.
(cd config && ansible-inventory --graph)
(cd config && ansible-playbook playbooks/site.yml --syntax-check)
(cd config && ansible-playbook playbooks/site.yml)
# Kendi VPS adresiniz ve alan adınızla gerçek yanıtı kontrol edin:
curl --fail --show-error --resolve app.example.com:80:192.0.2.10 http://app.example.com/

Güvenlik Sertleştirmesini Role Haline Getirmek

SSH ve firewall değişikliklerini web kurulumundan ayrı, tek hedefle başlayarak uygulayın. Önce yönetici hesabını oluşturup public key’i o hesaba dağıtın; gerekiyorsa sudoers kuralını visudo -cf ile doğrulayın. Örnekte mevcut ubuntu hesabı kullanılır: anahtarlı yeni bir SSH oturumunda sudo çalıştığını ve acil konsol erişimini gerçekten test etmeden aşağıdaki iki bayrağı true yapmayın. management_source_ip, NAT/bastion sonrasında sunucunun gördüğü gerçek yönetim adresidir; CIDR bu adresi kapsamalıdır. Bu rol değiştirilmeden kullanılacaksa ansible_host sunucunun bağlantıyı aldığı sayısal yerel IP adresi, ansible_port ise sunucudaki gerçek SSH portu olmalıdır. Hedef adresi veya portu NAT ile çevriliyorsa ya da envanterde hostname kullanılıyorsa, önce sshd -T -C içindeki laddr/lport değerlerini ve UFW portunu sunucunun gördüğü yerel değerlere göre ayrı değişkenlerle uyarlayın; dış bağlantı adresini yerel adres sanarak bu rolü çalıştırmayın. IPv4/IPv6 kurallarını, mevcut geniş izinleri ve diğer servis portlarını önceden inceleyin.

  • SSH drop-in’i için tam yapılandırmayı ve yönetici/root bağlamındaki etkili ayarları kontrol edin. Include sırası veya Match bloğu hedef politikayı bozuyorsa reload yapılmadan durulsun.
  • UFW’de mevcut yönetim CIDR/port iznini ekledikten sonra inbound default-deny uygulayın. Sayısal portlar, henüz kurulmamış bir uygulama profiline bağımlılığı kaldırır. Mevcut izinleri otomatik kaldırmaz.
  • Fail2ban ve unattended-upgrades paketlerini kurmak tek başına etkin koruma/politika değildir. Jail, backend, yönetim istisnaları, güncelleme/reboot penceresi ve logları ayrı yapılandırıp doğrulayın.

config/inventory/group_vars/hardening-inputs.example.yml

# Merge reviewed values into inventory/group_vars/app.yml; this file is not loaded.
admin_access_verified: false
console_access_verified: false
management_source_ip: 192.0.2.50
management_source_hostname: runner.example.com
management_cidr: 192.0.2.50/32

config/playbooks/hardening.yml

---
- name: Apply the separately reviewed access policy
  hosts: app
  become: true
  serial: 1
  any_errors_fatal: true
  roles:
    - hardening

config/roles/hardening/tasks/main.yml

---
- name: Require real access checks, console recovery and explicit network inputs
  ansible.builtin.assert:
    that:
      - ansible_facts.distribution == 'Ubuntu'
      - ansible_facts.distribution_version == '24.04'
      - ansible_user != 'root'
      - admin_access_verified | default(false) | bool
      - console_access_verified | default(false) | bool
      - management_source_ip is defined
      - management_source_hostname is defined
      - management_cidr is defined
      - management_source_hostname is match('^[a-zA-Z0-9.-]+$')
      - ansible_user is match('^[a-z_][a-z0-9_-]*$')
- name: Check that the actual management source is allowed
  ansible.builtin.command:
    argv:
      - python3
      - -c
      - 'import ipaddress,sys; assert ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2], strict=False)'
      - "{{ management_source_ip }}"
      - "{{ management_cidr }}"
  delegate_to: localhost
  become: false
  changed_when: false
- name: Verify sudo before changing access
  ansible.builtin.command: /usr/bin/true
  changed_when: false
- name: Install hardening tools without activating services
  ansible.builtin.apt:
    name: [ufw, fail2ban, unattended-upgrades]
    state: present
    update_cache: true
    cache_valid_time: 3600
    policy_rc_d: 101
- name: Inspect the owned SSH drop-in
  ansible.builtin.stat:
    path: /etc/ssh/sshd_config.d/00-iac-access.conf
    follow: false
  register: old_ssh
- name: Reject non-file collisions
  ansible.builtin.assert:
    that:
      - not old_ssh.stat.exists or old_ssh.stat.isreg | default(false)
- name: Preserve previous owned SSH bytes
  ansible.builtin.slurp:
    src: /etc/ssh/sshd_config.d/00-iac-access.conf
  register: old_ssh_bytes
  when: old_ssh.stat.exists
- name: Require ownership marker
  ansible.builtin.assert:
    that:
      - (old_ssh_bytes.content | b64decode).startswith('# Managed by iac-access example\n')
  when: old_ssh.stat.exists
- name: Install and check the effective SSH policy
  block:
    - name: Write the owned SSH drop-in
      ansible.builtin.copy:
        content: |
          # Managed by iac-access example
          PermitRootLogin no
          PasswordAuthentication no
          KbdInteractiveAuthentication no
          PubkeyAuthentication yes
        dest: /etc/ssh/sshd_config.d/00-iac-access.conf
        owner: root
        group: root
        mode: '0600'
        backup: true
      register: ssh_write
    - name: Validate the complete SSH configuration
      ansible.builtin.command: /usr/sbin/sshd -t
      changed_when: false
    - name: Read effective policy for the administrator and root
      ansible.builtin.command:
        argv:
          - /usr/sbin/sshd
          - -T
          - -C
          - "user={{ item }},addr={{ management_source_ip }},host={{ management_source_hostname }},laddr={{ ansible_host }},lport={{ ansible_port | default(22) }}"
      loop: ["{{ ansible_user }}", root]
      register: ssh_effective
      changed_when: false
    - name: Reject ignored or overridden authentication settings
      ansible.builtin.assert:
        that:
          - "'permitrootlogin no' in item.stdout_lines"
          - "'passwordauthentication no' in item.stdout_lines"
          - "'kbdinteractiveauthentication no' in item.stdout_lines"
          - "'pubkeyauthentication yes' in item.stdout_lines"
      loop: "{{ ssh_effective.results }}"
  rescue:
    - name: Restore previous owned SSH content
      ansible.builtin.copy:
        content: "{{ old_ssh_bytes.content | b64decode }}"
        dest: /etc/ssh/sshd_config.d/00-iac-access.conf
        owner: "{{ old_ssh.stat.uid }}"
        group: "{{ old_ssh.stat.gid }}"
        mode: "{{ old_ssh.stat.mode }}"
      when: old_ssh.stat.exists
    - name: Remove a newly created owned SSH drop-in
      ansible.builtin.file:
        path: /etc/ssh/sshd_config.d/00-iac-access.conf
        state: absent
      when: not old_ssh.stat.exists
    - name: Stop before reload
      ansible.builtin.fail:
        msg: SSH validation failed. Inspect includes, earlier settings and Match blocks.
- name: Reload validated SSH even after an earlier activation failure
  ansible.builtin.service:
    name: ssh
    state: reloaded
- name: Reconnect after SSH policy change
  ansible.builtin.meta: reset_connection
- name: Require fresh SSH access
  ansible.builtin.wait_for_connection:
    timeout: 60
- name: Allow the actual management CIDR and SSH port before enabling UFW
  community.general.ufw:
    rule: allow
    from_ip: "{{ management_cidr }}"
    port: "{{ ansible_port | default(22) }}"
    proto: tcp
- name: Allow public HTTP and HTTPS by numeric port
  community.general.ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop: ['80', '443']
- name: Enable inbound default-deny policy
  community.general.ufw:
    state: enabled
    direction: incoming
    policy: deny
- name: Reconnect after firewall policy change
  ansible.builtin.meta: reset_connection
- name: Verify management access again
  ansible.builtin.wait_for_connection:
    timeout: 60
# Substitute the verified host, account, key and actual SSH port.
ssh -o StrictHostKeyChecking=yes -o BatchMode=yes -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no -i ~/.ssh/id_ed25519 -p 22 ubuntu@192.0.2.10 'sudo -n true'
# After merging reviewed inputs into inventory/group_vars/app.yml:
(cd config && ansible-playbook playbooks/hardening.yml --syntax-check)
(cd config && ansible-playbook playbooks/hardening.yml)

SSH tam ve etkili yapılandırma kontrolleri geçince, drop-in değişmese de her başarılı çalıştırmada reload edilir. Bu tekrar, dosya kaydedildikten sonra başarısız olan reload işleminin sonraki denemede atlanmasını önler; reload hatası akışı durdurur. Etkin ayar kontrolü verilen bağlantı bağlamlarını kapsar; tüm kullanıcı/ağ Match kombinasyonlarını ispatlamaz. SSH veya firewall sonrası bağlantı kaybında Ansible rescue her zaman çalışamaz; test edilmiş konsoldan geri dönüş gerekir. Yeni bağlantıyı doğrulamadan eski oturumu kapatmayın.

Bu konuları derinlemesine ele aldığımız VPS güvenlik sertleştirme kontrol listesi ve SSH güvenliğini sağlamlaştırma yazılarını, Ansible rolünüzü tasarlarken referans rehber gibi kullanabilirsiniz.

Terraform Çıktısını Ansible Envanterine Bağlamak

Terraform’un ürettiği sabit IPv4 adreslerini JSON envanterine aktaracağız. Bu aktarım yeni makine oluşturmaz, sunucuya erişimi de kanıtlamaz. Doğrudan DCHost envanteri kullanıyorsanız bu adımı atlayıp make configure çalıştırın.

Terraform Çıktısıyla Envanter Üretmek

Aşağıdaki outputs.tf hem adres listesini hem Ansible’ın okuyacağı nesneyi üretir. Adresler public IP garantisi taşımaz; çalışma makinenizin ağ rotası, SSH host key kaydı ve izinleri hazır olmalıdır.

infra/outputs.tf

output "app_ips" {
  description = "Fixed IPv4 addresses; the runner must have a route to this network."
  value       = [for s in openstack_compute_instance_v2.app : s.access_ip_v4]
}
output "ansible_inventory" {
  value = {
    app = {
      hosts = {
        for s in openstack_compute_instance_v2.app : s.name => {
          ansible_host               = s.access_ip_v4
          ansible_user               = "ubuntu"
          ansible_port               = 22
          ansible_python_interpreter = "/usr/bin/python3"
          cloud_init_wait            = true
        }
      }
    }
  }
}

Script’i scripts/generate_inventory.sh olarak kaydedip çalıştırılabilir yapın. Terraform hatası, boş envanter veya geçersiz adres durumunda var olan hosts.json korunur. Başarılı kontrol sonrası aynı dizindeki geçici dosya atomik rename ile yerini alır. Üretilen hedef listesini çalıştırmadan önce yine inceleyin; root yetkili bir otomasyonun girdileri güvenilir olmalıdır.

scripts/generate_inventory.sh

#!/usr/bin/env bash
set -euo pipefail
cd -- "$(dirname -- "$0")/.."
umask 077
mkdir -p config/inventory
raw=$(mktemp config/inventory/.terraform.XXXXXX)
candidate=$(mktemp config/inventory/.inventory.XXXXXX.json)
trap 'rm -f -- "$raw" "$candidate"' EXIT
terraform -chdir=infra output -json ansible_inventory > "$raw"
python3 - "$raw" "$candidate" <<'PYTHON'
import ipaddress, json, re, sys
from pathlib import Path
value = json.loads(Path(sys.argv[1]).read_text())
hosts = value['app']['hosts']
assert isinstance(hosts, dict) and hosts, 'Empty or invalid app inventory'
for name, host in hosts.items():
    assert re.fullmatch(r'[a-z][a-z0-9-]{0,40}', name), 'Invalid host name'
    ipaddress.IPv4Address(host['ansible_host'])
    assert host['ansible_user'] == 'ubuntu'
    assert host['ansible_port'] == 22
    assert host['ansible_python_interpreter'] == '/usr/bin/python3'
    assert host['cloud_init_wait'] is True
Path(sys.argv[2]).write_text(json.dumps(value, indent=2) + '\n')
PYTHON
(cd config && ansible-inventory -i "${candidate#config/}" --list > /dev/null)
mv -- "$candidate" config/inventory/hosts.json
chmod 0755 scripts/generate_inventory.sh

Artık akışımız şöyle olabilir:

  • Doğru ortam dizininde yeni plan üretin ve değişiklikleri inceleyin.
  • İncelenen planı uygulayın; başarılıysa envanteri oluşturun.
  • Envanter/host key erişimini doğrulayın ve Ansible ile hedefi yapılandırın.

İncelenmiş adımları bir Makefile ile birleştirebilirsiniz. Ancak yeni veya yeniden oluşturulan hedeflerde ilk çalıştırmayı bölün: önce incelenmiş Terraform planını uygulayın ve envanteri üretin; sonra hedef listesini, host key’leri, SSH/sudo erişimini ve yeni sunucu ön kontrollerini doğrulayın. Ancak bundan sonra fresh_host_reviewed değerini true yapıp make configure çalıştırın. Aşağıdaki make all, apply ile Ansible arasında bu inceleme için durmaz; yalnız hedef kimliği ve erişim ön kontrolleri geçerliliğini koruyan değişikliklerde kullanın.

Makefile ile Tek Komutluk Akış

Makefile

SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c
.NOTPARALLEL:
.PHONY: plan all configure destroy

plan:
	umask 077; rm -f infra/reviewed.tfplan; terraform -chdir=infra init; terraform -chdir=infra validate; terraform -chdir=infra plan -out=reviewed.tfplan

all:
	test -f infra/reviewed.tfplan
	read -r -p 'Apply the reviewed plan in this checkout? Type APPLY: ' answer; test "$$answer" = APPLY
	terraform -chdir=infra apply reviewed.tfplan
	rm -- infra/reviewed.tfplan
	./scripts/generate_inventory.sh
	cd config && ansible-playbook playbooks/site.yml

configure:
	cd config && ansible-playbook playbooks/site.yml

destroy:
	umask 077; rm -f infra/destroy.tfplan; terraform -chdir=infra plan -destroy -out=destroy.tfplan
	read -r -p 'Destroy resources in this reviewed plan? Type DESTROY: ' answer; test "$$answer" = DESTROY
	terraform -chdir=infra apply destroy.tfplan
	rm -- infra/destroy.tfplan

all: infra config gibi bağımsız hedefler paralel Make altında yarışabilir. Burada tek tarif ve .NOTPARALLEL kullanılır; hata sonraki komutu durdurur. Bu yalnız aynı Make sürecini sıralar: eşzamanlı başka çalıştırıcıları CI kilidi ve backend state kilidiyle engelleyin. Ortam seçimi için aynı checkout’ta yalnız tfvars değiştirerek üretime geçmeyin.

Önce yeni planı üretip okuyun; ardından aynı checkout’ta onaylı planı uygulayın. İkinci komut APPLY yanıtı ister:

make plan
# Planı inceleyin.
make all

Adım adım şu işlemler gerçekleşir:

  • Yetkili OpenStack ortamında incelenmiş kaynak değişiklikleri uygulanır.
  • Başarılı çıktıdan JSON envanteri oluşturulur; önceki dosya doğrulama tamamlanana kadar korunur.
  • Ansible temel paket, zaman dilimi ve HTTP/PHP-FPM rolünü uygular. SSH/firewall politikası ayrı hardening.yml aşamasıdır.

Felaket kurtarmada altyapıyı yeniden oluşturmak yalnız bir adımdır. Yeni planın kaynak kimliklerini doğrulayın, verileri yedekten geri yükleyin, DNS/TLS ve uygulama kontrollerini tamamlayın. Gerçek RTO/RPO’yu bu işlemleri içeren bir tatbikatla ölçün; sabit bir saniye/dakika sonucu varsaymayın. Bu konuyu daha geniş ölçekteki felaket kurtarma planları ile birleştirmek isterseniz, felaket kurtarma planı nasıl yazılır yazımız size iyi bir çerçeve sunar.

Güvenlik, Secrets ve Erişim Yönetimi

API Anahtarları ve SSH Anahtarlarını Nasıl Saklamalı?

Terraform ve Ansible ile otomasyon kurarken en kritik konulardan biri, gizli anahtarların (secrets) yönetimidir. Hata yapmaya çok açık alanlar:

  • Terraform veya Ansible dosyalarında API token’larını düz metin olarak bırakmak
  • SSH özel anahtarlarını Git deposuna yanlışlıkla commit etmek
  • Üretim ortamı şifrelerini staging ortamıyla karıştırmak

En azından şu temel önlemleri almanızı öneririz:

  • Private key, provider kimlik bilgileri, state ve plan dosyalarını Git dışında, erişimi kısıtlı tutun. .gitignore geçmişte kaydedilmiş sırları silmez; açığa çıkan sırları değiştirin.
  • sensitive = true bazı çıktıları maskeler; state/plan içeriğini şifrelemez. terraform output -json hassas çıktı değerlerini düz metin verebilir. Backend için erişim kontrolü, aktarım/depolama şifrelemesi, sürümleme ve kilitleme ayrı ayarlanmalıdır.
  • Ansible Vault saklanan dosyayı korur; çalışma sırasında açılan sırlar için dar yetki, uygun no_log ve güvenli CI günlük politikası gerekir. SOPS/age gibi araçlarda anahtar kurtarmasını da planlayın.

.gitignore

infra/.terraform/
infra/*.tfstate*
infra/*.tfplan
infra/terraform.tfvars
config/inventory/hosts.json
config/collections/
.venv/
# Store provider credentials and private SSH keys outside this repository.

Gizli bilgilerin yönetimini derinlemesine ele aldığımız VPS’te secrets yönetimi rehberi, Terraform/Ansible akışınızı güvenli hale getirirken size somut örnekler sağlayacaktır.

SSH Erişimini Standartlaştırmak

SSH politikasını kodlamak, hesap ve erişim kurallarını gözden geçirmenizi kolaylaştırabilir:

  • Yönetim hesabı ve public key listesini kontrollü kaynaktan dağıtın; hesap ve sudo erişimini ayrı yeni oturumda sınayın.
  • SSHD’de Include ve Match önceliğini dikkate alın. Yalnız metni değiştirmek yerine sshd -t ve gerçek bağlantı bağlamlarıyla sshd -T -C kontrolü yapın.
  • Değişiklik sonrası yeni bağlantı, firewall ve Fail2ban loglarını inceleyin; her hedefte aynı güvenlik sonucunu varsaymayın.

Ayrı sertleştirme rolü erişim politikasını gözden geçirilebilir hale getirir. Mevcut bir sunucunun imajı, PAM, Match blokları ve ağ yolu farklı olabilir; standartlaşmayı dosya benzerliğiyle değil etkili ayar ve bağlantı testleriyle doğrulayın.

Gerçekçi Senaryolar

1. Staging ve Üretim Ortamlarını Aynı Hale Getirmek

Staging ortamında çalışan bir özellik üretimde farklı davranabilir. Yazılım ve veriyle birlikte altyapı farklarını da inceleyin: farklı PHP sürümü, farklı Nginx ayarı, farklı kernel parametresi…

Terraform ve Ansible ile şu modeli kurabilirsiniz:

  • Ortak Terraform modülünü kullanın; staging ve üretim için ayrı çalışma dizinleri, backend/state anahtarları ve erişim yetkileri ayırın. CLI workspace’leri state’i ayırabilir fakat tek başına güvenlik sınırı değildir.
  • Aynı Ansible rol setini ayrı envanter ve ilgili group_vars değerleriyle uygulayın. Komuttan önce hedef listesini kontrol edin.
  • Kaynak boyutu, PHP sürümü, imaj ve paket politikasındaki farkları kayıt altına alın. Daha küçük staging planı üretimin performansını birebir temsil etmez.

Bu yaklaşım staging ve üretim arasındaki farkları izlemeyi kolaylaştırabilir; test sonucunu üretime genellerken kapasite ve veri farklarını değerlendirin. Özellikle yüksek trafikli WordPress ve Laravel projelerinde, geliştirme–staging–canlı yolculuğu yazısında anlattığımız dağıtım akışları ile birleştiğinde, kod ve altyapı değişikliklerini birlikte gözden geçirebilirsiniz; veri, bağımlılık sürümleri ve dağıtım/geri dönüş testleri ayrıca doğrulanmalıdır.

2. Aynı Uygulamayı Farklı Bölgelerde Çoğaltmak

Yetkili API ve bölge kaynaklarınız varsa aynı modülden farklı bölgelerde VPS grupları oluşturabilirsiniz. Bölge başına imaj/flavor/ağ kimliklerini doğrulayın; gecikme, veri kopyalama, kapasite ve kesinti sınırlarını ölçün. Aynı Ansible rolü tutarlı yapılandırma sağlar, kendi başına düşük gecikme veya bölgesel felaket toleransı sağlamaz.

İleride DNS veya Anycast tabanlı bir çok bölgeli mimariye geçmek isterseniz, blogda detaylandırdığımız çok bölgeli mimariler rehberi ile Terraform/Ansible altyapınızı birleştirmeniz oldukça doğal bir adım olur.

3. Sıfırdan Kaldırmak Gerekebilen Riskli Denemeler

Bazen yeni bir veritabanı sürümü, yeni bir PHP versiyonu veya yeni bir cache mimarisi denemek istersiniz. Üretim sunucusu üzerinde doğrudan deneme yapmak yerine, Terraform + Ansible ile çöpe atılabilir test ortamları oluşturmak çok daha güvenlidir.

  • Üretimden ayrı yetki/state kullanan test checkout’unda make plan çalıştırıp planı inceleyin. Yeni hedef için incelenmiş planı uygulayın, envanteri üretin ve hedef kimliği, host key, SSH/sudo ile yeni sunucu ön kontrollerini tamamlayın; ardından fresh_host_reviewed değerini true yapıp make configure çalıştırın. make all yalnız bu kontrollerin geçerliliğini koruyan sonraki değişiklikler içindir.
  • Yetkili hedeflerde yük, sorgu ve yapılandırma denemelerini yürütün; yedek ve veri gereksinimini belirleyin.
  • make destroy önce silme planını gösterir ve DESTROY ister. State dışındaki DNS, yedek veya manuel kaynakları otomatik temizlediğini varsaymayın.

Test ortamları üretimden ayrıldığında deneme etkisi sınırlandırılabilir. Kota, çalışan süre, disk/IP ve saklanan yedek maliyetlerini izleyin; otomasyon tek başına maliyet düşürme garantisi değildir.

Sık Yapılan Hatalar ve İyileştirme İpuçları

Hata 1: Her Şeyi Tek Dev VPS Üzerine Yığmak

Veritabanı, uygulama ve cache’i ayırmak hata sınırlarını ve ölçeklemeyi kolaylaştırabilir; buna karşılık ağ gecikmesi, güvenlik, yedekleme ve işletim yükünü artırır. Küçük projede tek VPS uygun olabilir. Ayrıştırma kararını otomasyon kolaylığına göre değil yük, erişilebilirlik ve bakım gereksinimlerine göre verin.

Veritabanını ayrı bir VPS’e taşımanın ne zaman mantıklı olduğu sorusunu, blogdaki veritabanı sunucusunu uygulamadan ayırmak rehberinde detaylıca tartıştık. Altyapı kodu değişikliği destekleyebilir; veri taşıma, ağ ve uygulama geçişini ayrıca planlayın.

Hata 2: State Dosyasını Ciddiye Almamak

State, Terraform’un kaynak eşleştirmesi için kritik kayıttır; gerçek altyapının eksiksiz tek doğruluk kaynağı değildir. Rastgele yerel kopyalar aynı kaynak için farklı planlar üretmeye yol açabilir. Bu öğretici yerel state ile başlar; ekip/üretim kullanımına geçmeden backend ve erişim tasarımını tamamlayın.

Önerimiz:

  • Seçilen backend’in gerçek desteklediği şifreleme, erişim kontrolü, sürümleme ve state kilitlemesini yapılandırın. “S3 uyumlu” etiketi tüm backend özelliklerinin aynı olduğunu kanıtlamaz.
  • Aynı state için eşzamanlı apply işlemlerini backend kilidi ve CI eşzamanlılık kuralıyla engelleyin; kilidi zorla açmadan önce devam eden işlem olmadığını doğrulayın.
  • State yedeklerini ve erişim anahtarlarının kurtarılmasını test edin. State yedeği uygulama/veritabanı yedeğinin yerine geçmez.

Hata 3: Ansible Rollerini Spagetti Hale Getirmek

Zamanla Ansible rolleri büyüyüp iç içe geçtiğinde, “bu sunucuya ne kuruldu” sorusunun cevabı zorlaşmaya başlar. Bunu engellemek için:

  • Her rolün net bir sorumluluğu olsun: nginx, php_fpm, mysql, security_hardening gibi.
  • Rolleri yalnızca ihtiyaç duyan sunucu gruplarına atayın; her rolü her yere yüklemeye çalışmayın.
  • Değişkenleri rollere özel defaults klasöründe tutun; environment farklarını group_vars ile yönetin.

Hata 4: İzleme ve Loglamayı Unutmak

CPU, disk ve 5xx izlemesini dağıtımın kabul adımlarına ekleyin. Ajanın kurulu olması, alarmın doğru hedefe ulaştığını kanıtlamaz; metrik, log, dış HTTP kontrolü ve kontrollü alarm/iyileşme testi yapın. Yeni log akışında ömrü dolmuş Promtail yerine desteklenen bir toplayıcı seçin; Prometheus metrikleri ve Loki benzeri log depolarının görevlerini ayırın.

Bu aşamada, blogda detaylandırdığımız VPS izleme ve alarm kurulumu rehberindeki ajanları ve dashboard kurulumlarını Ansible rolleri haline getirip, her yeni VPS’inize otomatik olarak ekleyebilirsiniz.

DCHost ile Terraform ve Ansible Otomasyonunu Hayata Geçirmek

Buraya kadar anlattığımız yapı, ilk bakışta kapsamlı görünebilir; ancak küçük adımlarla gittiğinizde her adımın somut bir getirisi oluyor. Özetle:

  • Terraform, yetkili OpenStack API üzerinde örnekteki kaynakları yönetir; DCHost için bir API desteği varsayılmaz.
  • Ansible, mevcut uygun DCHost VPS’de de açıkça tanımlanan OS/web görevlerini uygular. SSH/firewall için ayrı kontrollü aşama kullanılır.
  • Plan, envanter, uygulama yanıtı, TLS, izleme ve geri yükleme kontrollerini birleştirerek yeniden çalıştırılabilir bir süreç oluşturun.

VPS, dedicated veya kendi sanallaştırmanızı çalıştırdığınız colocation altyapısında aynı yapılandırma ilkelerini kullanabilirsiniz; gerekli yönetim yetkileri ve OS desteği her platformda ayrıca doğrulanır. DCHost üzerindeki mevcut sunucular için Ansible ile başlamak, API entegrasyonu hakkında varsayım yapmadan ilerlemenizi sağlar.

Bir sonraki adım olarak şunları yapmanızı öneririm:

  • Yeni/ayrılmış bir Ubuntu 24.04 hedef seçin; mevcut VPS için doğrudan envanter yolunu veya yetkili OpenStack için incelenmiş plan yolunu kullanın.
  • Önce common/web rollerini ve gerçek HTTP/PHP davranışını, sonra ayrı erişim politikasını test edin.
  • DNS/TLS, veritabanı, cache, izleme ve yedek rollerini kendi kabul ve geri dönüş adımlarıyla ekleyin. HTTP-01 için genel internetten port 80 ve challenge yolu erişilebilir olmalıdır; mevcut şablonun noktayla başlayan yolları engelleyen kuralını challenge yoluna izin verecek şekilde uyarlayın. Wildcard için DNS-01 gerekir.

Yedeklerde saklama, şifreleme ve geri yükleme tatbikatını kod kadar önemseyin. Restic’in S3 backend’i ile Borg’un SSH/yerel depo modeli aynı değildir; seçtiğiniz araç için desteklenen hedefi kullanın. DCHost altyapı seçiminizi kaynak, erişim, izleme ve kurtarma gereksinimleriyle birlikte değerlendirin. Sürüm ve rol güncellemelerini küçük bir test ortamından geçirerek ilerleyin.

Sıkça Sorulan Sorular

Önce erişimi doğrulanmış, yeni veya ayrılmış bir Ubuntu 24.04 VPS üzerinde Ansible envanteri, açık roles_path/collections_path ayarı ve common/web rollerini kurun. Terraform gerekiyorsa yalnız yetkili ve desteklenen API ile başlayın; rehberde gerçek OpenStack provider kullanılır ve DCHost API desteği varsayılmaz. Sürüm/bağımlılıkları kurup planı inceleyin, SSH ve sudo erişimini doğrulayın. Envanter üretimini yalnız başarılı apply sonrasına bağlayın; HTTP, ayrı sertleştirme, TLS ve uygulama kabul kontrollerini tamamlamadan tam üretim kurulumu saymayın.

State kaynak eşleştirmesini ve hassas değerleri içerebilir; Git dışında, dar yetkiyle saklayın. sensitive = true yalnız bazı çıktıları maskeler, state/plan dosyalarını şifrelemez. Seçilen backend için şifreleme, erişim kontrolü, sürümleme ve desteklenen kilitlemeyi açıkça yapılandırın; her S3 uyumlu hizmetin bütün backend özelliklerini sunduğunu varsaymayın. Aynı state için CI eşzamanlılığını sınırlandırın ve yedekten state/anahtar kurtarmasını test edin. Uygulama verileri ayrıca yedeklenmeli ve geri yüklenmelidir.

Önce non-root hesap, public key ve sudo erişimini yeni bir oturumda ve acil konsol erişimini test edin. Ardından SSH Include/Match önceliğini dikkate alan tam sshd -t ve gerçek bağlantı bağlamıyla sshd -T -C kontrollerinden sonra, drop-in değişmese de her başarılı çalıştırmada reload kullanın. Bu kasıtlı tekrar önceki başarısız reload sonrası yeniden denemeyi tamamlar; doğrulama veya reload hatasında işlem durmalıdır. Firewall etkinleştirilmeden önce gerçek yönetim CIDR ve SSH portunu koruyan kuralı ekleyin; IPv6 ve mevcut izinleri de inceleyin. Fail2ban ve otomatik güncelleme paketlerini kurmak yeterli değildir: jail/backend, istisnalar, bakım/reboot politikası ve loglar ayrıca doğrulanmalıdır. Her hedefin sonucunu test edin.

Ortak modül ve rolleri kullanırken staging ve üretim için ayrı checkout, envanter, backend/state ve erişim yetkileri ayırın. CLI workspace veya farklı tfvars dosyası tek başına güvenlik sınırı değildir. İmaj, bölge, flavor, alan adı ve kaynak limitleri gibi farkları açık değişkenlerle kaydedin; her çalıştırmadan önce doğru hesap/state/host listesini kontrol edin. DCHost üzerindeki mevcut VPS’ler doğrudan Ansible ile yönetilebilir; Terraform için gerçek API yetkisi ayrıca gerekir. Daha küçük staging ortamının üretim kapasitesini birebir temsil ettiğini varsaymayın.
↑