ansible cleanup

This commit is contained in:
Graham McIntire 2025-07-23 12:34:33 -05:00
parent 8e6d434386
commit b80b2a9967
No known key found for this signature in database
46 changed files with 611 additions and 209 deletions

32
ansible/.gitignore vendored
View file

@ -1,3 +1,33 @@
# Ansible runtime files
ansible-playbook.log
*.retry
site.retry
fact_cache
# Fact cache
fact_cache/
# Python
__pycache__/
*.pyc
*.pyo
# Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Sensitive files
*.key
*.pem
vault-password.txt
# Temporary files
tmp/
temp/

104
ansible/CLAUDE.md Normal file
View file

@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
This is an Ansible infrastructure management repository that manages a mixed environment including network services, monitoring, VPN infrastructure, and web applications. The codebase supports multiple operating systems (Debian, Ubuntu, AlmaLinux) and uses a role-based architecture for modularity.
## Common Commands
### Bootstrap New Hosts
```bash
# Initial bootstrap (requires password)
make bootstrap-init HOSTNAME=hostname ANSIBLE_HOST=ip_address
# Full bootstrap with network config and reboot
make bootstrap HOSTNAME=hostname ANSIBLE_HOST=ip_address
```
### Run Playbooks
```bash
# Run main playbook
./run.sh
# Run all playbooks on specific host
make run-all HOSTNAME=hostname ANSIBLE_HOST=ip_address
# Run general playbook
ansible-playbook general.yml
# Run specific tags
ansible-playbook -t caddy general.yml
```
### Development Tasks
```bash
# Gather facts from all hosts
make facts
# Test changes on specific host
ansible-playbook -l hostname playbook.yml
# Check syntax
ansible-playbook --syntax-check playbook.yml
# Dry run
ansible-playbook --check playbook.yml
```
## Architecture
### Playbook Structure
- `playbook.yml` - Main playbook for common configuration, DNS, Tailscale, Caddy
- `general.yml` - General system setup including SNMP, K3s, Docker, syslog
- `bootstrap.yml` - Initial host preparation
### Role Organization
Roles follow a standard structure with:
- `tasks/main.yml` - Main task list
- `handlers/main.yml` - Service restart handlers
- `templates/` - Jinja2 templates for config files
- `vars/` - Role-specific variables
- `defaults/` - Default variable values
### Key Roles
- `base` - Combined base configuration (users, SSH, sudo, packages)
- `general` - General system configurations (motd, network, syslog)
- `caddy` - Web server and reverse proxy
- `ns` - Nameserver configuration
- `resolvers` - DNS resolver setup (Unbound)
- `docker` - Docker installation (stub)
- `k3s_servers` - Kubernetes K3s setup (stub)
- `snmp_clients` - SNMP client configuration (stub)
### Variable Precedence
1. Host-specific vars in `host_vars/`
2. Group vars in `group_vars/`
3. Role defaults in `roles/*/defaults/`
4. Base variables in `vars/`
### Service Dependencies
- SSH configuration managed by common role
- Firewall rules (firewalld) applied before service configs
- Handlers ensure services restart when configs change
- Tailscale VPN requires `TAILSCALE_KEY` environment variable
## Key Patterns
### User Management
- Ansible user (UID 10001) created on all hosts
- SSH keys fetched from GitHub for authentication
- Passwordless sudo configured for automation
### Network Services
- DNS resolvers use Unbound
- Nameservers configured via ns role
- Caddy serves as reverse proxy for web services
- Tailscale provides VPN connectivity
### Configuration Management
- Templates use host-specific variables
- Handlers manage service restarts
- Fact caching improves performance
- Collections required: ansible.posix, netbox.netbox

View file

@ -45,8 +45,13 @@ bootstrap-reboot:
bootstrap-init:
$(ANSIBLE_CMD) -l $(HOSTNAME) -e ansible_user=graham -e ansible_host=$(ANSIBLE_HOST) -k -K bootstrap.yml
bootstrap-init-root:
ANSIBLE_CONFIG=ansible-bootstrap.cfg $(ANSIBLE_CMD) -l $(HOSTNAME) -e ansible_user=root -e ansible_host=$(ANSIBLE_HOST) -e ansible_become=no bootstrap.yml
bootstrap: bootstrap-init bootstrap-network bootstrap-reboot
bootstrap-root: bootstrap-init-root bootstrap-network bootstrap-reboot
run-all:
$(ANSIBLE_CMD) -l $(HOSTNAME) -e ansible_host=$(ANSIBLE_HOST) general.yml

View file

@ -1 +1,60 @@
Ansible things
# Ansible Infrastructure Management
This repository contains Ansible playbooks and roles for managing infrastructure across multiple environments.
## Directory Structure
```
├── playbook.yml # Main playbook for common configuration
├── general.yml # General system setup playbook
├── bootstrap.yml # Bootstrap playbook for new hosts
├── roles/ # Ansible roles
│ ├── base/ # Base configuration (users, SSH, system)
│ ├── general/ # General system configurations
│ ├── caddy/ # Caddy web server
│ ├── ns/ # Nameserver configuration
│ ├── resolvers/ # DNS resolver setup
│ └── ... # Other roles
├── group_vars/ # Group-specific variables
├── host_vars/ # Host-specific variables
└── hosts # Inventory file
## Quick Start
### Bootstrap a new host
```bash
# For hosts with existing user access
make bootstrap-init HOSTNAME=hostname ANSIBLE_HOST=ip_address
# For hosts with only root access
make bootstrap-init-root HOSTNAME=hostname ANSIBLE_HOST=ip_address
```
### Run playbooks
```bash
# Run main playbook
./run.sh
# Run on specific host
ansible-playbook -l hostname playbook.yml
# Run with specific tags
ansible-playbook -t caddy playbook.yml
```
## Inventory Groups
- `vntx_servers` - VNTX network services
- `app_servers` - Application servers
- `home_servers` - Home network devices
- `tailscale_home` - Devices using Tailscale VPN
- `caddy_servers` - Servers running Caddy web server
## Key Features
- Automated user management with GitHub SSH key integration
- Base package installation across all systems
- Role-based configuration management
- Support for multiple OS families (Debian, Ubuntu, AlmaLinux)
- Tailscale VPN integration
- Caddy web server with reverse proxy configuration

View file

@ -0,0 +1,7 @@
[defaults]
host_key_checking = False
inventory = hosts
interpreter_python = auto_silent
[ssh_connection]
pipelining = true

View file

@ -3,7 +3,7 @@
hosts: all
tags: bootstrap
roles:
- bootstrap
- base
become: yes
# become_method: "{{ 'su' if (ansible_distribution | default('')) == 'Debian' else 'sudo' }}"
# become_method: "sudo"

View file

@ -5,8 +5,6 @@
become_method: sudo
roles:
- general
vars_files:
- vars/base.yml
tags:
- base
@ -41,8 +39,6 @@
hosts: netboot
become: yes
become_method: sudo
vars_files:
- vars/base.yml
- vars/netboot.yml
- vars/network.yml
vars:

View file

@ -0,0 +1,45 @@
---
# Global variables for all hosts
# Timezone configuration
timezone: America/Chicago
# GitHub usernames for SSH key retrieval
github_users:
- username: graham
github: gmcintire
- username: andy
github: nsnw
# Base packages to install on all systems
base:
packages:
- git
- fzf
- zsh
- jq
- sysstat
- smartmontools
- lm-sensors
- debconf-utils
- ntp
- ntpdate
- samba
- samba-common
- sssd
- krb5-user
- zsh
- wget
- vim
- htop
- mosh
- tmux
ntp:
server: 0.us.pool.ntp.org
# SSH service name (varies by distribution)
ssh_service: "{{ 'ssh' if ansible_os_family == 'Debian' else 'sshd' }}"
# NTP service name (varies by distribution)
ntp_service: "{{ 'ntp' if ansible_os_family == 'Debian' else 'ntpd' }}"

View file

@ -0,0 +1,6 @@
---
# Netbox server configuration
netbox:
domain: netbox.vntx.net
port: 8001
bind_address: tcp4/0.0.0.0

View file

@ -1,23 +0,0 @@
- name: Restart ntp
ansible.builtin.service:
name: ntp
state: restarted
become: true
- name: Restart ssh
ansible.builtin.service:
name: ssh
state: restarted
become: true
- name: Restart unbound
ansible.builtin.service:
name: unbound
state: restarted
become: true
- name: Restart firewalld
ansible.builtin.service:
name: firewalld
state: restarted
become: true

View file

@ -0,0 +1,3 @@
---
# Host-specific variables for dallas.aprs2.net
ansible_host: 204.110.191.232

View file

@ -0,0 +1,3 @@
---
# Host-specific variables for g.w5isp.com
ansible_user: ansible

View file

@ -0,0 +1,3 @@
---
# Host-specific variables for lab01.w5isp.com
ansible_host: 10.0.16.231

View file

@ -0,0 +1,3 @@
---
# Host-specific variables for lab02.w5isp.com
ansible_host: 10.0.16.232

View file

@ -0,0 +1,3 @@
---
# Host-specific variables for lab03.w5isp.com
ansible_host: 10.0.16.233

View file

@ -1,4 +1,4 @@
[vntx]
[vntx_servers]
#logs.vntx.net
#unimus.vntx.net
monitor.vntx.net
@ -10,25 +10,25 @@ ns1.vntx.net
ns2.vntx.net
#ntp.vntx.net
[common]
[app_servers]
#aprs.me
dallas.aprs2.net ansible_host=204.110.191.232
dallas.aprs2.net
apps.w5isp.com
[home]
[home_servers]
skippy.w5isp.com
g.w5isp.com ansible_user=ansible
#[proxmox]
#lab01 ansible_host=10.0.16.231 #ansible_user=root
#lab02 ansible_host=10.0.16.232 #ansible_user=root
#lab03 ansible_host=10.0.16.233 #ansible_user=root
[tailscale_home]
lab01.w5isp.com ansible_host=10.0.16.231 #ansible_user=root
lab02.w5isp.com ansible_host=10.0.16.232 #ansible_user=root
lab03.w5isp.com ansible_host=10.0.16.233 #ansible_user=root
g.w5isp.com
[caddy]
g.w5isp.com ansible_user=ansible
#[proxmox]
#lab01.w5isp.com
#lab02.w5isp.com
#lab03.w5isp.com
[tailscale_home]
lab01.w5isp.com
lab02.w5isp.com
lab03.w5isp.com
g.w5isp.com
[caddy_servers]
g.w5isp.com

View file

@ -6,28 +6,14 @@
gather_facts: true
remote_user: ansible
roles:
- common
vars_files:
- vars/common.yml
- base
tags:
- common
tasks:
- name: Set timezone
community.general.timezone:
name: America/Chicago
- name: Install Graham SSH keys
ansible.posix.authorized_key:
user: graham
state: present
key: https://github.com/gmcintire.keys
- name: Install Andys SSH keys
ansible.posix.authorized_key:
user: andy
state: present
key: https://github.com/nsnw.keys
name: "{{ timezone }}"
- name: Apply resolver config
hosts: resolvers
@ -53,8 +39,8 @@
caddy_systemd_capabilities_enabled: true
caddy_systemd_capabilities: "CAP_NET_BIND_SERVICE"
caddy_config: |
netbox.vntx.net {
bind tcp4/0.0.0.0
{{ netbox.domain }} {
bind {{ netbox.bind_address }}
log {
level error
}
@ -65,7 +51,7 @@
}
@notStatic not path /static*
encode gzip zstd
reverse_proxy @notStatic http://localhost:8001 {
reverse_proxy @notStatic http://localhost:{{ netbox.port }} {
header_up Host {http.request.host}
header_up X-Real-IP {http.request.remote.host}
header_up X-Forwarded-For {http.request.remote.host}
@ -78,6 +64,8 @@
hosts: netbox
become: true
# remote_user: graham
handlers:
- import_tasks: playbook_handlers.yml
tasks:
- name: Permit traffic in default zone for http service
@ -110,7 +98,7 @@
msg: "Key exists and starts with: {{ lookup('ansible.builtin.env', 'TAILSCALE_KEY')[0:10] }}..."
- name: Install and configure Caddy
hosts: caddy
hosts: caddy_servers
become: true
roles:
- caddy

View file

@ -0,0 +1,8 @@
---
# Handlers for tasks in playbook.yml
- name: Restart firewalld
ansible.builtin.service:
name: firewalld
state: restarted
become: true

View file

@ -0,0 +1,10 @@
---
- name: restart ntp
service:
name: "{{ ntp_service | default('ntp') }}"
state: restarted
- name: restart ssh
service:
name: "{{ ssh_service | default('sshd') }}"
state: restarted

View file

@ -0,0 +1,38 @@
---
# Base role - combines bootstrap and common functionality
# Initial system information
- name: Show hostname
debug:
msg: "{{ ansible_hostname }}"
- name: Show IPv4 address
debug:
msg: "{{ ansible_default_ipv4.address }}"
when: ansible_default_ipv4.address is defined
- name: Show IPv6 address
debug:
msg: "{{ ansible_default_ipv6.address }}"
when: ansible_default_ipv6.address is defined
# User and group setup
- import_tasks: users.yml
tags:
- users
- bootstrap
# Base system configuration
- import_tasks: system.yml
tags:
- system
- common
# OS-specific configurations
- include_role:
name: almalinux
when: ansible_os_family == "RedHat"
- include_role:
name: debian
when: ansible_os_family == "Debian"

View file

@ -0,0 +1,31 @@
---
# System configuration tasks
- name: Install SSH banner
template:
src: issue.net.j2
dest: /etc/issue.net
mode: '0644'
tags:
- ssh
- name: Configure SSH to use banner
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^Banner'
line: 'Banner /etc/issue.net'
state: present
notify: restart ssh
tags:
- ssh
# Uncomment if you want to disable root SSH access
# - name: Disallow root SSH access
# lineinfile:
# path: /etc/ssh/sshd_config
# regexp: "^PermitRootLogin"
# line: "PermitRootLogin no"
# state: present
# notify: restart ssh
# tags:
# - ssh

View file

@ -0,0 +1,66 @@
---
# User and group management tasks
- name: Install sudo package (Debian)
apt:
name: sudo
state: present
when: ansible_os_family == "Debian"
- name: Create ansible group
group:
name: ansible
gid: 10001
state: present
- name: Create ansible user
user:
name: ansible
group: ansible
uid: 10001
shell: /bin/bash
home: /home/ansible
state: present
- name: Create graham user
user:
name: graham
shell: /bin/bash
home: /home/graham
state: present
- name: Create andy user
user:
name: andy
shell: /bin/bash
home: /home/andy
state: present
- name: Set up SSH key for ansible user
authorized_key:
user: ansible
key: "{{ lookup('file', 'ansible.pub') }}"
exclusive: true
state: present
- name: Configure sudo for ansible user
template:
src: sudoers_ansible.j2
dest: /etc/sudoers.d/ansible
mode: '0440'
validate: 'visudo -cf %s'
- name: Enable passwordless sudo for %sudo
lineinfile:
path: /etc/sudoers
state: present
regexp: '^%sudo'
line: '%sudo ALL=(ALL) NOPASSWD: ALL'
validate: 'visudo -cf %s'
- name: Install SSH keys from GitHub
ansible.posix.authorized_key:
user: "{{ item.username }}"
state: present
key: "https://github.com/{{ item.github }}.keys"
loop: "{{ github_users }}"

View file

@ -0,0 +1 @@
ansible ALL=NOPASSWD: ALL

View file

@ -1,44 +0,0 @@
- name: Install sudo
apt:
name: sudo
when: ansible_distribution == "Debian"
- name: Ensure the 'ansible' group exists
group:
name: ansible
gid: 10001
state: present
- name: Ensure the 'ansible' user exists
user:
name: ansible
comment: Ansible User
uid: 10001
group: ansible
- name: Ensure the 'graham' user exists
user:
name: graham
comment: Graham
- name: Ensure the 'andy' user exists
user:
name: andy
comment: Andy
- name: Ensure that 'authorized_keys' is set correctly for the 'ansible' user
authorized_key:
user: ansible
state: present
key: "{{ lookup('file', 'files/ansible.pub') }}"
- name: Ensure sudo is configured for the 'ansible' user
template:
src: templates/etc/sudoers.d/ansible
dest: /etc/sudoers.d/ansible
owner: root
group: root
mode: '0440'
tags:
- sudoers

View file

@ -1,11 +0,0 @@
- name: Dump host information
debug:
msg:
- "Host : {{ inventory_hostname }}"
# - "Distro : {{ ansible_lsb.id }}"
# - "Release: {{ ansible_lsb.codename }}"
- "IPv4 : {{ ansible_all_ipv4_addresses }}"
- "IPv6 : {{ ansible_all_ipv6_addresses }}"
- name: Ansible setup
import_tasks: ansible.yml

View file

@ -1 +0,0 @@
{{ inventory_hostname }}

View file

@ -1,6 +0,0 @@
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
127.0.1.1 {{ inventory_hostname }} {{ inventory_hostname | split('.') | first }}

View file

@ -1 +0,0 @@
ansible ALL=NOPASSWD: ALL

View file

@ -1 +0,0 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFghGMnDdkfNC6JPEhMxrKhYDmNcXjHXp/y/mf3uLtzb Ansible SSH Key

View file

@ -1,14 +0,0 @@
---
# Handler to handle common notifications. Handlers are called by other plays.
# See http://docs.ansible.com/playbooks_intro.html for more information about handlers.
- name: Restart ntp
ansible.builtin.service:
name: ntp
state: restarted
become: true
# - name: Restart ssh
# ansible.builtin.service:
# name: ssh
# state: restarted
# become: true

View file

@ -1,38 +0,0 @@
---
- name: "Enable passwordless sudo"
ansible.builtin.lineinfile:
path: /etc/sudoers
state: present
regexp: "^%sudo"
line: "%sudo ALL=(ALL) NOPASSWD: ALL"
validate: "visudo -cf %s"
- name: "Apply AlmaLinux specific config"
when: ansible_distribution == 'AlmaLinux'
ansible.builtin.import_role:
name: almalinux
- name: "Apply Debian specific config"
when: ansible_distribution == 'Debian' or ansible_distribution == 'Ubuntu'
ansible.builtin.import_role:
name: debian
- name: Install sshd banner
ansible.builtin.template:
src: issue.net.j2
dest: /etc/issue.net
mode: "644"
- name: Setup sshd banner
ansible.builtin.lineinfile:
dest: /etc/ssh/sshd_config
line: "Banner /etc/issue.net"
state: present
notify: Restart ssh
# - name: Disallow root SSH access
# ansible.builtin.lineinfile:
# dest: /etc/ssh/sshd_config
# regexp: "^PermitRootLogin"
# line: "PermitRootLogin no"
# state: present
# notify: Restart ssh

View file

@ -1,25 +0,0 @@
8b d8
`8b d8'
`8b d8'
`8b d8' ,adPPYba, 8b,dPPYba, ,adPPYba, 8b,dPPYba, ,adPPYYba,
`8b d8' a8P_____88 88P' "Y8 a8" "8a 88P' `"8a "" `Y8
`8b d8' 8PP""""""" 88 8b d8 88 88 ,adPPPPP88
`888' "8b, ,aa 88 "8a, ,a8" 88 88 88, ,88
`8' `"Ybbd8"' 88 `"YbbdP"' 88 88 `"8bbdP"Y8
888b 88 88
8888b 88 ,d 88
88 `8b 88 88 88
88 `8b 88 ,adPPYba, MM88MMM 8b db d8 ,adPPYba, 8b,dPPYba, 88 ,d8 ,adPPYba,
88 `8b 88 a8P_____88 88 `8b d88b d8' a8" "8a 88P' "Y8 88 ,a8" I8[ ""
88 `8b 88 8PP""""""" 88 `8b d8'`8b d8' 8b d8 88 8888[ `"Y8ba,
88 `8888 "8b, ,aa 88, `8bd8' `8bd8' "8a, ,a8" 88 88`"Yba, aa ]8I
88 `888 `"Ybbd8"' "Y888 YP YP `"YbbdP"' 88 88 `Y8a `"YbbdP"'
###############################################################
# Welcome to Verona Networks #
# All connections are monitored and recorded #
# Disconnect IMMEDIATELY if you are not an authorized user! #
###############################################################

View file

@ -0,0 +1,3 @@
---
# Docker installation and configuration tasks
# TODO: Implement Docker installation

View file

@ -0,0 +1,35 @@
---
# General role - system-wide configurations
# Import Debian-specific tasks
- import_tasks: debian/misc.yml
when: ansible_os_family == "Debian"
tags:
- misc
- import_tasks: debian/motd.yml
when: ansible_os_family == "Debian"
tags:
- motd
- import_tasks: debian/network.yml
when: ansible_os_family == "Debian"
tags:
- network
- import_tasks: debian/syslog.yml
when: ansible_os_family == "Debian"
tags:
- syslog
- import_tasks: debian/icinga2.yml
when: ansible_os_family == "Debian"
tags:
- icinga2
- monitoring
- import_tasks: debian/k3s.yml
when: ansible_os_family == "Debian"
tags:
- k3s
- kubernetes

View file

@ -1 +0,0 @@
ansible ALL=NOPASSWD: ALL

View file

@ -0,0 +1,3 @@
---
# K3s server configuration tasks
# TODO: Implement K3s server installation and configuration

View file

@ -0,0 +1,3 @@
---
# Netboot server configuration tasks
# TODO: Implement netboot server configuration

View file

@ -0,0 +1,3 @@
---
# SNMP client configuration tasks
# TODO: Implement SNMP client configuration

View file

@ -0,0 +1,3 @@
---
# Syslog server configuration tasks
# TODO: Implement syslog server configuration

3
ansible/vars/common.yml Normal file
View file

@ -0,0 +1,3 @@
---
# Common variables used across playbooks
# Add common configuration here

5
ansible/vars/netboot.yml Normal file
View file

@ -0,0 +1,5 @@
---
# Netboot server configuration
netboot:
dist: []
arch: []

3
ansible/vars/network.yml Normal file
View file

@ -0,0 +1,3 @@
---
# Network configuration variables
# Add network-related configuration here

97
terraform/CLAUDE.md Normal file
View file

@ -0,0 +1,97 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
### Terraform Workflow
```bash
# Initialize terraform providers
terraform init
# Validate configuration
terraform validate
# Plan changes
terraform plan
# Apply changes
terraform apply
# Show current state
terraform show
# Format terraform files
terraform fmt
```
### State Management
```bash
# List resources in state
terraform state list
# Show specific resource
terraform state show <resource_name>
# Import existing resources
terraform import <resource_type>.<name> <id>
```
## Architecture
This Terraform codebase manages DNS infrastructure across multiple providers:
### DNS Providers
- **DNSimple**: Primary domain registrar and DNS hosting
- **Porkbun**: Alternative DNS provider
- **PowerDNS**: Manages reverse DNS zones (204.110.188-191.x ranges)
### File Organization
- `main.tf`: Provider configurations and requirements
- `vars.tf`: Variable definitions (⚠️ contains hardcoded credentials that should be moved to environment variables)
- `dns_*.tf`: DNS zone and record configurations organized by domain/provider
- State is managed locally (`terraform.tfstate`)
### Managed Domains
- w5isp.com
- manero.org
- mcintire.me
- vntx.net, vntx.org
- gridmap.org
- Reverse DNS zones for 204.110.188-191.x IP ranges
### Security Considerations
**Critical**: API keys and tokens are hardcoded in `vars.tf` defaults. When making changes:
1. Never commit new credentials to the repository
2. Use environment variables: `export TF_VAR_variable_name=value`
3. Or use `-var` flags: `terraform apply -var="dnsimple_token=xxx"`
### Common Tasks
#### Adding DNS Records
1. Identify the appropriate `dns_*.tf` file for the domain
2. Add the record using the appropriate resource type:
```hcl
resource "dnsimple_zone_record" "example" {
zone_name = "domain.com"
name = "subdomain"
value = "192.168.1.1"
type = "A"
ttl = 3600
}
```
#### Importing Existing DNS Records
```bash
# DNSimple records
terraform import dnsimple_zone_record.name {zone_id}_{record_id}
# PowerDNS records
terraform import powerdns_record.name zone.com.:record_name:type
```
### Development Notes
- No modules are used; resources are organized by domain/function
- No automated testing or CI/CD pipeline exists
- Direct commits to main branch are the current workflow
- Run `terraform fmt` before committing to ensure consistent formatting