initial commit

This commit is contained in:
Riedel
2026-08-19 09:53:22 +02:00
commit 6d707eca87
23 changed files with 671 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.vault_pass
vault/secrets.yml
*.retry
*.pyc
__pycache__/
.ansible/

112
README.md Normal file
View File

@@ -0,0 +1,112 @@
# Ansible Repository - AWX and GitLab Bootstrap
Automated setup of two independent single-VM services on Rocky Linux:
- **AWX**, deployed via the AWX Operator on a single-node k3s cluster
- **GitLab CE**, deployed via the official Omnibus package
Each service is meant to run on its own dedicated, otherwise-blank VM.
## Requirements
```bash
ansible-galaxy collection install -r requirements.yml
```
Target hosts: Rocky Linux 9 (minimal install), reachable via SSH with an
administrative sudo-capable account, with outbound internet access
(both roles pull packages/manifests from public sources at install time).
### Sizing recommendations
| Host | Minimum | Recommended |
|---|---|---|
| AWX (k3s + operator) | 2 vCPU / 4 GB RAM | 4 vCPU / 8 GB RAM |
| GitLab CE | 2 vCPU / 4 GB RAM (with `gitlab_install_reduce_memory_footprint: true`) | 4 vCPU / 8 GB RAM |
`common_min_memory_mb` in group_vars enforces a baseline check before
either install proceeds.
## Getting started
1. Update `inventories/production/hosts.ini` with real hostnames/IPs.
2. Review `inventories/production/group_vars/all.yml`,
`group_vars/awx_servers.yml`, and `group_vars/gitlab_servers.yml`.
3. Check the current release tags before rollout - both are pinned on
purpose for reproducibility and should not be left at whatever was
current when this repo was written:
- k3s: https://github.com/k3s-io/k3s/releases (`k3s_version`)
- AWX Operator: https://github.com/ansible/awx-operator/releases (`awx_operator_version`)
4. Set up secrets:
```bash
cp vault/secrets.yml.example vault/secrets.yml
# fill in awx_admin_password and gitlab_root_password
ansible-vault encrypt vault/secrets.yml
```
5. Store the vault password in `.vault_pass` (chmod 600, do not commit it).
## Running
```bash
# Both services
ansible-playbook playbooks/site.yml --ask-vault-pass
# AWX only
ansible-playbook playbooks/awx.yml --ask-vault-pass
# GitLab only
ansible-playbook playbooks/gitlab.yml --ask-vault-pass
```
AWX deployment can take 10-20 minutes after the operator applies the
custom resource (image pulls, database init); the role waits for the web
deployment to become available with a generous timeout, but a first run
on a slow connection can still take longer.
## Quality checks
```bash
ansible-lint
ansible-playbook playbooks/site.yml --syntax-check
```
## Role overview
| Role | Purpose |
|---|---|
| `common` | Base packages, time sync, hostname, firewalld, memory check |
| `k3s_install` | Single-node k3s cluster, kernel/sysctl prerequisites, Python kubernetes client |
| `awx_operator` | AWX Operator + AWX custom resource via k3s |
| `gitlab_install` | GitLab CE Omnibus package, `gitlab.rb`, firewall, health check |
## Access after rollout
- **AWX**: `http://<awx-host>:<awx_operator_nodeport>` (default port 30080), user
`admin`, password from `vault/secrets.yml` (`awx_admin_password`).
- **GitLab**: `<gitlab_install_external_url>`, user `root`, password from
`vault/secrets.yml` (`gitlab_root_password`) - change it on first login.
## Open items before rollout
- Pin and verify `k3s_version` and `awx_operator_version` against current
upstream releases (see links above).
- The GitLab yum repository definition (`roles/gitlab_install/tasks/main.yml`)
uses the primary GitLab GPG key; cross-check against
https://packages.gitlab.com/gitlab/gitlab-ce/install if package
verification fails on first run.
- AWX is exposed via plain HTTP NodePort by default
(`awx_operator_service_type`/`awx_operator_nodeport`) - put a reverse proxy or load
balancer with TLS in front of it for anything beyond initial testing.
- GitLab's `external_url` defaults to `https://` with
`gitlab_install_letsencrypt_enabled: false`, so Omnibus will auto-generate a
self-signed certificate on first `reconfigure`. Enable Let's Encrypt or
supply real certificates before using this beyond initial testing.
- Both roles assume outbound internet access on the target VM (k3s
install script, AWX operator manifests from GitHub, GitLab packages).
Air-gapped rollout would need a different distribution mechanism and is
not covered here.
- `gitlab_install_version` is unpinned by default (installs latest) - pin it for
reproducible deployments once you've qualified a specific version.
- No backup/restore automation is included for either service - both
hold state (AWX: Postgres in a k3s PVC; GitLab: `/var/opt/gitlab`) that
needs its own backup strategy before production use.

21
ansible.cfg Normal file
View File

@@ -0,0 +1,21 @@
[defaults]
inventory = inventories/production/hosts.ini
roles_path = roles
remote_user = ansible_svc
host_key_checking = True
retry_files_enabled = False
interpreter_python = auto_silent
vault_password_file = .vault_pass
stdout_callback = default
result_format = yaml
forks = 5
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey

View File

@@ -0,0 +1,9 @@
---
# Applies to all hosts in this inventory.
timezone: "Europe/Berlin"
ntp_servers:
- "0.de.pool.ntp.org"
- "1.de.pool.ntp.org"
firewall_default_zone: "drop"
firewall_admin_subnet: "10.10.5.0/24"
common_min_memory_mb: 4096

View File

@@ -0,0 +1,5 @@
---
# Check the current stable k3s release before rollout:
# https://github.com/k3s-io/k3s/releases
k3s_version: "v1.30.6+k3s1"
k3s_disable_traefik: true

View File

@@ -0,0 +1,3 @@
---
gitlab_install_external_url: "https://gitlab.example.corp"
gitlab_install_edition: "ce"

View File

@@ -0,0 +1,12 @@
; Example inventory - update hostnames/IPs before first run.
; Two independent single-VM targets: one for AWX (via k3s), one for GitLab.
[awx_servers]
awx01 ansible_host=10.10.30.11
[gitlab_servers]
gitlab01 ansible_host=10.10.30.12
[all:vars]
ansible_user=ansible_svc
ansible_python_interpreter=/usr/bin/python3

17
playbooks/awx.yml Normal file
View File

@@ -0,0 +1,17 @@
---
# AWX only. Run: ansible-playbook playbooks/awx.yml --ask-vault-pass
- name: Base setup
hosts: awx_servers
become: true
roles:
- role: common
- name: Install and configure AWX (via k3s)
hosts: awx_servers
become: true
vars_files:
- ../vault/secrets.yml
roles:
- role: k3s_install
- role: awx_operator

16
playbooks/gitlab.yml Normal file
View File

@@ -0,0 +1,16 @@
---
# GitLab only. Run: ansible-playbook playbooks/gitlab.yml --ask-vault-pass
- name: Base setup
hosts: gitlab_servers
become: true
roles:
- role: common
- name: Install and configure GitLab
hosts: gitlab_servers
become: true
vars_files:
- ../vault/secrets.yml
roles:
- role: gitlab_install

26
playbooks/site.yml Normal file
View File

@@ -0,0 +1,26 @@
---
# Full rollout: base setup on all hosts, then AWX and GitLab in parallel groups.
# Full run: ansible-playbook playbooks/site.yml --ask-vault-pass
- name: Base setup for all hosts
hosts: awx_servers:gitlab_servers
become: true
roles:
- role: common
- name: Install and configure AWX (via k3s)
hosts: awx_servers
become: true
vars_files:
- ../vault/secrets.yml
roles:
- role: k3s_install
- role: awx_operator
- name: Install and configure GitLab
hosts: gitlab_servers
become: true
vars_files:
- ../vault/secrets.yml
roles:
- role: gitlab_install

9
requirements.yml Normal file
View File

@@ -0,0 +1,9 @@
---
# Install with: ansible-galaxy collection install -r requirements.yml
collections:
- name: kubernetes.core
version: ">=3.0.0"
- name: community.general
version: ">=8.0.0"
- name: ansible.posix
version: ">=1.5.0"

View File

@@ -0,0 +1,12 @@
---
# Check the current release tag before rollout:
# https://github.com/ansible/awx-operator/releases
awx_operator_version: "2.19.1"
awx_operator_namespace: "awx"
awx_operator_name: "awx"
awx_operator_admin_user: "admin"
awx_operator_service_type: "NodePort"
awx_operator_nodeport: 30080
awx_operator_postgres_storage_size: "8Gi"
awx_operator_web_replicas: 1
awx_operator_task_replicas: 1

View File

@@ -0,0 +1,73 @@
---
- name: Create the AWX namespace
kubernetes.core.k8s:
name: "{{ awx_operator_namespace }}"
api_version: v1
kind: Namespace
state: present
tags: [awx_operator]
- name: Deploy the AWX Operator (pinned version)
ansible.builtin.command: >
/usr/local/bin/k3s kubectl apply -k
github.com/ansible/awx-operator/config/default?ref={{ awx_operator_version }}
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
register: awx_operator_deploy_result
changed_when: "'unchanged' not in awx_operator_deploy_result.stdout"
tags: [awx_operator]
- name: Set the operator's default namespace annotation
ansible.builtin.command: >
/usr/local/bin/k3s kubectl config set-context --current --namespace={{ awx_operator_namespace }}
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
changed_when: true
tags: [awx_operator]
- name: Wait for the AWX Operator deployment to become available
ansible.builtin.command: >
/usr/local/bin/k3s kubectl wait deployment/awx-operator-controller-manager
-n {{ awx_operator_namespace }} --for=condition=Available --timeout=300s
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
changed_when: false
tags: [awx_operator]
- name: Create the AWX admin password secret
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ awx_operator_name }}-admin-password"
namespace: "{{ awx_operator_namespace }}"
type: Opaque
stringData:
password: "{{ awx_admin_password }}"
no_log: true
tags: [awx_operator]
- name: Deploy the AWX custom resource
kubernetes.core.k8s:
state: present
definition: "{{ lookup('ansible.builtin.template', 'awx-instance.yml.j2') }}"
tags: [awx_operator]
- name: Wait for the AWX web deployment to become available
ansible.builtin.command: >
/usr/local/bin/k3s kubectl wait deployment/{{ awx_operator_name }}-web
-n {{ awx_operator_namespace }} --for=condition=Available --timeout=900s
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
changed_when: false
register: awx_operator_web_ready
tags: [awx_operator]
- name: Report AWX access details
ansible.builtin.debug:
msg: >-
AWX is reachable at http://{{ ansible_host | default(inventory_hostname) }}:{{ awx_operator_nodeport }}
with user '{{ awx_operator_admin_user }}'. Password is stored in vault/secrets.yml (awx_admin_password).
tags: [awx_operator]

View File

@@ -0,0 +1,17 @@
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: {{ awx_operator_name }}
namespace: {{ awx_operator_namespace }}
spec:
service_type: {{ awx_operator_service_type }}
{% if awx_operator_service_type == "NodePort" %}
nodeport_port: {{ awx_operator_nodeport }}
{% endif %}
admin_user: {{ awx_operator_admin_user }}
admin_password_secret: {{ awx_operator_name }}-admin-password
postgres_storage_requirements:
requests:
storage: {{ awx_operator_postgres_storage_size }}
web_replicas: {{ awx_operator_web_replicas }}
task_replicas: {{ awx_operator_task_replicas }}

View File

@@ -0,0 +1,5 @@
---
- name: restart chronyd
ansible.builtin.systemd:
name: chronyd
state: restarted

View File

@@ -0,0 +1,75 @@
---
- name: Install base packages
ansible.builtin.dnf:
name:
- curl
- vim-enhanced
- chrony
- firewalld
- sudo
- tar
- policycoreutils-python-utils
state: present
tags: [common]
- name: Set timezone
community.general.timezone:
name: "{{ timezone }}"
tags: [common]
- name: Deploy chrony configuration
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony.conf
owner: root
group: root
mode: "0644"
notify: restart chronyd
tags: [common]
- name: Enable and start chronyd
ansible.builtin.systemd:
name: chronyd
enabled: true
state: started
tags: [common]
- name: Set hostname from inventory
ansible.builtin.hostname:
name: "{{ inventory_hostname }}"
tags: [common]
- name: Enable and start firewalld
ansible.builtin.systemd:
name: firewalld
enabled: true
state: started
tags: [common]
- name: Set default firewalld zone
ansible.builtin.command: "firewall-cmd --set-default-zone={{ firewall_default_zone }}"
changed_when: true
tags: [common]
- name: Allow SSH from the administration subnet
ansible.posix.firewalld:
zone: "{{ firewall_default_zone }}"
rich_rule: >-
rule family="ipv4" source address="{{ firewall_admin_subnet }}"
port protocol="tcp" port="22" accept
permanent: true
immediate: true
state: enabled
tags: [common]
- name: Check available memory meets the minimum requirement
ansible.builtin.assert:
that:
- ansible_facts['memtotal_mb'] >= common_min_memory_mb
fail_msg: >-
{{ inventory_hostname }} has {{ ansible_facts['memtotal_mb'] }} MB RAM,
below the configured minimum of {{ common_min_memory_mb }} MB
(common_min_memory_mb). Both AWX/k3s and GitLab are memory-hungry;
review sizing before continuing.
success_msg: "Memory check passed ({{ ansible_facts['memtotal_mb'] }} MB available)."
tags: [common]

View File

@@ -0,0 +1,9 @@
# {{ ansible_managed }}
{% for server in ntp_servers %}
server {{ server }} iburst
{% endfor %}
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony

View File

@@ -0,0 +1,8 @@
---
gitlab_install_external_url: "https://gitlab.example.corp"
gitlab_install_edition: "ce"
gitlab_install_version: "" # empty = latest available; pin e.g. "17.5.2-ce.0" for reproducible installs
gitlab_install_reduce_memory_footprint: true
gitlab_install_puma_workers: 2
gitlab_install_sidekiq_concurrency: 10
gitlab_install_letsencrypt_enabled: false

View File

@@ -0,0 +1,4 @@
---
- name: reconfigure gitlab
ansible.builtin.command: gitlab-ctl reconfigure
changed_when: true

View File

@@ -0,0 +1,74 @@
---
- name: Install prerequisite packages
ansible.builtin.dnf:
name:
- curl
- policycoreutils-python-utils
- openssh-server
- perl
- firewalld
state: present
tags: [gitlab_install]
- name: Configure the GitLab package repository
ansible.builtin.yum_repository:
name: "gitlab_gitlab-{{ gitlab_install_edition }}"
description: "GitLab {{ gitlab_install_edition | upper }} packages"
baseurl: "https://packages.gitlab.com/gitlab/gitlab-{{ gitlab_install_edition }}/el/$releasever/$basearch"
gpgcheck: true
repo_gpgcheck: true
gpgkey: "https://packages.gitlab.com/gitlab/gitlab-{{ gitlab_install_edition }}/gpgkey"
sslverify: true
enabled: true
tags: [gitlab_install]
- name: Install GitLab
ansible.builtin.dnf:
name: "gitlab-{{ gitlab_install_edition }}{{ ('-' + gitlab_install_version) if gitlab_install_version | length > 0 else '' }}"
state: present
tags: [gitlab_install]
- name: Deploy gitlab.rb configuration
ansible.builtin.template:
src: gitlab.rb.j2
dest: /etc/gitlab/gitlab.rb
owner: root
group: root
mode: "0600"
notify: reconfigure gitlab
tags: [gitlab_install]
- name: Open firewall ports for HTTP/HTTPS
ansible.posix.firewalld:
zone: "{{ firewall_default_zone }}"
service: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop:
- http
- https
tags: [gitlab_install]
- name: Flush handlers to apply gitlab.rb changes now
ansible.builtin.meta: flush_handlers
tags: [gitlab_install]
- name: Wait for GitLab to report healthy
ansible.builtin.uri:
url: "{{ gitlab_install_external_url }}/-/health"
validate_certs: false
status_code: 200
register: gitlab_install_health
until: gitlab_install_health.status == 200
retries: 30
delay: 20
tags: [gitlab_install]
- name: Report GitLab access details
ansible.builtin.debug:
msg: >-
GitLab is reachable at {{ gitlab_install_external_url }} with user 'root'.
Initial password is stored in vault/secrets.yml (gitlab_root_password) -
change it on first login.
tags: [gitlab_install]

View File

@@ -0,0 +1,14 @@
# {{ ansible_managed }}
external_url '{{ gitlab_install_external_url }}'
letsencrypt['enable'] = {{ gitlab_install_letsencrypt_enabled | lower }}
gitlab_rails['initial_root_password'] = '{{ gitlab_root_password }}'
{% if gitlab_install_reduce_memory_footprint %}
# Reduced footprint for a single small VM
puma['worker_processes'] = {{ gitlab_install_puma_workers }}
sidekiq['max_concurrency'] = {{ gitlab_install_sidekiq_concurrency }}
prometheus_monitoring['enable'] = false
grafana['enable'] = false
{% endif %}

View File

@@ -0,0 +1,132 @@
---
- name: Disable swap
ansible.builtin.command: swapoff -a
changed_when: true
tags: [k3s_install]
- name: Remove swap entries from fstab
ansible.builtin.replace:
path: /etc/fstab
regexp: '^([^#].*\sswap\s.*)$'
replace: '# \1'
tags: [k3s_install]
- name: Load required kernel modules
community.general.modprobe:
name: "{{ item }}"
state: present
loop:
- overlay
- br_netfilter
tags: [k3s_install]
- name: Persist required kernel modules across reboots
ansible.builtin.copy:
dest: /etc/modules-load.d/k3s.conf
owner: root
group: root
mode: "0644"
content: |
overlay
br_netfilter
tags: [k3s_install]
- name: Set required sysctl parameters
ansible.posix.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
sysctl_set: true
state: present
reload: true
loop:
- { name: "net.bridge.bridge-nf-call-iptables", value: "1" }
- { name: "net.ipv4.ip_forward", value: "1" }
tags: [k3s_install]
- name: Open firewall ports required by k3s
ansible.posix.firewalld:
zone: "{{ firewall_default_zone }}"
rich_rule: >-
rule family="ipv4" source address="{{ firewall_admin_subnet }}"
port protocol="{{ item.proto }}" port="{{ item.port }}" accept
permanent: true
immediate: true
state: enabled
loop:
- { port: "6443", proto: "tcp" }
- { port: "10250", proto: "tcp" }
- { port: "8472", proto: "udp" }
- { port: "{{ awx_operator_nodeport }}", proto: "tcp" }
tags: [k3s_install]
- name: Install pip
ansible.builtin.dnf:
name: python3-pip
state: present
tags: [k3s_install]
- name: Install the Python kubernetes client library
ansible.builtin.pip:
name: kubernetes
state: present
tags: [k3s_install]
- name: Check whether k3s is already installed
ansible.builtin.stat:
path: /usr/local/bin/k3s
register: k3s_install_binary
tags: [k3s_install]
- name: Download the k3s install script
ansible.builtin.get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: "0700"
when: not k3s_install_binary.stat.exists
tags: [k3s_install]
- name: Run the k3s install script
ansible.builtin.command: /tmp/k3s-install.sh
environment:
INSTALL_K3S_VERSION: "{{ k3s_version }}"
INSTALL_K3S_EXEC: "server {{ '--disable traefik' if k3s_disable_traefik else '' }}"
when: not k3s_install_binary.stat.exists
changed_when: true
tags: [k3s_install]
- name: Remove the k3s install script
ansible.builtin.file:
path: /tmp/k3s-install.sh
state: absent
tags: [k3s_install]
- name: Enable and start k3s
ansible.builtin.systemd:
name: k3s
enabled: true
state: started
tags: [k3s_install]
- name: Wait for the Kubernetes API to accept connections
ansible.builtin.wait_for:
port: 6443
host: 127.0.0.1
timeout: 180
tags: [k3s_install]
- name: Wait for the node to reach Ready status
ansible.builtin.command: /usr/local/bin/k3s kubectl wait node --for=condition=Ready --all --timeout=180s
register: k3s_install_node_ready
changed_when: false
retries: 3
delay: 15
until: k3s_install_node_ready.rc == 0
tags: [k3s_install]
- name: Make the kubeconfig readable for subsequent Kubernetes modules
ansible.builtin.file:
path: /etc/rancher/k3s/k3s.yaml
owner: root
group: root
mode: "0600"
tags: [k3s_install]

12
vault/secrets.yml.example Normal file
View File

@@ -0,0 +1,12 @@
---
# Usage:
# 1. cp secrets.yml.example secrets.yml
# 2. Fill in the values below
# 3. ansible-vault encrypt vault/secrets.yml
# 4. Store the vault password in .vault_pass (chmod 600, do NOT commit it)
# awx_operator
awx_admin_password: "CHANGE_ME"
# gitlab_install
gitlab_root_password: "CHANGE_ME_MIN_8_CHARS"