Dynamic Inventory #
Static inventory dalam bentuk hosts.ini atau file YAML bekerja baik untuk infrastruktur yang kecil dan stabil. Namun begitu kita mengelola cloud environment dengan ratusan instance yang spin up dan terminate setiap hari, atau hybrid infrastructure yang sebagian hostnya berada di AWS, sebagian di on-premise, sebagian di CMDB — memelihara file inventory secara manual menjadi pekerjaan yang mustahil dan rentan kesalahan. Dynamic inventory memecahkan masalah ini: Ansible mengambil daftar host dari sumber yang authoritative — AWS API, GCP Compute, Azure ARM, vCenter, atau CMDB internal — setiap kali playbook dijalankan, sehingga inventory selalu mencerminkan state infrastruktur saat ini.
Artikel ini membahas tiga bentuk dynamic inventory di Ansible: inventory plugin (cara modern, YAML-based, paling direkomendasikan), inventory script (legacy, executable yang return JSON), dan cara menggabungkan banyak sumber inventory menjadi satu ekosistem yang terkoordinasi.
Mengapa Dynamic Inventory Penting #
Bayangkan skenario ini: tim SRE deploy 50 EC2 instance baru untuk auto-scaling group, menghapus 30 instance lama, dan mengubah tag Environment pada 20 instance yang sudah ada. Dengan static inventory, kita harus memperbarui file hosts.ini secara manual — pekerjaan yang tidak skalabel dan rentan salah. Dengan dynamic inventory, Ansible memanggil AWS API setiap kali playbook dijalankan dan mendapatkan state terbaru secara real-time.
flowchart LR
A["Playbook: ansible-playbook -i aws_ec2.yml"] --> B["Ansible baca inventory file"]
B --> C{"YAML atau executable?"}
C -->|YAML| D["Inventory plugin"]
C -->|Executable| E["Inventory script"]
D --> F["AWS API / GCP API / Azure API"]
E --> F
F --> G["JSON host data"]
G --> H["Ansible populate inventory"]
H --> I["keyed_groups & compose"]
I --> J["Groups: role_webserver, env_production, dll."]
J --> K["Playbook berjalan dengan host yang benar"]
Keuntungan dynamic inventory dibanding static:
- Selalu up-to-date — Ansible melihat state infrastruktur saat playbook dijalankan, bukan saat file di-edit terakhir.
- Self-service — developer bisa deploy instance baru tanpa harus memberitahu SRE untuk update inventory.
- Tag-based grouping — group dibuat otomatis dari tag cloud (e.g.,
tag:Role=webserver→ grouprole_webserver). - Skala besar — ratusan hingga ribuan host ditangani tanpa intervensi manual.
- Hybrid-ready — gabungkan inventory dari AWS, GCP, dan CMDB dalam satu direktori.
Inventory Plugin: Cara Modern #
Inventory plugin adalah cara paling direkomendasikan untuk dynamic inventory. Plugin berbasis YAML, terinstal via collection, dan punya fitur bawaan seperti caching, keyed_groups, compose, dan filter. Ansible otomatis memanggil plugin yang tepat berdasarkan nama file dan konfigurasi plugin: di baris pertama.
AWS EC2 dengan amazon.aws.aws_ec2
#
Collection amazon.aws menyediakan plugin aws_ec2 yang mengambil instance langsung dari AWS API:
# Install dependency
ansible-galaxy collection install amazon.aws
pip install boto3
# inventory/aws_ec2.yml
# Nama file HARUS diakhiri dengan aws_ec2.yml atau aws_ec2.yaml
plugin: amazon.aws.aws_ec2
# Region yang akan di-scan
regions:
- ap-southeast-1 # Singapura
- ap-southeast-3 # Jakarta
- us-east-1 # Virginia
# Filter instance yang akan diambil
filters:
instance-state-name: running
"tag:Environment": production
# Field yang digunakan sebagai hostname Ansible
hostnames:
- private-ip-address
# Group otomatis berdasarkan tag
keyed_groups:
- prefix: role
key: tags.Role # Group: role_webserver, role_database, dll.
- prefix: env
key: tags.Environment # Group: env_production, env_staging
- prefix: az
key: placement.availability_zone
# Group berdasarkan kondisi custom
groups:
singapore: "'ap-southeast-1' in placement.region"
jakarta: "'ap-southeast-3' in placement.region"
high_memory: "instance_type.startswith('r5') or instance_type.startswith('r6')"
# Map field AWS ke variabel Ansible
compose:
ansible_host: private_ip_address
ansible_user: "'ec2-user' if 'Windows' not in image_id else 'Administrator'"
instance_type: instance_type
availability_zone: placement.availability_zone
vpc_id: vpc_id
# Variable untuk semua host
groups:
aws_production:
vars:
ansible_ssh_private_key_file: /opt/keys/aws-prod.pem
# Test inventory sebelum digunakan
ansible-inventory -i inventory/aws_ec2.yml --list
ansible-inventory -i inventory/aws_ec2.yml --graph
# Output --graph:
# @all:
# |--@aws_production:
# | |--@env_production:
# | | |--@ap_southeast_1:
# | | | |--ip-10-0-1-10.ap-southeast-1.compute.internal
# | | | |--ip-10-0-1-11.ap-southeast-1.compute.internal
# | | |--@ap_southeast_3:
# | | | |--ip-10-0-2-5.ap-southeast-3.compute.internal
# | |--@role_webserver:
# | | |--ip-10-0-1-10.ap-southeast-1.compute.internal
# Gunakan dalam playbook
ansible-playbook -i inventory/aws_ec2.yml site.yml
GCP Compute dengan google.cloud.gcp_compute
#
# inventory/gcp_compute.yml
plugin: google.cloud.gcp_compute
projects:
- my-production-project
- my-shared-services-project
zones:
- asia-southeast2-a # Jakarta
- asia-southeast2-b
- asia-southeast2-c
# Filter instance — pakai sintaks JMESPath GCP
filters:
- status = RUNNING
- labels.environment = production
- labels.managed_by = ansible
# Group otomatis
keyed_groups:
- prefix: role
key: labels.role
- prefix: zone
key: zone
- prefix: env
key: labels.environment
# Map field GCP ke variabel Ansible
compose:
ansible_host: networkInterfaces[0].networkIP
ansible_user: "'ubuntu' if 'debian' in labels.base_os else 'centos'"
machine_type: machineType
project: project
Azure dengan azure.azcollection.azure_rm
#
# inventory/azure_rm.yml
plugin: azure.azcollection.azure_rm
include_vm_resource_groups:
- production-rg
- staging-rg
# Filter
filters:
- power_state == 'running'
- tags.environment == 'production'
# Group
keyed_groups:
- prefix: role
key: tags.role
- prefix: env
key: tags.environment
# Compose
compose:
ansible_host: properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.privateIPAddress
Tabel Perbandingan Sumber Inventory #
| Aspek | Inventory Plugin (AWS/GCP/Azure) | Inventory Script (Python) | Static File (YAML/INI) |
|---|---|---|---|
| Format | YAML | Executable yang return JSON | YAML atau INI |
| Caching | Built-in, configurable | Manual, harus implement sendiri | Tidak perlu |
| keyed_groups | Native | Harus di-generate manual | Manual |
| compose | Native | Harus di-handle manual | Manual |
| Filter | Native (JMESPath/YAML) | Logika Python penuh | Tidak berlaku |
| Performance (1000 host) | 1 API call (cached) | 1 API call per run | Instant |
| Reusability | Sharing via collection | Sharing via repo internal | One project |
| Testing | ansible-inventory --list |
Manual script execution | Lihat file |
| Direkomendasikan untuk | Cloud, hybrid, modern | CMDB custom, legacy systems | < 50 host stabil |
Alur Kerja ansible-inventory
#
Setiap kali kita menjalankan ansible-playbook -i inventory/, Ansible memanggil ansible-inventory di belakang layar. Plugin atau script kita dieksekusi untuk menghasilkan representasi JSON dari inventory. Diagram berikut menunjukkan alur lengkapnya:
sequenceDiagram
participant User
participant AP as "ansible-playbook"
participant AI as "ansible-inventory"
participant Plugin as "aws_ec2 plugin"
participant AWS as "AWS EC2 API"
participant Cache as "JSON Cache"
User->>AP: "ansible-playbook -i aws_ec2.yml site.yml"
AP->>AI: "Load inventory"
AI->>Plugin: "parse(config)"
Plugin->>Cache: "Cek cache valid?"
alt Cache valid
Cache-->>Plugin: "Return cached data"
else Cache expired/missing
Plugin->>AWS: "DescribeInstances(filters)"
AWS-->>Plugin: "List of instances"
Plugin->>Cache: "Write cache"
end
Plugin->>Plugin: "Apply keyed_groups"
Plugin->>Plugin: "Apply compose"
Plugin-->>AI: "Inventory dict"
AI-->>AP: "Populated inventory"
AP->>AP: "Run tasks on matched hosts"
Caching adalah fitur penting untuk inventory plugin: tanpa caching, setiap ansible-playbook call akan memicu API call ke AWS, yang bisa kena rate limit jika playbook dijalankan dari CI setiap push. Aktifkan caching di ansible.cfg:
# ansible.cfg
[inventory]
cache = true
cache_plugin = jsonfile
cache_connection = /tmp/ansible_inventory_cache
cache_timeout = 300 # Cache berlaku 5 menit
Menulis Inventory Script Sendiri #
Untuk sumber inventory yang tidak memiliki plugin bawaan — CMDB internal, database legacy, sistem monitoring yang mengekspos data host — kita bisa menulis inventory script Python yang mengembalikan JSON dalam format yang diharapkan Ansible. Script ini menerima flag --list (semua host) dan --host <hostname> (variabel untuk satu host).
Anatomi Script #
#!/usr/bin/env python3
# inventory/cmdb_inventory.py
# Ambil inventory dari CMDB internal perusahaan
import json
import sys
import argparse
import os
try:
import requests
except ImportError:
print(json.dumps({"_meta": {"hostvars": {}}}))
sys.exit(0)
CMDB_URL = os.environ.get(
'CMDB_URL', 'https://cmdb.company.internal/api'
)
CMDB_TOKEN = os.environ.get('CMDB_API_TOKEN', '')
def get_inventory():
"""Ambil semua server dari CMDB dan kelompokkan."""
if not CMDB_TOKEN:
sys.stderr.write(
"ERROR: Environment variable CMDB_API_TOKEN belum di-set.\n"
)
return {"_meta": {"hostvars": {}}}
headers = {"Authorization": f"Bearer {CMDB_TOKEN}"}
try:
response = requests.get(
f"{CMDB_URL}/servers",
params={"status": "active"},
headers=headers,
timeout=15
)
response.raise_for_status()
servers = response.json()
except requests.exceptions.RequestException as e:
sys.stderr.write(f"Error mengambil inventory dari CMDB: {e}\n")
return {"_meta": {"hostvars": {}}}
inventory = {
"_meta": {"hostvars": {}},
"all": {"children": []},
}
for server in servers:
hostname = server["hostname"]
env = server.get("environment", "unknown")
role = server.get("role", "unknown")
datacenter = server.get("datacenter", "unknown")
# Host variables
inventory["_meta"]["hostvars"][hostname] = {
"ansible_host": server.get("ip_address", hostname),
"server_id": server.get("id"),
"datacenter": datacenter,
"os": server.get("os"),
"ansible_user": "ubuntu" if "ubuntu" in server.get("os", "") else "centos",
}
# Group by environment
env_group = f"env_{env}"
if env_group not in inventory:
inventory[env_group] = {
"hosts": [],
"vars": {"env": env},
}
inventory["all"]["children"].append(env_group)
inventory[env_group]["hosts"].append(hostname)
# Group by role
role_group = f"role_{role}"
if role_group not in inventory:
inventory[role_group] = {"hosts": []}
inventory["all"]["children"].append(role_group)
inventory[role_group]["hosts"].append(hostname)
# Group by datacenter
dc_group = f"dc_{datacenter}"
if dc_group not in inventory:
inventory[dc_group] = {"hosts": []}
inventory["all"]["children"].append(dc_group)
inventory[dc_group]["hosts"].append(hostname)
return inventory
def get_host(hostname):
"""Ambil variabel untuk satu host."""
headers = {"Authorization": f"Bearer {CMDB_TOKEN}"}
try:
response = requests.get(
f"{CMDB_URL}/servers/{hostname}",
headers=headers,
timeout=10
)
response.raise_for_status()
server = response.json()
return {
"ansible_host": server.get("ip_address", hostname),
"server_id": server.get("id"),
"datacenter": server.get("datacenter"),
"os": server.get("os"),
}
except requests.exceptions.RequestException:
return {}
def main():
parser = argparse.ArgumentParser(description='CMDB Inventory Script')
parser.add_argument('--list', action='store_true',
help='Tampilkan semua inventory')
parser.add_argument('--host', type=str,
help='Tampilkan variabel untuk host tertentu')
args = parser.parse_args()
if args.list:
print(json.dumps(get_inventory(), indent=2))
elif args.host:
print(json.dumps(get_host(args.host), indent=2))
else:
print(json.dumps({}))
if __name__ == '__main__':
main()
# Beri permission executable
chmod +x inventory/cmdb_inventory.py
# Test manual
./inventory/cmdb_inventory.py --list
./inventory/cmdb_inventory.py --host web-01.company.com
# Gunakan dalam playbook
ansible-playbook -i inventory/cmdb_inventory.py site.yml
Tip — Selalu keluar dengan JSON yang valid, bahkan saat terjadi error. Jika script kita crash dengan stack trace Python, Ansible akan memunculkan error cryptic. Lebih baik mengembalikan
{"_meta": {"hostvars": {}}}kosong dan menulis error kestderragar playbook bisa menentukan apakah lanjut atau stop.
Anti-Pattern: Inventory Script yang Mengembalikan Semua Host Tanpa Filter #
# ANTI-PATTERN: ambil semua host, biarkan playbook filter
def get_inventory():
response = requests.get(f"{CMDB_URL}/servers") # 5000 host!
servers = response.json()
inventory = {"_meta": {"hostvars": {}}, "all": {"hosts": []}}
for s in servers:
inventory["all"]["hosts"].append(s["hostname"])
return inventory
# BENAR: filter di source, expose hanya host yang relevan
def get_inventory():
params = {"status": "active", "managed_by": "ansible"}
response = requests.get(f"{CMDB_URL}/servers", params=params)
servers = response.json()
# ... build inventory dengan group yang bermakna
return inventory
# Pemakaian: gunakan --limit untuk filter tambahan
ansible-playbook -i inventory/cmdb_inventory.py site.yml --limit env_production
Konsekuensi anti-pattern di atas: playbook memuat 5000 host ke memory hanya untuk menjalankan task pada 50 host di production. Group all jadi membengkak, --list output tidak informatif, dan filter harus dilakukan di playbook layer (yang seharusnya bisa lebih awal).
Warning — Inventory script yang return semua host tanpa filter bisa menyebabkan playbook hang atau kehabisan memory di infrastruktur besar. Selalu filter di source (CMDB/API) dan expose group yang sudah relevan. Gunakan
--limituntuk filter tambahan saat run, tapi jangan andalkan itu sebagai strategi utama.
Anti-Pattern: Hard-code Inventory di Playbook #
# ANTI-PATTERN: hard-code host di playbook
- name: "Deploy web app"
hosts: web-01.company.com,web-02.company.com,web-03.company.com
tasks: [...]
# BENAR: pakai group dari dynamic inventory
- name: "Deploy web app"
hosts: role_webserver
tasks: [...]
Hard-code host di playbook mengikat playbook ke state infrastruktur saat ini. Saat instance baru ditambahkan atau dihapus, playbook harus diedit ulang — pola yang tidak skalabel. Dynamic inventory + group-based selection membuat playbook agnostic terhadap jumlah host: jalankan untuk 3 host hari ini, 30 host besok, tanpa edit satu baris pun.
Menggabungkan Banyak Sumber Inventory #
Ansible bisa menggabungkan inventory dari beberapa sumber sekaligus. Cukup arahkan -i ke sebuah direktori, dan Ansible akan otomatis memuat semua file yang sesuai pola *.yml, *.yaml, *.ini, atau executable yang diakhiri tanpa ekstensi:
inventory/
├── aws_ec2.yml # Server di AWS
├── gcp_compute.yml # Server di GCP
├── cmdb_inventory.py # Server on-premise dari CMDB
├── static_hosts.ini # Server khusus yang tidak ada di cloud/CMDB
└── group_vars/
├── all.yml # Variabel untuk semua host
├── role_webserver.yml # Variabel untuk group role_webserver
├── role_database.yml # Variabel untuk group role_database
└── env_production.yml # Variabel untuk group env_production
# Ansible otomatis gabungkan semua sumber
ansible-playbook -i inventory/ site.yml
# Lihat inventory gabungan
ansible-inventory -i inventory/ --graph
# Output:
# @all:
# |--@env_production:
# | |--@role_database:
# | | |--db-01.aws.internal
# | | |--db-gcp-01.c.internal
# | | |--db-onprem.cmp.internal
# | |--@role_webserver:
# | | |--web-01.aws.internal
# | | |--web-02.aws.internal
Urutan Loading dan Precedence #
flowchart TD
A["Ansible scan direktori inventory/"] --> B["Load file *.yml"]
A --> C["Load file *.yaml"]
A --> D["Load file *.ini"]
A --> E["Load executable files"]
B --> F["Merge ke inventory global"]
C --> F
D --> F
E --> F
F --> G["Apply group_vars/*.yml per group"]
G --> H["Apply host_vars/*.yml per host"]
H --> I["Final inventory siap dipakai"]
Precedence aturan (dari tertinggi ke terendah):
host_vars/<hostname>.yml— variabel per hostgroup_vars/<groupname>.yml— variabel per groupgroup_vars/all.yml— variabel untuk semua hostcompose:di inventory plugin — variabel dinamishostvars:di inventory script — variabel dinamis- Variabel inline di playbook (
vars:)
Info — File di
group_vars/otomatis diasosiasikan dengan group berdasarkan nama file.group_vars/role_webserver.ymlotomatis di-apply ke grouprole_webservertanpa perlu konfigurasi tambahan. Ini adalah pattern powerful yang membuat inventory + konfigurasi tetap rapi di satu tempat.
Caching untuk Inventory Besar #
Untuk infrastruktur dengan ratusan hingga ribuan host, memanggil AWS API setiap kali playbook dijalankan lambat dan bisa kena rate limit. Aktifkan caching di ansible.cfg:
# ansible.cfg
[inventory]
cache = true
cache_plugin = jsonfile
cache_connection = /tmp/ansible_inventory_cache
cache_timeout = 300 # Cache berlaku 5 menit
# Optional: cache hanya untuk plugin tertentu
cache_plugin_omit = ['hostfile'] # Skip caching untuk static file
# Refresh cache manual saat diperlukan
ansible-inventory -i inventory/ --list --flush-cache
# Lihat isi cache
ls -la /tmp/ansible_inventory_cache/
cat /tmp/ansible_inventory_cache/aws_ec2_*.json | python -m json.tool | head -30
Strategi Caching yang Tepat #
| Skenario | Cache TTL | Alasan |
|---|---|---|
| Dev environment, sering berubah | 60 detik | Ingin perubahan cepat terlihat |
| Staging, deploy setiap jam | 300 detik (5 menit) | Balance antara freshness dan API call |
| Production, jarang berubah | 3600 detik (1 jam) | Minimalkan API call |
| On-call incident, butuh fresh data | 0 (off) atau manual flush | Jangan andalkan cache saat troubleshoot |
| CI/CD pipeline | Disabled | Setiap run butuh data fresh |
Tip — Untuk CI/CD, nonaktifkan cache atau set TTL sangat rendah. Cache yang expired bisa menyebabkan playbook deploy ke host yang sudah di-terminate, atau skip host yang baru di-provision. Selalu
flush-cachedi awal CI job yang critical.
Pattern Lanjutan: Multi-Region dan Multi-Account #
Untuk organisasi yang punya beberapa AWS account atau region, pattern inventory berikut membantu:
# inventory/aws_production.yml
plugin: amazon.aws.aws_ec2
regions:
- ap-southeast-1
- ap-southeast-3
- us-east-1
filters:
instance-state-name: running
"tag:Environment": production
"tag:ManagedBy": ansible
keyed_groups:
- prefix: role
key: tags.Role
- prefix: env
key: tags.Environment
- prefix: account
key: tags.Account
compose:
ansible_host: private_ip_address
aws_account_id: tags.Account
# inventory/aws_staging.yml
plugin: amazon.aws.aws_ec2
regions:
- ap-southeast-1
filters:
instance-state-name: running
"tag:Environment": staging
keyed_groups:
- prefix: role
key: tags.Role
groups:
staging: true
# Pilih environment dengan -i
ansible-playbook -i inventory/aws_production.yml site.yml # Production
ansible-playbook -i inventory/aws_staging.yml site.yml # Staging
Atau gabungkan semua dalam satu direktori dan filter dengan --limit:
ansible-playbook -i inventory/ site.yml --limit env_production
ansible-playbook -i inventory/ site.yml --limit env_staging
Pattern Lanjutan: Inventory untuk Kubernetes #
Untuk cluster Kubernetes, inventory plugin kubernetes.core.k8s menghasilkan inventory dari object K8s (pod, service, node):
# inventory/k8s.yml
plugin: kubernetes.core.k8s
connections:
- kubeconfig: ~/.kube/config
name: production-cluster
# Ambil node dari cluster
# Filter berdasarkan label
Penggunaan umum: orchestrating workload di K8s dan VM secara bersamaan dalam satu playbook.
Testing dan Validasi Inventory #
Sebelum pakai inventory di production, selalu test dulu:
# Lihat inventory sebagai JSON
ansible-inventory -i inventory/aws_ec2.yml --list | jq 'keys'
# Lihat host di group tertentu
ansible-inventory -i inventory/aws_ec2.yml --list | \
jq '._meta.hostvars | keys'
# Lihat host di group env_production
ansible-inventory -i inventory/aws_ec2.yml --list | \
jq '.env_production.hosts'
# Lihat variable untuk satu host
ansible-inventory -i inventory/aws_ec2.yml --host web-01.internal
# Test koneksi ke semua host di group
ansible all -i inventory/aws_ec2.yml -m ping --limit env_production
# Dry-run playbook (--check) untuk validasi tanpa eksekusi
ansible-playbook -i inventory/aws_ec2.yml site.yml --check --diff
# Simpan inventory ke file untuk inspeksi offline
ansible-inventory -i inventory/aws_ec2.yml --list > /tmp/inv_dump.json
# Bandingkan inventory antara dua waktu
diff <(jq -S . /tmp/inv_dump.json) <(ansible-inventory -i inventory/aws_ec2.yml --list | jq -S .)
Kapan Pakai Static vs Dynamic #
Jumlah host < 20 dan jarang berubah?
→ Static inventory (YAML/INI) cukup
Contoh: lab environment, 5 VPS pribadi
20-100 host, berubah beberapa kali sebulan?
→ Semi-dynamic: inventory script sederhana atau hybrid
100+ host, auto-scaling, multi-cloud?
→ Dynamic inventory plugin dengan caching
Contoh: production AWS + GCP + on-premise
Host dari banyak sumber (cloud + CMDB + file)?
→ Direktori inventory dengan multiple file
Butuh testing/development tanpa real infrastructure?
→ Static inventory dengan mock host
Contoh: localhost, Vagrant, Docker
Ringkasan #
- Inventory plugin (AWS EC2, GCP Compute, Azure ARM) adalah cara paling direkomendasikan untuk dynamic inventory — berbasis YAML, support caching,
keyed_groups, dancomposesecara native.- Gunakan
keyed_groupsuntuk otomatis membuat grup dari tag cloud —tag:Role=webserver→ grouprole_webservertanpa konfigurasi tambahan per host.composememetakan field API ke variabel Ansible —ansible_host: private_ip_addressmemastikan Ansible pakai private IP, bukan public.- Inventory script Python harus support
--list(semua host) dan--host <hostname>(variabel host) — ini adalah interface yang diharapkan Ansible untuk executable.- Gabungkan banyak sumber dengan
-i inventory/dir/— Ansible otomatis load semua file di direktori tersebut, lalu merge ke satu inventory.- Aktifkan caching untuk inventory besar — tanpa cache, setiap
ansible-playbookcall memicu API call ke cloud, lambat dan rentan rate limit.- Filter di source, bukan di playbook — inventory script yang return semua host tanpa filter akan membengkakan memory dan membuat output
--listtidak informatif.- Jangan hard-code host di playbook — pakai group (
hosts: role_webserver) agar playbook agnostic terhadap jumlah host.- Selalu test inventory dengan
ansible-inventory -i <inv> --listdanansible all -m ping --limit <group>sebelum menjalankan playbook yang mengubah state.- Untuk multi-environment, pakai direktori inventory + filter dengan
--limitatau file inventory terpisah per environment.