Skip to content

Commit bdae8a8

Browse files
feat: update SVCs on K3s nodes via ANsible avoiding MAAS redeploy (#49)
When redeploying an instance through MAAS, the cloud-init process still creates all SVCs listed in `data/service_accounts.yaml`. After the initial deployment, Terraform ignores how the initial cloud-init config might have to look like, preventing attempts to redeploy instances. Editing the SVCs on K3s cluster nodes is now possible through `ansible/playbooks/service_accounts.yaml` with the tags `create` and `remove`.
1 parent 54196e4 commit bdae8a8

File tree

7 files changed

+203
-0
lines changed

7 files changed

+203
-0
lines changed

.ansible-lint

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
skip_list:
2+
- name[template]
23
- var-naming[no-role-prefix]
34

45
exclude_paths:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
- name: Set service accounts file
3+
hosts: localhost
4+
gather_facts: false
5+
tags: always
6+
7+
vars:
8+
service_accounts_file: "{{ playbook_dir }}/../../data/service_accounts.yaml"
9+
10+
tasks:
11+
- name: Load service accounts from YAML (list)
12+
ansible.builtin.set_fact:
13+
service_accounts: "{{ lookup('file', service_accounts_file) | from_yaml }}"
14+
15+
16+
- name: Create service accounts from data/service_accounts.yaml
17+
hosts: k3s_cluster
18+
become: true
19+
gather_facts: false
20+
tags: [create]
21+
vars:
22+
is_create: "{{ 'remove' not in ansible_run_tags }}"
23+
24+
tasks:
25+
- name: Create account if `remove` tag is not passed
26+
when: is_create
27+
ansible.builtin.include_role:
28+
name: account_creator
29+
loop: "{{ hostvars['localhost']['service_accounts'] }}"
30+
loop_control:
31+
loop_var: acct
32+
vars:
33+
account: "{{ acct }}"
34+
35+
36+
- name: Remove service accounts from data/service_accounts.yaml
37+
hosts: k3s_cluster
38+
become: true
39+
gather_facts: false
40+
tags: [remove]
41+
vars:
42+
is_remove: "{{ 'remove' in ansible_run_tags }}"
43+
44+
tasks:
45+
- name: Remove service accounts if `remove` tag is passed
46+
when: is_remove
47+
ansible.builtin.include_role:
48+
name: account_remover
49+
loop: "{{ hostvars['localhost']['service_accounts'] }}"
50+
loop_control:
51+
loop_var: acct
52+
vars:
53+
account: "{{ acct }}"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
account:
3+
# Required: UNIX username
4+
account_name: ""
5+
6+
# Optional: Full name / description (GECOS)
7+
description: ""
8+
9+
# Optional: Public SSH key to install for the account.
10+
# Use either:
11+
# - Literal public key string (e.g., 'ssh-ed25519 AAAA... comment')
12+
# - GitHub reference in the form 'gh:<github-username>' to import all keys
13+
public_ssh_key: ""
14+
15+
# Optional: Restrict SSH key usage to the given CIDR(s).
16+
# Can be a string (single CIDR) or a list of strings.
17+
allow_connections_from: []
18+
19+
# Optional: List of commands or aliases the user can run with NOPASSWD via sudo.
20+
# When empty or undefined, no sudoers file is created for the account.
21+
sudo_permissions: []
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
- name: Ensure service account user {{ account.account_name }} exists
3+
ansible.builtin.user:
4+
name: "{{ account.account_name }}"
5+
comment: "{{ account.description | default('') }}"
6+
shell: "/bin/bash"
7+
create_home: true
8+
state: present
9+
10+
- name: Ensure .ssh directory for {{ account.account_name }} exists with correct permissions
11+
ansible.builtin.file:
12+
path: "/home/{{ account.account_name }}/.ssh"
13+
state: directory
14+
owner: "{{ account.account_name }}"
15+
group: "{{ account.account_name }}"
16+
mode: "0700"
17+
18+
- name: Build connections_from options for authorized_key for {{ account.account_name }}
19+
ansible.builtin.set_fact:
20+
account_connections_from_options: >-
21+
{{
22+
(
23+
'from="' ~ (
24+
account.allow_connections_from
25+
if account.allow_connections_from is string
26+
else (account.allow_connections_from | default([])) | join(',')
27+
) ~ '"'
28+
)
29+
if account.allow_connections_from is defined and (
30+
(account.allow_connections_from is string and account.allow_connections_from | length > 0) or
31+
(account.allow_connections_from is iterable and (account.allow_connections_from | length) > 0)
32+
)
33+
else omit
34+
}}
35+
when:
36+
- account.public_ssh_key is defined
37+
38+
- name: "Install GitHub SSH public keys when public_ssh_key starts with `gh:` for {{ account.account_name }}"
39+
vars:
40+
gh_user: "{{ (account.public_ssh_key | string).split(':', 1)[1] }}"
41+
ansible.posix.authorized_key:
42+
user: "{{ account.account_name }}"
43+
key: "{{ lookup('community.general.github_keys', gh_user) }}"
44+
key_options: "{{ account_connections_from_options | default(omit) }}"
45+
manage_dir: false
46+
path: "/home/{{ account.account_name }}/.ssh/authorized_keys"
47+
when:
48+
- account.public_ssh_key is defined
49+
- (account.public_ssh_key | string).startswith('gh:')
50+
51+
- name: Install provided SSH public keys for {{ account.account_name }} (non-GitHub keys)
52+
ansible.posix.authorized_key:
53+
user: "{{ account.account_name }}"
54+
key: "{{ account.public_ssh_key }}"
55+
key_options: "{{ account_connections_from_options | default(omit) }}"
56+
manage_dir: false
57+
path: "/home/{{ account.account_name }}/.ssh/authorized_keys"
58+
when:
59+
- account.public_ssh_key is defined
60+
- not (account.public_ssh_key | string).startswith('gh:')
61+
62+
- name: Check if authorized_keys exists for {{ account.account_name }}
63+
ansible.builtin.stat:
64+
path: "/home/{{ account.account_name }}/.ssh/authorized_keys"
65+
register: account_authorized_keys_stat
66+
67+
- name: Ensure authorized_keys for {{ account.account_name }} has correct permissions
68+
ansible.builtin.file:
69+
path: "/home/{{ account.account_name }}/.ssh/authorized_keys"
70+
state: file
71+
owner: "{{ account.account_name }}"
72+
group: "{{ account.account_name }}"
73+
mode: "0600"
74+
when: account_authorized_keys_stat.stat.exists | default(false)
75+
76+
- name: Configure sudoers for {{ account.account_name }} with sudo_permissions
77+
ansible.builtin.copy:
78+
dest: "/etc/sudoers.d/{{ account.account_name }}"
79+
owner: root
80+
group: root
81+
mode: "0440"
82+
content: "{{ account.account_name }} ALL=(ALL) NOPASSWD: {{ (account.sudo_permissions | list) | join(',') }}\n"
83+
validate: "visudo -cf %s"
84+
when:
85+
- account.sudo_permissions is defined
86+
- (account.sudo_permissions | length) > 0
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
account:
3+
# Required: UNIX username to remove
4+
account_name: ""
5+
6+
# Optional: Whether to remove the user's home directory
7+
remove_home: true
8+
9+
# Optional: Whether to force removal even if the user is logged in
10+
force: false
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
- name: Remove sudoers file of {{ account.account_name }} if present
3+
ansible.builtin.file:
4+
path: "/etc/sudoers.d/{{ account.account_name }}"
5+
state: absent
6+
become: true
7+
8+
- name: Remove authorized_keys of {{ account.account_name }} if present
9+
ansible.builtin.file:
10+
path: "/home/{{ account.account_name }}/.ssh/authorized_keys"
11+
state: absent
12+
become: true
13+
14+
- name: Remove .ssh directory of {{ account.account_name }} if present
15+
ansible.builtin.file:
16+
path: "/home/{{ account.account_name }}/.ssh"
17+
state: absent
18+
become: true
19+
20+
- name: Remove user {{ account.account_name }}
21+
ansible.builtin.user:
22+
name: "{{ account.account_name }}"
23+
state: absent
24+
remove: "{{ account.remove_home | default(true) }}"
25+
force: "{{ account.force | default(false) }}"
26+
become: true

terraform/modules/server/main.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,10 @@ resource "maas_instance" "this" {
4242
name = maas_network_interface_physical.this.name
4343
subnet_cidr = "192.168.150.0/24"
4444
}
45+
46+
lifecycle {
47+
ignore_changes = [
48+
deploy_params[0].user_data
49+
]
50+
}
4551
}

0 commit comments

Comments
 (0)