- Intro
- Target
- Architecture
- Module Set
- Repository Layout
- Azure Topologies
- Testing
- Security Note
- License
Puppet control repo for a compact, Hiera-driven Elastic Stack lab.
The default role provisions Filebeat, Logstash, Elasticsearch, Kibana, and Nginx on a supported EL-family host. It is intended for local development, integration testing, and demonstration of a roles-and-profiles Puppet control repo.
- Puppet 8
- Ruby 3.x for local tooling
- Elastic Stack 8 package repositories
- Rocky/EL 9 style hosts
- One Elasticsearch service per node
- Hiera-managed profile data
- RSpec catalog tests and Litmus acceptance tests
+---------------------+
| Managed log file |
| /var/log/messages |
| /var/log/testlog |
+---------+-----------+
|
v
+-------------+
| Filebeat |
| filestream |
+------+------+ HTTP :80
| ^
| Beats :5044 |
v |
+-------------+ +------+------+
| Logstash | | Nginx |
| pipe | | reverse |
+------+------+ | proxy |
| +------+------+
| HTTP :9200 |
v |
+---------+---------+ |
| Elasticsearch | |
| cluster | |
+---------+---------+ |
^ |
| HTTP :9200 |
| v
+------+------+ HTTP :5601
| Kibana |<------------------+
| dashboards |
+-------------+
The stack is intentionally direct: Filebeat ships to Logstash, Logstash writes to Elasticsearch, Kibana reads from Elasticsearch, and Nginx fronts Kibana.
The control repo pins the Vox Pupuli Elastic module line:
mod 'puppet/elastic_stack', '10.0.0'
mod 'puppet/elasticsearch', '10.0.0'
mod 'puppet/logstash', '9.0.0'
mod 'puppet/kibana', '9.0.0'Supporting stack modules are pinned in spec/fixtures/Puppetfile and
.fixtures.yml so local catalog tests and CI resolve the same dependency set.
Litmus utility modules are pulled from their upstream GitHub repositories by
.fixtures.yml.
site/modules/role: role classes that compose profilessite/modules/profile: profile classes and managed files/templatesspec/fixtures/hieradata: sample Hiera data for the lab stackspec/classes: catalog testsspec/acceptance: Litmus acceptance tests.github/workflows: CI checks
Each Azure deployment topology lives in its own directory under infra/.
The current topologies are infra/azure-one-node and
infra/azure-multi-node.
Azure is used here because Resource Groups give the acceptance test environment a clear lifecycle boundary. The ephemeral test tasks can create a dedicated resource group, deploy every lab resource into it, run the tests, and then delete the group as the cleanup step. That keeps the cloud resources for a test run easier to reason about than relying on tag-based cleanup or tracking each VM, disk, NIC, public IP, and security rule separately.
Destroy the lab resource group when finished:
bundle exec rake azure:destroyThe infra/azure-one-node Bicep template provisions a single EL9-compatible
Azure VM for end-to-end testing on real systemd/package infrastructure.
Azure subscription
|
v
+-----------------------+
| Resource group |
| rg-elk-lab |
+-----------+-----------+
|
+-----------------+-----------------+
| |
v v
+---------------------+ +---------------------+
| Virtual network | | Public IP |
| 10.42.0.0/16 | | static |
+----------+----------+ +----------+----------+
| |
v |
+---------------------+ |
| Subnet | |
| 10.42.1.0/24 | |
+----------+----------+ |
| |
v |
+---------------------+ |
| Network security | |
| group | |
| SSH/HTTP/ELK ports | |
| from LAPTOP_IP only | |
+----------+----------+ |
| |
v v
+-----+-----------------------------------+-----+
| Network interface |
+----------------------+------------------------+
|
v
+-----------------------------------------------+
| AlmaLinux 9 VM |
| one-node ELK stack |
| OS disk + managed data disk at LUN 0 |
| cloud-init: packages, iptables, vm.max_map |
| facts: espv + elk_lab_source_cidr |
| Puppet acceptance target |
+-----------------------------------------------+
Before deploying, export the two values that are intentionally local to your
machine: your current public IPv4 address and the SSH public key Azure should
install for the VM admin user. The parameter file appends /32 to LAPTOP_IP
automatically so the Azure NSG and host firewall open only for that single IP.
export LAPTOP_IP=<your-public-ipv4>
export AZURE_ONE_NODE_ADMIN_SSH_PUBLIC_KEY="$(<~/.ssh/id_ed25519.pub)"The one-node topology otherwise uses fixed lab values recorded in
infra/azure-one-node/main.bicepparam: resource group rg-elk-lab, location
australiaeast, deployment name elk-one-node, admin user azureuser, VM size
Standard_D4s_v4, AlmaLinux 9, and a 128 GiB managed data disk.
The one-node template also attaches a managed data disk at LUN 0. Cloud-init
resolves that Azure LUN symlink and publishes the resulting block device as the
espv external fact, so profile::elasticsearch::data_node can build the
Elasticsearch LVM volume at /srv/es. Cloud-init also publishes
elk_lab_source_cidr from the same LAPTOP_IP-derived CIDR used by the Azure
network security group, so Puppet opens the lab ports only to that trusted
client on the VM firewall.
Log in to Azure and choose the subscription:
az login
az account set --subscription <subscription-id-or-name>Then to validate and deploy:
bundle exec rake azure:one_node:deploy
bundle exec rake azure:one_node:outputsThe deployment output includes the public IP and SSH command.
If your public IP changes after the VM has been created, update the lab
allow-list instead of redeploying the VM. Azure does not allow customData to
change after VM creation, and the one-node bootstrap data includes the source
CIDR used by Puppet firewall facts.
export LAPTOP_IP="$(curl -4 -s https://ifconfig.me)"
bundle exec rake azure:one_node:update_source_ipThe infra/azure-multi-node Bicep template provisions four AlmaLinux 9 VMs
for a distributed ELK lab:
Azure subscription
|
v
+-----------------------+
| Resource group |
| rg-elk-lab |
+-----------+-----------+
|
v
+-----------------------+
| Virtual network |
| 10.43.0.0/16 |
+-----------+-----------+
|
v
+-----------------------+
| Subnet |
| 10.43.1.0/24 |
+-----------+-----------+
|
v
+-----------------------+
| Network security |
| group |
| SSH from LAPTOP_IP |
| ELK ports internal |
+-----------+-----------+
|
+---------------------+---------------------+
| | |
v v v
+----------------+ +----------------+ +----------------+
| Elasticsearch | | Logstash | | Kibana |
| 10.43.1.10 |<---| 10.43.1.11 | | 10.43.1.12 |
| data disk LUN0 | | Beats :5044 | | HTTP :5601 |
+----------------+ +----------------+ +----------+-----+
^
|
+-------+--------+
| Edge / Nginx |
| 10.43.1.13 |
| HTTP :80 |
+----------------+
Each VM gets its own public IP for SSH during lab work, with SSH and exposed lab
ports restricted to LAPTOP_IP. The NSG also allows the ELK ports within the
lab subnet so the nodes can communicate over their private addresses.
The multi-node Puppet role uses the elk_lab_role external fact written by
cloud-init to choose the correct node profile. Elasticsearch, Logstash, Kibana,
and Edge/Nginx nodes use role-specific Hiera data under
spec/fixtures/hieradata/roles.
Export the multi-node SSH key input, then validate, deploy, and inspect the topology:
export LAPTOP_IP=<your-public-ipv4>
export AZURE_MULTI_NODE_ADMIN_SSH_PUBLIC_KEY="$(< ~/.ssh/id_ed25519.pub)"
bundle exec rake azure:multi_node:deploy
bundle exec rake azure:multi_node:outputsInstall Ruby dependencies:
bundle installRun static checks and catalog tests:
bundle exec rake testThe generated Puppet Strings reference lives in docs/INFO.md. Regenerate it
after changing Puppet classes, then let the normal lint task verify it is
current:
bundle exec rake docs_generate
bundle exec rake lintRun the Azure static checks. These lint the Bicep template, validate the cloud-init schema, compile the Bicep template and parameter file, and run RSpec assertions against the compiled ARM JSON:
export LAPTOP_IP=<your-public-ipv4>
export AZURE_ONE_NODE_ADMIN_SSH_PUBLIC_KEY="$(< ~/.ssh/id_ed25519.pub)"
bundle exec rake azure:one_node:staticThe Azure static check is equivalent to running these individual checks:
bundle exec rake azure:one_node:lint
bundle exec rake azure:one_node:cloud_init_schema
bundle exec rake azure:one_node:assert_compiledRun the multi-node Azure static checks:
export LAPTOP_IP=<your-public-ipv4>
export AZURE_MULTI_NODE_ADMIN_SSH_PUBLIC_KEY="$(< ~/.ssh/id_ed25519.pub)"
bundle exec rake azure:multi_node:staticRun acceptance tests on fresh Azure infrastructure and clean up afterwards:
export LAPTOP_IP="$(curl -4 -s https://ifconfig.me)"
export AZURE_ONE_NODE_ADMIN_SSH_PUBLIC_KEY="$(< ~/.ssh/id_ed25519.pub)"
bundle exec rake azure:one_node:acceptance_ephemeralThat task creates the resource group, validates and deploys the one-node topology, runs the acceptance tests, then deletes the resource group and waits until Azure reports it has gone.
To run the acceptance tests against an existing Azure VM created by the Bicep deployment:
bundle exec rake azure:one_node:acceptanceThat task writes spec/fixtures/litmus_inventory.yaml from the Azure resources,
checks that LAPTOP_IP still matches the Azure SSH allow-list,
checks SSH connectivity, installs the Puppet 8 agent with Litmus, stages the
control repo fixtures on the VM, applies role::elk_stack, and runs
spec/acceptance/role_elk_stack_spec.rb. It then runs the shared pipeline
acceptance spec to prove a log event reaches Elasticsearch through Filebeat and
Logstash.
The acceptance flow can also be run in smaller steps:
bundle exec rake azure:one_node:inventory
bundle exec rake azure:one_node:source_ip
bundle exec rake azure:one_node:check_connectivity
bundle exec rake azure:one_node:install_agent
TARGET_HOST=<public-ip> bundle exec rspec spec/acceptance/role_elk_stack_spec.rb
LOG_SOURCE_TARGET=<public-ip> \
ELASTICSEARCH_TARGET=<public-ip> \
ELASTICSEARCH_URL=http://localhost:9200 \
bundle exec rake azure:pipeline_acceptanceRun multi-node acceptance tests on fresh Azure infrastructure and clean up afterwards:
export LAPTOP_IP="$(curl -4 -s https://ifconfig.me)"
export AZURE_MULTI_NODE_ADMIN_SSH_PUBLIC_KEY="$(< ~/.ssh/id_ed25519.pub)"
bundle exec rake azure:multi_node:acceptance_ephemeralTo run the multi-node acceptance tests against an existing deployment:
bundle exec rake azure:multi_node:acceptanceThat task writes a Litmus inventory with all four Azure VMs, checks that your
LAPTOP_IP still matches the Azure SSH allow-list, checks SSH
connectivity, installs the Puppet 8 agent on each VM, stages the control repo
fixtures, applies role::elk_multi_node, and runs
spec/acceptance/role_elk_multi_node_spec.rb. The acceptance spec checks the
role-specific services and cross-node configuration for Elasticsearch,
Logstash, Kibana, and Edge/Nginx. It then runs the shared pipeline acceptance
spec from the Edge VM to the Elasticsearch VM to prove a log event reaches
Elasticsearch through Filebeat and Logstash.
The multi-node acceptance flow can also be run in smaller steps. Unlike the
one-node spec, the multi-node spec must be run once per VM with both the role
and target host set. Use the public IPs from azure:multi_node:outputs when
filling in the commands below:
bundle exec rake azure:multi_node:outputs
bundle exec rake azure:multi_node:inventory
bundle exec rake azure:multi_node:source_ip
bundle exec rake azure:multi_node:check_connectivity
bundle exec rake azure:multi_node:install_agent
ELK_LAB_ROLE=elasticsearch \
TARGET_HOST=<elasticsearch-public-ip> \
bundle exec rspec spec/acceptance/role_elk_multi_node_spec.rb
ELK_LAB_ROLE=logstash \
TARGET_HOST=<logstash-public-ip> \
bundle exec rspec spec/acceptance/role_elk_multi_node_spec.rb
ELK_LAB_ROLE=kibana \
TARGET_HOST=<kibana-public-ip> \
bundle exec rspec spec/acceptance/role_elk_multi_node_spec.rb
ELK_LAB_ROLE=edge \
TARGET_HOST=<edge-public-ip> \
bundle exec rspec spec/acceptance/role_elk_multi_node_spec.rb
LOG_SOURCE_TARGET=<edge-public-ip> \
ELASTICSEARCH_TARGET=<elasticsearch-public-ip> \
ELASTICSEARCH_URL=http://localhost:9200 \
bundle exec rake azure:pipeline_acceptanceThe sample Hiera disables Elasticsearch security to keep the single-node lab easy to apply and test:
profile::elasticsearch::data_node::config:
'xpack.security.enabled': falseDo not use that setting for production. The lab narrows Azure NSG and host
iptables access to LAPTOP_IP, but Elasticsearch security is still disabled. A
production deployment should manage TLS, credentials, Kibana service
authentication, snapshots, and lifecycle policies explicitly.
MIT.