├── requirements.txt ├── .gitignore ├── .ansible-lint ├── .yamllint ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── meta └── main.yml ├── LICENSE ├── tasks ├── load_balancer.yml └── main.yml ├── defaults └── main.yml └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | hcloud 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.retry 2 | */__pycache__ 3 | *.pyc 4 | .cache 5 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | skip_list: 2 | - 'yaml' 3 | - 'risky-shell-pipe' 4 | - 'no-handler' 5 | - 'role-name' 6 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: 6 | max: 120 7 | level: warning 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | --- 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: [] 3 | 4 | galaxy_info: 5 | role_name: hcloud 6 | author: resmo 7 | description: Manages Hetzner Cloud resources 8 | license: MIT 9 | min_ansible_version: "2.11" 10 | platforms: 11 | - name: EL 12 | versions: 13 | - all 14 | - name: Fedora 15 | versions: 16 | - all 17 | - name: Ubuntu 18 | versions: 19 | - all 20 | - name: Debian 21 | versions: 22 | - all 23 | galaxy_tags: 24 | - cloud 25 | - hetzner 26 | - hcloud 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | "on": 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | tags: 9 | - "v*" 10 | 11 | defaults: 12 | run: 13 | working-directory: "ngine_io.hcloud" 14 | 15 | jobs: 16 | lint: 17 | name: Lint 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Check out the codebase. 21 | uses: actions/checkout@v6 22 | with: 23 | path: "ngine_io.hcloud" 24 | 25 | - name: Set up Python 3. 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: "3.x" 29 | 30 | - name: Install test dependencies. 31 | run: pip3 install yamllint ansible ansible-lint 32 | 33 | - name: Lint code. 34 | run: | 35 | yamllint . 36 | 37 | - name: Lint code. 38 | run: | 39 | ansible-lint . 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | "on": 4 | release: 5 | types: [created] 6 | 7 | defaults: 8 | run: 9 | working-directory: "ngine_io.hcloud" 10 | 11 | jobs: 12 | release: 13 | name: Release 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Check out the codebase. 17 | uses: actions/checkout@v6 18 | with: 19 | path: "ngine_io.hcloud" 20 | 21 | - name: Set up Python 3. 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: "3.x" 25 | 26 | - name: Install Ansible. 27 | run: pip3 install ansible-core 28 | 29 | - name: Trigger a new import on Galaxy. 30 | env: 31 | ANSIBLE_GALAXY_API_KEY: ${{ secrets.ANSIBLE_GALAXY_API_KEY }} 32 | run: >- 33 | ansible-galaxy role import 34 | --api-key "$ANSIBLE_GALAXY_API_KEY" 35 | --role hcloud 36 | ngine_io 37 | $(echo ${{ github.repository }} | cut -d/ -f2) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ngine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tasks/load_balancer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create a load balancer 3 | hetzner.hcloud.load_balancer: 4 | name: "{{ load_balancer.name }}" 5 | load_balancer_type: "{{ load_balancer.type }}" 6 | location: "{{ load_balancer.location | default(hcloud__location) }}" 7 | labels: "{{ load_balancer.labels | default(hcloud__labels) }}" 8 | api_token: "{{ hcloud__api_token }}" 9 | 10 | - name: Create load balancer services 11 | hetzner.hcloud.load_balancer_service: 12 | load_balancer: "{{ load_balancer.name }}" 13 | protocol: "{{ service.protocol }}" 14 | listen_port: "{{ service.port }}" 15 | destination_port: "{{ service.destination_port | default(omit) }}" 16 | proxyprotocol: "{{ service.proxyprotocol | default(omit) }}" 17 | health_check: "{{ service.health_check | default(omit) }}" 18 | http: "{{ service.http | default(omit) }}" 19 | api_token: "{{ hcloud__api_token }}" 20 | with_items: "{{ load_balancer.services | default([]) }}" 21 | loop_control: 22 | loop_var: service 23 | 24 | - name: Create load balancer targets 25 | hetzner.hcloud.load_balancer_target: 26 | load_balancer: "{{ load_balancer.name }}" 27 | type: "{{ target.type }}" 28 | ip: "{{ target.ip | default(omit) }}" 29 | label_selector: "{{ target.label_selector | default(omit) }}" 30 | server: "{{ target.server | default(omit) }}" 31 | use_private_ip: "{{ target.use_private_ip | default(omit) }}" 32 | api_token: "{{ hcloud__api_token }}" 33 | with_items: "{{ load_balancer.targets | default([]) }}" 34 | loop_control: 35 | loop_var: target 36 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | hcloud__location: fsn1 3 | hcloud__labels: "{{ omit }}" 4 | 5 | hcloud__server_name: "{{ inventory_hostname_short }}" 6 | hcloud__server_type: cx11 7 | hcloud__server_networks: [] 8 | # hcloud__server_networks: 9 | # - name: internal 10 | # alias_ips: 11 | # - 10.1.0.1 12 | # - 10.2.0.1 13 | 14 | hcloud__server_image: debian-11 15 | hcloud__server_location: "{{ hcloud__location }}" 16 | hcloud__server_labels: "{{ hcloud__labels }}" 17 | hcloud__server_firewalls: 18 | - default 19 | 20 | hcloud__server_user_data: "" 21 | hcloud__server_volumes: [] 22 | # hcloud__server_volumes: 23 | # - name: "{{ inventory_hostname_short }}-vol1" 24 | # size: 10 25 | 26 | hcloud__server_force: false 27 | hcloud__server_placement_group: "{{ omit }}" 28 | hcloud__server_loadbalancer_name: 29 | hcloud__server_loadbalancer_use_private_ip: false 30 | hcloud__server_state: started 31 | 32 | hcloud__ssh_key_name: "{{ lookup('env', 'USER') }}@{{ lookup('pipe', 'hostname') }}" 33 | hcloud__ssh_public_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}" 34 | 35 | hcloud__networks: [] 36 | # hcloud__networks: 37 | # - name: internal 38 | # network: 10.10.1.0/24 39 | 40 | hcloud__firewalls: 41 | - name: default 42 | rules: 43 | - direction: in 44 | protocol: icmp 45 | source_ips: 46 | - 0.0.0.0/0 47 | - ::/0 48 | - direction: in 49 | protocol: tcp 50 | port: "22" 51 | 52 | hcloud__placement_groups: [] 53 | # hcloud__placement_groups: 54 | # - name: my placement group 55 | # labels: 56 | # mylabel: test 57 | 58 | hcloud__loadbalancers: [] 59 | # hcloud__loadbalancers: 60 | # - name: my load balancer 61 | # type: lb11 62 | # targets: 63 | # - type: label_selector 64 | # label_selector: app=public 65 | # services: 66 | # - protocol: http 67 | # port: 80 68 | # health_check: 69 | # interval: 15 70 | # port: 80 71 | # timeout: 3 72 | # retries: 3 73 | # protocol: http 74 | # http: 75 | # # Single wildcard char '?' or glob wildcard '*' 76 | # status_codes: 77 | # - 2* 78 | # - 3* 79 | # - 40? 80 | # # Optional 81 | # path: / 82 | # domain: www.example.com 83 | # response: hello world 84 | # - protocol: tcp 85 | # port: 443 86 | # destination_port: 443 87 | 88 | hcloud__api_token: "{{ omit }}" 89 | -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Load balancers handling 3 | ansible.builtin.include_tasks: load_balancer.yml 4 | with_items: "{{ hcloud__loadbalancers }}" 5 | loop_control: 6 | loop_var: load_balancer 7 | run_once: true 8 | 9 | - name: Create placement groups 10 | hetzner.hcloud.placement_group: 11 | name: "{{ item.name }}" 12 | type: "{{ item.type | default('spread') }}" 13 | labels: "{{ item.labels | default(hcloud__labels | default(omit)) }}" 14 | api_token: "{{ hcloud__api_token }}" 15 | with_items: "{{ hcloud__placement_groups }}" 16 | run_once: true 17 | 18 | - name: Create firewalls 19 | hetzner.hcloud.firewall: 20 | name: "{{ item.name }}" 21 | rules: "{{ item.rules | default([]) }}" 22 | api_token: "{{ hcloud__api_token }}" 23 | with_items: "{{ hcloud__firewalls }}" 24 | run_once: true 25 | 26 | - name: Ensure SSH key exists 27 | hetzner.hcloud.ssh_key: 28 | name: "{{ hcloud__ssh_key_name }}" 29 | public_key: "{{ hcloud__ssh_public_key }}" 30 | api_token: "{{ hcloud__api_token }}" 31 | run_once: true 32 | 33 | - name: Create networks 34 | hetzner.hcloud.network: 35 | name: "{{ item.name }}" 36 | ip_range: "{{ item.network }}" 37 | api_token: "{{ hcloud__api_token }}" 38 | with_items: "{{ hcloud__networks }}" 39 | run_once: true 40 | 41 | - name: Ensure server is created 42 | hetzner.hcloud.server: 43 | name: "{{ hcloud__server_name }}" 44 | server_type: "{{ hcloud__server_type }}" 45 | image: "{{ hcloud__server_image }}" 46 | location: "{{ hcloud__server_location }}" 47 | ssh_keys: "{{ hcloud__ssh_key_name }}" 48 | labels: "{{ hcloud__server_labels }}" 49 | firewalls: "{{ hcloud__server_firewalls }}" 50 | placement_group: "{{ hcloud__server_placement_group }}" 51 | user_data: "{{ hcloud__server_user_data }}" 52 | api_token: "{{ hcloud__api_token }}" 53 | force: "{{ hcloud__server_force }}" 54 | state: "{{ hcloud__server_state }}" 55 | register: server 56 | 57 | - name: Ensure volume is created and formatted 58 | hetzner.hcloud.volume: 59 | name: "{{ item.name }}" 60 | server: "{{ hcloud__server_name }}" 61 | automount: true 62 | format: "{{ item.format | default('ext4') }}" 63 | size: "{{ item.size }}" 64 | api_token: "{{ hcloud__api_token }}" 65 | with_items: "{{ hcloud__server_volumes }}" 66 | 67 | - name: Assign server to networks 68 | hetzner.hcloud.server_network: 69 | network: "{{ item.name }}" 70 | server: "{{ hcloud__server_name }}" 71 | alias_ips: "{{ item.alias_ips | default(omit) }}" 72 | api_token: "{{ hcloud__api_token }}" 73 | with_items: "{{ hcloud__server_networks }}" 74 | 75 | - name: Assign server to load balancer 76 | hetzner.hcloud.load_balancer_target: 77 | type: server 78 | load_balancer: "{{ hcloud__server_loadbalancer_name }}" 79 | server: "{{ hcloud__server_name }}" 80 | use_private_ip: "{{ hcloud__server_loadbalancer_use_private_ip }}" 81 | api_token: "{{ hcloud__api_token }}" 82 | when: hcloud__server_loadbalancer_name is not none 83 | 84 | - name: Show server infos 85 | when: server.hcloud_server.name is defined 86 | block: 87 | - name: Show server infos 88 | ansible.builtin.debug: 89 | msg: "Server: {{ server.hcloud_server.name }} --> {{ server.hcloud_server.ipv4_address }}" 90 | - name: Assing default IP to ansible_host 91 | ansible.builtin.set_fact: 92 | ansible_host: "{{ server.hcloud_server.ipv4_address }}" 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role: Hetzner Cloud hcloud 2 | 3 | [![CI](https://github.com/ngine-io/ansible-role-hcloud/workflows/CI/badge.svg?event=push)](https://github.com/ngine-io/ansible-role-hcloud/actions?query=workflow%3ACI) 4 | 5 | Manages compute resources on [Hetzner Cloud](https://www.hetzner.com/cloud). 6 | 7 | ## Dependencies 8 | 9 | See `requirements.txt` for python library dependencies. 10 | 11 | ## Examples 12 | 13 | ### Inventory 14 | 15 | Given is a inventory similar as: 16 | 17 | ```ini 18 | [web] 19 | web-1 20 | web-2 21 | web-3 22 | ``` 23 | 24 | ### Variables 25 | 26 | The following variables are meant to be set in a top level group, e.g.`group_vars/all.yml`. 27 | 28 | API key: 29 | 30 | > NOTE: Recommended to encrypt this token with Ansible Vault or use a lookup to your secrets management system! 31 | 32 | ```yaml 33 | hcloud__api_token: ... 34 | ``` 35 | 36 | Ensures a private network is created: 37 | 38 | ```yaml 39 | hcloud__networks: 40 | - name: internal 41 | network: 10.10.10.0/24 42 | ``` 43 | 44 | Ensure some firewalls and rules are created: 45 | 46 | ```yaml 47 | hcloud__firewalls: 48 | - name: default 49 | rules: 50 | - direction: in 51 | protocol: icmp 52 | source_ips: 53 | - 0.0.0.0/0 54 | - ::/0 55 | - direction: in 56 | protocol: tcp 57 | port: "22" 58 | source_ips: 59 | - 0.0.0.0/0 60 | - name: web 61 | rules: 62 | - direction: in 63 | protocol: tcp 64 | port: "80" 65 | source_ips: 66 | - 0.0.0.0/0 67 | - ::/0 68 | - direction: in 69 | protocol: tcp 70 | port: "443" 71 | source_ips: 72 | - 0.0.0.0/0 73 | - ::/0 74 | ``` 75 | 76 | Ensure placment groups are created: 77 | 78 | ```yaml 79 | hcloud__placement_groups: 80 | - name: my placement group 81 | labels: 82 | project: genesis 83 | ``` 84 | 85 | #### Loadbalancers 86 | 87 | Configure a simple load balancer and add servers as targets later (see server configs): 88 | 89 | ```yaml 90 | hcloud__loadbalancers: 91 | - name: my load balancer 92 | type: lb11 93 | services: 94 | - protocol: http 95 | port: 80 96 | - protocol: tcp 97 | port: 443 98 | destination_port: 443 99 | ``` 100 | 101 | Configure a load balancer and use label selector hcloud feature: 102 | 103 | ```yaml 104 | hcloud__loadbalancers: 105 | - name: my load balancer 106 | type: lb11 107 | targets: 108 | - type: label_selector 109 | label_selector: app=public 110 | services: 111 | - protocol: http 112 | port: 80 113 | health_check: 114 | interval: 15 115 | port: 80 116 | timeout: 3 117 | retries: 3 118 | protocol: http 119 | http: 120 | # Single wildcard char '?' or glob wildcard '*' 121 | status_codes: 122 | - 2* 123 | - 3* 124 | - 40? 125 | # Optional 126 | path: / 127 | domain: www.example.com 128 | response: hello world 129 | - protocol: tcp 130 | port: 443 131 | destination_port: 443 132 | ``` 133 | 134 | #### Server Configs 135 | 136 | The following configs are server specific and meant to be set in a lower level `group_vars` or `host_vars`, e.g. `group_vars/web.yml` 137 | 138 | Server image to use: 139 | 140 | ```yaml 141 | hcloud__server_image: debian-11 142 | ``` 143 | 144 | Location to deploy into: 145 | 146 | ```yaml 147 | hcloud__server_location: fsn1 148 | ``` 149 | 150 | Ensure web servers have these firewalls configured: 151 | 152 | ```yaml 153 | hcloud__server_firewalls: 154 | - default 155 | - web 156 | ``` 157 | 158 | Ensure web servers are attached to these networks: 159 | 160 | ```yaml 161 | hcloud__server_networks: 162 | - name: internal 163 | ``` 164 | 165 | Ensure web servers are in a placement group: 166 | 167 | ```yaml 168 | hcloud__server_placement_group: my placement group 169 | ``` 170 | 171 | Add server to an existing loadbalancer as server target (optionally use private IP): 172 | 173 | ```yaml 174 | hcloud__server_loadbalancer_name: my load balancer 175 | hcloud__server_loadbalancer_use_private_ip: true 176 | ``` 177 | 178 | Ensure web server have this type: 179 | 180 | ```yaml 181 | hcloud__server_type: cpx11 182 | ``` 183 | 184 | User data to be executed on boot, e.g.: 185 | 186 | ```yaml 187 | hcloud__server_user_data: | 188 | #cloud-config 189 | users: 190 | - name: admin 191 | groups: users, admin 192 | sudo: ALL=(ALL) NOPASSWD:ALL 193 | shell: /bin/bash 194 | ssh_authorized_keys: 195 | - 196 | packages: 197 | - fail2ban 198 | package_update: true 199 | package_upgrade: true 200 | ``` 201 | 202 | Ensure a volume is created and attached to the server: 203 | 204 | > HINT: Use the special variable `inventory_hostname_short` as prefix to easily identify which volume is attached to which server. 205 | 206 | ```yaml 207 | hcloud__server_volumes: 208 | - name: "{{ inventory_hostname_short }}-vol1" 209 | format: ext4 210 | size: 10 211 | ``` 212 | 213 | ### Playbook 214 | 215 | A typical playbook would look like this. 216 | 217 | ```yaml 218 | --- 219 | - name: Provision Cloud servers 220 | hosts: all 221 | serial: 5 222 | gather_facts: false 223 | roles: 224 | - role: ngine_io.hcloud 225 | delegate_to: localhost 226 | 227 | post_tasks: 228 | - name: Wait for SSH access 229 | delegate_to: localhost 230 | wait_for: 231 | host: "{{ ansible_host }}" 232 | port: 22 233 | timeout: 60 234 | ``` 235 | 236 | ## License 237 | 238 | MIT 239 | 240 | ## Author Information 241 | 242 | René Moser (@resmo) 243 | --------------------------------------------------------------------------------