diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..756f1c7 --- /dev/null +++ b/Readme.md @@ -0,0 +1,357 @@ + +# Consul Ansible Role + +image + + +## Table of Contents + +1. [Overview](#1-overview) +2. [Supported Operating Systems](#2-supported-operating-systems) +3. [Prerequisites & Known Limitations](#3-prerequisites--known-limitations) +4. [Architecture & Core Components](#4-architecture--core-components) +5. [Configuration Overview](#5-configuration-overview) +6. [Installation Flow](#6-installation-flow) +7. [Running Consul](#7-running-consul) +8. [Validation & Testing](#8-validation--testing) +9. [Best Practices Followed](#9-best-practices-followed) +10. [Troubleshooting](#10-troubleshooting) +11. [Conclusion](#11-conclusion) +12. [References](#12-references) +13. [Author](#13-author) + +## 1. Overview + +**HashiCorp Consul** is a service networking platform that enables: + +- Service Discovery +- Service Mesh +- Health Checking +- Key-Value Storage +- Secure Service-to-Service Communication + +It is widely used in distributed systems and microservices architectures to provide dynamic infrastructure management. + +### Problems Consul Solves + +- How do services find each other? +- How do we secure service communication? +- How do we monitor service health? +- How do we manage configuration centrally? + +### Supported Environments + +Consul works across: + +- Virtual Machines +- Kubernetes +- Hybrid environments +- Multi-cloud deployments + +--- + +## 2. Supported Operating Systems + +Consul supports multiple operating systems: + +### Linux +- Ubuntu +- Debian +- RHEL +- CentOS +- Amazon Linux + +### Other Platforms +- Windows +- macOS +- Kubernetes environments +- Cloud platforms: + - AWS + - Azure + - GCP + +> Consul is written in **Go** and distributed as a single binary. + +--- + +## 3. Prerequisites & Known Limitations + +### Prerequisites + +- Network connectivity between cluster nodes +- Minimum **3 nodes** for production cluster (recommended) +- Proper firewall configuration (ports **8300–8600**) +- Stable DNS or IP addressing +- TLS certificates (for secure production environments) + +### Known Limitations + +- Requires quorum for leader election +- Performance depends on cluster size and network latency +- Misconfigured ACLs can block cluster operations +- Not a replacement for full configuration management tools + +--- + +## 4. Architecture & Core Components + +Consul architecture consists of the following components: + +### 4.1 Servers + +- Maintain cluster state +- Participate in **Raft consensus** +- Handle leader election + +### 4.2 Clients (Agents) + +- Run on application nodes +- Register services +- Perform health checks + +### 4.3 Datacenter + +- Logical grouping of nodes in a specific environment + +### 4.4 Gossip Protocol + +- Used for node membership +- Handles failure detection + +### 4.5 Raft Consensus + +- Provides strong consistency +- Manages leader election among servers + +### 4.6 Key-Value Store + +- Stores configuration data centrally +- Used for dynamic application configuration + +### 4.7 Service Mesh (Connect) + +- Provides secure service-to-service communication +- Uses **mTLS (Mutual TLS)** +- Enables zero-trust networking between services + +--- + + + +## Role Structure +``` +. +├── inventory.ini +├── site.yml +└── roles/ + └── consul/ + ├── defaults/ + │ └── main.yml + ├── files/ + │ └── read-policy.hcl + ├── handlers/ + │ └── main.yml + ├── meta/ + │ └── main.yml + ├── tasks/ + │ ├── acl.yml + │ ├── acl_policies.yml + │ ├── acl_tokens.yml + │ ├── config.yml + │ ├── directories.yml + │ ├── install.yml + │ ├── main.yml + │ ├── rbac.yml + │ └── service.yml + ├── templates/ + │ ├── consul.service.j2 + │ ├── prometheus-consul.yml.j2 + │ ├── server.hcl.j2 + │ └── policies/ + │ ├── agent-policy.hcl.j2 + │ ├── monitoring-policy.hcl.j2 + │ ├── readonly-policy.hcl.j2 + │ └── service-policy.hcl.j2 + ├── tests/ + │ └── test.yml + └── vars/ + └── main.yml +``` + +## 5. Configuration Overview + +Consul can be configured using: + +- HCL files +- JSON configuration files +- Command-line flags +- Environment variables + +### Important Configuration Parameters + +| Parameter | Description | +|-------------------|------------| +| `node_name` | Unique node identifier | +| `bind_addr` | Address to bind Consul to | +| `data_dir` | Directory for Consul data | +| `server` | Defines server or client mode (`true/false`) | +| `bootstrap_expect` | Number of servers expected for cluster formation | +| `retry_join` | List of nodes to join cluster | +| `acl` | Enables Access Control Lists | +| `ui_config` | UI configuration settings | + +### ACL Configuration Includes + +- Enable/Disable ACLs +- Default policies +- Token management +- Token persistence + +--- + +## 6. Installation Flow +### Step 1: Download Binary +``` +wget https://releases.hashicorp.com/consul/1.17.0/consul_1.17.0_linux_amd64.zip +``` + +### Step 2: Unzip +``` +unzip consul_1.17.0_linux_amd64.zip +``` + +### Step 3: Move Binary +``` +sudo mv consul /usr/local/bin/ +``` + +### Step 4: Verify Installation +``` +consul --version +``` + +## 7. Running the Playbook +``` +ansible-playbook -i inventory.ini site.yml +``` + +## 8. Validation & Testing +## Check Cluster Members +``` +consul members +``` +## Check Leader +``` +consul operator raft list-peers +``` +## Check Services +``` +consul catalog services +``` +## Access UI + +Default UI URL: + +``` +http://:8500 +``` + +## Vault Usage (IMPORTANT) + +Sensitive values like: + +- consul_master_token + +- consul_gossip_key + +are stored using Ansible Vault. + +### To Run Playbook: +``` +ansible-playbook -i inventory.ini site.yml --ask-vault-pass +``` + +### OR using password file: +``` +ansible-playbook -i inventory.ini site.yml --vault-password-file vault_pass.txt +``` + +## 9. Best Practices Followed + +- Always use minimum 3 server nodes + +- Enable ACLs in production + +- Use TLS encryption + +- Avoid running in -dev mode in production + +- Monitor health checks continuously + +- Secure gossip communication + +- Use proper token management + +# 10. Troubleshooting +## Consul Not Starting + +- Check systemd logs + +``` +journalctl -u consul +``` +## No Leader Elected + +- Ensure minimum quorum + +- Verify bootstrap_expect value + +- Check network connectivity + +## ACL Errors + +- Verify bootstrap token + +- Ensure token persistence is enabled + +- Check default policy + +## Node Not Joining + +- Verify retry_join + +- Check firewall ports + +- Validate bind address + +# 11. Conclusion + +HashiCorp Consul is a powerful service networking solution designed for modern distributed systems. + +It provides: + +- Reliable service discovery + +- Secure service communication + +- Centralized configuration + +- High availability clustering + +Consul simplifies infrastructure complexity and enables scalable microservices architecture. + +# 12. References + + +| Purpose | Link | +|---------|------| +| Consul Official Documentation | https://developer.hashicorp.com/consul/docs | +| Consul Installation Guide | https://developer.hashicorp.com/consul/docs/install | +| Consul ACL Documentation | https://developer.hashicorp.com/consul/docs/security/acl | +| Consul Service Mesh Guide | https://developer.hashicorp.com/consul/docs/connect | + +# 13. Author + +**Author**: Annem Anitha +**Last Updated:** 25-Feb-2026 + diff --git a/defaults/main.yml b/defaults/main.yml index 4ece191..a243fb2 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,37 +1,69 @@ --- -# common -consul_owner: consul -consul_group: consul - -# consul binary -consul_binary_dir: /usr/local/bin -consul_binary_perm: 0440 -consul_binary_owner: root -consul_binary_group: root - -# consul user -consul_user_home: /etc/consul.d -consul_user_shell: /bin/false - -# consul-hcl file -consul_hcl_dest: /etc/consul.d/consul.hcl -consul_hcl_mode: 0640 - -# server-hcl file -server_hcl_dest: /etc/consul.d/server.hcl -server_hcl_mode: 0640 - -# consul-service file -consul_service_file: /etc/systemd/system/consul.service -description: HashiCorp Consul - A service mesh solution -documentation: https://www.consul.io/ -requires: network-online.target -after: network-online.target -cond_file_not_empty: /etc/consul.d/consul.hcl -type: notify -exec_start: /usr/local/bin/consul agent -config-dir=/etc/consul.d/ -exec_reload: /usr/local/bin/consul reload -kill_mode: process -restart: on-failure -limit_file: 65536 -wanted_by: multi-user.target +# defaults file for consul +consul_version: "1.17.0" +consul_user: "consul" +consul_group: "consul" + +consul_install_dir: "/usr/local/bin" +consul_data_dir: "/var/lib/consul" +consul_config_dir: "/etc/consul.d" + +consul_node_name: "{{ inventory_hostname }}" +consul_bind_addr: "{{ ansible_host }}" + +consul_is_server: true +consul_bootstrap_expect: "{{ play_hosts | length }}" + +consul_enable_ui: true + +# Ports +consul_http_port: 8500 +consul_https_port: 8501 +consul_use_tls: true + +# TLS +consul_tls_server_name: "opstree.dev" +consul_tls_verify: false +consul_tls_enabled: false + +# ACL +consul_acl_enabled: true +consul_acl_default_policy: "deny" +consul_acl_token_persistence: false +consul_agent_token: "" + +consul_client_addr: "0.0.0.0" + +consul_oidc_enabled: true +consul_public_url: "https://opstree.dev" + +# Internal URL to bypass Cloudflare 522. Use 127.0.0.1 if Keycloak is on the same node. +keycloak_internal_url: "https://keycloak.opstree.dev" +keycloak_url: "https://keycloak.opstree.dev" +keycloak_realm: "master" + +consul_oidc_client_id: "consul" +consul_oidc_client_secret: "r2kGOxYip1srrwhxiqRIAXv4bFaJroo3" + +consul_oidc_insecure_tls: true +consul_oidc_max_token_ttl: "1h" +consul_oidc_redirect_uris: + - "{{ consul_public_url }}/ui/oidc/callback" + - "http://localhost:8550/oidc/callback" + +# Gossip Encryption +consul_gossip_key: "" + +# Monitoring +consul_telemetry_enabled: true +consul_prometheus_retention: "60s" + +# Backup & DR Configuration +consul_backup_enabled: true +consul_backup_minio_endpoint: "{{ lookup('env', 'MINIO_Endpoint') }}" +consul_backup_bucket: "consul" +consul_backups_to_keep_remote: 7 + +# S3/MinIO backup credentials +consul_backup_s3_access_key: "{{ lookup('env', 'MINIO_consul_accesskey') }}" +consul_backup_s3_secret_key: "{{ lookup('env', 'MINIO_consul_secretkey') }}" diff --git a/files/read-policy.hcl b/files/read-policy.hcl new file mode 100644 index 0000000..f3e1480 --- /dev/null +++ b/files/read-policy.hcl @@ -0,0 +1,8 @@ +# Allow DNS to find nodes and services +node_prefix "" { + policy = "read" +} + +service_prefix "" { + policy = "write" +} diff --git a/handlers/main.yml b/handlers/main.yml index cd1e12b..b37f139 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -1,16 +1,8 @@ --- +# handlers file for consul -- name: daemon_reload - systemd: - daemon_reload: true - -- name: restart_consul - service: +- name: Restart Consul + ansible.builtin.systemd: name: consul - enabled: true state: restarted - -- name: stop_consul - service: - name: consul - state: stopped + daemon_reload: true diff --git a/meta/main.yml b/meta/main.yml index 5be9dd6..c65c44f 100644 --- a/meta/main.yml +++ b/meta/main.yml @@ -1,17 +1,34 @@ ---- galaxy_info: - author: Shatrujeet + author: Annem Anitha description: Create consul cluster with any number of nodes + company: opstree - min_ansible_version: 2.0 + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. - platforms: - - name: Ubuntu - versions: - - xenial - - precise - - trusty - - name: consul - versions: - - 1.7.2 dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/tasks/acl.yml b/tasks/acl.yml new file mode 100644 index 0000000..1d1fe60 --- /dev/null +++ b/tasks/acl.yml @@ -0,0 +1,77 @@ +--- +- name: Wait for Consul HTTPs API + ansible.builtin.wait_for: + host: "{{ consul_bind_addr }}" + port: "{{ consul_https_port }}" + delay: 15 + timeout: 120 + +# Check bootstrap token +- name: Check for existing bootstrap token file + ansible.builtin.stat: + path: "{{ consul_config_dir }}/bootstrap.token" + register: token_file_on_disk + +- name: Load existing token from disk + ansible.builtin.slurp: + src: "{{ consul_config_dir }}/bootstrap.token" + register: slurped_token + become: true + when: + - token_file_on_disk.stat.exists + - token_file_on_disk.stat.size > 0 + +- name: Set master token fact from file + ansible.builtin.set_fact: + consul_master_token: "{{ slurped_token.content | b64decode | trim }}" + when: + - token_file_on_disk.stat.exists + - slurped_token.content is defined + +# Bootstrap ACL +- name: Bootstrap ACL + ansible.builtin.command: consul acl bootstrap -format=json + register: consul_bootstrap + run_once: true + failed_when: false + when: consul_master_token is not defined or consul_master_token == "" + environment: + CONSUL_HTTP_ADDR: "https://{{ consul_bind_addr }}:{{ consul_https_port }}" + + +- name: Extract new token from bootstrap output + ansible.builtin.set_fact: + consul_master_token: "{{ (consul_bootstrap.stdout | from_json).SecretID }}" + run_once: true + no_log: true + when: + - consul_bootstrap.changed + - consul_bootstrap.stdout | length > 0 + - "'SecretID' in consul_bootstrap.stdout" + +# Share token to all hosts +- name: Propagate token to all hosts + ansible.builtin.set_fact: + consul_master_token: "{{ hostvars[ansible_play_hosts[0]]['consul_master_token'] }}" + no_log: true + when: consul_master_token is not defined or consul_master_token == "" + +# Save token +- name: Save bootstrap token to file + ansible.builtin.copy: + content: "{{ consul_master_token }}" + dest: "{{ consul_config_dir }}/bootstrap.token" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0600" + become: true + when: + - consul_master_token is defined + - consul_master_token != "" + +# Next tasks +- name: Include ACL policies + ansible.builtin.include_tasks: acl_policies.yml + +- name: Include ACL tokens + ansible.builtin.include_tasks: acl_tokens.yml diff --git a/tasks/acl_policies.yml b/tasks/acl_policies.yml new file mode 100644 index 0000000..1d0f8d4 --- /dev/null +++ b/tasks/acl_policies.yml @@ -0,0 +1,50 @@ +--- +- name: List existing policies via Envoy TLS + ansible.builtin.command: consul acl policy list + register: existing_policies + run_once: true + become: true + environment: + CONSUL_HTTP_ADDR: "https://{{ consul_bind_addr }}:{{ consul_https_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_CACERT: "{{ consul_config_dir }}/certs/tls.crt" + CONSUL_CLIENT_CERT: "{{ consul_config_dir }}/certs/tls.crt" + CONSUL_CLIENT_KEY: "{{ consul_config_dir }}/certs/tls.key" + CONSUL_TLS_SERVER_NAME: "{{ consul_tls_server_name }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + when: consul_tls_enabled + +- name: Copy policy files + ansible.builtin.template: + src: "policies/{{ item }}.hcl.j2" + dest: "/tmp/{{ item }}.hcl" + mode: "0644" + loop: + - agent-policy + - service-policy + - readonly-policy + - monitoring-policy + +- name: Create Consul policies + ansible.builtin.command: > + consul acl policy create + -name {{ item }} + -rules @/tmp/{{ item }}.hcl + loop: + - agent-policy + - service-policy + - readonly-policy + - monitoring-policy + when: + - consul_tls_enabled + - item not in existing_policies.stdout + run_once: true + become: true + environment: + CONSUL_HTTP_ADDR: "https://{{ consul_bind_addr }}:{{ consul_https_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_CACERT: "{{ consul_config_dir }}/certs/tls.crt" + CONSUL_CLIENT_CERT: "{{ consul_config_dir }}/certs/tls.crt" + CONSUL_CLIENT_KEY: "{{ consul_config_dir }}/certs/tls.key" + CONSUL_TLS_SERVER_NAME: "{{ consul_tls_server_name }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" diff --git a/tasks/acl_tokens.yml b/tasks/acl_tokens.yml new file mode 100644 index 0000000..e9a8899 --- /dev/null +++ b/tasks/acl_tokens.yml @@ -0,0 +1,145 @@ +--- +# Check existing tokens +- name: Check existing tokens in consul + ansible.builtin.command: consul acl token list + register: existing_tokens + changed_when: false + run_once: true + environment: &consul_env + CONSUL_HTTP_ADDR: "https://{{ consul_bind_addr }}:{{ consul_https_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + CONSUL_CACERT: "{{ consul_config_dir }}/certs/tls.crt" + +#################################### +# Agent Token +#################################### + +- name: Check if agent token exists on disk + ansible.builtin.stat: + path: "{{ consul_config_dir }}/agent.token" + register: agent_token_stat + +- name: Create agent token + ansible.builtin.command: > + consul acl token create + -description "Agent Token" + -policy-name agent-policy + register: agent_token_output + when: + - not agent_token_stat.stat.exists + - "'Agent Token' not in existing_tokens.stdout" + run_once: true + environment: *consul_env + no_log: true + +- name: Save agent token + ansible.builtin.copy: + content: "{{ agent_token_output.stdout | regex_search('SecretID:\\s+([a-fA-F0-9-]+)', '\\1') | first }}" + dest: "{{ consul_config_dir }}/agent.token" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0600" + when: + - not agent_token_stat.stat.exists + - agent_token_output.stdout is defined + - "'SecretID' in agent_token_output.stdout" + no_log: true + +#################################### +# Service Token +#################################### + +- name: Check if service token exists on disk + ansible.builtin.stat: + path: "{{ consul_config_dir }}/service.token" + register: service_token_stat + +- name: Create service token + ansible.builtin.command: > + consul acl token create + -description "Service Token" + -policy-name service-policy + register: service_token_output + when: + - not service_token_stat.stat.exists + - "'Service Token' not in existing_tokens.stdout" + run_once: true + environment: *consul_env + no_log: true + +- name: Save service token + ansible.builtin.copy: + content: "{{ service_token_output.stdout | regex_search('SecretID:\\s+([a-fA-F0-9-]+)', '\\1') | first }}" + dest: "{{ consul_config_dir }}/service.token" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0600" + when: + - not service_token_stat.stat.exists + - agent_token_output.stdout is defined + - "'SecretID' in service_token_output.stdout" + run_once: true + no_log: true + +#################################### +# Monitoring Token Logic +#################################### + +- name: Check if monitoring token exists on disk (Node 1) + ansible.builtin.stat: + path: "{{ consul_config_dir }}/prometheus.token" + register: prom_token_stat_node1 + run_once: true + delegate_to: "{{ ansible_play_hosts[0] }}" + +- name: Create Prometheus monitoring token if missing + ansible.builtin.command: > + consul acl token create + -description "Prometheus Metrics Token" + -policy-name monitoring-policy + register: prom_token_output + when: + - not prom_token_stat_node1.stat.exists + - "'Prometheus Metrics Token' not in existing_tokens.stdout" + run_once: true + environment: *consul_env + +- name: Slurp existing token if it was already on disk + ansible.builtin.slurp: + src: "{{ consul_config_dir }}/prometheus.token" + register: slurped_prom_token + when: prom_token_stat_node1.stat.exists + run_once: true + delegate_to: "{{ ansible_play_hosts[0] }}" + +- name: Set Prometheus token fact + ansible.builtin.set_fact: + consul_prometheus_token: >- + {{ + (prom_token_output.stdout | regex_search('SecretID:\s+([a-fA-F0-9-]+)', '\1') | first) + if (prom_token_output.changed) + else (slurped_prom_token.content | b64decode | trim) + }} + run_once: true + +- name: Ensure Prometheus token is on all nodes + ansible.builtin.copy: + content: "{{ hostvars[ansible_play_hosts[0]]['consul_prometheus_token'] }}" + dest: "{{ consul_config_dir }}/prometheus.token" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0600" + become: true + +#################################### +# Anonymous Token (Metrics Access) +#################################### + +- name: Allow Anonymous Token to read metrics + ansible.builtin.command: > + consul acl token update -id 00000000-0000-0000-0000-000000000002 + -description "Anonymous Token - Metrics Access" + -policy-name monitoring-policy + run_once: true + environment: *consul_env diff --git a/tasks/backup.yml b/tasks/backup.yml new file mode 100644 index 0000000..e01c531 --- /dev/null +++ b/tasks/backup.yml @@ -0,0 +1,90 @@ +--- +- name: Check if AWS CLI is installed + ansible.builtin.command: aws --version + register: aws_check + ignore_errors: true + changed_when: false + +- name: Install AWS CLI v2 + block: + - name: Install unzip + ansible.builtin.package: + name: unzip + state: present + become: true + + - name: Download AWS CLI v2 bundle + ansible.builtin.get_url: + url: "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" + dest: "/tmp/awscliv2.zip" + mode: '0644' + + - name: Unarchive AWS CLI bundle + ansible.builtin.unarchive: + src: "/tmp/awscliv2.zip" + dest: "/tmp" + remote_src: true + + - name: Run AWS CLI installation script + ansible.builtin.command: + cmd: "/tmp/aws/install --update" + become: true + when: aws_check.failed + +- name: "Set backup constants" + ansible.builtin.set_fact: + consul_backup_dir: "/var/backups/consul" + consul_backup_filename: "consul_backup_{{ lookup('pipe', 'date +%Y%m%d_%H%M%S') }}.snap" + +- name: Ensure secure backup directory exists + ansible.builtin.file: + path: "{{ consul_backup_dir }}" + state: directory + owner: "{{ consul_user | default('consul') }}" + group: "{{ consul_group | default('consul') }}" + mode: "0700" + become: true + +- name: Take Consul snapshot + ansible.builtin.command: + cmd: "consul snapshot save {{ consul_backup_dir }}/{{ consul_backup_filename }}" + environment: + CONSUL_HTTP_ADDR: "https://127.0.0.1:{{ consul_https_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "false" + register: snapshot_result + become: true + changed_when: snapshot_result.rc == 0 + +- name: Verify snapshot file exists before upload + ansible.builtin.stat: + path: "{{ consul_backup_dir }}/{{ consul_backup_filename }}" + register: snapshot_file + +- name: Upload backup to MinIO (S3) + ansible.builtin.command: > + aws s3 cp {{ consul_backup_dir }}/{{ consul_backup_filename }} + s3://{{ consul_backup_bucket }}/{{ consul_backup_filename }} + --endpoint-url {{ consul_backup_minio_endpoint }} + --no-verify-ssl + environment: + AWS_ACCESS_KEY_ID: "{{ consul_backup_s3_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ consul_backup_s3_secret_key }}" + AWS_REGION: "us-east-1" + AWS_EC2_METADATA_DISABLED: "true" + register: upload_result + retries: 3 + delay: 20 + until: upload_result.rc == 0 + when: snapshot_file.stat.exists + become: true + +- name: Debug Upload Output + ansible.builtin.debug: + var: upload_result.stdout + +- name: Delete local backup file + ansible.builtin.file: + path: "{{ consul_backup_dir }}/{{ consul_backup_filename }}" + state: absent + when: upload_result is succeeded diff --git a/tasks/config.yml b/tasks/config.yml new file mode 100644 index 0000000..332fd44 --- /dev/null +++ b/tasks/config.yml @@ -0,0 +1,48 @@ +- name: Create Consul directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0750" + loop: + - "{{ consul_data_dir }}" + - "{{ consul_config_dir }}" + +- name: Create TLS cert directory + ansible.builtin.file: + path: "{{ consul_config_dir }}/certs" + state: directory + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0750" + when: consul_tls_enabled + +- name: Copy TLS certificate + ansible.builtin.copy: + src: tls.crt + dest: "{{ consul_config_dir }}/certs/tls.crt" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0644" + notify: Restart Consul + when: consul_tls_enabled + +- name: Copy TLS private key + ansible.builtin.copy: + src: tls.key + dest: "{{ consul_config_dir }}/certs/tls.key" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0600" + notify: Restart Consul + when: consul_tls_enabled + +- name: Deploy Consul server configuration + ansible.builtin.template: + src: server.hcl.j2 + dest: "{{ consul_config_dir }}/server.hcl" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0640" + notify: Restart Consul diff --git a/tasks/consul-Debian.yml b/tasks/consul-Debian.yml deleted file mode 100644 index a68a0d7..0000000 --- a/tasks/consul-Debian.yml +++ /dev/null @@ -1,73 +0,0 @@ ---- - -- name: Setting private Ip of leader - when: group_names[0] == "consul-leader" - set_fact: - ip_leader: "{{ ansible_default_ipv4.address }}" - -- name: Installing unzip - apt: - name: unzip - state: present - -- name: Downloading archived consul binary - when: (group_names[0] == "consul-leader") or (group_names[0] == "consul-server") or (group_names[0] == "consul-client") - unarchive: - src: "{{ consul_binary_url }}" - dest: "{{ consul_binary_dir }}" - owner: "{{ consul_binary_owner }}" - group: "{{ consul_binary_group }}" - remote_src: true - -- name: Executing command to generate encrypt key - when: group_names[0] == "consul-leader" - command: consul keygen - register: encr_key - -- name: Stopping consul service if running - when: (group_names[0] == "consul-leader") or (group_names[0] == "consul-server") or (group_names[0] == "consul-client") - service: - name: consul - state: stopped - ignore_errors: true - -- name: Including file for consul user - when: (group_names[0] == "consul-leader") or (group_names[0] == "consul-server") or (group_names[0] == "consul-client") - include: consul-user.yml - -- name: Creating a Consul service file - when: (group_names[0] == "consul-leader") or (group_names[0] == "consul-server") or (group_names[0] == "consul-client") - template: - src: consul-service.j2 - dest: "{{ consul_service_file }}" - -- name: Creating consul.hcl file - when: (group_names[0] == "consul-leader") or (group_names[0] == "consul-server") or (group_names[0] == "consul-client") - template: - src: consul-hcl.j2 - dest: "{{ consul_hcl_dest }}" - -- name: Creating server.hcl file for leader - when: group_names[0] == "consul-leader" - template: - src: server-hcl-leader.j2 - dest: "{{ server_hcl_dest }}" - owner: "{{ consul_owner }}" - group: "{{ consul_group }}" - mode: "{{ server_hcl_mode }}" - -- name: Creating server.hcl file for server - when: group_names[0] == "consul-server" - template: - src: server-hcl.j2 - dest: "{{ server_hcl_dest }}" - owner: "{{ consul_owner }}" - group: "{{ consul_group }}" - mode: "{{ server_hcl_mode }}" - -- name: Reloading systemd and restarting consul - when: (group_names[0] == "consul-leader") or (group_names[0] == "consul-server") or (group_names[0] == "consul-client") - command: /bin/true - notify: - - daemon_reload - - restart_consul diff --git a/tasks/consul-user.yml b/tasks/consul-user.yml deleted file mode 100644 index cc7e597..0000000 --- a/tasks/consul-user.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -- name: Adding a non-privileged user 'consul' with no shell - user: - name: consul - home: "{{ consul_user_home }}" - shell: "{{ consul_user_shell }}" - -- name: Checking and Removing consul storage directory if exists - file: - path: "{{ consul_data_dir }}" - state: absent - -- name: Creating directories for consul persistent storage - file: - path: "{{ consul_data_dir }}" - state: directory - owner: "{{ consul_owner }}" - group: "{{ consul_group }}" diff --git a/tasks/install.yml b/tasks/install.yml new file mode 100644 index 0000000..8da5aeb --- /dev/null +++ b/tasks/install.yml @@ -0,0 +1,34 @@ +- name: Install required packages + ansible.builtin.apt: + name: + - unzip + - wget + - curl + state: present + update_cache: true + +- name: Create consul group + ansible.builtin.group: + name: "{{ consul_group }}" + system: true + +- name: Create consul user + ansible.builtin.user: + name: "{{ consul_user }}" + group: "{{ consul_group }}" + system: true + shell: /sbin/nologin + create_home: false + +- name: Download Consul binary + ansible.builtin.get_url: + url: "{{ consul_binary_url }}" + dest: "{{ consul_zip_path }}" + mode: "0644" + +- name: Unarchive Consul + ansible.builtin.unarchive: + src: "{{ consul_zip_path }}" + dest: "{{ consul_install_dir }}" + remote_src: true + mode: "0755" diff --git a/tasks/main.yml b/tasks/main.yml index 543501b..5c759c8 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -1,5 +1,25 @@ --- +- name: Include installation tasks + ansible.builtin.include_tasks: install.yml + +- name: Include configuration tasks + ansible.builtin.include_tasks: config.yml -- name: Include OS Specific setup file - when: ansible_os_family == "Debian" - include: consul-Debian.yml +- name: Include service tasks + ansible.builtin.include_tasks: service.yml + +- name: Include ACL tasks + ansible.builtin.include_tasks: acl.yml + when: consul_acl_enabled + +- name: Include RBAC tasks + ansible.builtin.include_tasks: rbac.yml + when: consul_acl_enabled + +- name: Include Backup tasks + ansible.builtin.include_tasks: backup.yml + when: consul_backup_enabled | bool + +- name: Include OIDC SSO tasks + ansible.builtin.include_tasks: oidc.yml + when: consul_oidc_enabled | bool diff --git a/tasks/oidc.yml b/tasks/oidc.yml new file mode 100644 index 0000000..9a3b5b0 --- /dev/null +++ b/tasks/oidc.yml @@ -0,0 +1,65 @@ +--- +# ✅ Step 1: Check if the existing method exists +- name: Check current Keycloak auth method + ansible.builtin.uri: + url: "http://127.0.0.1:8500/v1/acl/auth-method/keycloak" + method: GET + headers: + X-Consul-Token: "{{ consul_master_token }}" + status_code: [200, 404] + register: existing_method + run_once: true + +# ✅ Step 2: Delete ONLY if it is not the correct configuration +- name: Delete existing method if not jwt + ansible.builtin.uri: + url: "http://127.0.0.1:8500/v1/acl/auth-method/keycloak" + method: DELETE + headers: + X-Consul-Token: "{{ consul_master_token }}" + status_code: [200, 204] + run_once: true + when: + - existing_method.status == 200 + - existing_method.json.Type != 'jwt' + +# ✅ Step 3: Create or Update the JWT Auth Method (Updated for UI Visibility) +- name: Upsert Keycloak JWT Auth Method via API + ansible.builtin.uri: + url: "http://127.0.0.1:8500/v1/acl/auth-method/keycloak" + method: PUT + headers: + X-Consul-Token: "{{ consul_master_token }}" + body_format: json + body: + Name: "keycloak" + Type: "jwt" + Description: "JWT Auth via Keycloak" + Config: + JWKSURL: "{{ keycloak_internal_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/certs" + BoundIssuer: "{{ keycloak_url }}/realms/{{ keycloak_realm }}" + JWTSupportedAlgs: ["RS256"] + BoundAudiences: ["{{ consul_oidc_client_id }}"] + ClaimMappings: + preferred_username: "username" + ListClaimMappings: + groups: "groups" + status_code: [200, 201, 204] + run_once: true + +# ✅ Step 4: Create Binding Rule for SSO Users +- name: Create Binding Rule for SSO Users + ansible.builtin.command: > + consul acl binding-rule create + -method="keycloak" + -bind-type="role" + -bind-name="readonly-role" + -selector='list.groups contains "consul-users"' + environment: + CONSUL_HTTP_ADDR: "http://127.0.0.1:8500" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + run_once: true + register: binding_rule_out + failed_when: + - binding_rule_out.rc != 0 + - "'already exists' not in binding_rule_out.stderr" diff --git a/tasks/rbac.yml b/tasks/rbac.yml new file mode 100644 index 0000000..b8d8c0d --- /dev/null +++ b/tasks/rbac.yml @@ -0,0 +1,88 @@ +--- +- name: Copy read policy file + ansible.builtin.copy: + src: read-policy.hcl + dest: /tmp/read-policy.hcl + mode: "0644" + +- name: Check existing policies + ansible.builtin.command: consul acl policy list + register: policy_list + changed_when: false + run_once: true + environment: + CONSUL_HTTP_ADDR: "{{ 'https' if consul_tls_enabled else 'http' }}://{{ consul_bind_addr }}:{{ consul_https_port if consul_tls_enabled else consul_http_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + +- name: Create read policy + ansible.builtin.command: > + consul acl policy create + -name read-policy + -rules @/tmp/read-policy.hcl + when: "'read-policy' not in policy_list.stdout" + run_once: true + environment: + CONSUL_HTTP_ADDR: "{{ 'https' if consul_tls_enabled else 'http' }}://{{ consul_bind_addr }}:{{ consul_https_port if consul_tls_enabled else consul_http_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + +- name: Check existing roles + ansible.builtin.command: consul acl role list + register: role_list + changed_when: false + run_once: true + environment: + CONSUL_HTTP_ADDR: "{{ 'https' if consul_tls_enabled else 'http' }}://{{ consul_bind_addr }}:{{ consul_https_port if consul_tls_enabled else consul_http_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + +- name: Create read role + ansible.builtin.command: > + consul acl role create + -name read-role + -policy-name read-policy + when: "'read-role' not in role_list.stdout" + run_once: true + environment: + CONSUL_HTTP_ADDR: "{{ 'https' if consul_tls_enabled else 'http' }}://{{ consul_bind_addr }}:{{ consul_https_port if consul_tls_enabled else consul_http_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + +# ------------------------------------------------ +# APPLICATION TOKEN (NO DUPLICATES) +# ------------------------------------------------ + +- name: Check if application token exists on disk + ansible.builtin.stat: + path: "{{ consul_config_dir }}/app.token" + register: app_token_stat + run_once: true + +- name: Create application token + ansible.builtin.command: > + consul acl token create + -description "App Read Token" + -role-name read-role + register: app_token_output + when: not app_token_stat.stat.exists + run_once: true + no_log: true + environment: + CONSUL_HTTP_ADDR: "{{ 'https' if consul_tls_enabled else 'http' }}://{{ consul_bind_addr }}:{{ consul_https_port if consul_tls_enabled else consul_http_port }}" + CONSUL_HTTP_TOKEN: "{{ consul_master_token }}" + CONSUL_HTTP_SSL_VERIFY: "{{ consul_tls_verify | ternary('true','false') }}" + +- name: Save application token + ansible.builtin.copy: + content: "{{ app_token_output.stdout | regex_search('SecretID:\\s+([a-fA-F0-9-]+)', '\\1') | first }}" + dest: "{{ consul_config_dir }}/app.token" + owner: "{{ consul_user }}" + group: "{{ consul_group }}" + mode: "0600" + when: + - not app_token_stat.stat.exists + - app_token_output.stdout is defined + - "'SecretID' in app_token_output.stdout" + run_once: true + no_log: true diff --git a/tasks/service.yml b/tasks/service.yml new file mode 100644 index 0000000..cf1e6a8 --- /dev/null +++ b/tasks/service.yml @@ -0,0 +1,13 @@ +- name: Deploy systemd service + ansible.builtin.template: + src: consul.service.j2 + dest: /etc/systemd/system/consul.service + mode: "0644" + notify: + - Restart Consul + +- name: Enable and start Consul + ansible.builtin.systemd: + name: consul + enabled: true + state: started diff --git a/templates/consul-hcl.j2 b/templates/consul-hcl.j2 deleted file mode 100644 index 98fd01d..0000000 --- a/templates/consul-hcl.j2 +++ /dev/null @@ -1,7 +0,0 @@ -datacenter = "{{ datacenter }}" -data_dir = "{{ consul_data_dir }}" -encrypt = "{{ hostvars[groups['consul-leader'][0]].encr_key.stdout }}" -retry_join = ["{{ hostvars[groups['consul-leader'][0]].ip_leader }}"] -performance { - raft_multiplier = {{ raft_mul }} -} diff --git a/templates/consul-service.j2 b/templates/consul-service.j2 deleted file mode 100644 index e25a5de..0000000 --- a/templates/consul-service.j2 +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description="{{ description }}" -Documentation={{ documentation }} -Requires={{ requires }} -After={{ after }} -ConditionFileNotEmpty={{ cond_file_not_empty }} - -[Service] -Type={{ type }} -User={{ consul_owner }} -Group={{ consul_group }} -ExecStart={{ exec_start }} -ExecReload={{ exec_reload }} -KillMode={{ kill_mode }} -Restart={{ restart }} -LimitNOFILE={{ limit_file }} - -[Install] -WantedBy={{ wanted_by }} diff --git a/templates/consul.service.j2 b/templates/consul.service.j2 new file mode 100644 index 0000000..281d3cc --- /dev/null +++ b/templates/consul.service.j2 @@ -0,0 +1,16 @@ +[Unit] +Description=Consul Agent +Requires=network-online.target +After=network-online.target + +[Service] +User={{ consul_user }} +Group={{ consul_group }} +ExecStart={{ consul_install_dir }}/consul agent -config-dir={{ consul_config_dir }} +ExecReload=/bin/kill -HUP $MAINPID +KillSignal=SIGINT +Restart=on-failure +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target diff --git a/templates/policies/agent-policy.hcl.j2 b/templates/policies/agent-policy.hcl.j2 new file mode 100644 index 0000000..6ad8420 --- /dev/null +++ b/templates/policies/agent-policy.hcl.j2 @@ -0,0 +1,11 @@ +node_prefix "" { + policy = "write" +} + +service_prefix "" { + policy = "read" +} + +agent_prefix "" { + policy = "write" +} diff --git a/templates/policies/monitoring-policy.hcl.j2 b/templates/policies/monitoring-policy.hcl.j2 new file mode 100644 index 0000000..d96bc6d --- /dev/null +++ b/templates/policies/monitoring-policy.hcl.j2 @@ -0,0 +1,9 @@ +agent_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "read" +} +service_prefix "" { + policy = "read" +} diff --git a/templates/policies/readonly-policy.hcl.j2 b/templates/policies/readonly-policy.hcl.j2 new file mode 100644 index 0000000..09b4b13 --- /dev/null +++ b/templates/policies/readonly-policy.hcl.j2 @@ -0,0 +1,11 @@ +node_prefix "" { + policy = "read" +} + +service_prefix "" { + policy = "read" +} + +key_prefix "" { + policy = "read" +} diff --git a/templates/policies/service-policy.hcl.j2 b/templates/policies/service-policy.hcl.j2 new file mode 100644 index 0000000..47bd12c --- /dev/null +++ b/templates/policies/service-policy.hcl.j2 @@ -0,0 +1,7 @@ +service_prefix "" { + policy = "write" +} + +node_prefix "" { + policy = "read" +} diff --git a/templates/prometheus-consul.yml.j2 b/templates/prometheus-consul.yml.j2 new file mode 100644 index 0000000..842060e --- /dev/null +++ b/templates/prometheus-consul.yml.j2 @@ -0,0 +1,27 @@ +# Prometheus scrape configuration for Consul Cluster +scrape_configs: + - job_name: 'consul-cluster' + scheme: https + metrics_path: '/v1/agent/metrics' + params: + format: ['prometheus'] + tls_config: + insecure_skip_verify: true + # This matches your Envoy DNS name + server_name: "consul.opstree.dev" + + # This pulls the token that was generated in your acl_tokens.yml task + bearer_token: "{{ consul_prometheus_token }}" + + static_configs: + - targets: +{% for host in play_hosts %} + - "{{ hostvars[host]['ansible_host'] }}:8501" +{% endfor %} + + # This cleans up the labels in Prometheus so you see the IP instead of IP:8501 + relabel_configs: + - source_labels: [__address__] + target_label: instance + regex: '([^:]+)(?::\d+)?' + replacement: '${1}' diff --git a/templates/server-hcl-leader.j2 b/templates/server-hcl-leader.j2 deleted file mode 100644 index 8e773c8..0000000 --- a/templates/server-hcl-leader.j2 +++ /dev/null @@ -1,2 +0,0 @@ -server = true -bootstrap_expect = 1 diff --git a/templates/server-hcl.j2 b/templates/server-hcl.j2 deleted file mode 100644 index cb15f46..0000000 --- a/templates/server-hcl.j2 +++ /dev/null @@ -1 +0,0 @@ -server = true diff --git a/templates/server.hcl.j2 b/templates/server.hcl.j2 new file mode 100644 index 0000000..067b339 --- /dev/null +++ b/templates/server.hcl.j2 @@ -0,0 +1,54 @@ +node_name = "{{ consul_node_name }}" +bind_addr = "{{ consul_bind_addr }}" +client_addr = "{{ consul_client_addr }}" +data_dir = "{{ consul_data_dir }}" + +encrypt = "{{ consul_gossip_key }}" + +server = {{ consul_is_server | lower }} +bootstrap_expect = {{ consul_bootstrap_expect }} + +ui_config { + enabled = {{ consul_enable_ui | lower }} +} + +retry_join = [ +{% for host in play_hosts %} + "{{ hostvars[host]['ansible_host'] | default(host) }}"{% if not loop.last %},{% endif %} +{% endfor %} +] + +telemetry { + prometheus_retention_time = "{{ consul_prometheus_retention }}" + disable_hostname = true +} + +acl { + enabled = {{ consul_acl_enabled | lower }} + default_policy = "{{ consul_acl_default_policy }}" + enable_token_persistence = {{ consul_acl_token_persistence | lower }} + + tokens { + # Added initial_management for SSO UI functionality + initial_management = "{{ consul_master_token }}" + {% if consul_agent_token is defined and consul_agent_token != "" %} + agent = "{{ consul_agent_token }}" + {% endif %} + } +} + +ports { + http = {{ consul_http_port }} + https = {{ consul_https_port }} +} + +tls { + defaults { + ca_file = "{{ consul_config_dir }}/certs/tls.crt" + cert_file = "{{ consul_config_dir }}/certs/tls.crt" + key_file = "{{ consul_config_dir }}/certs/tls.key" + + verify_incoming = false + verify_outgoing = false + } +} diff --git a/tests/test.yml b/tests/test.yml new file mode 100644 index 0000000..eca63c9 --- /dev/null +++ b/tests/test.yml @@ -0,0 +1,6 @@ +- name: Deploy Consul Cluster + hosts: all + become: true + roles: + - consul-role + diff --git a/vars/main.yml b/vars/main.yml index cc4abb6..5092e2d 100644 --- a/vars/main.yml +++ b/vars/main.yml @@ -1,10 +1,28 @@ --- -# consul binary -consul_binary_url: https://releases.hashicorp.com/consul/1.7.2/consul_1.7.2_linux_amd64.zip +# vars file for consul -# consul persistent storage -consul_data_dir: /opt/consul -# consul-hcl -datacenter: dc1 -raft_mul: 1 +# Installation variables +consul_binary_url: "https://releases.hashicorp.com/consul/{{ consul_version }}/consul_{{ consul_version }}_linux_amd64.zip" +consul_zip_path: "/tmp/consul_{{ consul_version }}.zip" + + +# Sensitive Keys +consul_master_token: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 64363638396462636134353637383865643931373664373034623536366134623235306438356164 + 3665393634643363396437333436363936303835343166650a343639663665373161376433613932 + 62343336303839303038376437393965396165633039636339326363396530636564313630326265 + 3134613636396265300a633636396164363365353066633964306534316163303264623764643532 + 39323234346661383638313135346537613530333537636461343631653639663232373632646665 + 3634393638313962393166316439633230643331383665623634 + + +consul_gossip_key: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 63666262323562663362346564363332353364663338396337333031616437373863316130613631 + 3966663365653036316364323537386530643666653564370a653438393136666436396230326464 + 39393738346630353432623434353063666161653832346438343566366664303464353334306532 + 3937633962373832620a353432336365323433306165633264363638353235643666633564393761 + 61353532316133616630396362333265316132333038323639396139333761326537636330346334 + 3336343330373533333435306264313430313662346364643637