mirror of
https://github.com/KevinMidboe/playbooks-retailor.git
synced 2026-09-21 12:12:07 +00:00
update elasticsearch w/ ssl & xpack support
This commit is contained in:
@@ -1,12 +1,6 @@
|
|||||||
---
|
---
|
||||||
- name: Install ELK stack using Docker
|
- name: Install ELK stack using Docker
|
||||||
hosts: all
|
hosts: all
|
||||||
vars:
|
|
||||||
elk_version: "9.4.4"
|
|
||||||
roles:
|
roles:
|
||||||
# TODO check for docker install before purging it. This nukes containers
|
|
||||||
# we might not want destroyed.
|
|
||||||
# Leave a file, or read the same input requirements into a apt check
|
|
||||||
# which skips purge if satisfied.
|
|
||||||
- role: roles/docker
|
- role: roles/docker
|
||||||
- role: roles/elasticsearch
|
- role: roles/elasticsearch
|
||||||
|
|||||||
+237
-13
@@ -1,23 +1,247 @@
|
|||||||
# elasticsearch
|
# elk_stack
|
||||||
|
|
||||||
Play configures ELK stack using docker & is available without HTTPS. Configure container variables in `tasks/SERVICE.yml` files, environment variables for the services & Java can also be configured here.
|
Deploys Elasticsearch, Logstash and Kibana as a single Docker Compose stack
|
||||||
|
on a Debian host that already has Docker (and the `docker compose` plugin)
|
||||||
|
installed and running. All three services join a shared bridge network
|
||||||
|
(`elk-network` by default) so they can reach each other by container/service
|
||||||
|
name — no hardcoded IPs, no `--link`.
|
||||||
|
|
||||||
The following are manual steps required during setup.
|
## Requirements
|
||||||
|
|
||||||
## elastic
|
- Debian host with Docker Engine + Docker Compose plugin already installed
|
||||||
|
- Ansible collection: `community.docker` (`ansible-galaxy collection install community.docker`)
|
||||||
|
- Python `docker` + `requests` modules on the target (the role installs these via pip)
|
||||||
|
- Enough RAM for the JVM heaps you configure (Elasticsearch + Logstash default to ~1.5G combined; leave headroom for OS/Docker)
|
||||||
|
|
||||||
After creating elasticsearch container SSH into the running host and generate a new password for user `elastic` using command:
|
## What it does
|
||||||
|
|
||||||
```bash
|
1. Verifies Docker and Docker Compose are usable.
|
||||||
docker exec -it elasticsearch /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic
|
2. Creates `{{ elk_base_dir }}` (default `/opt/elk`) with a `config/` tree.
|
||||||
|
3. Templates:
|
||||||
|
- `config/elasticsearch/elasticsearch.yml`
|
||||||
|
- `config/kibana/kibana.yml`
|
||||||
|
- `config/logstash/logstash.yml`, `pipelines.yml`, and three pipeline
|
||||||
|
configs: `pipeline/main.conf` (generic beats/tcp/udp), `pipeline/pipeline-nginx.conf`
|
||||||
|
(nginx access logs), `pipeline/pipeline-laravel.conf` (Laravel logs)
|
||||||
|
- `docker-compose.yml` — one file, three services, one shared network, a named volume for ES data
|
||||||
|
4. Brings the stack up with `docker compose up`.
|
||||||
|
5. Waits for Elasticsearch to respond, then (if security is enabled) sets the
|
||||||
|
`kibana_system` and `logstash_system` built-in user passwords via the
|
||||||
|
Elasticsearch security API, and recreates Kibana/Logstash so they pick up
|
||||||
|
working credentials.
|
||||||
|
6. Waits for Kibana's `/api/status` to come back healthy.
|
||||||
|
|
||||||
|
## Networking
|
||||||
|
|
||||||
|
All three containers run on the `elk_network_name` bridge network
|
||||||
|
(default `elk-network`, subnet `172.28.0.0/24`). Inside that network, all
|
||||||
|
inter-service traffic uses `http://` or `https://` consistently based on
|
||||||
|
`elk_tls_enabled` (default stack, TLS on):
|
||||||
|
|
||||||
|
- Elasticsearch is reachable at `https://elasticsearch:9200`
|
||||||
|
- Kibana is configured with `elasticsearch.hosts: ["https://elasticsearch:9200"]`
|
||||||
|
- Logstash's pipelines all point at `https://elasticsearch:9200`
|
||||||
|
|
||||||
|
With `elk_tls_enabled: false`, all three of the above use `http://` instead.
|
||||||
|
|
||||||
|
Host ports (`elk_elasticsearch_http_port`, `elk_kibana_http_port`,
|
||||||
|
`elk_logstash_beats_port`, `elk_logstash_nginx_beats_port`,
|
||||||
|
`elk_logstash_laravel_beats_port`, etc.) are only for reaching the stack
|
||||||
|
*from outside* — the containers themselves never use `localhost` to talk to
|
||||||
|
each other.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
`elk_security_enabled: true` by default (recommended). This turns on
|
||||||
|
Elastic's built-in security (basic auth, no TLS between containers since
|
||||||
|
traffic stays on the internal Docker network). Change these before running
|
||||||
|
in anything but a scratch environment — ideally via Ansible Vault:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
elk_elastic_password: "..."
|
||||||
|
elk_kibana_system_password: "..."
|
||||||
|
elk_logstash_system_password: "..."
|
||||||
|
elk_kibana_encryption_key: "..." # 32+ random chars, e.g. `openssl rand -hex 32`
|
||||||
```
|
```
|
||||||
|
|
||||||
## kibana
|
Set `elk_security_enabled: false` for a quick, unauthenticated local/dev
|
||||||
|
stack (no passwords) — do not do this on anything reachable from untrusted
|
||||||
|
networks.
|
||||||
|
|
||||||
Create a password for `kibana_system` user:
|
**Note:** `elk_tls_enabled` and `elk_security_enabled` are independent
|
||||||
|
switches you can set separately, but Elasticsearch only applies TLS
|
||||||
|
(`xpack.security.http.ssl.*`) when X-Pack security itself is on. So if
|
||||||
|
`elk_tls_enabled: true` and `elk_security_enabled: false`, this role
|
||||||
|
force-enables security anyway purely to make TLS possible — `elastic`,
|
||||||
|
`kibana_system`, and `logstash_system` all get real passwords from
|
||||||
|
`elk_elastic_password`/`elk_kibana_system_password`/
|
||||||
|
`elk_logstash_system_password` in that mode too, exactly as if
|
||||||
|
`elk_security_enabled: true` had been set, so change those from their
|
||||||
|
placeholder defaults the same way you would for a normal secured stack.
|
||||||
|
Setting both `elk_tls_enabled: false` and `elk_security_enabled: false` is
|
||||||
|
the only way to get a fully open, unauthenticated, plain-HTTP stack.
|
||||||
|
|
||||||
```bash
|
## Example playbook
|
||||||
export ELASTIC_PASSWORD=
|
|
||||||
export KIBANA_PASSWORD=
|
```yaml
|
||||||
curl -s -X POST -u "elastic:${ELASTIC_PASSWORD}" -H "Content-Type: application/json" http://elasticsearch:9200/_security/user/kibana_system/_password -d "{\"password\":\"${KIBANA_PASSWORD}\"}" | grep -q "^{}";
|
- hosts: elk_servers
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- role: elk_stack
|
||||||
|
vars:
|
||||||
|
elk_version: "8.15.3"
|
||||||
|
elk_elasticsearch_heap_size: "2g"
|
||||||
|
elk_logstash_heap_size: "1g"
|
||||||
|
elk_elastic_password: "{{ vault_elk_elastic_password }}"
|
||||||
|
elk_kibana_system_password: "{{ vault_elk_kibana_password }}"
|
||||||
|
elk_logstash_system_password: "{{ vault_elk_logstash_password }}"
|
||||||
|
elk_kibana_encryption_key: "{{ vault_elk_kibana_enc_key }}"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Customizing the Logstash pipeline
|
||||||
|
|
||||||
|
Three pipelines run side by side inside the same Logstash process, each on
|
||||||
|
its own beats port (they can't share a port):
|
||||||
|
|
||||||
|
| Pipeline | Template | Beats port var | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `main` | `main.conf.j2` | `elk_logstash_beats_port` (5044) | Generic beats input, plus raw JSON over TCP/UDP (5000) |
|
||||||
|
| `nginx_pipeline` | `pipeline-nginx.conf.j2` | `elk_logstash_nginx_beats_port` (5045) | nginx access log parsing (grok + geoip) |
|
||||||
|
| `laravel_pipeline` | `pipeline-laravel.conf.j2` | `elk_logstash_laravel_beats_port` (5046) | Laravel log ingestion |
|
||||||
|
|
||||||
|
All three ship to Elasticsearch using the same `elk_tls_enabled`/
|
||||||
|
`elk_security_enabled` logic as the rest of the stack (TLS scheme + the
|
||||||
|
`logstash_system` credentials, when applicable). To add your own
|
||||||
|
filters/inputs/outputs, either edit these templates directly, or drop an
|
||||||
|
additional `*.conf.j2` file into `templates/` and extend `pipelines.yml.j2`
|
||||||
|
(with a new `pipeline.id`/`path.config` entry and a beats port that doesn't
|
||||||
|
collide with the ones above) and `tasks/main.yml` (a matching `template:`
|
||||||
|
task) to deploy it.
|
||||||
|
|
||||||
|
## Idempotency notes
|
||||||
|
|
||||||
|
- Re-running the role only recreates containers whose config actually
|
||||||
|
changed (via handlers), plus a compose reconciliation pass.
|
||||||
|
- Setting the `kibana_system`/`logstash_system` passwords is safe to repeat;
|
||||||
|
Elasticsearch just resets them to the same value each time and the role
|
||||||
|
only recreates those two containers when the API call reports a change.
|
||||||
|
|
||||||
|
## TLS (public certs for a private-IP node)
|
||||||
|
|
||||||
|
`elk_tls_enabled: true` (default) gets Elasticsearch, Kibana, and Logstash a
|
||||||
|
**real, publicly-trusted certificate** for a DNS name that happens to point
|
||||||
|
at a private IP (e.g. `logs-eu.domain.com -> 10.0.99.5`), using the ACME
|
||||||
|
**DNS-01** challenge against Cloudflare. This works even though the host
|
||||||
|
itself is unreachable from the internet, because DNS-01 only requires the
|
||||||
|
ability to create a `_acme-challenge` TXT record — no inbound HTTP/HTTPS
|
||||||
|
access to the node is needed at all.
|
||||||
|
|
||||||
|
### Why DNS-01, and why one shared cert
|
||||||
|
|
||||||
|
- Public CAs don't issue certificates for bare IP SANs (especially not
|
||||||
|
RFC1918/private addresses) — the workaround is a cert for a **DNS name**
|
||||||
|
that resolves to the private IP, with clients connecting via that name.
|
||||||
|
- Since all three services live on the same node behind the same DNS name,
|
||||||
|
they share **one cert** rather than one per service. Elastic explicitly
|
||||||
|
supports (and this is a common pattern) using [a single shared HTTP CA
|
||||||
|
across Elasticsearch, Kibana, and other stack components](https://www.elastic.co/docs/deploy-manage/deploy/self-managed/tutorial-self-managed-secure)
|
||||||
|
— separate CAs/certs per component exist to isolate trust domains, which
|
||||||
|
isn't a goal here since everything is already inside the same trust
|
||||||
|
boundary (one box, one operator, one Docker network).
|
||||||
|
- Because the cert comes from a **public** CA (Let's Encrypt, via lego's
|
||||||
|
default ACME server), Kibana and Logstash don't need a custom CA
|
||||||
|
certificate distributed to them to trust Elasticsearch's cert — their
|
||||||
|
default trust stores already trust Let's Encrypt's root. This removes an
|
||||||
|
entire category of "distribute the CA cert everywhere" complexity that
|
||||||
|
self-signed/internal-CA setups require.
|
||||||
|
|
||||||
|
### What this role does
|
||||||
|
|
||||||
|
1. Expects `elk_cloudflare_token_env_file` (default
|
||||||
|
`/etc/lego/cloudflare-token.env`) to **already exist** on the box,
|
||||||
|
containing `CF_DNS_API_TOKEN=...`, root-owned, mode `0600`. This role
|
||||||
|
only *reads* that file — it never receives, logs, or stores the token
|
||||||
|
itself. See "IaC responsibilities" below for where this file comes from.
|
||||||
|
2. Installs the [`lego`](https://github.com/go-acme/lego) ACME client (a
|
||||||
|
single static Go binary — no Python/snap dependencies, easy to pin an
|
||||||
|
exact version).
|
||||||
|
3. On first run, issues a certificate for `elk_tls_domain` via `lego ... run`,
|
||||||
|
landing at `{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.{crt,key,issuer.crt}`.
|
||||||
|
4. Mounts that directory read-only into all three containers and wires
|
||||||
|
`xpack.security.http.ssl.*` (Elasticsearch), `server.ssl.*` (Kibana), and
|
||||||
|
`ssl_enabled` (Logstash's elasticsearch output) to point at it.
|
||||||
|
5. Installs a `lego-renew.service` + `lego-renew.timer` systemd pair that
|
||||||
|
runs daily, checks expiry, and **only** re-issues (and only then restarts
|
||||||
|
the three containers, via lego's `--run-hook`) when the cert is within
|
||||||
|
`elk_tls_renew_days` of expiring. No-op checks never restart anything.
|
||||||
|
|
||||||
|
### IaC responsibilities (Pulumi, not this role)
|
||||||
|
|
||||||
|
This role deliberately does **not** manage Cloudflare DNS records, zones, or
|
||||||
|
API tokens — that's Pulumi's job, since it already holds the Cloudflare
|
||||||
|
credentials and provisions the box. The pseudocode below sketches what
|
||||||
|
Pulumi needs to set up per node:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Pulumi pseudocode - see conversation history / infra repo for the real version
|
||||||
|
apex_zone = cloudflare.get_zone(name="domain.com")
|
||||||
|
|
||||||
|
# A record already exists in this setup: logs-eu.domain.com -> 10.0.99.x
|
||||||
|
# (proxied=False - a private IP can't sit behind Cloudflare's proxy/edge)
|
||||||
|
|
||||||
|
# Scoped API token: Zone.DNS.Edit, restricted to the apex zone's ID (or a
|
||||||
|
# delegated child zone, e.g. logs-eu.domain.com as its own zone, for harder
|
||||||
|
# isolation). This is the tightest scope Cloudflare's token model supports -
|
||||||
|
# there's no way to restrict a token to a single record name/pattern.
|
||||||
|
acme_dns_token = cloudflare.ApiToken(
|
||||||
|
"logs-eu-acme-dns-token",
|
||||||
|
policies=[{
|
||||||
|
"permission_groups": [dns_edit_permission_group],
|
||||||
|
"resources": {f"com.cloudflare.api.account.zone.{apex_zone.id}": "*"},
|
||||||
|
}],
|
||||||
|
# optional: condition.request_ip.in_ to pin the token to the node's
|
||||||
|
# egress IP, and/or expires_on for periodic rotation via Pulumi re-runs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hand the token to the VM via cloud-init, written root-only - Pulumi never
|
||||||
|
# passes this to Ansible as a variable.
|
||||||
|
cloud_init_write_files = [{
|
||||||
|
"path": "/etc/lego/cloudflare-token.env",
|
||||||
|
"owner": "root:root",
|
||||||
|
"permissions": "0600",
|
||||||
|
"content": f"CF_DNS_API_TOKEN={acme_dns_token.value}\n",
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
For stronger isolation than a single shared zone token, NS-delegate
|
||||||
|
`logs-eu.domain.com` (and `logs-us.domain.com`, etc.) to their own
|
||||||
|
Cloudflare zones, and scope each region's token to only its own zone ID —
|
||||||
|
that way a compromised `logs-eu` node's token can't touch `logs-us` or the
|
||||||
|
apex domain's records at all.
|
||||||
|
|
||||||
|
### Key variables
|
||||||
|
|
||||||
|
| Variable | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `elk_tls_enabled` | Master switch; `false` reverts to plain HTTP |
|
||||||
|
| `elk_tls_domain` | The DNS name (e.g. `logs-eu.domain.com`) the cert covers and clients must connect via |
|
||||||
|
| `elk_tls_acme_email` | Contact address registered with the ACME account |
|
||||||
|
| `elk_tls_acme_server` | ACME endpoint — point at Let's Encrypt **staging** while testing to avoid production rate limits |
|
||||||
|
| `elk_cloudflare_token_env_file` | Path to the pre-existing, cloud-init-provisioned token file |
|
||||||
|
| `elk_tls_renew_days` | Renew when fewer than this many days remain before expiry |
|
||||||
|
| `elk_lego_version` | Pinned lego release to install |
|
||||||
|
|
||||||
|
### Operational notes
|
||||||
|
|
||||||
|
- **Testing**: set `elk_tls_acme_server` to
|
||||||
|
`https://acme-staging-v02.api.letsencrypt.org/directory` first — staging
|
||||||
|
certs aren't publicly trusted but validate the whole DNS-01/Cloudflare
|
||||||
|
flow without touching Let's Encrypt's production rate limits.
|
||||||
|
- **Downtime on renewal**: renewal restarts all three containers via
|
||||||
|
`docker compose restart`, which is brief (~10-20s) but not zero-downtime.
|
||||||
|
Acceptable for a single-node stack; a zero-downtime rolling reload would
|
||||||
|
require a multi-node Elasticsearch cluster, which is out of scope here.
|
||||||
|
- **Clients must use the DNS name, not the raw IP** — connecting to
|
||||||
|
`https://10.0.99.5:9200` directly will fail certificate hostname
|
||||||
|
verification even though the cert is otherwise valid, since the cert's
|
||||||
|
SAN is `logs-eu.domain.com`, not the IP.
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Environment variable overrides
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Every setting below can also be supplied as an env var on the CONTROL node
|
||||||
|
# (i.e. wherever `ansible-playbook` runs - a CI job, your laptop, etc.), not
|
||||||
|
# the remote target host. `lookup('env', ...)` reads from the control node's
|
||||||
|
# process environment at template-render time, which is exactly what you
|
||||||
|
# want for injecting CI/CD secrets (e.g. GitHub Actions `env:`/`secrets:`,
|
||||||
|
# a `.env` file sourced before the playbook runs) without touching inventory
|
||||||
|
# or vault files.
|
||||||
|
#
|
||||||
|
# Precedence, highest first: `-e` / extra vars on the CLI > host/group_vars
|
||||||
|
# in inventory > playbook `vars:` > these defaults (env var, if set, else
|
||||||
|
# the literal fallback shown). So an env var set here is easy to override
|
||||||
|
# per-host or per-run if needed - it's just the last thing consulted.
|
||||||
|
#
|
||||||
|
# Booleans: env vars are always strings, so `elk_tls_enabled`/
|
||||||
|
# `elk_security_enabled` run through `| bool` to turn "true"/"false"/"1"/"0"/
|
||||||
|
# "yes"/"no" (case-insensitive) into real Ansible booleans - a raw string
|
||||||
|
# "false" would otherwise be truthy in Jinja `{% if %}` conditionals.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# General
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
elk_version: "{{ lookup('env', 'ELK_VERSION') | default('8.15.3', true) }}"
|
||||||
|
|
||||||
|
elk_base_dir: /opt/elk
|
||||||
|
elk_compose_project_name: elk
|
||||||
|
|
||||||
|
elk_network_name: elk-network
|
||||||
|
elk_network_subnet: "172.28.0.0/24"
|
||||||
|
|
||||||
|
# Set true only after you've configured elk_elastic_password etc. for real use.
|
||||||
|
# When false, security is disabled for a quick local/dev stack.
|
||||||
|
elk_security_enabled: "{{ lookup('env', 'ELK_SECURITY_ENABLED') | default('true', true) | bool }}"
|
||||||
|
|
||||||
|
# Change these in production (e.g. via vault, or ELK_ELASTIC_PASSWORD etc.
|
||||||
|
# from CI secrets). These fallbacks are intentionally obvious placeholders
|
||||||
|
# so nobody ships them by accident.
|
||||||
|
elk_elastic_password: "{{ lookup('env', 'ELK_ELASTIC_PASSWORD') | default('ChangeMe_Elastic_123!', true) }}"
|
||||||
|
elk_kibana_system_password: "{{ lookup('env', 'ELK_KIBANA_SYSTEM_PASSWORD') | default('ChangeMe_Kibana_123!', true) }}"
|
||||||
|
elk_logstash_system_password: "{{ lookup('env', 'ELK_LOGSTASH_SYSTEM_PASSWORD') | default('ChangeMe_Logstash_123!', true) }}"
|
||||||
|
|
||||||
|
# A pre-shared 32+ char key used to encrypt the Kibana saved-objects store.
|
||||||
|
elk_kibana_encryption_key: "{{ lookup('env', 'ELK_KIBANA_ENCRYPTION_KEY') | default('ChangeMe_32Char_Kibana_EncKey!!!', true) }}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Elasticsearch
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
elk_elasticsearch_container_name: elasticsearch
|
||||||
|
elk_elasticsearch_http_port: 9200
|
||||||
|
elk_elasticsearch_transport_port: 9300
|
||||||
|
elk_elasticsearch_heap_size: "1g"
|
||||||
|
elk_elasticsearch_cluster_name: "elk-retailor"
|
||||||
|
elk_elasticsearch_node_name: "{{ lookup('env', 'ELK_ELASTIC_NODE_NAME') | default('es01', true) }}"
|
||||||
|
elk_elasticsearch_data_volume: elk_esdata
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Kibana
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
elk_kibana_container_name: kibana
|
||||||
|
elk_kibana_http_port: 5601
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Logstash
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
elk_logstash_container_name: logstash
|
||||||
|
elk_logstash_heap_size: "512m"
|
||||||
|
elk_logstash_tcp_port: 5000
|
||||||
|
elk_logstash_udp_port: 5000
|
||||||
|
elk_logstash_http_port: 9600
|
||||||
|
|
||||||
|
# Each pipeline that has its own beats input needs its own port - they all
|
||||||
|
# run inside the same Logstash process, so two pipelines can't bind the same
|
||||||
|
# port. main.conf (generic beats/tcp/udp) and the nginx/laravel example
|
||||||
|
# pipelines are each wired to a distinct one below.
|
||||||
|
elk_logstash_beats_port: 5044
|
||||||
|
elk_logstash_nginx_beats_port: 5045
|
||||||
|
elk_logstash_laravel_beats_port: 5046
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Docker image repository (override for an internal mirror/proxy)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
elk_docker_image_repo: "docker.elastic.co"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TLS via lego (ACME DNS-01 against Cloudflare)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Master switch. When false, the stack runs plain HTTP as before (nothing
|
||||||
|
# below this point applies).
|
||||||
|
elk_tls_enabled: "{{ lookup('env', 'ELK_TLS_ENABLED') | default('true', true) | bool }}"
|
||||||
|
|
||||||
|
# The public DNS name this node is reachable at, e.g. "logs.domain.com".
|
||||||
|
# An A record for this name -> the node's private IP is assumed to already
|
||||||
|
# exist (managed by IaC, not this role). This name is what ES/Kibana/Logstash
|
||||||
|
# put in their certs and what clients must connect to (not the raw IP) for
|
||||||
|
# TLS verification to succeed.
|
||||||
|
elk_tls_domain: "{{ lookup('env', 'ELK_TLS_DOMAIN') | default('logs.domain.com', true) }}"
|
||||||
|
|
||||||
|
# Contact email lego registers with the ACME account (Let's Encrypt notifies
|
||||||
|
# this address about certificate/account issues, not renewal reminders,
|
||||||
|
# since renewal is automated).
|
||||||
|
elk_tls_acme_email: "{{ lookup('env', 'ELK_TLS_ACME_EMAIL') | default('webmaster@domain.com', true) }}"
|
||||||
|
|
||||||
|
# Let's Encrypt production endpoint by default. Point this at the staging
|
||||||
|
# endpoint while testing to avoid production rate limits:
|
||||||
|
# https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
elk_tls_acme_server: "{{ lookup('env', 'ELK_TLS_ACME_SERVER') | default('https://acme-v02.api.letsencrypt.org/directory', true) }}"
|
||||||
|
|
||||||
|
# lego binary version/arch and install location.
|
||||||
|
elk_lego_version: "4.20.4"
|
||||||
|
elk_lego_arch: "amd64" # amd64 | arm64
|
||||||
|
elk_lego_install_dir: /usr/local/bin
|
||||||
|
elk_lego_path: /etc/lego # lego's --path (accounts + certificates live under here)
|
||||||
|
elk_lego_certs_dir: "{{ elk_lego_path }}/certificates"
|
||||||
|
|
||||||
|
# Path to the Cloudflare token env file. This is expected to already exist,
|
||||||
|
# dropped by cloud-init at provisioning time (see IaC notes) as
|
||||||
|
# root:root 0600, containing a line like:
|
||||||
|
# CF_DNS_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
# This role only READS this file - it never receives or stores the token
|
||||||
|
# itself, so the Cloudflare credential never touches Ansible inventory,
|
||||||
|
# vault, logs, or the control node.
|
||||||
|
elk_cloudflare_token_env_file: "{{ lookup('env', 'ELK_CLOUDFLARE_TOKEN_ENV_FILE') | default('/etc/lego/cloudflare-token.env', true) }}"
|
||||||
|
|
||||||
|
# Threshold used on renewal checks - lego renews if the current cert has
|
||||||
|
# fewer than this many days left before expiry.
|
||||||
|
elk_tls_renew_days: 30
|
||||||
|
|
||||||
|
# Renewal check cadence via systemd timer.
|
||||||
|
elk_tls_renew_on_calendar: "22:00:00"
|
||||||
|
elk_tls_renew_randomized_delay: "30m"
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
- name: restart elk stack
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ elk_base_dir }}"
|
||||||
|
project_name: "{{ elk_compose_project_name }}"
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
listen: "restart elk stack"
|
||||||
|
|
||||||
|
- name: recreate elasticsearch
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ elk_base_dir }}"
|
||||||
|
project_name: "{{ elk_compose_project_name }}"
|
||||||
|
services:
|
||||||
|
- elasticsearch
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
listen: "recreate elasticsearch"
|
||||||
|
|
||||||
|
- name: recreate logstash
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ elk_base_dir }}"
|
||||||
|
project_name: "{{ elk_compose_project_name }}"
|
||||||
|
services:
|
||||||
|
- logstash
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
listen: "recreate logstash"
|
||||||
|
|
||||||
|
- name: recreate kibana
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ elk_base_dir }}"
|
||||||
|
project_name: "{{ elk_compose_project_name }}"
|
||||||
|
services:
|
||||||
|
- kibana
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
listen: "recreate kibana"
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
---
|
---
|
||||||
galaxy_info:
|
galaxy_info:
|
||||||
author: Your Name
|
role_name: elk_stack
|
||||||
description: Ansible role to deploy prometheus & grafana using Docker
|
author: your-name
|
||||||
license: MIT
|
description: Deploys Elasticsearch, Logstash and Kibana via Docker Compose on a shared network
|
||||||
min_ansible_version: "2.9"
|
min_ansible_version: "2.14"
|
||||||
platforms:
|
platforms:
|
||||||
- name: Debian
|
- name: Debian
|
||||||
versions:
|
versions:
|
||||||
- all
|
- bullseye
|
||||||
galaxy_tags:
|
- bookworm
|
||||||
- monitoring
|
|
||||||
- cadvisor
|
|
||||||
dependencies:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
|
dependencies: []
|
||||||
|
|
||||||
|
collections:
|
||||||
|
- community.docker
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Pull Elasticsearch Docker image
|
|
||||||
docker_image:
|
|
||||||
name: docker.elastic.co/elasticsearch/elasticsearch-wolfi:{{ elk_version }}
|
|
||||||
source: pull
|
|
||||||
|
|
||||||
- name: Create Elasticsearch configuration file directory on host
|
|
||||||
file:
|
|
||||||
path: /etc/elasticsearch
|
|
||||||
state: directory
|
|
||||||
mode: '0755'
|
|
||||||
|
|
||||||
# - name: Create Elasticsearch configuration file
|
|
||||||
# template:
|
|
||||||
# src: elasticsearch.yml.j2
|
|
||||||
# dest: /etc/elasticsearch/elasticsearch.yml
|
|
||||||
|
|
||||||
- name: Start Elasticsearch container
|
|
||||||
docker_container:
|
|
||||||
name: elasticsearch
|
|
||||||
image: docker.elastic.co/elasticsearch/elasticsearch-wolfi:{{ elk_version }}
|
|
||||||
state: started
|
|
||||||
restart: yes
|
|
||||||
restart_policy: unless-stopped
|
|
||||||
published_ports:
|
|
||||||
- "9200:9200"
|
|
||||||
volumes:
|
|
||||||
- /etc/elasticsearch/esdata:/usr/share/elasticsearch/data
|
|
||||||
networks:
|
|
||||||
- name: elk_network
|
|
||||||
ipv4_address: 172.19.0.2
|
|
||||||
env:
|
|
||||||
node.name: elasticsearch
|
|
||||||
cluster.name: retailor-elk
|
|
||||||
discovery.type: single-node
|
|
||||||
bootstrap.memory_lock: "true"
|
|
||||||
# limits elasticsearch to 2 GB of RAM
|
|
||||||
ES_JAVA_OPTS: "-Xms1g -Xmx2g"
|
|
||||||
# disables SSL & xpack security
|
|
||||||
xpack.security.http.ssl.enabled: "false"
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create a Docker network for Elasticsearch
|
|
||||||
docker_network:
|
|
||||||
name: elk_network
|
|
||||||
state: present
|
|
||||||
driver: bridge
|
|
||||||
ipam_config:
|
|
||||||
- subnet: "172.19.0.0/16"
|
|
||||||
gateway: "172.19.0.1"
|
|
||||||
iprange: "172.19.0.0/24"
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create a Docker network for Kibana
|
|
||||||
docker_network:
|
|
||||||
name: elk_network
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create kibana directory on host
|
|
||||||
file:
|
|
||||||
path: /etc/kibana
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Pull Kibana Docker image
|
|
||||||
docker_image:
|
|
||||||
name: docker.elastic.co/kibana/kibana:{{ elk_version }}
|
|
||||||
source: pull
|
|
||||||
|
|
||||||
# TODO rember to move CA cert from elastic to Kibana
|
|
||||||
# docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt .
|
|
||||||
# docker cp http_ca.crt kibana:/usr/share/kibana/config/certs/ca/http_ca.crt
|
|
||||||
- name: Start Kibana container
|
|
||||||
docker_container:
|
|
||||||
name: kibana
|
|
||||||
image: docker.elastic.co/kibana/kibana:{{ elk_version }}
|
|
||||||
state: started
|
|
||||||
restart: yes
|
|
||||||
restart_policy: unless-stopped
|
|
||||||
published_ports:
|
|
||||||
- "5601:5601"
|
|
||||||
env:
|
|
||||||
ELASTICSEARCH_HOSTS: "{{ env_vars.ELASTIC_HOSTS }}"
|
|
||||||
ELASTICSEARCH_USERNAME: kibana_system
|
|
||||||
ELASTICSEARCH_PASSWORD: "{{ env_vars.KIBANA_PASSWORD }}"
|
|
||||||
TELEMETRY_ENABLED: "false"
|
|
||||||
networks:
|
|
||||||
- name: elk_network
|
|
||||||
ipv4_address: 172.19.0.3
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create a Docker network for Logstash
|
|
||||||
docker_network:
|
|
||||||
name: elk_network
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Create logstash directory on host
|
|
||||||
file:
|
|
||||||
path: /etc/logstash
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Copy logstash config
|
|
||||||
copy:
|
|
||||||
src: templates/pipelines.yml.j2
|
|
||||||
dest: /etc/logstash/pipelines.yml
|
|
||||||
|
|
||||||
- name: Create logstash directory on host
|
|
||||||
file:
|
|
||||||
path: /etc/logstash/pipeline
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Copy logstash input configs
|
|
||||||
copy:
|
|
||||||
src: "{{ item }}"
|
|
||||||
dest: /etc/logstash/pipeline/{{ item | basename | regex_replace('\.j2$', '') }}
|
|
||||||
loop: "{{ query('fileglob', 'templates/logstash-conf.d/*.j2') }}"
|
|
||||||
|
|
||||||
- name: Pull Logstash Docker image
|
|
||||||
docker_image:
|
|
||||||
name: docker.elastic.co/logstash/logstash:{{ elk_version }}
|
|
||||||
source: pull
|
|
||||||
|
|
||||||
# TODO rember to move CA cert from elastic to Logstash
|
|
||||||
# docker cp elasticsearch:/usr/share/elasticsearch/config/certs/http_ca.crt .
|
|
||||||
# docker cp http_ca.crt logstash:/usr/share/logstash/config/certs/ca/http_ca.crt
|
|
||||||
- name: Start Logstash container
|
|
||||||
docker_container:
|
|
||||||
name: logstash
|
|
||||||
image: docker.elastic.co/logstash/logstash:{{ elk_version }}
|
|
||||||
state: started
|
|
||||||
restart: yes
|
|
||||||
restart_policy: unless-stopped
|
|
||||||
command:
|
|
||||||
- /bin/bash
|
|
||||||
- -c
|
|
||||||
- |
|
|
||||||
echo "Waiting for Elasticsearch availability";
|
|
||||||
until curl -s {{ env_vars.ELASTIC_HOSTS }} | grep -q "missing authentication credentials"; do sleep 1; done;
|
|
||||||
echo "Starting logstash";
|
|
||||||
/usr/share/logstash/bin/logstash
|
|
||||||
published_ports:
|
|
||||||
- "5044-5049:5044-5049"
|
|
||||||
volumes:
|
|
||||||
- /etc/logstash/pipelines.yml:/usr/share/logstash/config/pipelines.yml
|
|
||||||
- /etc/logstash/pipeline:/usr/share/logstash/pipeline
|
|
||||||
env:
|
|
||||||
xpack.monitoring.enabled: "false"
|
|
||||||
ELASTIC_USER: elastic
|
|
||||||
ELASTIC_PASSWORD: "{{ env_vars.ELASTIC_PASSWORD }}"
|
|
||||||
ELASTIC_HOSTS: "{{ env_vars.ELASTIC_HOSTS }}"
|
|
||||||
networks:
|
|
||||||
- name: elk_network
|
|
||||||
ipv4_address: 172.19.0.4
|
|
||||||
@@ -1,8 +1,213 @@
|
|||||||
---
|
---
|
||||||
# ensure we have variables from .env files
|
# ---------------------------------------------------------------------------
|
||||||
- include_tasks: ../roles/env/tasks/main.yml
|
# TLS (elk_tls_enabled) and X-Pack security (elk_security_enabled) are
|
||||||
|
# documented as independent switches, but Elasticsearch only honors
|
||||||
|
# xpack.security.http.ssl.* when xpack.security.enabled is also true. So
|
||||||
|
# whenever TLS is on, security (and therefore basic auth / ELASTIC_PASSWORD)
|
||||||
|
# is force-enabled too - elk_effective_security_enabled is the single fact
|
||||||
|
# every template and task below should use instead of elk_security_enabled
|
||||||
|
# directly, so this coupling only has to be expressed once.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Derive effective security state (TLS implies security)
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
elk_effective_security_enabled: "{{ elk_security_enabled or elk_tls_enabled }}"
|
||||||
|
|
||||||
- include_tasks: elk-network.yml
|
- name: Assert Docker is available
|
||||||
- include_tasks: elasticsearch.yml
|
ansible.builtin.command: docker info
|
||||||
- include_tasks: kibana.yml
|
register: elk_docker_info
|
||||||
- include_tasks: logstash.yml
|
changed_when: false
|
||||||
|
failed_when: elk_docker_info.rc != 0
|
||||||
|
|
||||||
|
- name: Ensure Docker Compose plugin is installed
|
||||||
|
ansible.builtin.command: docker compose version
|
||||||
|
register: elk_compose_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: elk_compose_check.rc != 0
|
||||||
|
|
||||||
|
- name: Ensure we have pip installed
|
||||||
|
apt:
|
||||||
|
pkg: "python3-pip"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Ensure python docker SDK is present for community.docker modules
|
||||||
|
apt:
|
||||||
|
pkg:
|
||||||
|
- python3-docker
|
||||||
|
- python3-requests
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create base ELK directory structure
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
loop:
|
||||||
|
- "{{ elk_base_dir }}"
|
||||||
|
- "{{ elk_base_dir }}/config"
|
||||||
|
- "{{ elk_base_dir }}/config/elasticsearch"
|
||||||
|
- "{{ elk_base_dir }}/config/kibana"
|
||||||
|
- "{{ elk_base_dir }}/config/logstash"
|
||||||
|
- "{{ elk_base_dir }}/config/logstash/pipeline"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TLS (must happen before compose/config templating below, since those
|
||||||
|
# templates reference the cert file paths lego produces)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Issue/renew TLS certificate and set up renewal automation
|
||||||
|
ansible.builtin.include_tasks: tls.yml
|
||||||
|
when: elk_tls_enabled
|
||||||
|
|
||||||
|
- name: Set connection facts used by health checks below
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
elk_scheme: "{{ 'https' if elk_tls_enabled else 'http' }}"
|
||||||
|
elk_validate_certs: "{{ not elk_tls_enabled }}"
|
||||||
|
elk_elasticsearch_client_host: "{{ elk_tls_domain if elk_tls_enabled else elk_elasticsearch_container_name }}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configs
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Template elasticsearch.yml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: elasticsearch.yml.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/elasticsearch/elasticsearch.yml"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate elasticsearch"
|
||||||
|
|
||||||
|
- name: Template kibana.yml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: kibana.yml.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/kibana/kibana.yml"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate kibana"
|
||||||
|
|
||||||
|
- name: Template logstash.yml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: logstash.yml.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/logstash/logstash.yml"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate logstash"
|
||||||
|
|
||||||
|
- name: Template logstash pipelines.yml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: pipelines.yml.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/logstash/pipelines.yml"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate logstash"
|
||||||
|
|
||||||
|
- name: Template logstash main pipeline
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: main.conf.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/logstash/pipeline/main.conf"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate logstash"
|
||||||
|
|
||||||
|
- name: Template logstash nginx pipeline
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: pipeline-nginx.conf.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/logstash/pipeline/pipeline-nginx.conf"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate logstash"
|
||||||
|
|
||||||
|
- name: Template logstash laravel pipeline
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: pipeline-laravel.conf.j2
|
||||||
|
dest: "{{ elk_base_dir }}/config/logstash/pipeline/pipeline-laravel.conf"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "recreate logstash"
|
||||||
|
|
||||||
|
- name: Template docker-compose.yml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: docker-compose.yml.j2
|
||||||
|
dest: "{{ elk_base_dir }}/docker-compose.yml"
|
||||||
|
mode: "0644"
|
||||||
|
notify: "restart elk stack"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bring up the stack
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Flush handlers so config changes apply before we continue
|
||||||
|
ansible.builtin.meta: flush_handlers
|
||||||
|
|
||||||
|
- name: Start ELK stack via docker compose
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ elk_base_dir }}"
|
||||||
|
project_name: "{{ elk_compose_project_name }}"
|
||||||
|
state: present
|
||||||
|
register: elk_compose_result
|
||||||
|
|
||||||
|
- name: Wait for Elasticsearch to answer on its HTTP port
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ elk_scheme }}://localhost:{{ elk_elasticsearch_http_port }}"
|
||||||
|
validate_certs: "{{ elk_validate_certs }}"
|
||||||
|
status_code:
|
||||||
|
- 200
|
||||||
|
- 401 # 401 is expected once security is enabled but before we authenticate
|
||||||
|
register: elk_es_wait
|
||||||
|
until: elk_es_wait.status in [200, 401]
|
||||||
|
retries: 30
|
||||||
|
delay: 10
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Security bootstrap: set built-in user passwords (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Set kibana_system password
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ elk_scheme }}://localhost:{{ elk_elasticsearch_http_port }}/_security/user/kibana_system/_password"
|
||||||
|
validate_certs: "{{ elk_validate_certs }}"
|
||||||
|
method: POST
|
||||||
|
user: elastic
|
||||||
|
password: "{{ elk_elastic_password }}"
|
||||||
|
force_basic_auth: true
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
password: "{{ elk_kibana_system_password }}"
|
||||||
|
status_code: 200
|
||||||
|
when: elk_effective_security_enabled
|
||||||
|
register: elk_set_kibana_pw
|
||||||
|
retries: 5
|
||||||
|
delay: 10
|
||||||
|
until: elk_set_kibana_pw is succeeded
|
||||||
|
|
||||||
|
- name: Set logstash_system password
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ elk_scheme }}://localhost:{{ elk_elasticsearch_http_port }}/_security/user/logstash_system/_password"
|
||||||
|
validate_certs: "{{ elk_validate_certs }}"
|
||||||
|
method: POST
|
||||||
|
user: elastic
|
||||||
|
password: "{{ elk_elastic_password }}"
|
||||||
|
force_basic_auth: true
|
||||||
|
body_format: json
|
||||||
|
body:
|
||||||
|
password: "{{ elk_logstash_system_password }}"
|
||||||
|
status_code: 200
|
||||||
|
when: elk_effective_security_enabled
|
||||||
|
register: elk_set_logstash_pw
|
||||||
|
retries: 5
|
||||||
|
delay: 10
|
||||||
|
until: elk_set_logstash_pw is succeeded
|
||||||
|
|
||||||
|
- name: Recreate kibana and logstash if passwords were just set for the first time
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ elk_base_dir }}"
|
||||||
|
project_name: "{{ elk_compose_project_name }}"
|
||||||
|
services:
|
||||||
|
- kibana
|
||||||
|
- logstash
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
when: >
|
||||||
|
elk_effective_security_enabled and
|
||||||
|
(elk_set_kibana_pw is changed or elk_set_logstash_pw is changed)
|
||||||
|
|
||||||
|
- name: Wait for Kibana to answer on its HTTP port
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "{{ elk_scheme }}://localhost:{{ elk_kibana_http_port }}/api/status"
|
||||||
|
validate_certs: "{{ elk_validate_certs }}"
|
||||||
|
status_code:
|
||||||
|
- 200
|
||||||
|
register: elk_kibana_wait
|
||||||
|
until: elk_kibana_wait.status == 200
|
||||||
|
retries: 30
|
||||||
|
delay: 10
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
---
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# TLS certificate lifecycle via lego (ACME DNS-01 against Cloudflare).
|
||||||
|
#
|
||||||
|
# This file only READS the Cloudflare token from elk_cloudflare_token_env_file.
|
||||||
|
# It never receives the token as an Ansible variable - the token is expected
|
||||||
|
# to already exist on disk (root:root, 0600), placed there by cloud-init at
|
||||||
|
# provisioning time. See the role README for the IaC side of this.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
- name: Assert the Cloudflare token file exists
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ elk_cloudflare_token_env_file }}"
|
||||||
|
register: elk_cf_token_stat
|
||||||
|
|
||||||
|
- name: Fail early with a clear message if the token file is missing
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: >
|
||||||
|
{{ elk_cloudflare_token_env_file }} does not exist. This role expects
|
||||||
|
cloud-init (or equivalent provisioning) to have already written a
|
||||||
|
scoped Cloudflare API token here before Ansible runs. See the IaC
|
||||||
|
notes in the role README.
|
||||||
|
when: not elk_cf_token_stat.stat.exists
|
||||||
|
|
||||||
|
- name: Verify the token file is not world/group readable
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- elk_cf_token_stat.stat.mode == "0600"
|
||||||
|
fail_msg: >
|
||||||
|
{{ elk_cloudflare_token_env_file }} should be mode 0600
|
||||||
|
(currently {{ elk_cf_token_stat.stat.mode }}) since it holds a
|
||||||
|
Cloudflare API credential.
|
||||||
|
success_msg: "Cloudflare token file permissions look correct."
|
||||||
|
|
||||||
|
- name: Create lego directories
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
loop:
|
||||||
|
- "{{ elk_lego_path }}"
|
||||||
|
- "{{ elk_lego_path }}/accounts"
|
||||||
|
|
||||||
|
- name: Create lego certificates directory (traversable by container users)
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ elk_lego_certs_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Check installed lego version (if any)
|
||||||
|
ansible.builtin.command: "{{ elk_lego_install_dir }}/lego --version"
|
||||||
|
register: elk_lego_installed_version
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
- name: Download and install lego binary
|
||||||
|
when: >
|
||||||
|
elk_lego_installed_version.rc != 0 or
|
||||||
|
elk_lego_version not in (elk_lego_installed_version.stdout | default(''))
|
||||||
|
block:
|
||||||
|
- name: Download lego release tarball
|
||||||
|
ansible.builtin.get_url:
|
||||||
|
url: "https://github.com/go-acme/lego/releases/download/v{{ elk_lego_version }}/lego_v{{ elk_lego_version }}_linux_{{ elk_lego_arch }}.tar.gz"
|
||||||
|
dest: "/tmp/lego_{{ elk_lego_version }}.tar.gz"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
- name: Extract lego binary
|
||||||
|
ansible.builtin.unarchive:
|
||||||
|
src: "/tmp/lego_{{ elk_lego_version }}.tar.gz"
|
||||||
|
dest: /tmp
|
||||||
|
remote_src: true
|
||||||
|
include:
|
||||||
|
- lego
|
||||||
|
|
||||||
|
- name: Install lego binary
|
||||||
|
ansible.builtin.copy:
|
||||||
|
src: /tmp/lego
|
||||||
|
dest: "{{ elk_lego_install_dir }}/lego"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
remote_src: true
|
||||||
|
|
||||||
|
- name: Clean up downloaded artifacts
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: absent
|
||||||
|
loop:
|
||||||
|
- "/tmp/lego_{{ elk_lego_version }}.tar.gz"
|
||||||
|
- /tmp/lego
|
||||||
|
|
||||||
|
- name: Check whether a certificate already exists for this domain
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.crt"
|
||||||
|
register: elk_cert_stat
|
||||||
|
|
||||||
|
# First issuance uses `lego ... run`, which is what actually requests a new
|
||||||
|
# certificate; `renew` (used by the systemd timer below) refuses to run
|
||||||
|
# against a domain with no existing certificate.
|
||||||
|
- name: Issue the initial certificate (first run only)
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
set -o pipefail
|
||||||
|
set -a
|
||||||
|
source {{ elk_cloudflare_token_env_file }}
|
||||||
|
set +a
|
||||||
|
{{ elk_lego_install_dir }}/lego \
|
||||||
|
--accept-tos \
|
||||||
|
--email "{{ elk_tls_acme_email }}" \
|
||||||
|
--server "{{ elk_tls_acme_server }}" \
|
||||||
|
--domains "{{ elk_tls_domain }}" \
|
||||||
|
--dns cloudflare \
|
||||||
|
--path "{{ elk_lego_path }}" \
|
||||||
|
run
|
||||||
|
args:
|
||||||
|
executable: /bin/bash
|
||||||
|
when: not elk_cert_stat.stat.exists
|
||||||
|
register: elk_lego_first_issue
|
||||||
|
notify: "restart elk stack"
|
||||||
|
|
||||||
|
- name: Ensure issued cert/key are readable by the containers (root-owned, group-readable)
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
loop:
|
||||||
|
- "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.crt"
|
||||||
|
- "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.issuer.crt"
|
||||||
|
when: elk_cert_stat.stat.exists or elk_lego_first_issue is not skipped
|
||||||
|
|
||||||
|
- name: Private key stays root-only-readable but group-readable for the container runtime
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ elk_lego_certs_dir }}/{{ elk_tls_domain }}.key"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0640"
|
||||||
|
when: elk_cert_stat.stat.exists or elk_lego_first_issue is not skipped
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ongoing renewal: systemd oneshot service + daily timer. lego's `renew`
|
||||||
|
# subcommand only actually re-issues (and only then fires --run-hook) when
|
||||||
|
# the certificate is within elk_tls_renew_days of expiring, so this is safe
|
||||||
|
# to run daily without causing unnecessary container restarts.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
- name: Template lego renewal systemd service
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: lego-renew.service.j2
|
||||||
|
dest: /etc/systemd/system/lego-renew.service
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
register: elk_lego_service_unit
|
||||||
|
|
||||||
|
- name: Template lego renewal systemd timer
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: lego-renew.timer.j2
|
||||||
|
dest: /etc/systemd/system/lego-renew.timer
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
register: elk_lego_timer_unit
|
||||||
|
|
||||||
|
- name: Reload systemd if units changed
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
when: elk_lego_service_unit is changed or elk_lego_timer_unit is changed
|
||||||
|
|
||||||
|
- name: Enable and start the lego renewal timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: lego-renew.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
# ansible_managed
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
{{ elk_network_name }}:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: {{ elk_network_subnet }}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
{{ elk_elasticsearch_data_volume }}:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
services:
|
||||||
|
elasticsearch:
|
||||||
|
image: "{{ elk_docker_image_repo }}/elasticsearch/elasticsearch:{{ elk_version }}"
|
||||||
|
container_name: "{{ elk_elasticsearch_container_name }}"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- node.name={{ elk_elasticsearch_node_name }}
|
||||||
|
- cluster.name={{ elk_elasticsearch_cluster_name }}
|
||||||
|
- discovery.type=single-node
|
||||||
|
- bootstrap.memory_lock=true
|
||||||
|
- "ES_JAVA_OPTS=-Xms{{ elk_elasticsearch_heap_size }} -Xmx{{ elk_elasticsearch_heap_size }}"
|
||||||
|
# xpack.security.enabled must be true whenever TLS is on - Elasticsearch
|
||||||
|
# only honors xpack.security.http.ssl.* when security itself is
|
||||||
|
# enabled. elk_effective_security_enabled captures that (it's true if
|
||||||
|
# either elk_security_enabled or elk_tls_enabled is true). Whenever
|
||||||
|
# security ends up on for either reason, ELASTIC_PASSWORD must also be
|
||||||
|
# set - otherwise the image generates a random, unknown elastic
|
||||||
|
# password and nothing else in this stack could authenticate.
|
||||||
|
- xpack.security.enabled={{ elk_effective_security_enabled | lower }}
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
- ELASTIC_PASSWORD={{ elk_elastic_password }}
|
||||||
|
{% endif %}
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
- xpack.security.http.ssl.enabled=true
|
||||||
|
- xpack.security.http.ssl.certificate=/usr/share/elasticsearch/config/certs/{{ elk_tls_domain }}.crt
|
||||||
|
- xpack.security.http.ssl.key=/usr/share/elasticsearch/config/certs/{{ elk_tls_domain }}.key
|
||||||
|
- xpack.security.http.ssl.certificate_authorities=/usr/share/elasticsearch/config/certs/{{ elk_tls_domain }}.issuer.crt
|
||||||
|
# Single node - transport SSL stays off. Turn this on (and add a
|
||||||
|
# matching transport cert) if this ever becomes a multi-node cluster.
|
||||||
|
- xpack.security.transport.ssl.enabled=false
|
||||||
|
{% else %}
|
||||||
|
- xpack.security.http.ssl.enabled=false
|
||||||
|
- xpack.security.transport.ssl.enabled=false
|
||||||
|
{% endif %}
|
||||||
|
ulimits:
|
||||||
|
memlock:
|
||||||
|
soft: -1
|
||||||
|
hard: -1
|
||||||
|
volumes:
|
||||||
|
- {{ elk_elasticsearch_data_volume }}:/usr/share/elasticsearch/data
|
||||||
|
- {{ elk_base_dir }}/config/elasticsearch/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
- {{ elk_lego_certs_dir }}:/usr/share/elasticsearch/config/certs:ro
|
||||||
|
{% endif %}
|
||||||
|
ports:
|
||||||
|
- "{{ elk_elasticsearch_http_port }}:9200"
|
||||||
|
- "{{ elk_elasticsearch_transport_port }}:9300"
|
||||||
|
networks:
|
||||||
|
{{ elk_network_name }}:
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
aliases:
|
||||||
|
- "{{ elk_tls_domain }}"
|
||||||
|
{% endif %}
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD-SHELL
|
||||||
|
{% set es_scheme = 'https' if elk_tls_enabled else 'http' %}
|
||||||
|
{% set curl_tls_flag = '-k ' if elk_tls_enabled else '' %}
|
||||||
|
{# xpack.security.enabled follows elk_effective_security_enabled (see
|
||||||
|
above), so basic auth is required here under that same condition. #}
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
- "curl -s {{ curl_tls_flag }}-u elastic:{{ elk_elastic_password }} {{ es_scheme }}://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"
|
||||||
|
{% else %}
|
||||||
|
- "curl -s {{ curl_tls_flag }}{{ es_scheme }}://localhost:9200/_cluster/health | grep -q '\"status\":\"green\"\\|\"status\":\"yellow\"'"
|
||||||
|
{% endif %}
|
||||||
|
interval: 10s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 20
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
logstash:
|
||||||
|
image: "{{ elk_docker_image_repo }}/logstash/logstash:{{ elk_version }}"
|
||||||
|
container_name: "{{ elk_logstash_container_name }}"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- "LS_JAVA_OPTS=-Xms{{ elk_logstash_heap_size }} -Xmx{{ elk_logstash_heap_size }}"
|
||||||
|
- xpack.monitoring.enabled=false
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
- ELASTIC_USER=logstash_system
|
||||||
|
- ELASTIC_PASSWORD={{ elk_logstash_system_password }}
|
||||||
|
{% endif %}
|
||||||
|
volumes:
|
||||||
|
- {{ elk_base_dir }}/config/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
|
||||||
|
- {{ elk_base_dir }}/config/logstash/pipelines.yml:/usr/share/logstash/config/pipelines.yml:ro
|
||||||
|
- {{ elk_base_dir }}/config/logstash/pipeline:/usr/share/logstash/pipeline:ro
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
- {{ elk_lego_certs_dir }}:/usr/share/logstash/config/certs:ro
|
||||||
|
{% endif %}
|
||||||
|
ports:
|
||||||
|
- "{{ elk_logstash_beats_port }}:5044"
|
||||||
|
- "{{ elk_logstash_nginx_beats_port }}:5045"
|
||||||
|
- "{{ elk_logstash_laravel_beats_port }}:5046"
|
||||||
|
- "{{ elk_logstash_tcp_port }}:5000/tcp"
|
||||||
|
- "{{ elk_logstash_udp_port }}:5000/udp"
|
||||||
|
- "{{ elk_logstash_http_port }}:9600"
|
||||||
|
networks:
|
||||||
|
- {{ elk_network_name }}
|
||||||
|
depends_on:
|
||||||
|
elasticsearch:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
kibana:
|
||||||
|
image: "{{ elk_docker_image_repo }}/kibana/kibana:{{ elk_version }}"
|
||||||
|
container_name: "{{ elk_kibana_container_name }}"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- SERVER_NAME={{ elk_kibana_container_name }}
|
||||||
|
- ELASTICSEARCH_HOSTS={{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
- ELASTICSEARCH_USERNAME=kibana_system
|
||||||
|
- ELASTICSEARCH_PASSWORD={{ elk_kibana_system_password }}
|
||||||
|
- XPACK_SECURITY_ENCRYPTIONKEY={{ elk_kibana_encryption_key }}
|
||||||
|
{% endif %}
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
- SERVER_SSL_ENABLED=true
|
||||||
|
- SERVER_SSL_CERTIFICATE=/usr/share/kibana/config/certs/{{ elk_tls_domain }}.crt
|
||||||
|
- SERVER_SSL_KEY=/usr/share/kibana/config/certs/{{ elk_tls_domain }}.key
|
||||||
|
# Public CA (Let's Encrypt) is already trusted by the container's
|
||||||
|
# system trust store, so no custom CA needs to be supplied here for
|
||||||
|
# the Elasticsearch connection.
|
||||||
|
{% endif %}
|
||||||
|
volumes:
|
||||||
|
- {{ elk_base_dir }}/config/kibana/kibana.yml:/usr/share/kibana/config/kibana.yml:ro
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
- {{ elk_lego_certs_dir }}:/usr/share/kibana/config/certs:ro
|
||||||
|
{% endif %}
|
||||||
|
ports:
|
||||||
|
- "{{ elk_kibana_http_port }}:5601"
|
||||||
|
networks:
|
||||||
|
- {{ elk_network_name }}
|
||||||
|
depends_on:
|
||||||
|
elasticsearch:
|
||||||
|
condition: service_healthy
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# ansible_managed
|
||||||
|
cluster.name: "{{ elk_elasticsearch_cluster_name }}"
|
||||||
|
node.name: "{{ elk_elasticsearch_node_name }}"
|
||||||
|
|
||||||
|
network.host: 0.0.0.0
|
||||||
|
discovery.type: single-node
|
||||||
|
|
||||||
|
# TLS (elk_tls_enabled) is a transport-layer concern independent of X-Pack
|
||||||
|
# security (elk_security_enabled) - HOWEVER Elasticsearch only honors
|
||||||
|
# xpack.security.http.ssl.* when xpack.security.enabled is true, so security
|
||||||
|
# is force-enabled (elk_effective_security_enabled, set in tasks/main.yml)
|
||||||
|
# whenever TLS is on, even if elk_security_enabled itself is false. The
|
||||||
|
# ELASTIC_PASSWORD env var is set under the same condition (see
|
||||||
|
# docker-compose.yml.j2), so basic auth is always usable whenever this is true.
|
||||||
|
xpack.security.enabled: {{ elk_effective_security_enabled | lower }}
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
xpack.security.http.ssl.enabled: true
|
||||||
|
xpack.security.http.ssl.certificate: certs/{{ elk_tls_domain }}.crt
|
||||||
|
xpack.security.http.ssl.key: certs/{{ elk_tls_domain }}.key
|
||||||
|
xpack.security.http.ssl.certificate_authorities: ["certs/{{ elk_tls_domain }}.issuer.crt"]
|
||||||
|
xpack.security.transport.ssl.enabled: false
|
||||||
|
{% else %}
|
||||||
|
xpack.security.http.ssl.enabled: false
|
||||||
|
xpack.security.transport.ssl.enabled: false
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# Reachable at {{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200 from other
|
||||||
|
# containers on the {{ elk_network_name }} docker network, and from outside
|
||||||
|
# the host at {{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_tls_domain if elk_tls_enabled else elk_elasticsearch_client_host }}:{{ elk_elasticsearch_http_port }}
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
###################### Filebeat Configuration Example #########################
|
|
||||||
|
|
||||||
# This file is an example configuration file highlighting only the most common
|
|
||||||
# options. The filebeat.reference.yml file from the same directory contains all the
|
|
||||||
# supported options with more comments. You can use it as a reference.
|
|
||||||
#
|
|
||||||
# You can find the full configuration reference here:
|
|
||||||
# https://www.elastic.co/guide/en/beats/filebeat/index.html
|
|
||||||
|
|
||||||
# For more available modules and options, please see the filebeat.reference.yml sample
|
|
||||||
# configuration file.
|
|
||||||
|
|
||||||
# ============================== Filebeat inputs ===============================
|
|
||||||
|
|
||||||
filebeat.inputs:
|
|
||||||
|
|
||||||
# Each - is an input. Most options can be set at the input level, so
|
|
||||||
# you can use different inputs for various configurations.
|
|
||||||
# Below are the input specific configurations.
|
|
||||||
|
|
||||||
- type: log
|
|
||||||
|
|
||||||
# Change to true to enable this input configuration.
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
# Paths that should be crawled and fetched. Glob based paths.
|
|
||||||
paths:
|
|
||||||
- /var/log/ngnix/*.log
|
|
||||||
#- c:\programdata\elasticsearch\logs\*
|
|
||||||
|
|
||||||
# Exclude lines. A list of regular expressions to match. It drops the lines that are
|
|
||||||
# matching any regular expression from the list.
|
|
||||||
#exclude_lines: ['^DBG']
|
|
||||||
|
|
||||||
# Include lines. A list of regular expressions to match. It exports the lines that are
|
|
||||||
# matching any regular expression from the list.
|
|
||||||
#include_lines: ['^ERR', '^WARN']
|
|
||||||
|
|
||||||
# Exclude files. A list of regular expressions to match. Filebeat drops the files that
|
|
||||||
# are matching any regular expression from the list. By default, no files are dropped.
|
|
||||||
#exclude_files: ['.gz$']
|
|
||||||
|
|
||||||
# Optional additional fields. These fields can be freely picked
|
|
||||||
# to add additional information to the crawled log files for filtering
|
|
||||||
#fields:
|
|
||||||
# level: debug
|
|
||||||
# review: 1
|
|
||||||
|
|
||||||
### Multiline options
|
|
||||||
|
|
||||||
# Multiline can be used for log messages spanning multiple lines. This is common
|
|
||||||
# for Java Stack Traces or C-Line Continuation
|
|
||||||
|
|
||||||
# The regexp Pattern that has to be matched. The example pattern matches all lines starting with [
|
|
||||||
#multiline.pattern: ^\[
|
|
||||||
|
|
||||||
# Defines if the pattern set under pattern should be negated or not. Default is false.
|
|
||||||
#multiline.negate: false
|
|
||||||
|
|
||||||
# Match can be set to "after" or "before". It is used to define if lines should be append to a pattern
|
|
||||||
# that was (not) matched before or after or as long as a pattern is not matched based on negate.
|
|
||||||
# Note: After is the equivalent to previous and before is the equivalent to to next in Logstash
|
|
||||||
#multiline.match: after
|
|
||||||
|
|
||||||
# filestream is an input for collecting log messages from files. It is going to replace log input in the future.
|
|
||||||
- type: filestream
|
|
||||||
|
|
||||||
# Change to true to enable this input configuration.
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# Paths that should be crawled and fetched. Glob based paths.
|
|
||||||
paths:
|
|
||||||
- /var/log/nginx/*.log
|
|
||||||
#- c:\programdata\elasticsearch\logs\*
|
|
||||||
|
|
||||||
exclude_files: ['\.gz$']
|
|
||||||
|
|
||||||
# Exclude lines. A list of regular expressions to match. It drops the lines that are
|
|
||||||
# matching any regular expression from the list.
|
|
||||||
#exclude_lines: ['^DBG']
|
|
||||||
|
|
||||||
# Include lines. A list of regular expressions to match. It exports the lines that are
|
|
||||||
# matching any regular expression from the list.
|
|
||||||
#include_lines: ['^ERR', '^WARN']
|
|
||||||
|
|
||||||
# Exclude files. A list of regular expressions to match. Filebeat drops the files that
|
|
||||||
# are matching any regular expression from the list. By default, no files are dropped.
|
|
||||||
#prospector.scanner.exclude_files: ['.gz$']
|
|
||||||
|
|
||||||
# Optional additional fields. These fields can be freely picked
|
|
||||||
# to add additional information to the crawled log files for filtering
|
|
||||||
#fields:
|
|
||||||
# level: debug
|
|
||||||
# review: 1
|
|
||||||
|
|
||||||
# ============================== Filebeat modules ==============================
|
|
||||||
|
|
||||||
filebeat.config.modules:
|
|
||||||
# Glob pattern for configuration loading
|
|
||||||
path: ${path.config}/modules.d/*.yml
|
|
||||||
|
|
||||||
# Set to true to enable config reloading
|
|
||||||
reload.enabled: false
|
|
||||||
|
|
||||||
# Period on which files under path should be checked for changes
|
|
||||||
#reload.period: 10s
|
|
||||||
|
|
||||||
# ======================= Elasticsearch template setting =======================
|
|
||||||
|
|
||||||
setup.template.settings:
|
|
||||||
index.number_of_shards: 1
|
|
||||||
#index.codec: best_compression
|
|
||||||
#_source.enabled: false
|
|
||||||
|
|
||||||
|
|
||||||
# ================================== General ===================================
|
|
||||||
|
|
||||||
# The name of the shipper that publishes the network data. It can be used to group
|
|
||||||
# all the transactions sent by a single shipper in the web interface.
|
|
||||||
#name:
|
|
||||||
|
|
||||||
# The tags of the shipper are included in their own field with each
|
|
||||||
# transaction published.
|
|
||||||
#tags: ["service-X", "web-tier"]
|
|
||||||
|
|
||||||
# Optional fields that you can specify to add additional information to the
|
|
||||||
# output.
|
|
||||||
#fields:
|
|
||||||
# env: staging
|
|
||||||
|
|
||||||
# ================================= Dashboards =================================
|
|
||||||
# These settings control loading the sample dashboards to the Kibana index. Loading
|
|
||||||
# the dashboards is disabled by default and can be enabled either by setting the
|
|
||||||
# options here or by using the `setup` command.
|
|
||||||
#setup.dashboards.enabled: false
|
|
||||||
|
|
||||||
# The URL from where to download the dashboards archive. By default this URL
|
|
||||||
# has a value which is computed based on the Beat name and version. For released
|
|
||||||
# versions, this URL points to the dashboard archive on the artifacts.elastic.co
|
|
||||||
# website.
|
|
||||||
#setup.dashboards.url:
|
|
||||||
|
|
||||||
# =================================== Kibana ===================================
|
|
||||||
|
|
||||||
# Starting with Beats version 6.0.0, the dashboards are loaded via the Kibana API.
|
|
||||||
# This requires a Kibana endpoint configuration.
|
|
||||||
setup.kibana:
|
|
||||||
|
|
||||||
# Kibana Host
|
|
||||||
# Scheme and port can be left out and will be set to the default (http and 5601)
|
|
||||||
# In case you specify and additional path, the scheme is required: http://localhost:5601/path
|
|
||||||
# IPv6 addresses should always be defined as: https://[2001:db8::1]:5601
|
|
||||||
#host: "localhost:5601"
|
|
||||||
|
|
||||||
# Kibana Space ID
|
|
||||||
# ID of the Kibana Space into which the dashboards should be loaded. By default,
|
|
||||||
# the Default Space will be used.
|
|
||||||
#space.id:
|
|
||||||
|
|
||||||
# =============================== Elastic Cloud ================================
|
|
||||||
|
|
||||||
# These settings simplify using Filebeat with the Elastic Cloud (https://cloud.elastic.co/).
|
|
||||||
|
|
||||||
# The cloud.id setting overwrites the `output.elasticsearch.hosts` and
|
|
||||||
# `setup.kibana.host` options.
|
|
||||||
# You can find the `cloud.id` in the Elastic Cloud web UI.
|
|
||||||
#cloud.id:
|
|
||||||
|
|
||||||
# The cloud.auth setting overwrites the `output.elasticsearch.username` and
|
|
||||||
# `output.elasticsearch.password` settings. The format is `<user>:<pass>`.
|
|
||||||
#cloud.auth:
|
|
||||||
|
|
||||||
# ================================== Outputs ===================================
|
|
||||||
|
|
||||||
# Configure what output to use when sending the data collected by the beat.
|
|
||||||
|
|
||||||
# ---------------------------- Elasticsearch Output ----------------------------
|
|
||||||
# output.elasticsearch:
|
|
||||||
# Array of hosts to connect to.
|
|
||||||
# hosts: ["elastic.schleppe:9200"]
|
|
||||||
|
|
||||||
# Protocol - either `http` (default) or `https`.
|
|
||||||
# protocol: "https"
|
|
||||||
|
|
||||||
# Authentication credentials - either API key or username/password.
|
|
||||||
#api_key: "id:api_key"
|
|
||||||
#username: "elastic"
|
|
||||||
#password: "changeme"
|
|
||||||
|
|
||||||
# ------------------------------ Logstash Output -------------------------------
|
|
||||||
output.logstash:
|
|
||||||
# The Logstash hosts
|
|
||||||
hosts: ["elasticsearch:5400"]
|
|
||||||
|
|
||||||
# Optional SSL. By default is off.
|
|
||||||
# List of root certificates for HTTPS server verifications
|
|
||||||
# ssl.certificate_authorities: ["/etc/elk-certs/elk-ssl.crt"]
|
|
||||||
|
|
||||||
# Certificate for SSL client authentication
|
|
||||||
# ssl.certificate: "/etc/elk-certs/elk-ssl.crt"
|
|
||||||
|
|
||||||
# Client Certificate Key
|
|
||||||
# ssl.key: "/etc/elk-certs/elk-ssl.key"
|
|
||||||
|
|
||||||
# ================================= Processors =================================
|
|
||||||
processors:
|
|
||||||
- add_host_metadata:
|
|
||||||
when.not.contains.tags: forwarded
|
|
||||||
- add_cloud_metadata: ~
|
|
||||||
- add_docker_metadata: ~
|
|
||||||
- add_kubernetes_metadata: ~
|
|
||||||
|
|
||||||
# ================================== Logging ===================================
|
|
||||||
|
|
||||||
# Sets log level. The default log level is info.
|
|
||||||
# Available log levels are: error, warning, info, debug
|
|
||||||
# logging.level: debug
|
|
||||||
|
|
||||||
# At debug level, you can selectively enable logging only for some components.
|
|
||||||
# To enable all selectors use ["*"]. Examples of other selectors are "beat",
|
|
||||||
# "publisher", "service".
|
|
||||||
#logging.selectors: ["*"]
|
|
||||||
|
|
||||||
# ============================= X-Pack Monitoring ==============================
|
|
||||||
# Filebeat can export internal metrics to a central Elasticsearch monitoring
|
|
||||||
# cluster. This requires xpack monitoring to be enabled in Elasticsearch. The
|
|
||||||
# reporting is disabled by default.
|
|
||||||
|
|
||||||
# Set to true to enable the monitoring reporter.
|
|
||||||
#monitoring.enabled: false
|
|
||||||
|
|
||||||
# Sets the UUID of the Elasticsearch cluster under which monitoring data for this
|
|
||||||
# Filebeat instance will appear in the Stack Monitoring UI. If output.elasticsearch
|
|
||||||
# is enabled, the UUID is derived from the Elasticsearch cluster referenced by output.elasticsearch.
|
|
||||||
#monitoring.cluster_uuid:
|
|
||||||
|
|
||||||
# Uncomment to send the metrics to Elasticsearch. Most settings from the
|
|
||||||
# Elasticsearch output are accepted here as well.
|
|
||||||
# Note that the settings should point to your Elasticsearch *monitoring* cluster.
|
|
||||||
# Any setting that is not set is automatically inherited from the Elasticsearch
|
|
||||||
# output configuration, so if you have the Elasticsearch output configured such
|
|
||||||
# that it is pointing to your Elasticsearch monitoring cluster, you can simply
|
|
||||||
# uncomment the following line.
|
|
||||||
#monitoring.elasticsearch:
|
|
||||||
|
|
||||||
# ============================== Instrumentation ===============================
|
|
||||||
|
|
||||||
# Instrumentation support for the filebeat.
|
|
||||||
#instrumentation:
|
|
||||||
# Set to true to enable instrumentation of filebeat.
|
|
||||||
#enabled: false
|
|
||||||
|
|
||||||
# Environment in which filebeat is running on (eg: staging, production, etc.)
|
|
||||||
#environment: ""
|
|
||||||
|
|
||||||
# APM Server hosts to report instrumentation results to.
|
|
||||||
#hosts:
|
|
||||||
# - http://localhost:8200
|
|
||||||
|
|
||||||
# API Key for the APM Server(s).
|
|
||||||
# If api_key is set then secret_token will be ignored.
|
|
||||||
#api_key:
|
|
||||||
|
|
||||||
# Secret token for the APM Server(s).
|
|
||||||
#secret_token:
|
|
||||||
|
|
||||||
|
|
||||||
# ================================= Migration ==================================
|
|
||||||
|
|
||||||
# This allows to enable 6.7 migration aliases
|
|
||||||
#migration.6_to_7.enabled: true
|
|
||||||
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ansible_managed
|
||||||
|
server.name: "{{ elk_kibana_container_name }}"
|
||||||
|
server.host: "0.0.0.0"
|
||||||
|
server.port: 5601
|
||||||
|
|
||||||
|
# Talk to Elasticsearch over the shared docker network using its service name.
|
||||||
|
elasticsearch.hosts: ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
|
||||||
|
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
elasticsearch.username: "kibana_system"
|
||||||
|
elasticsearch.password: "{{ elk_kibana_system_password }}"
|
||||||
|
xpack.security.encryptionKey: "{{ elk_kibana_encryption_key }}"
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
server.ssl.enabled: true
|
||||||
|
server.ssl.certificate: /usr/share/kibana/config/certs/{{ elk_tls_domain }}.crt
|
||||||
|
server.ssl.key: /usr/share/kibana/config/certs/{{ elk_tls_domain }}.key
|
||||||
|
|
||||||
|
server.publicBaseUrl: "https://{{ elk_tls_domain }}:{{ elk_kibana_http_port }}"
|
||||||
|
|
||||||
|
# No elasticsearch.ssl.certificateAuthorities entry needed here: the cert
|
||||||
|
# chain is issued by a public CA (Let's Encrypt) which the container's
|
||||||
|
# default trust store already trusts.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
monitoring.ui.container.elasticsearch.enabled: true
|
||||||
|
i18n.locale: "en"
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ansible_managed
|
||||||
|
[Unit]
|
||||||
|
Description=Renew ELK TLS certificate via lego (Cloudflare DNS-01)
|
||||||
|
After=network-online.target docker.service
|
||||||
|
Wants=network-online.target
|
||||||
|
Requires=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
EnvironmentFile={{ elk_cloudflare_token_env_file }}
|
||||||
|
ExecStart={{ elk_lego_install_dir }}/lego \
|
||||||
|
--accept-tos \
|
||||||
|
--email "{{ elk_tls_acme_email }}" \
|
||||||
|
--server "{{ elk_tls_acme_server }}" \
|
||||||
|
--domains "{{ elk_tls_domain }}" \
|
||||||
|
--dns cloudflare \
|
||||||
|
--path "{{ elk_lego_path }}" \
|
||||||
|
--run-hook "/usr/bin/docker compose -f {{ elk_base_dir }}/docker-compose.yml -p {{ elk_compose_project_name }} restart elasticsearch kibana logstash" \
|
||||||
|
renew --days {{ elk_tls_renew_days }}
|
||||||
|
User=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=lego-renew
|
||||||
|
|
||||||
|
# Light hardening. NoNewPrivileges/ProtectSystem kept loose enough that
|
||||||
|
# lego can write certs under elk_lego_path and reach the docker socket
|
||||||
|
# via the docker CLI.
|
||||||
|
PrivateTmp=yes
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# ansible_managed
|
||||||
|
[Unit]
|
||||||
|
Description=Daily timer for ELK TLS certificate renewal check (lego)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* {{ elk_tls_renew_on_calendar }}
|
||||||
|
RandomizedDelaySec={{ elk_tls_renew_randomized_delay }}
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
input {
|
|
||||||
beats {
|
|
||||||
port => 5045
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filter {
|
|
||||||
}
|
|
||||||
|
|
||||||
output {
|
|
||||||
elasticsearch {
|
|
||||||
index => "laravel-logs-%{+YYYY.MM}"
|
|
||||||
hosts => "${ELASTIC_HOSTS}"
|
|
||||||
user => "elastic"
|
|
||||||
password => "${ELASTIC_PASSWORD}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
input {
|
|
||||||
beats {
|
|
||||||
port => 5044
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filter {
|
|
||||||
if [pipeline_id] == "nginx" {
|
|
||||||
mutate { add_field => { "route" => "nginx_pipeline" } }
|
|
||||||
} else if [pipeline_id] == "laravel" {
|
|
||||||
mutate { add_field => { "route" => "laravel_pipeline" } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output {
|
|
||||||
if [pipeline_id] == "nginx" {
|
|
||||||
pipeline { send_to => "nginx_pipeline" }
|
|
||||||
} else if [pipeline_id] == "laravel" {
|
|
||||||
pipeline { send_to => "laravel_pipeline" }
|
|
||||||
} else {
|
|
||||||
# Handle unknown cases
|
|
||||||
stdout { codec => rubydebug }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# ansible_managed
|
||||||
|
http.host: "0.0.0.0"
|
||||||
|
http.port: 9600
|
||||||
|
|
||||||
|
path.config: /usr/share/logstash/pipeline
|
||||||
|
xpack.monitoring.enabled: false
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# ansible_managed
|
||||||
|
input {
|
||||||
|
beats {
|
||||||
|
port => 5044
|
||||||
|
}
|
||||||
|
tcp {
|
||||||
|
port => 5000
|
||||||
|
codec => json_lines
|
||||||
|
}
|
||||||
|
udp {
|
||||||
|
port => 5000
|
||||||
|
codec => json_lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter {
|
||||||
|
# Add filters here as needed (grok, mutate, date, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
elasticsearch {
|
||||||
|
# Reach Elasticsearch over the shared docker network using its service name.
|
||||||
|
hosts => ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
user => "logstash_system"
|
||||||
|
password => "{{ elk_logstash_system_password }}"
|
||||||
|
{% endif %}
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
ssl_enabled => true
|
||||||
|
# Public CA (Let's Encrypt) - trusted by Logstash's default JVM trust
|
||||||
|
# store already, no custom CA needs to be supplied here.
|
||||||
|
{% endif %}
|
||||||
|
index => "logstash-%{+YYYY.MM.dd}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Uncomment for local debugging of pipeline output:
|
||||||
|
# stdout { codec => rubydebug }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ansible_managed
|
||||||
|
input {
|
||||||
|
beats {
|
||||||
|
port => 5046
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filter {
|
||||||
|
}
|
||||||
|
|
||||||
|
output {
|
||||||
|
elasticsearch {
|
||||||
|
# Reach Elasticsearch over the shared docker network using its service name.
|
||||||
|
hosts => ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
user => "logstash_system"
|
||||||
|
password => "{{ elk_logstash_system_password }}"
|
||||||
|
{% endif %}
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
ssl_enabled => true
|
||||||
|
# Public CA (Let's Encrypt) - trusted by Logstash's default JVM trust
|
||||||
|
# store already, no custom CA needs to be supplied here.
|
||||||
|
{% endif %}
|
||||||
|
index => "laravel-logs-%{+YYYY.MM}"
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-5
@@ -1,6 +1,7 @@
|
|||||||
|
# ansible_managed
|
||||||
input {
|
input {
|
||||||
beats {
|
beats {
|
||||||
port => 5044
|
port => 5045
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,10 +37,17 @@ filter {
|
|||||||
|
|
||||||
output {
|
output {
|
||||||
elasticsearch {
|
elasticsearch {
|
||||||
|
# Reach Elasticsearch over the shared docker network using its service name.
|
||||||
|
hosts => ["{{ 'https' if elk_tls_enabled else 'http' }}://{{ elk_elasticsearch_client_host }}:9200"]
|
||||||
|
{% if elk_effective_security_enabled %}
|
||||||
|
user => "logstash_system"
|
||||||
|
password => "{{ elk_logstash_system_password }}"
|
||||||
|
{% endif %}
|
||||||
|
{% if elk_tls_enabled %}
|
||||||
|
ssl_enabled => true
|
||||||
|
# Public CA (Let's Encrypt) - trusted by Logstash's default JVM trust
|
||||||
|
# store already, no custom CA needs to be supplied here.
|
||||||
|
{% endif %}
|
||||||
index => "weblogs-%{+YYYY.MM}"
|
index => "weblogs-%{+YYYY.MM}"
|
||||||
hosts => "${ELASTIC_HOSTS}"
|
|
||||||
user => "elastic"
|
|
||||||
password => "${ELASTIC_PASSWORD}"
|
|
||||||
document_type => "nginx_logs"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
|
# ansible_managed
|
||||||
|
- pipeline.id: main
|
||||||
|
path.config: "/usr/share/logstash/pipeline/main.conf"
|
||||||
|
|
||||||
- pipeline.id: nginx_pipeline
|
- pipeline.id: nginx_pipeline
|
||||||
path.config: "/usr/share/logstash/pipeline/nginx_pipeline.conf"
|
path.config: "/usr/share/logstash/pipeline/pipeline-nginx.conf"
|
||||||
|
|
||||||
- pipeline.id: laravel_pipeline
|
- pipeline.id: laravel_pipeline
|
||||||
path.config: "/usr/share/logstash/pipeline/laravel_pipeline.conf"
|
path.config: "/usr/share/logstash/pipeline/pipeline-laravel.conf"
|
||||||
|
|||||||
Reference in New Issue
Block a user