Ansible-based infrastructure automation for server normalization.
- DDNS: Dynamic DNS registration via dynv6.com
- Passwordless sudo: Configures sudo group for passwordless access
- User management: Creates user from
ANSIBLE_USERenv var
.
├── .github/workflows/
│ └── deploy.yaml # CI/CD workflow (supports manual runs)
├── ansible/
│ ├── ansible.cfg # Ansible configuration
│ ├── normalize.yaml # Main playbook
│ ├── inventory/
│ │ └── dev/
│ │ └── hosts.yaml # Development inventory
│ └── roles/
│ ├── ddns/ # Dynamic DNS registration
│ ├── sudo/ # Passwordless sudo configuration
│ ├── user/ # User creation and management
│ └── nftables/ # Firewall rules
├── pyproject.toml # Python dependencies
└── README.md
The normalize.yaml playbook applies the following roles in order:
-
sudo - Configures passwordless sudo access
- Creates sudoers configuration for
sudogroup members - Enables passwordless sudo for administrative tasks
- Creates sudoers configuration for
-
ddns - Registers host with dynv6.com DDNS service
- Deploys update script and systemd service
- Configures periodic IP updates via cron
-
user - Creates and configures user account
- Creates user from
ANSIBLE_USERenvironment variable - Adds user to
sudogroup - Sets system-wide
ENVIRONMENT=devmarker
- Creates user from
Note: The nftables role exists in ansible/roles/ but is not currently applied by the playbook.
cd ansible
ansible-playbook normalize.yaml -vEdit ansible/inventory/dev/hosts.yaml:
hosts:
host_2:
ansible_host: "{{ lookup('env', 'HOST_2') | default('failed_to_provide_host') }}"
ansible_host_fallback: "{{ lookup('env', 'HOST_2_IP') | default('') }}"
ansible_become_password: "{{ lookup('env', 'HOST_2_ROOT_PASSWORD') | default('') }}"
ddns_hostname: "{{ lookup('env', 'HOST_2') | default('failed_to_provide_host') }}"
ddns_token: "{{ lookup('env', 'DDNS_TOKEN') | default('failed_to_provide_ddns_token') }}"Edit .github/workflows/deploy.yaml to add environment variables:
- name: Set environment variables
run: |
# ... existing vars ...
# - host_2
echo "HOST_2=${{ vars.HOST_2 }}" >> $GITHUB_ENV
echo "HOST_2_IP=${{ vars.HOST_2_IP }}" >> $GITHUB_ENV
echo "HOST_2_ROOT_PASSWORD=${{ secrets.HOST_2_ROOT_PASSWORD }}" >> $GITHUB_ENVIn your GitHub repository settings:
Variables (Settings → Secrets and variables → Actions → Variables):
HOST_2: Hostname (e.g.,server.example.com)HOST_2_IP: Fallback IP address (e.g.,203.0.113.1)
Secrets (Settings → Secrets and variables → Actions → Secrets):
HOST_2_ROOT_PASSWORD: Root/sudo password for initial setup
ANSIBLE_USER: Username to create (default: ansible)DDNS_TOKEN: Dynamic DNS tokenHOST_N: Hostname for host NHOST_N_IP: Fallback IP for host NHOST_N_ROOT_PASSWORD: Root password for host N
Note: INBOUND_ALLOWED_IPV4_SUBNETS and INBOUND_ALLOWED_IPV6_SUBNETS are defined in the workflow but not used since the nftables role is not applied.
If the primary hostname (HOST_N) is unreachable, Ansible automatically falls back to the IP address (HOST_N_IP). Connection timeout is 10 seconds.
GitHub Actions workflow (.github/workflows/deploy.yaml):
- Manual runs enabled: Trigger workflow manually via
workflow_dispatchwith environment selection (dev/prod) - Automatic runs: Triggers on merge commits to
developmentbranch - Steps: Syntax check → ansible-lint → deploy to remote host
- Python version: 3.12 (CI/CD), >=3.13 (local development per pyproject.toml)