Environment Management #
Hampir semua sistem produksi berjalan di beberapa lingkungan: development untuk eksperimen, staging untuk validasi, production untuk pengguna nyata. Tantangannya adalah memastikan konfigurasi yang tepat diterapkan ke lingkungan yang tepat — tanpa duplikasi, tanpa variasi yang tidak disengaja, dan tanpa risiko konfigurasi staging bocor ke production atau sebaliknya. Ansible menyediakan struktur yang memungkinkan ini dilakukan dengan bersih dan aman. Artikel ini membahas pola environment management yang teruji untuk deployment pipeline Ansible — dari struktur inventory, hierarki variabel, promotion flow, hingga secret management dengan Ansible Vault.
Struktur Inventory Per Environment #
Pola yang paling scalable adalah satu direktori inventory per environment. Setiap environment berdiri sendiri dengan hosts, variabel, dan vault-nya sendiri:
inventory/
├── development/
│ ├── hosts.ini
│ └── group_vars/
│ ├── all.yml # Variabel untuk semua host di dev
│ ├── webservers.yml
│ └── databases.yml
│
├── staging/
│ ├── hosts.ini
│ └── group_vars/
│ ├── all.yml
│ ├── webservers.yml
│ └── databases.yml
│
└── production/
├── hosts.ini
└── group_vars/
├── all.yml
├── webservers.yml
└── vault.yml # Variabel sensitif — dienkripsi
Setiap environment punya all.yml sendiri yang mendefinisikan nilai yang spesifik untuk environment tersebut:
# inventory/development/group_vars/all.yml
env: development
app_replicas: 1
app_debug: true
log_level: debug
db_pool_size: 5
enable_https: false
# inventory/staging/group_vars/all.yml
env: staging
app_replicas: 2
app_debug: false
log_level: info
db_pool_size: 10
enable_https: true
# inventory/production/group_vars/all.yml
env: production
app_replicas: 4
app_debug: false
log_level: warning
db_pool_size: 50
enable_https: true
Dengan struktur ini, menjalankan playbook ke environment tertentu hanya butuh satu argumen: -i inventory/production/. Tidak ada risiko salah pilih group, tidak ada variabel yang tidak disengaja terbawa dari environment lain. Pipeline CI/CD kita cukup memiliki tiga job — deploy-dev, deploy-staging, deploy-prod — masing-masing menunjuk ke inventaris yang berbeda.
Promotion Flow Antar Environment #
Tiga environment bukan hanya berbeda secara konfigurasi, tetapi juga berbeda dalam hal siapa yang memicu deploy, kapan, dan apa yang dipromosikan. Promotion flow yang sehat terlihat seperti ini:
flowchart LR
A["Push ke feature/*"] -->|Auto-deploy| B["Development"]
B -->|"PR merged ke main"| C["Staging"]
C -->|"Manual approval"| D["Production"]
B -. "Tag v*.*.*" .-> E["Container Registry"]
C -. "Tag v*.*.*" .-> E
D -. "Tag v*.*.*" .-> E
E -. "Pull image" .-> B
E -. "Pull image" .-> C
E -. "Pull image" .-> D
Perhatikan poin krusialnya: artifact yang sama (Docker image dengan tag v2.1.0-abc123) dipromosikan dari satu environment ke environment berikutnya. Image yang lulus testing di staging adalah image yang sama persis yang di-deploy ke production. Jangan pernah re-build image untuk setiap environment — itu menghilangkan jaminan bahwa staging merepresentasikan production.
Pipeline kita bisa direpresentasikan sebagai sequence diagram seperti ini:
sequenceDiagram
participant Dev as Developer
participant Git as Git Repository
participant CI as CI Pipeline
participant Reg as Container Registry
participant Dev2 as Dev Environment
participant Stg as Staging
participant Prd as Production
Dev->>Git: push commit ke main
Git->>CI: trigger pipeline
CI->>CI: build & test image
CI->>Reg: push image v2.1.0-abc123
CI->>Dev2: deploy otomatis
Dev2-->>CI: health check OK
CI->>Stg: deploy otomatis
Stg-->>CI: integration test OK
CI->>Prd: butuh approval
Prd-->>CI: approved by oncall
CI->>Prd: deploy
Prd-->>CI: health check OK
Sequence diagram ini memperjelas gate-gate promotion: development auto-deploy, staging auto-deploy setelah integration test, production butuh human approval. Kita bisa mengotomatisasi sebanyak yang kita inginkan, tetapi gate production harus selalu manual sampai kita benar-benar yakin.
Shared vs Environment-Specific Variables #
Banyak variabel sama di semua environment — tidak perlu diduplikasi. Ansible memiliki hierarki variable precedence yang elegan: variabel di lokasi yang lebih spesifik menimpa variabel di lokasi yang lebih umum. Manfaatkan ini untuk mencegah duplikasi.
group_vars/ ← Di luar direktori inventory (shared)
└── all.yml ← Berlaku untuk semua environment
inventory/
├── development/
│ └── group_vars/
│ └── all.yml ← Override variabel shared untuk dev
├── staging/
└── production/
# group_vars/all.yml (shared — berlaku semua environment)
app_port: 8080
app_name: myapp
app_user: deployer
app_dir: /opt/myapp
backup_retention_days: 30
# Nilai default yang akan di-override per environment:
app_replicas: 1
log_level: info
# inventory/production/group_vars/all.yml (override untuk production)
app_replicas: 4
log_level: warning
# Tidak perlu mendefinisikan ulang app_port, app_name, dll.
# Mereka di-inherit dari shared group_vars
Ansible menentukan variabel mana yang digunakan dengan urutan presedensi: host_vars/<host> > group_vars/<group>/ di inventory > group_vars/ shared > defaults di role. Variabel di lokasi yang lebih spesifik otomatis menang. Ini berarti: taruh semua yang universal di shared group_vars/, override hanya yang berbeda per environment.
Aturan praktisnya saat debug variabel yang tidak berperilaku seperti yang diharapkan: semakin dekat definisi variabel ke host, semakin tinggi prioritasnya. host_vars/hostname.yml menimpa group_vars/all.yml menimpa group_vars/shared/all.yml menimpa default role. Untuk melihat nilai efektif dari sebuah variabel, gunakan ansible -i inventory/production all -m debug -a "var=app_replicas" — output menunjukkan nilai akhir yang akan dipakai host tersebut, memperhitungkan seluruh rantai presedensi.
ANTI-PATTERN: Hard-Coded Environment vs Externalized Config #
Salah satu jebakan paling umum adalah menulis konfigurasi environment langsung di dalam playbook atau template. Ini membuat konfigurasi tidak portable, sulit diuji, dan hampir mustahil di-promote ke environment lain.
# ANTI-PATTERN: konfigurasi di-hardcode di dalam playbook
# playbooks/deploy-app.yml
- name: Deploy aplikasi
hosts: appservers
vars:
db_host: "prod-db-01.internal" # Hard-coded!
db_password: "s3cr3t!" # Hard-coded DAN bocor!
redis_url: "redis://prod-redis.internal:6379" # Hard-coded!
app_replicas: 4 # Hard-coded!
enable_debug: false # Hard-coded!
tasks:
- name: Deploy
# ... kode yang menggunakan semua variabel di atas
Masalahnya: playbook ini hanya bisa dipakai untuk production. Untuk staging, kita harus melakukan copy-paste dan mengubah semua nilainya. Untuk development, lakukan copy-paste lagi. Tiga playbook yang 95% sama persis tetapi dengan nilai yang berbeda-beda. Saat kita ingin memperbarui satu baris logika, kita harus mengingat untuk mengubahnya di tiga tempat — dan cepat atau lambat kita akan lupa.
# BENAR: konfigurasi externalized, dibaca dari inventory per environment
# playbooks/deploy-app.yml
- name: Deploy aplikasi
hosts: appservers
# Tidak ada vars hardcoded — semua diambil dari inventory
tasks:
- name: Tulis konfigurasi aplikasi
template:
src: app-config.j2
dest: "{{ app_dir }}/config.yml"
owner: "{{ app_user }}"
mode: '0644'
vars:
db_host: "{{ db_host }}" # Dari group_vars
db_password: "{{ vault_db_password }}" # Dari vault
redis_url: "{{ redis_url }}" # Dari group_vars
app_replicas: "{{ app_replicas }}" # Dari group_vars
enable_debug: "{{ app_debug | bool }}" # Dari group_vars
# inventory/development/group_vars/all.yml
db_host: "dev-db.internal"
redis_url: "redis://dev-redis.internal:6379"
app_replicas: 1
app_debug: true
# inventory/production/group_vars/all.yml
db_host: "prod-db.internal"
redis_url: "redis://prod-redis.internal:6379"
app_replicas: 4
app_debug: false
Sekarang satu playbook bisa dipakai di semua environment. Pipeline cukup swap inventory direktori: ansible-playbook -i inventory/production/ deploy-app.yml. Tidak ada duplikasi, tidak ada risiko lupa update satu tempat.
Environment Promotion dengan Tag Git #
Pola yang umum: setiap environment di-deploy dari branch atau tag Git yang berbeda. Ini memberikan traceability yang kuat — dari versi aplikasi yang berjalan, kita bisa langsung mengetahui commit mana yang membuatnya.
Git Flow untuk Deployment:
feature/* → main (auto-deploy ke development)
main → release candidate (manual promote ke staging)
tag v*.*.*→ production (setelah staging verified)
# playbooks/deploy.yml
---
- name: Deploy aplikasi
hosts: appservers
vars:
# Di-override dari pipeline berdasarkan environment dan tag Git
deploy_version: "{{ version | mandatory }}"
deploy_env: "{{ env }}" # Dari inventory group_vars
pre_tasks:
- name: Verifikasi kita deploy ke environment yang benar
assert:
that:
- deploy_env == expected_env | default(deploy_env)
fail_msg: >
Mismatch environment! Inventory menunjuk ke '{{ deploy_env }}'
tapi pipeline mengharapkan '{{ expected_env }}'.
- name: Verifikasi versi production hanya dari tag resmi
assert:
that:
- deploy_version is match('^v?[0-9]+\.[0-9]+\.[0-9]+$')
fail_msg: >
Deployment production hanya boleh menggunakan tag semver.
Versi '{{ deploy_version }}' tidak valid untuk production.
when: deploy_env == 'production'
- name: Catat metadata deployment ke server
copy:
content: |
version={{ deploy_version }}
env={{ deploy_env }}
git_sha={{ git_sha | default('unknown') }}
deployed_at={{ ansible_date_time.iso8601 }}
deployed_by={{ lookup('env', 'CI_JOB_URL') | default(lookup('env', 'USER') | default('manual')) }}
dest: "{{ app_dir }}/DEPLOYMENT_INFO"
mode: '0644'
assert di pre_tasks adalah jaring pengaman terakhir. Jika pipeline salah passing environment (misalnya job production tapi variabel env menunjuk ke staging), playbook gagal sebelum perubahan apapun terjadi. Ini berbeda dari sekadar log warning — Ansible assert adalah hard stop.
Jangan percaya label job di pipeline sebagai satu-satunya penanda environment. Selalu baca environment dari inventory file, dan validasi bahwa environment yang dituju oleh inventory cocok dengan yang diharapkan oleh pipeline. Kebanyakan insiden “kok production kena?” bermula dari inventory yang salah dipilih atau variabel environment yang keliru di-passing.
Konfigurasi Berbeda tapi Playbook Sama #
Kekuatan utama pola ini: satu playbook yang bekerja di semua environment berdasarkan variabel. Semua keputusan — SSL aktif atau tidak, jumlah worker, level log — ditentukan oleh variabel yang di-resolve Ansible dari inventory.
# playbooks/setup-nginx.yml
---
- name: Setup nginx untuk semua environment
hosts: webservers
become: true
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Deploy konfigurasi nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Reload nginx
# Konfigurasi SSL — hanya jika enable_https: true
- name: Deploy SSL certificate
copy:
src: "files/ssl/{{ inventory_hostname }}.crt"
dest: /etc/ssl/certs/app.crt
when: enable_https | bool
# Worker processes — berbeda per environment
- name: Set nginx worker processes
lineinfile:
path: /etc/nginx/nginx.conf
regexp: '^worker_processes'
line: "worker_processes {{ ansible_processor_vcpus if env == 'production' else 1 }};"
notify: Reload nginx
{# templates/nginx.conf.j2 — template yang aware lingkungan #}
worker_processes {{ ansible_processor_vcpus if env == 'production' else 1 }};
events {
worker_connections {{ 1024 if env == 'production' else 256 }};
}
http {
{% if enable_https | bool %}
server {
listen 443 ssl;
ssl_certificate /etc/ssl/certs/app.crt;
# ... SSL config
}
{% else %}
server {
listen 80;
# Dev/staging: tidak pakai SSL
}
{% endif %}
}
Template nginx.conf.j2 ini menarik karena tidak peduli environment apa yang menjalankan — semua keputusan ada di variabel. Production dapat worker sebanyak core CPU dan SSL; development dapat 1 worker dan HTTP plain. Playbook yang sama, hasil yang berbeda, berdasarkan nilai env dan enable_https saja.
Perbandingan Strategi Environment #
Tidak ada satu strategi environment yang universal. Pilihan yang tepat tergantung pada ukuran tim, frekuensi deployment, dan toleransi kita terhadap trade-off tertentu.
| Aspek | Single-Env + Feature Flag | Multi-Env Klasik | Per-PR Ephemeral Env |
|---|---|---|---|
| Jumlah environment persisten | 1 (production) | 2–3 (dev/staging/prod) | 0 (semua transient) |
| Waktu setup environment baru | Tidak perlu (satu env) | Hari untuk provision | Menit (otomatis) |
| Biaya infrastruktur | Sangat rendah (1 env) | Sedang–tinggi (env persisten) | Tinggi (banyak env hidup-mati) |
| Isolasi antar PR | Lemah (semua PR share state) | Sedang (dev share, staging shared) | Kuat (setiap PR punya env sendiri) |
| Cocok untuk | Startup kecil, monorepo | Tim menengah, frekuensi deploy harian | Tim besar, microservices, mobile |
| Kompleksitas tooling | Rendah | Sedang | Tinggi (perlu orchestrator) |
| Contoh stack | Heroku, Vercel + flags | Klasik Ansible inventory | Vercel Preview, Kubernetes + Argo |
| Test integration realistis | Lemah | Kuat (staging ≈ prod) | Kuat per-PR, lemah antar-PR |
| Rollback strategy | Disable flag | Deploy ulang image lama | Hapus env PR |
Untuk tim kecil yang baru mulai, multi-env klasik (dev/staging/prod) adalah titik awal yang paling pragmatis. Saat jumlah contributor dan frekuensi PR meningkat, evaluasi per-PR ephemeral env. Untuk produk consumer dengan traffic rendah dan toleransi kita terhadap trade-off tertentu yang tinggi, single-env + feature flag bisa cukup. Pilih berdasarkan konteks, bukan berdasarkan “best practice absolut” yang kita baca di blog.
ANTI-PATTERN: Shared Staging Cluster vs Per-PR Ephemeral Env #
Untuk tim yang sering mengalami “tapi di staging jalan, kok di production error?” masalahnya sering karena staging dipakai bersama oleh banyak PR. Engineer A deploy ke staging, lalu engineer B juga deploy ke staging sebelum engineer A selesai testing. Hasil: engineer A tidak yakin apakah bug yang ia lihat kodenya, atau tercemar dari engineer B.
# ANTI-PATTERN: shared staging cluster
# inventory/staging/hosts.ini
[appservers]
staging-01.internal
staging-02.internal
# playbooks/deploy-staging.yml
- hosts: appservers
tasks:
- name: Deploy PR terbaru
docker_container:
name: myapp
image: "registry.company.com/myapp:{{ pr_number }}"
Semua PR di-deploy ke server yang sama. Saat dua PR deploy di waktu yang berdekatan, tidak ada isolasi. Engineer A testing checkout flow bisa terganggu oleh engineer B yang deploy perubahan ke payment service. Lebih buruk lagi, race condition saat deploy bisa menyebabkan downtime sebentar di staging yang membuat kedua engineer bingung — “kok error, padahal kodenya gak berubah?”
# BENAR: per-PR ephemeral environment
# playbooks/create-pr-env.yml
- name: Buat environment ephemeral untuk PR
hosts: localhost
vars:
pr_number: "{{ pr_number | mandatory }}"
tasks:
- name: Provision namespace Kubernetes untuk PR
kubernetes.core.k8s:
name: "pr-{{ pr_number }}"
api_version: v1
kind: Namespace
state: present
- name: Deploy aplikasi ke namespace PR
kubernetes.core.k8s:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: "pr-{{ pr_number }}"
spec:
template:
spec:
containers:
- name: myapp
image: "registry.company.com/myapp:pr-{{ pr_number }}"
state: present
- name: Tulis URL preview ke PR comment
uri:
url: "https://api.github.com/repos/{{ repo }}/issues/{{ pr_number }}/comments"
method: POST
body_format: json
body:
body: "Preview environment: https://pr-{{ pr_number }}.preview.company.com"
headers:
Authorization: "token {{ vault_github_token }}"
Setiap PR sekarang punya environment sendiri dengan URL unik seperti https://pr-1234.preview.company.com. Tester dan engineer bisa review PR di environment yang terisolasi penuh. Setelah PR di-merge atau di-close, environment-nya otomatis di-destroy. Tidak ada lagi “kok staging error setelah deploy?” karena setiap deploy punya rumah sendiri.
Environment Lock: Cegah Deploy ke Environment yang Salah #
Defense-in-depth selain validasi di pipeline: tulis file di server yang menandai environment mana yang seharusnya berjalan di server tersebut. Playbook yang salah environment akan gagal sebelum melakukan perubahan.
# roles/common/tasks/env-lock.yml
# Jalankan ini di awal setiap playbook deployment
- name: Cek environment lock file
stat:
path: /etc/ansible-env-lock
register: env_lock
- name: Verifikasi environment sesuai jika lock file ada
block:
- name: Baca expected environment dari lock file
slurp:
src: /etc/ansible-env-lock
register: lock_content
- name: Validasi environment yang dikonfigurasi
assert:
that:
- lock_content.content | b64decode | trim == env
fail_msg: >
ENVIRONMENT MISMATCH!
Server ini dikonfigurasi untuk: {{ lock_content.content | b64decode | trim }}
Inventory yang digunakan: {{ env }}
Hentikan deployment untuk mencegah konfigurasi yang salah!
when: env_lock.stat.exists
- name: Tulis environment lock file jika belum ada
copy:
content: "{{ env }}\n"
dest: /etc/ansible-env-lock
mode: '0444' # Read-only
when: not env_lock.stat.exists
Lock file di server adalah pengaman terakhir, bukan pengganti validasi di pipeline. Kombinasi terbaik: (1) pipeline memastikan environment yang dituju benar sebelum eksekusi, (2) inventory hanya berisi hosts untuk environment tersebut, (3) server memverifikasi environment yang masuk akal untuk dirinya. Tiga lapis pertahanan ini membuat “kok production kena deploy staging?” hampir mustahil terjadi.
Secret Management dengan Ansible Vault #
Environment management tanpa secret management adalah bom waktu. Setiap environment punya secret sendiri (database password, API key, TLS private key) yang harus dijaga agar tidak bocor, tapi tetap bisa digunakan oleh playbook.
Ansible Vault mengenkripsi file YAML dengan AES256. File yang dienkripsi tetap bisa di-include ke dalam playbook seperti biasa, tapi isinya tidak terbaca di repository Git tanpa password.
# Buat vault baru — Ansible akan prompt password
ansible-vault create inventory/production/group_vars/vault.yml
# Edit vault yang ada
ansible-vault edit inventory/production/group_vars/vault.yml
# Encrypt file yang sudah ada
ansible-vault encrypt inventory/production/group_vars/vault.yml
# Decrypt untuk dilihat (hanya untuk debugging lokal, jangan commit!)
ansible-vault decrypt inventory/production/group_vars/vault.yml
# Lihat isi tanpa decrypt ke file
ansible-vault view inventory/production/group_vars/vault.yml
# inventory/production/group_vars/vault.yml (encrypted, isi tidak terbaca)
$ANSIBLE_VAULT;1.1;AES256
35383532316537623534346434643731303636303037303762653965333235646564623935383032
6430346231653137633334336238303130383134323362610a643866393537303332636162326238
# ... baris terenkripsi
Untuk menjalankan playbook dengan vault:
# Prompt password saat eksekusi
ansible-playbook -i inventory/production/ deploy.yml --ask-vault-pass
# Atau pakai file password (untuk CI/CD)
ansible-playbook -i inventory/production/ deploy.yml --vault-password-file ~/.vault-pass
# Atau via environment variable
ANSIBLE_VAULT_PASSWORD_FILE=/run/secrets/vault-pass ansible-playbook ...
File.vault-passJANGAN PERNAH di-commit ke Git. Simpan di tempat yang aman: secrets manager (HashiCorp Vault, AWS Secrets Manager), CI/CD secrets (GitHub Actions secrets, GitLab CI variables), atau local file di workstation developer yang di-ignore oleh.gitignore. Untuk CI/CD, inject password sebagai environment variable atau mount sebagai file di/run/secrets/yang tidak persistent.
Per-Environment Vault Password #
Untuk tim yang mengelola banyak environment, setiap environment bisa punya vault password sendiri. Ini memungkinkan kontrol akses granular: developer junior punya akses ke vault development, senior engineer ke staging, dan tim SRE ke production.
# Setup multiple vault passwords
ansible-vault create inventory/development/group_vars/vault.yml
# Prompt: masukkan password untuk vault development
ansible-vault create inventory/staging/group_vars/vault.yml
# Prompt: masukkan password untuk vault staging
ansible-vault create inventory/production/group_vars/vault.yml
# Prompt: masukkan password untuk vault production
# Jalankan dengan vault password spesifik
ansible-playbook -i inventory/production/ \
--vault-id production@~/.vault-pass-production \
deploy.yml
Atau, untuk tim kecil, satu vault password untuk semua environment adalah pragmatis. Trade-off: keamanan sedikit berkurang (siapa yang punya password punya akses ke semua vault), tapi operasional jauh lebih sederhana. Pilih sesuai kebutuhan tim.
Ringkasan #
- Satu direktori inventory per environment adalah pola paling scalable — setiap environment punya hosts, variabel, dan vault-nya sendiri yang terpisah.
- Shared
group_vars/di luar direktori inventory untuk variabel yang sama di semua environment — override diinventory/<env>/group_vars/hanya untuk yang berbeda.- Promote artifact yang sama antar environment (jangan re-build) — image yang lulus testing di staging harus identik dengan yang di-deploy ke production.
- Validasi di pre_tasks: pastikan kita deploy ke environment yang benar dan versi yang valid sebelum melakukan perubahan apapun.
- Environment lock file di
/etc/ansible-env-lockmencegah kecelakaan fatal menjalankan inventory production ke server staging atau sebaliknya.- Deployment production hanya dari tag semver yang resmi — enforce ini di playbook dengan
assertsehingga tidak ada yang bisa bypass.- Externalized configuration: tidak ada konfigurasi yang hard-coded di playbook — semua nilai berasal dari inventory dan vault.
- Ansible Vault mengenkripsi secret per environment — gabungkan dengan secret manager untuk CI/CD.
- Pilih strategi environment (single-env, multi-env, per-PR ephemeral) berdasarkan ukuran tim dan frekuensi deploy, bukan berdasarkan dogma.