├── .ansible-lint ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .yamllint ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Vagrantfile ├── ansible.cfg ├── collections └── requirements.yml ├── galaxy.yml ├── inventory-sample.yml ├── meta └── runtime.yml ├── playbooks ├── reboot.yml ├── reset.yml ├── site.yml └── upgrade.yml └── roles ├── airgap └── tasks │ └── main.yml ├── k3s_agent ├── defaults │ └── main.yml ├── tasks │ └── main.yml └── templates │ └── k3s-agent.service.j2 ├── k3s_server ├── defaults │ └── main.yml ├── tasks │ └── main.yml └── templates │ ├── k3s-cluster-init.service.j2 │ ├── k3s-ha.service.j2 │ └── k3s-single.service.j2 ├── k3s_upgrade ├── defaults │ └── main.yml └── tasks │ └── main.yml ├── prereq ├── defaults │ └── main.yml ├── tasks │ └── main.yml └── vars │ └── main.yml └── raspberrypi ├── handlers └── main.yml └── tasks ├── main.yml └── prereq ├── Archlinux.yml ├── CentOS.yml ├── Debian.yml ├── Raspbian.yml ├── Ubuntu.yml └── default.yml /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | warn_list: 3 | - var-naming[no-role-prefix] 4 | - yaml[comments-indentation] 5 | - yaml[line-length] 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.yml linguist-detectable=true 2 | *.yml linguist-language=YAML -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### Changes #### 2 | 3 | #### Linked Issues #### -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | 10 | test: 11 | name: Build Ansible Galaxy collection artifact. 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Check out the codebase. 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 3. 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.x' 22 | 23 | - name: Install test dependencies. 24 | run: pip3 install ansible 25 | 26 | - name: Build artifact. 27 | run: ansible-galaxy collection build 28 | 29 | - name: Upload artifact. 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: galaxy-collection 33 | path: k3s-orchestration-*.tar.gz 34 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint 3 | 'on': 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | 11 | test: 12 | name: Lint 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out the codebase. 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 3.7. 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: '3.x' 23 | 24 | - name: Install test dependencies. 25 | run: pip3 install yamllint ansible-lint ansible 26 | 27 | - name: Run yamllint. 28 | run: yamllint . 29 | 30 | - name: Run ansible-lint. 31 | run: ansible-lint 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | .vscode 4 | .vagrant 5 | inventory.yml 6 | playbook/debug.yml -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: 6 | max: 180 7 | level: warning 8 | truthy: 9 | allowed-values: ["true", "false"] 10 | braces: 11 | max-spaces-inside: 1 12 | octal-values: 13 | forbid-implicit-octal: true 14 | forbid-explicit-octal: true 15 | comments: 16 | min-spaces-from-content: 1 17 | comments-indentation: false 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # `k3s-ansible` changelog (`k3s.orchestration`) 2 | ## 1.0.0 3 | Initial Release 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build a Kubernetes cluster using K3s via Ansible 2 | 3 | Author: 4 | Current Maintainer: 5 | 6 | Easily bring up a cluster on machines running: 7 | 8 | - [X] Debian 9 | - [X] Ubuntu 10 | - [X] Raspberry Pi OS 11 | - [X] RHEL Family (CentOS, Redhat, Rocky Linux...) 12 | - [X] SUSE Family (SLES, OpenSUSE Leap, Tumbleweed...) 13 | - [X] ArchLinux 14 | 15 | on processor architectures: 16 | 17 | - [X] x64 18 | - [X] arm64 19 | - [X] armhf 20 | 21 | ## System requirements 22 | 23 | The control node **must** have Ansible 8.0+ (ansible-core 2.15+) 24 | 25 | All managed nodes in inventory must have: 26 | - Passwordless SSH access 27 | - Root access (or a user with equivalent permissions) 28 | 29 | It is also recommended that all managed nodes disable firewalls and swap. See [K3s Requirements](https://docs.k3s.io/installation/requirements) for more information. 30 | 31 | ## Installation 32 | 33 | ### With ansible-galaxy 34 | 35 | `k3s-ansible` is a Ansible collection and can be installed with the `ansible-galaxy` command: 36 | 37 | ```console 38 | $ ansible-galaxy collection install git+https://github.com/k3s-io/k3s-ansible.git 39 | ``` 40 | 41 | ### From source 42 | 43 | Alternatively to an installation with `ansible-galaxy`, the `k3s-ansible` repository can simply be cloned from github: 44 | 45 | ```console 46 | $ git clone https://github.com/k3s-io/k3s-ansible.git 47 | $ cd k3s-ansible 48 | ``` 49 | 50 | ## Usage 51 | 52 | First copy the sample inventory to `inventory.yml`. 53 | 54 | ```bash 55 | cp inventory-sample.yml inventory.yml 56 | ``` 57 | 58 | If you have installed `k3s-ansible` with ansible-galaxy, you can grab the [inventory-sample.yml](./inventory-sample.yml) from github. 59 | 60 | Second edit the inventory file to match your cluster setup. For example: 61 | ```bash 62 | k3s_cluster: 63 | children: 64 | server: 65 | hosts: 66 | 192.16.35.11: 67 | agent: 68 | hosts: 69 | 192.16.35.12: 70 | 192.16.35.13: 71 | ``` 72 | 73 | If needed, you can also edit `vars` section at the bottom to match your environment. 74 | 75 | If multiple hosts are in the server group the playbook will automatically setup k3s in HA mode with embedded etcd. 76 | An odd number of server nodes is required (3,5,7). Read the [official documentation](https://docs.k3s.io/datastore/ha-embedded) for more information. 77 | 78 | Setting up a loadbalancer or VIP beforehand to use as the API endpoint is possible but not covered here. 79 | 80 | 81 | Start provisioning of the cluster using one of the following commands. The command to be used depends on whether you installed `k3s-ansible` with `ansible-galaxy` or if you run the playbook from within the cloned git repository: 82 | 83 | *Installed with ansible-galaxy* 84 | 85 | ```bash 86 | ansible-playbook k3s.orchestration.site -i inventory.yml 87 | ``` 88 | 89 | *Running the playbook from inside the repository* 90 | 91 | ```bash 92 | ansible-playbook playbooks/site.yml -i inventory.yml 93 | ``` 94 | 95 | ### Using an external database 96 | 97 | If an external database is preferred, this can be achieved by passing the `--datastore-endpoint` as an extra server argument as well as setting the `use_external_database` flag to true. 98 | 99 | ```bash 100 | k3s_cluster: 101 | children: 102 | server: 103 | hosts: 104 | 192.16.35.11: 105 | 192.16.35.12: 106 | agent: 107 | hosts: 108 | 192.16.35.13: 109 | 110 | vars: 111 | use_external_database: true 112 | extra_server_args: "--datastore-endpoint=postgres://username:password@hostname:port/database-name" 113 | ``` 114 | 115 | The `use_external_database` flag is required when more than one server is defined, as otherwise an embedded etcd cluster will be created instead. 116 | 117 | The format of the datastore-endpoint parameter is dependent upon the datastore backend, please visit the [K3s datastore endpoint format](https://docs.k3s.io/datastore#datastore-endpoint-format-and-functionality) for details on the format and supported datastores. 118 | 119 | ## Upgrading 120 | 121 | A playbook is provided to upgrade K3s on all nodes in the cluster. To use it, update `k3s_version` with the desired version in `inventory.yml` and run one of the following commands. Again, the syntax is slightly different depending on whether you installed `k3s-ansible` with `ansible-galaxy` or if you run the playbook from within the cloned git repository: 122 | 123 | 124 | *Installed with ansible-galaxy* 125 | 126 | ```bash 127 | ansible-playbook k3s.orchestration.upgrade -i inventory.yml 128 | ``` 129 | 130 | *Running the playbook from inside the repository* 131 | 132 | ```bash 133 | ansible-playbook playbooks/upgrade.yml -i inventory.yml 134 | ``` 135 | 136 | ## Airgap Install 137 | 138 | Airgap installation is supported via the `airgap_dir` variable. This variable should be set to the path of a directory containing the K3s binary and images. The release artifacts can be downloaded from the [K3s Releases](https://github.com/k3s-io/k3s/releases). You must download the appropriate images for you architecture (any of the compression formats will work). 139 | 140 | An example folder for an x86_64 cluster: 141 | ```bash 142 | $ ls ./playbooks/my-airgap/ 143 | total 248M 144 | -rwxr-xr-x 1 $USER $USER 58M Nov 14 11:28 k3s 145 | -rw-r--r-- 1 $USER $USER 190M Nov 14 11:30 k3s-airgap-images-amd64.tar.gz 146 | 147 | $ cat inventory.yml 148 | ... 149 | airgap_dir: ./my-airgap # Paths are relative to the playbooks directory 150 | ``` 151 | 152 | Additionally, if deploying on a OS with SELinux, you will also need to download the latest [k3s-selinux RPM](https://github.com/k3s-io/k3s-selinux/releases/latest) and place it in the airgap folder. 153 | 154 | 155 | It is assumed that the control node has access to the internet. The playbook will automatically download the k3s install script on the control node, and then distribute all three artifacts to the managed nodes. 156 | 157 | ## Kubeconfig 158 | 159 | After successful bringup, the kubeconfig of the cluster is copied to the control node and merged with `~/.kube/config` under the `k3s-ansible` context. 160 | Assuming you have [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) installed, you can confirm access to your **Kubernetes** cluster with the following: 161 | 162 | ```bash 163 | kubectl config use-context k3s-ansible 164 | kubectl get nodes 165 | ``` 166 | 167 | If you wish for your kubeconfig to be copied elsewhere and not merged, you can set the `kubeconfig` variable in `inventory.yml` to the desired path. 168 | 169 | ## Local Testing 170 | 171 | A Vagrantfile is provided that provision a 5 nodes cluster using Vagrant (LibVirt or Virtualbox as provider). To use it: 172 | 173 | ```bash 174 | vagrant up 175 | ``` 176 | 177 | By default, each node is given 2 cores and 2GB of RAM and runs Ubuntu 20.04. You can customize these settings by editing the `Vagrantfile`. 178 | 179 | ## Need More Features? 180 | 181 | This project is intended to provide a "vanilla" K3s install. If you need more features, such as: 182 | - Private Registry 183 | - Advanced Storage (Longhorn, Ceph, etc) 184 | - External Database 185 | - External Load Balancer or VIP 186 | - Alternative CNIs 187 | 188 | See these other projects: 189 | - https://github.com/PyratLabs/ansible-role-k3s 190 | - https://github.com/techno-tim/k3s-ansible 191 | - https://github.com/jon-stumpf/k3s-ansible 192 | - https://github.com/alexellis/k3sup 193 | - https://github.com/axivo/k3s-cluster 194 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # ENV['VAGRANT_NO_PARALLEL'] = 'no' 2 | NODE_ROLES = ["server-0", "server-1", "server-2", "agent-0", "agent-1"] 3 | NODE_BOXES = ['bento/ubuntu-24.04', 'bento/ubuntu-24.04', 'bento/ubuntu-24.04', 'bento/ubuntu-24.04', 'bento/ubuntu-24.04'] 4 | NODE_CPUS = 2 5 | NODE_MEMORY = 2048 6 | # Virtualbox >= 6.1.28 require `/etc/vbox/network.conf` for expanded private networks 7 | NETWORK_PREFIX = "10.10.10" 8 | 9 | def provision(vm, role, node_num) 10 | vm.box = NODE_BOXES[node_num] 11 | vm.hostname = role 12 | # We use a private network because the default IPs are dynamically assigned 13 | # during provisioning. This makes it impossible to know the server-0 IP when 14 | # provisioning subsequent servers and agents. A private network allows us to 15 | # assign static IPs to each node, and thus provide a known IP for the API endpoint. 16 | node_ip = "#{NETWORK_PREFIX}.#{100+node_num}" 17 | # An expanded netmask is required to allow VM<-->VM communication, virtualbox defaults to /32 18 | vm.network "private_network", ip: node_ip, netmask: "255.255.255.0" 19 | 20 | vm.provision "ansible", run: 'once' do |ansible| 21 | ansible.compatibility_mode = "2.0" 22 | ansible.playbook = "playbooks/site.yml" 23 | ansible.groups = { 24 | "server" => NODE_ROLES.grep(/^server/), 25 | "agent" => NODE_ROLES.grep(/^agent/), 26 | "k3s_cluster:children" => ["server", "agent"], 27 | } 28 | ansible.extra_vars = { 29 | k3s_version: "v1.28.14+k3s1", 30 | api_endpoint: "#{NETWORK_PREFIX}.100", 31 | # Required for vagrant ansible provisioner 32 | token: "myvagrant", 33 | # Required to use the private network configured above 34 | extra_server_args: "--node-external-ip #{node_ip} --flannel-iface eth1", 35 | extra_agent_args: "--node-external-ip #{node_ip} --flannel-iface eth1", 36 | # Airgap setup, left as reference 37 | # airgap_dir: "./my_airgap", 38 | # Optional, left as reference for ruby-ansible syntax 39 | # extra_service_envs: [ "NO_PROXY='localhost'" ], 40 | # server_config_yaml: <<~YAML 41 | # write-kubeconfig-mode: 644 42 | # kube-apiserver-arg: 43 | # - advertise-port=1234 44 | # YAML 45 | } 46 | end 47 | end 48 | 49 | Vagrant.configure("2") do |config| 50 | # Default provider is libvirt, virtualbox is only provided as a backup 51 | config.vm.provider "libvirt" do |v| 52 | v.cpus = NODE_CPUS 53 | v.memory = NODE_MEMORY 54 | end 55 | config.vm.provider "virtualbox" do |v| 56 | v.cpus = NODE_CPUS 57 | v.memory = NODE_MEMORY 58 | v.linked_clone = true 59 | end 60 | 61 | NODE_ROLES.each_with_index do |name, i| 62 | config.vm.define name do |node| 63 | provision(node.vm, name, i) 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | nocows = True 3 | roles_path = ./roles 4 | inventory = ./inventory.yml 5 | 6 | remote_tmp = $HOME/.ansible/tmp 7 | local_tmp = $HOME/.ansible/tmp 8 | pipelining = True 9 | become = True 10 | host_key_checking = False 11 | deprecation_warnings = False 12 | callback_whitelist = profile_tasks 13 | -------------------------------------------------------------------------------- /collections/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: community.general 4 | - name: ansible.posix 5 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ### REQUIRED 3 | # The namespace of the collection. This can be a company/brand/organization or product namespace under which all 4 | # content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with 5 | # underscores or numbers and cannot contain consecutive underscores 6 | namespace: k3s 7 | 8 | # The name of the collection. Has the same character restrictions as 'namespace' 9 | name: orchestration 10 | 11 | # The version of the collection. Must be compatible with semantic versioning 12 | version: 1.0.0 13 | 14 | # The path to the Markdown (.md) readme file. This path is relative to the root of the collection 15 | readme: README.md 16 | 17 | # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) 18 | # @nicks:irc/im.site#channel' 19 | authors: 20 | - Julien DOCHE 21 | - Derek Nola 22 | - David Putzolu 23 | - Jeff Geerling 24 | - Staf Wagemakers 25 | - Vincent RABAH 26 | 27 | ### OPTIONAL but strongly recommended 28 | # A short summary description of the collection 29 | description: Build a Kubernetes cluster using K3s via Ansible 30 | 31 | # Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only 32 | # accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' 33 | license: 34 | - Apache-2.0 35 | 36 | # The path to the license file for the collection. This path is relative to the root of the collection. This key is 37 | # mutually exclusive with 'license' 38 | # license_file: '' 39 | 40 | # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character 41 | # requirements as 'namespace' and 'name' 42 | tags: ['infrastructure', 'linux', 'tools'] 43 | 44 | # Collections that this collection requires to be installed for it to be usable. The key of the dict is the 45 | # collection label 'namespace.name'. The value is a version range 46 | # L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version 47 | # range specifiers can be set and are separated by ',' 48 | dependencies: 49 | community.general: ">=7.0.0" 50 | ansible.posix: ">=1.5.0" 51 | 52 | # The URL of the originating SCM repository 53 | repository: https://github.com/k3s-io/k3s-ansible 54 | 55 | # The URL to any online docs 56 | documentation: https://github.com/k3s-io/k3s-ansible/blob/master/README.md 57 | 58 | # The URL to the homepage of the collection/project 59 | homepage: https://github.com/k3s-io/k3s-ansible 60 | 61 | # The URL to the collection issue tracker 62 | issues: https://github.com/k3s-io/k3s-ansible/issues 63 | 64 | # A list of file glob-like patterns used to filter any files or directories that should not be included in the build 65 | # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This 66 | # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', 67 | # and '.git' are always filtered. Mutually exclusive with 'manifest' 68 | build_ignore: [] 69 | # A dict controlling use of manifest directives used in building the collection artifact. The key 'directives' is a 70 | # list of MANIFEST.in style 71 | # L(directives,https://packaging.python.org/en/latest/guides/using-manifest-in/#manifest-in-commands). The key 72 | # 'omit_default_directives' is a boolean that controls whether the default directives are used. Mutually exclusive 73 | # with 'build_ignore' 74 | # manifest: null 75 | -------------------------------------------------------------------------------- /inventory-sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | k3s_cluster: 3 | children: 4 | server: 5 | hosts: 6 | 192.16.35.11: 7 | agent: 8 | hosts: 9 | 192.16.35.12: 10 | 192.16.35.13: 11 | 12 | # Required Vars 13 | vars: 14 | ansible_port: 22 15 | ansible_user: debian 16 | k3s_version: v1.30.2+k3s1 17 | # The token should be a random string of reasonable length. You can generate 18 | # one with the following commands: 19 | # - openssl rand -base64 64 20 | # - pwgen -s 64 1 21 | # You can use ansible-vault to encrypt this value / keep it secret. 22 | # Or you can omit it if not using Vagrant and let the first server automatically generate one. 23 | token: "changeme!" 24 | api_endpoint: "{{ hostvars[groups['server'][0]]['ansible_host'] | default(groups['server'][0]) }}" 25 | 26 | # Optional vars 27 | # extra_server_args: "" 28 | # extra_agent_args: "" 29 | # cluster_context: k3s-ansible 30 | # api_port: 6443 31 | # k3s_server_location: /var/lib/rancher/k3s 32 | # systemd_dir: /etc/systemd/system 33 | # extra_service_envs: [ 'ENV_VAR1=VALUE1', 'ENV_VAR2=VALUE2' ] 34 | # user_kubectl: true, by default kubectl is symlinked and configured for use by ansible_user. Set to false to only kubectl via root user. 35 | 36 | # Manifests or Airgap should be either full paths or relative to the playbook directory. 37 | # List of locally available manifests to apply to the cluster, useful for PVCs or Traefik modifications. 38 | # extra_manifests: [ '/path/to/manifest1.yaml', '/path/to/manifest2.yaml' ] 39 | # airgap_dir: /tmp/k3s-airgap-images 40 | 41 | # server_config_yaml: | 42 | # This is now an inner yaml file. Maintain the indentation. 43 | # YAML here will be placed as the content of /etc/rancher/k3s/config.yaml 44 | # See https://docs.k3s.io/installation/configuration#configuration-file 45 | # agent_config_yaml: | 46 | # Same as server_config_yaml, but for the agent nodes. 47 | # YAML here will be placed as the content of /etc/rancher/k3s/config.yaml 48 | # See https://docs.k3s.io/installation/configuration#configuration-file 49 | # registries_config_yaml: | 50 | # Containerd can be configured to connect to private registries and use them to pull images as needed by the kubelet. 51 | # YAML here will be placed as the content of /etc/rancher/k3s/registries.yaml 52 | # See https://docs.k3s.io/installation/private-registry 53 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | requires_ansible: ">=2.15.0" 3 | -------------------------------------------------------------------------------- /playbooks/reboot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot cluster servers staggered 3 | hosts: server 4 | become: true 5 | gather_facts: true 6 | serial: 1 7 | tasks: 8 | - name: Reboot 9 | ansible.builtin.reboot: 10 | test_command: kubectl get nodes 11 | 12 | - name: Reboot cluster agents staggered 13 | hosts: agent 14 | become: true 15 | gather_facts: true 16 | serial: 1 17 | tasks: 18 | - name: Reboot 19 | ansible.builtin.reboot: 20 | -------------------------------------------------------------------------------- /playbooks/reset.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Undo cluster setup 3 | hosts: k3s_cluster 4 | become: true 5 | tasks: 6 | - name: Run K3s Uninstall script [server] 7 | when: "'server' in group_names" 8 | ansible.builtin.command: 9 | cmd: k3s-uninstall.sh 10 | removes: /var/lib/rancher/k3s/* 11 | - name: Run K3s Uninstall script [agent] 12 | when: "'agent' in group_names" 13 | ansible.builtin.command: 14 | cmd: k3s-agent-uninstall.sh 15 | removes: /var/lib/rancher/k3s/* 16 | - name: Remove user kubeconfig 17 | ansible.builtin.file: 18 | path: ~{{ ansible_user }}/.kube/config 19 | state: absent 20 | - name: Remove k3s install script 21 | ansible.builtin.file: 22 | path: /usr/local/bin/k3s-install.sh 23 | state: absent 24 | - name: Remove contents of K3s server location 25 | when: k3s_server_location is defined 26 | ansible.builtin.shell: 27 | cmd: "rm -rf {{ k3s_server_location }}/*" 28 | removes: "{{ k3s_server_location }}/*" 29 | - name: Remove K3s config 30 | when: server_config_yaml is defined 31 | ansible.builtin.file: 32 | path: /etc/rancher/k3s/config.yaml 33 | state: absent 34 | 35 | - name: Undo user setup 36 | hosts: server 37 | tasks: 38 | - name: Remove K3s commands from ~/.bashrc 39 | ansible.builtin.lineinfile: 40 | path: "~{{ ansible_user }}/.bashrc" 41 | search_string: "Added by k3s-ansible" 42 | state: absent 43 | -------------------------------------------------------------------------------- /playbooks/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Cluster prep 3 | hosts: k3s_cluster 4 | gather_facts: true 5 | become: true 6 | roles: 7 | - role: prereq 8 | - role: airgap 9 | - role: raspberrypi 10 | 11 | - name: Setup K3S server 12 | hosts: server 13 | become: true 14 | roles: 15 | - role: k3s_server 16 | 17 | - name: Setup K3S agent 18 | hosts: agent 19 | become: true 20 | roles: 21 | - role: k3s_agent 22 | -------------------------------------------------------------------------------- /playbooks/upgrade.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | # Servers should be restarted sequientally to avoid etcd learner issues 4 | # Agents have no such limitation 5 | - name: Upgrade K3s Servers 6 | hosts: server 7 | become: true 8 | serial: 1 9 | roles: 10 | - role: k3s_upgrade 11 | 12 | - name: Upgrade K3s Agents 13 | hosts: agent 14 | become: true 15 | roles: 16 | - role: k3s_upgrade 17 | -------------------------------------------------------------------------------- /roles/airgap/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check for Airgap 3 | when: airgap_dir is defined 4 | block: 5 | 6 | - name: Verify Ansible meets airgap version requirements. 7 | ansible.builtin.assert: 8 | that: "ansible_version.full is version_compare('2.12', '>=')" 9 | msg: "The Airgap role requires at least ansible-core 2.12" 10 | 11 | - name: Check for existing install script 12 | become: false 13 | delegate_to: localhost 14 | ansible.builtin.stat: 15 | path: "{{ airgap_dir + '/k3s-install.sh' }}" 16 | register: host_install_script 17 | 18 | - name: Download k3s install script 19 | become: false 20 | delegate_to: localhost 21 | # Workaround for https://github.com/ansible/ansible/issues/64016 22 | when: not host_install_script.stat.exists 23 | ansible.builtin.get_url: 24 | url: https://get.k3s.io/ 25 | timeout: 120 26 | dest: "{{ airgap_dir }}/k3s-install.sh" 27 | mode: "0755" 28 | 29 | - name: Distribute K3s install script 30 | ansible.builtin.copy: 31 | src: "{{ airgap_dir }}/k3s-install.sh" 32 | dest: /usr/local/bin/k3s-install.sh 33 | owner: root 34 | group: root 35 | mode: "0755" 36 | 37 | - name: Determine architecture and set k3s_arch 38 | ansible.builtin.set_fact: 39 | k3s_arch: "{{ 'arm64' if ansible_architecture == 'aarch64' else 'arm' if ansible_architecture == 'armv7l' else 'amd64' }}" 40 | 41 | - name: Distribute K3s binary {{ k3s_arch }} 42 | ansible.builtin.copy: 43 | src: "{{ item }}" 44 | dest: /usr/local/bin/k3s 45 | owner: root 46 | group: root 47 | mode: "0755" 48 | with_first_found: 49 | - files: 50 | - "{{ airgap_dir }}/k3s-{{ k3s_arch }}" 51 | - "{{ airgap_dir }}/k3s" 52 | # with_first_found always runs, even inside the when block 53 | # so we need to skip it if the file is not found 54 | skip: true 55 | 56 | - name: Distribute K3s SELinux RPM 57 | ansible.builtin.copy: 58 | src: "{{ item }}" 59 | dest: /tmp/ 60 | owner: root 61 | group: root 62 | mode: "0755" 63 | with_fileglob: 64 | - "{{ airgap_dir }}/k3s-selinux*.rpm" 65 | register: selinux_copy 66 | ignore_errors: true 67 | 68 | - name: Install K3s SELinux RPM 69 | when: 70 | - ansible_os_family == 'RedHat' 71 | - selinux_copy.skipped is false 72 | ansible.builtin.dnf: 73 | name: "{{ selinux_copy.results[0].dest }}" 74 | state: present 75 | disable_gpg_check: true 76 | disablerepo: "*" 77 | 78 | - name: Make images directory 79 | ansible.builtin.file: 80 | path: "/var/lib/rancher/k3s/agent/images/" 81 | mode: "0755" 82 | state: directory 83 | 84 | - name: Distribute K3s images {{ k3s_arch }} 85 | ansible.builtin.copy: 86 | src: "{{ item }}" 87 | dest: /var/lib/rancher/k3s/agent/images/{{ item | basename }} 88 | owner: root 89 | group: root 90 | mode: "0755" 91 | with_first_found: 92 | - files: 93 | - "{{ airgap_dir }}/k3s-airgap-images-{{ k3s_arch }}.tar.zst" 94 | - "{{ airgap_dir }}/k3s-airgap-images-{{ k3s_arch }}.tar.gz" 95 | - "{{ airgap_dir }}/k3s-airgap-images-{{ k3s_arch }}.tar" 96 | # with_first_found always runs, even inside the when block 97 | # so we need to skip it if the file is not found 98 | skip: true 99 | 100 | - name: Run K3s Install [server] 101 | when: inventory_hostname in groups['server'] or ansible_host in groups['server'] 102 | ansible.builtin.command: 103 | cmd: /usr/local/bin/k3s-install.sh 104 | environment: 105 | INSTALL_K3S_SKIP_ENABLE: "true" 106 | INSTALL_K3S_SKIP_DOWNLOAD: "true" 107 | changed_when: true 108 | 109 | - name: Run K3s Install [agent] 110 | when: inventory_hostname in groups['agent'] or ansible_host in groups['agent'] 111 | ansible.builtin.command: 112 | cmd: /usr/local/bin/k3s-install.sh 113 | environment: 114 | INSTALL_K3S_SKIP_ENABLE: "true" 115 | INSTALL_K3S_SKIP_DOWNLOAD: "true" 116 | INSTALL_K3S_EXEC: "agent" 117 | changed_when: true 118 | -------------------------------------------------------------------------------- /roles/k3s_agent/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | server_group: server # noqa var-naming[no-role-prefix] 3 | k3s_server_location: "/var/lib/rancher/k3s" # noqa var-naming[no-role-prefix] 4 | systemd_dir: "/etc/systemd/system" # noqa var-naming[no-role-prefix] 5 | api_port: 6443 # noqa var-naming[no-role-prefix] 6 | extra_agent_args: "" # noqa var-naming[no-role-prefix] 7 | -------------------------------------------------------------------------------- /roles/k3s_agent/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get k3s installed version 3 | ansible.builtin.command: k3s --version 4 | register: k3s_version_output 5 | changed_when: false 6 | ignore_errors: true 7 | 8 | - name: Set k3s installed version 9 | when: not ansible_check_mode and k3s_version_output.rc == 0 10 | ansible.builtin.set_fact: 11 | installed_k3s_version: "{{ k3s_version_output.stdout_lines[0].split(' ')[2] }}" 12 | 13 | # If airgapped, all K3s artifacts are already on the node. 14 | # We should be downloading and installing the newer version only if we are in one of the following cases : 15 | # - we couldn't get k3s installed version in the first task of this role 16 | # - the installed version of K3s on the nodes is older than the requested version in ansible vars 17 | - name: Download artifact only if needed 18 | when: not ansible_check_mode and airgap_dir is undefined and ( k3s_version_output.rc != 0 or installed_k3s_version is version(k3s_version, '<') ) 19 | block: 20 | - name: Download K3s install script 21 | ansible.builtin.get_url: 22 | url: https://get.k3s.io/ 23 | timeout: 120 24 | dest: /usr/local/bin/k3s-install.sh 25 | owner: root 26 | group: root 27 | mode: "0755" 28 | 29 | - name: Download K3s binary 30 | ansible.builtin.command: 31 | cmd: /usr/local/bin/k3s-install.sh 32 | environment: 33 | INSTALL_K3S_SKIP_START: "true" 34 | INSTALL_K3S_VERSION: "{{ k3s_version }}" 35 | INSTALL_K3S_EXEC: "agent" 36 | changed_when: true 37 | 38 | - name: Setup optional config file 39 | when: agent_config_yaml is defined 40 | block: 41 | - name: Make config directory 42 | ansible.builtin.file: 43 | path: "/etc/rancher/k3s" 44 | mode: "0755" 45 | state: directory 46 | - name: Copy config values 47 | ansible.builtin.copy: 48 | content: "{{ agent_config_yaml }}" 49 | dest: "/etc/rancher/k3s/config.yaml" 50 | mode: "0644" 51 | register: _agent_config_result 52 | 53 | - name: Get the token from the first server 54 | ansible.builtin.set_fact: 55 | token: "{{ hostvars[groups[server_group][0]].token }}" 56 | 57 | - name: Delete any existing token from the environment if different from the new one 58 | ansible.builtin.lineinfile: 59 | state: absent 60 | path: "{{ systemd_dir }}/k3s-agent.service.env" 61 | regexp: "^K3S_TOKEN=\\s*(?!{{ token }}\\s*$)" 62 | 63 | - name: Add the token for joining the cluster to the environment 64 | no_log: true # avoid logging the server token 65 | ansible.builtin.lineinfile: 66 | path: "{{ systemd_dir }}/k3s-agent.service.env" 67 | line: "{{ item }}" 68 | with_items: 69 | - "K3S_TOKEN={{ token }}" 70 | 71 | - name: Copy K3s service file 72 | register: k3s_agent_service 73 | ansible.builtin.template: 74 | src: "k3s-agent.service.j2" 75 | dest: "{{ systemd_dir }}/k3s-agent.service" 76 | owner: root 77 | group: root 78 | mode: "u=rw,g=r,o=r" 79 | 80 | - name: Enable and check K3s service 81 | ansible.builtin.systemd: 82 | name: k3s-agent 83 | daemon_reload: "{{ true if k3s_agent_service.changed else false }}" 84 | state: "{{ 'restarted' if (k3s_agent_service.changed or _agent_config_result.changed) else 'started' }}" 85 | enabled: true 86 | -------------------------------------------------------------------------------- /roles/k3s_agent/templates/k3s-agent.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Lightweight Kubernetes 3 | Documentation=https://k3s.io 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Install] 8 | WantedBy=multi-user.target 9 | 10 | [Service] 11 | Type=notify 12 | EnvironmentFile=-/etc/default/%N 13 | EnvironmentFile=-/etc/sysconfig/%N 14 | EnvironmentFile=-/etc/systemd/system/k3s-agent.service.env 15 | KillMode=process 16 | Delegate=yes 17 | # Having non-zero Limit*s causes performance problems due to accounting overhead 18 | # in the kernel. We recommend using cgroups to do container-local accounting. 19 | LimitNOFILE=1048576 20 | LimitNPROC=infinity 21 | LimitCORE=infinity 22 | TasksMax=infinity 23 | TimeoutStartSec=0 24 | Restart=always 25 | RestartSec=5s 26 | ExecStartPre=/bin/sh -xc '! /usr/bin/systemctl is-enabled --quiet nm-cloud-setup.service' 27 | ExecStartPre=-/sbin/modprobe br_netfilter 28 | ExecStartPre=-/sbin/modprobe overlay 29 | ExecStart=/usr/local/bin/k3s agent --data-dir {{ k3s_server_location }} --server https://{{ api_endpoint }}:{{ api_port }} {{ extra_agent_args }} 30 | -------------------------------------------------------------------------------- /roles/k3s_server/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | k3s_server_location: "/var/lib/rancher/k3s" 3 | systemd_dir: "/etc/systemd/system" # noqa var-naming[no-role-prefix] 4 | api_port: 6443 # noqa var-naming[no-role-prefix] 5 | kubeconfig: ~/.kube/config.new # noqa var-naming[no-role-prefix] 6 | user_kubectl: true # noqa var-naming[no-role-prefix] 7 | cluster_context: k3s-ansible # noqa var-naming[no-role-prefix] 8 | server_group: server # noqa var-naming[no-role-prefix] 9 | agent_group: agent # noqa var-naming[no-role-prefix] 10 | use_external_database: false # noqa var-naming[no-role-prefix] 11 | extra_server_args: "" # noqa var-naming[no-role-prefix] 12 | -------------------------------------------------------------------------------- /roles/k3s_server/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get k3s installed version 3 | ansible.builtin.command: k3s --version 4 | register: k3s_version_output 5 | changed_when: false 6 | ignore_errors: true 7 | 8 | - name: Set k3s installed version 9 | when: not ansible_check_mode and k3s_version_output.rc == 0 10 | ansible.builtin.set_fact: 11 | installed_k3s_version: "{{ k3s_version_output.stdout_lines[0].split(' ')[2] }}" 12 | 13 | # If airgapped, all K3s artifacts are already on the node. 14 | # We should be downloading and installing the newer version only if we are in one of the following cases : 15 | # - we couldn't get k3s installed version in the first task of this role 16 | # - the installed version of K3s on the nodes is older than the requested version in ansible vars 17 | - name: Download artifact only if needed 18 | when: not ansible_check_mode and airgap_dir is undefined and ( k3s_version_output.rc != 0 or installed_k3s_version is version(k3s_version, '<') ) 19 | block: 20 | - name: Download K3s install script 21 | ansible.builtin.get_url: 22 | url: https://get.k3s.io/ 23 | timeout: 120 24 | dest: /usr/local/bin/k3s-install.sh 25 | owner: root 26 | group: root 27 | mode: "0755" 28 | 29 | - name: Download K3s binary 30 | ansible.builtin.command: 31 | cmd: /usr/local/bin/k3s-install.sh 32 | environment: 33 | INSTALL_K3S_SKIP_START: "true" 34 | INSTALL_K3S_VERSION: "{{ k3s_version }}" 35 | changed_when: true 36 | 37 | - name: Add K3s autocomplete to user bashrc 38 | when: ansible_user is defined 39 | ansible.builtin.lineinfile: 40 | path: "~{{ ansible_user }}/.bashrc" 41 | regexp: '\.\s+<\(k3s completion bash\)' 42 | line: ". <(k3s completion bash) # Added by k3s-ansible" 43 | 44 | - name: Setup optional config file 45 | when: server_config_yaml is defined 46 | block: 47 | - name: Make config directory 48 | ansible.builtin.file: 49 | path: "/etc/rancher/k3s" 50 | mode: "0755" 51 | state: directory 52 | - name: Copy config values 53 | ansible.builtin.copy: 54 | content: "{{ server_config_yaml }}" 55 | dest: "/etc/rancher/k3s/config.yaml" 56 | mode: "0644" 57 | register: _server_config_result 58 | 59 | - name: Init first server node 60 | when: inventory_hostname == groups[server_group][0] or ansible_host == groups[server_group][0] 61 | block: 62 | - name: Copy K3s service file [Single] 63 | when: groups[server_group] | length == 1 or use_external_database 64 | ansible.builtin.template: 65 | src: "k3s-single.service.j2" 66 | dest: "{{ systemd_dir }}/k3s.service" 67 | owner: root 68 | group: root 69 | mode: "0644" 70 | register: service_file_single 71 | 72 | - name: Copy K3s service file [HA] 73 | when: 74 | - groups[server_group] | length > 1 75 | - not use_external_database 76 | ansible.builtin.template: 77 | src: "k3s-cluster-init.service.j2" 78 | dest: "{{ systemd_dir }}/k3s.service" 79 | owner: root 80 | group: root 81 | mode: "0644" 82 | register: service_file_ha 83 | 84 | - name: Add service environment variables 85 | when: extra_service_envs is defined 86 | ansible.builtin.lineinfile: 87 | path: "{{ systemd_dir }}/k3s.service.env" 88 | line: "{{ item }}" 89 | with_items: "{{ extra_service_envs }}" 90 | 91 | - name: Delete any existing token from the environment if different from the new one 92 | ansible.builtin.lineinfile: 93 | state: absent 94 | path: "{{ systemd_dir }}/k3s.service.env" 95 | regexp: "^K3S_TOKEN=\\s*(?!{{ token | default('') | regex_escape }}\\s*$)" 96 | 97 | # Add the token to the environment if it has been provided. 98 | # Otherwise, let the first server create one on the first run. 99 | - name: Add token as an environment variable 100 | no_log: true # avoid logging the server token 101 | ansible.builtin.lineinfile: 102 | path: "{{ systemd_dir }}/k3s.service.env" 103 | line: "K3S_TOKEN={{ token }}" 104 | when: token is defined 105 | 106 | - name: Restart K3s service 107 | when: 108 | - ansible_facts.services['k3s.service'] is defined 109 | - ansible_facts.services['k3s.service'].state == 'running' 110 | - service_file_single.changed or service_file_ha.changed or _server_config_result.changed 111 | ansible.builtin.systemd: 112 | name: k3s 113 | daemon_reload: true 114 | state: restarted 115 | 116 | - name: Enable and check K3s service 117 | when: ansible_facts.services['k3s.service'] is not defined or ansible_facts.services['k3s.service'].state != 'running' 118 | ansible.builtin.systemd: 119 | name: k3s 120 | daemon_reload: true 121 | state: started 122 | enabled: true 123 | 124 | - name: Pause to allow first server startup 125 | when: (groups[server_group] | length) > 1 126 | ansible.builtin.pause: 127 | seconds: 10 128 | 129 | - name: Check whether kubectl is installed on control node 130 | ansible.builtin.command: 'kubectl' 131 | register: kubectl_installed 132 | ignore_errors: true 133 | delegate_to: 127.0.0.1 134 | become: false 135 | changed_when: false 136 | 137 | # Copy the k3s config to a second file to detect changes. 138 | # If no changes are found, we can skip copying the kubeconfig to the control node. 139 | - name: Copy k3s.yaml to second file 140 | ansible.builtin.copy: 141 | src: /etc/rancher/k3s/k3s.yaml 142 | dest: /etc/rancher/k3s/k3s-copy.yaml 143 | mode: "0600" 144 | remote_src: true 145 | register: copy_k3s_yaml_file 146 | 147 | - name: Apply K3S kubeconfig to control node 148 | when: 149 | - kubectl_installed.rc == 0 150 | - copy_k3s_yaml_file.changed 151 | block: 152 | - name: Copy kubeconfig to control node 153 | ansible.builtin.fetch: 154 | src: /etc/rancher/k3s/k3s.yaml 155 | dest: "{{ kubeconfig }}" 156 | flat: true 157 | 158 | - name: Change server address in kubeconfig on control node 159 | ansible.builtin.shell: | 160 | KUBECONFIG={{ kubeconfig }} kubectl config set-cluster default --server=https://{{ api_endpoint }}:{{ api_port }} 161 | delegate_to: 127.0.0.1 162 | become: false 163 | register: csa_result 164 | changed_when: 165 | - csa_result.rc == 0 166 | 167 | - name: Setup kubeconfig context on control node - {{ cluster_context }} 168 | when: kubeconfig == "~/.kube/config.new" 169 | ansible.builtin.replace: 170 | path: "{{ kubeconfig }}" 171 | regexp: 'default' 172 | replace: '{{ cluster_context }}' 173 | delegate_to: 127.0.0.1 174 | become: false 175 | 176 | - name: Merge with any existing kubeconfig on control node 177 | when: kubeconfig == "~/.kube/config.new" 178 | ansible.builtin.shell: | 179 | TFILE=$(mktemp) 180 | KUBECONFIG={{ kubeconfig }}:~/.kube/config kubectl config set-context {{ cluster_context }} --user={{ cluster_context }} --cluster={{ cluster_context }} 181 | KUBECONFIG={{ kubeconfig }}:~/.kube/config kubectl config view --flatten > ${TFILE} 182 | mv ${TFILE} ~/.kube/config 183 | delegate_to: 127.0.0.1 184 | become: false 185 | register: mv_result 186 | changed_when: 187 | - mv_result.rc == 0 188 | 189 | - name: Get the token if randomly generated 190 | when: token is not defined 191 | block: 192 | - name: Wait for token 193 | ansible.builtin.wait_for: 194 | path: /var/lib/rancher/k3s/server/token 195 | 196 | - name: Read node-token from master 197 | ansible.builtin.slurp: 198 | src: /var/lib/rancher/k3s/server/token 199 | register: node_token 200 | 201 | - name: Store Master node-token 202 | ansible.builtin.set_fact: 203 | token: "{{ node_token.content | b64decode | regex_replace('\n', '') }}" 204 | 205 | - name: Start other server if any and verify status 206 | when: 207 | - (groups[server_group] | length) > 1 208 | - inventory_hostname != groups[server_group][0] and ansible_host != groups[server_group][0] 209 | block: 210 | - name: Get the token from the first server 211 | ansible.builtin.set_fact: 212 | token: "{{ hostvars[groups[server_group][0]].token }}" 213 | 214 | - name: Delete any existing token from the environment if different from the new one 215 | ansible.builtin.lineinfile: 216 | state: absent 217 | path: "{{ systemd_dir }}/k3s.service.env" 218 | regexp: "^K3S_TOKEN=\\s*(?!{{ token }}\\s*$)" 219 | 220 | - name: Add the token for joining the cluster to the environment 221 | no_log: true # avoid logging the server token 222 | ansible.builtin.lineinfile: 223 | path: "{{ systemd_dir }}/k3s.service.env" 224 | line: "{{ item }}" 225 | with_items: 226 | - "K3S_TOKEN={{ token }}" 227 | 228 | - name: Copy K3s service file [HA] 229 | when: not use_external_database 230 | ansible.builtin.template: 231 | src: "k3s-ha.service.j2" 232 | dest: "{{ systemd_dir }}/k3s.service" 233 | owner: root 234 | group: root 235 | mode: "0644" 236 | register: service_file_ha 237 | 238 | - name: Copy K3s service file [External DB] 239 | when: use_external_database 240 | ansible.builtin.template: 241 | src: "k3s-single.service.j2" 242 | dest: "{{ systemd_dir }}/k3s.service" 243 | owner: root 244 | group: root 245 | mode: "0644" 246 | register: service_file_external_db 247 | 248 | - name: Restart K3s service 249 | when: 250 | - ansible_facts.services['k3s.service'] is defined 251 | - ansible_facts.services['k3s.service'].state == 'running' 252 | - service_file_ha.changed or service_file_external_db.changed or _server_config_result.changed 253 | ansible.builtin.systemd: 254 | name: k3s 255 | daemon_reload: true 256 | state: restarted 257 | 258 | - name: Enable and check K3s service 259 | when: ansible_facts.services['k3s.service'] is not defined or ansible_facts.services['k3s.service'].state != 'running' 260 | ansible.builtin.systemd: 261 | name: k3s 262 | daemon_reload: true 263 | state: started 264 | enabled: true 265 | 266 | - name: Verify that all server nodes joined 267 | when: not ansible_check_mode and (groups[server_group] | length) > 1 268 | ansible.builtin.command: 269 | cmd: > 270 | k3s kubectl get nodes -l "node-role.kubernetes.io/control-plane=true" -o=jsonpath="{.items[*].metadata.name}" 271 | register: nodes 272 | until: nodes.rc == 0 and (nodes.stdout.split() | length) == (groups[server_group] | length) 273 | retries: 20 274 | delay: 10 275 | changed_when: false 276 | 277 | - name: Setup kubectl for user 278 | when: user_kubectl 279 | block: 280 | 281 | - name: Create directory .kube 282 | ansible.builtin.file: 283 | path: ~{{ ansible_user }}/.kube 284 | state: directory 285 | owner: "{{ ansible_user }}" 286 | mode: "u=rwx,g=rx,o=" 287 | 288 | - name: Copy config file to user home directory 289 | ansible.builtin.copy: 290 | src: /etc/rancher/k3s/k3s.yaml 291 | dest: ~{{ ansible_user }}/.kube/config 292 | remote_src: true 293 | owner: "{{ ansible_user }}" 294 | mode: "u=rw,g=,o=" 295 | 296 | - name: Configure default KUBECONFIG for user 297 | ansible.builtin.lineinfile: 298 | path: ~{{ ansible_user }}/.bashrc 299 | regexp: 'export KUBECONFIG=~/.kube/config' 300 | line: 'export KUBECONFIG=~/.kube/config # Added by k3s-ansible' 301 | state: present 302 | 303 | - name: Configure kubectl autocomplete 304 | ansible.builtin.lineinfile: 305 | path: ~{{ ansible_user }}/.bashrc 306 | regexp: '\.\s+<\(kubectl completion bash\)' 307 | line: ". <(kubectl completion bash) # Added by k3s-ansible" 308 | -------------------------------------------------------------------------------- /roles/k3s_server/templates/k3s-cluster-init.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Lightweight Kubernetes 3 | Documentation=https://k3s.io 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Install] 8 | WantedBy=multi-user.target 9 | 10 | [Service] 11 | Type=notify 12 | EnvironmentFile=-/etc/default/%N 13 | EnvironmentFile=-/etc/sysconfig/%N 14 | EnvironmentFile=-/etc/systemd/system/k3s.service.env 15 | KillMode=process 16 | Delegate=yes 17 | # Having non-zero Limit*s causes performance problems due to accounting overhead 18 | # in the kernel. We recommend using cgroups to do container-local accounting. 19 | LimitNOFILE=1048576 20 | LimitNPROC=infinity 21 | LimitCORE=infinity 22 | TasksMax=infinity 23 | TimeoutStartSec=0 24 | Restart=always 25 | RestartSec=5s 26 | ExecStartPre=-/sbin/modprobe br_netfilter 27 | ExecStartPre=-/sbin/modprobe overlay 28 | ExecStart=/usr/local/bin/k3s server --cluster-init --data-dir {{ k3s_server_location }} {{ extra_server_args }} 29 | -------------------------------------------------------------------------------- /roles/k3s_server/templates/k3s-ha.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Lightweight Kubernetes 3 | Documentation=https://k3s.io 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Install] 8 | WantedBy=multi-user.target 9 | 10 | [Service] 11 | Type=notify 12 | EnvironmentFile=-/etc/default/%N 13 | EnvironmentFile=-/etc/sysconfig/%N 14 | EnvironmentFile=-/etc/systemd/system/k3s.service.env 15 | KillMode=process 16 | Delegate=yes 17 | # Having non-zero Limit*s causes performance problems due to accounting overhead 18 | # in the kernel. We recommend using cgroups to do container-local accounting. 19 | LimitNOFILE=1048576 20 | LimitNPROC=infinity 21 | LimitCORE=infinity 22 | TasksMax=infinity 23 | TimeoutStartSec=0 24 | Restart=always 25 | RestartSec=5s 26 | ExecStartPre=-/sbin/modprobe br_netfilter 27 | ExecStartPre=-/sbin/modprobe overlay 28 | ExecStart=/usr/local/bin/k3s server --data-dir {{ k3s_server_location }} --server https://{{ api_endpoint }}:{{ api_port }} {{ extra_server_args }} 29 | -------------------------------------------------------------------------------- /roles/k3s_server/templates/k3s-single.service.j2: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Lightweight Kubernetes 3 | Documentation=https://k3s.io 4 | Wants=network-online.target 5 | After=network-online.target 6 | 7 | [Install] 8 | WantedBy=multi-user.target 9 | 10 | [Service] 11 | Type=notify 12 | EnvironmentFile=-/etc/default/%N 13 | EnvironmentFile=-/etc/sysconfig/%N 14 | EnvironmentFile=-/etc/systemd/system/k3s.service.env 15 | KillMode=process 16 | Delegate=yes 17 | # Having non-zero Limit*s causes performance problems due to accounting overhead 18 | # in the kernel. We recommend using cgroups to do container-local accounting. 19 | LimitNOFILE=1048576 20 | LimitNPROC=infinity 21 | LimitCORE=infinity 22 | TasksMax=infinity 23 | TimeoutStartSec=0 24 | Restart=always 25 | RestartSec=5s 26 | ExecStartPre=-/sbin/modprobe br_netfilter 27 | ExecStartPre=-/sbin/modprobe overlay 28 | ExecStart=/usr/local/bin/k3s server --data-dir {{ k3s_server_location }} {{ extra_server_args }} 29 | -------------------------------------------------------------------------------- /roles/k3s_upgrade/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | systemd_dir: /etc/systemd/system # noqa var-naming[no-role-prefix] 3 | server_group: server # noqa var-naming[no-role-prefix] 4 | agent_group: agent # noqa var-naming[no-role-prefix] 5 | -------------------------------------------------------------------------------- /roles/k3s_upgrade/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # with_fileglob doesn't work with remote_src, it tries to find the file on the 3 | # local control-plane instead of the remote host. Shell supports wildcards. 4 | - name: Get k3s installed version 5 | ansible.builtin.command: k3s --version 6 | register: k3s_version_output 7 | changed_when: false 8 | check_mode: false 9 | 10 | - name: Set k3s installed version 11 | ansible.builtin.set_fact: 12 | installed_k3s_version: "{{ k3s_version_output.stdout_lines[0].split(' ')[2] }}" 13 | check_mode: false 14 | 15 | # We should be downloading and installing the newer version only if we are in the following case : 16 | # - the installed version of K3s on the nodes is older than the requested version in ansible vars 17 | - name: Update node only if needed 18 | when: installed_k3s_version is version(k3s_version, '<') 19 | block: 20 | - name: Find K3s service files 21 | ansible.builtin.find: 22 | paths: "{{ systemd_dir }}" 23 | patterns: "k3s*.service" 24 | register: k3s_service_files 25 | 26 | - name: Save current K3s service 27 | ansible.builtin.copy: 28 | src: "{{ item.path }}" 29 | dest: "{{ item.path }}.bak" 30 | remote_src: true 31 | mode: preserve 32 | force: true 33 | loop: "{{ k3s_service_files.files }}" 34 | 35 | - name: Install new K3s Version 36 | ansible.builtin.command: 37 | cmd: /usr/local/bin/k3s-install.sh 38 | environment: 39 | INSTALL_K3S_SKIP_START: "true" 40 | INSTALL_K3S_VERSION: "{{ k3s_version }}" 41 | changed_when: true 42 | 43 | - name: Restore K3s service 44 | ansible.builtin.copy: 45 | src: "{{ item.path }}.bak" 46 | dest: "{{ item.path }}" 47 | remote_src: true 48 | mode: preserve 49 | force: true 50 | loop: "{{ k3s_service_files.files }}" 51 | 52 | - name: Clean up temporary K3s service backups 53 | ansible.builtin.file: 54 | path: "{{ item.path }}.bak" 55 | state: absent 56 | loop: "{{ k3s_service_files.files }}" 57 | 58 | - name: Restart K3s service [server] 59 | when: "server_group in group_names" 60 | ansible.builtin.systemd: 61 | state: restarted 62 | daemon_reload: true 63 | name: k3s 64 | 65 | - name: Restart K3s service [agent] 66 | when: "agent_group in group_names" 67 | ansible.builtin.systemd: 68 | state: restarted 69 | daemon_reload: true 70 | name: k3s-agent 71 | -------------------------------------------------------------------------------- /roles/prereq/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | api_port: 6443 # noqa var-naming[no-role-prefix] 3 | server_group: server # noqa var-naming[no-role-prefix] 4 | agent_group: agent # noqa var-naming[no-role-prefix] 5 | -------------------------------------------------------------------------------- /roles/prereq/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Enforce minimum Ansible version 3 | ansible.builtin.assert: 4 | that: 5 | - ansible_version.full is version('2.14', '>=') 6 | msg: "Minimum ansible-core version required is 2.14" 7 | 8 | - name: Install Dependent Ubuntu Packages 9 | when: ansible_distribution in ['Ubuntu'] 10 | ansible.builtin.apt: 11 | name: policycoreutils # Used by install script to restore SELinux context 12 | update_cache: true 13 | 14 | - name: Enable IPv4 forwarding 15 | ansible.posix.sysctl: 16 | name: net.ipv4.ip_forward 17 | value: "1" 18 | state: present 19 | reload: true 20 | 21 | - name: Enable IPv6 forwarding 22 | ansible.posix.sysctl: 23 | name: net.ipv6.conf.all.forwarding 24 | value: "1" 25 | state: present 26 | reload: true 27 | when: ansible_all_ipv6_addresses | length > 0 28 | 29 | - name: Populate service facts 30 | ansible.builtin.service_facts: 31 | 32 | - name: Allow UFW Exceptions 33 | when: 34 | - ansible_facts.services['ufw'] is defined 35 | - ansible_facts.services['ufw'].state == 'running' 36 | block: 37 | - name: Get ufw status 38 | ansible.builtin.command: 39 | cmd: ufw status 40 | changed_when: false 41 | register: ufw_status 42 | 43 | - name: If ufw enabled, open api port 44 | when: 45 | - "'Status: active' in ufw_status['stdout']" 46 | community.general.ufw: 47 | rule: allow 48 | port: "{{ api_port }}" 49 | proto: tcp 50 | 51 | - name: If ufw enabled, open etcd ports 52 | when: 53 | - "'Status: active' in ufw_status['stdout']" 54 | - groups[server_group] | length > 1 55 | community.general.ufw: 56 | rule: allow 57 | port: "2379:2381" 58 | proto: tcp 59 | 60 | - name: If ufw enabled, allow default CIDRs 61 | when: 62 | - "'Status: active' in ufw_status['stdout']" 63 | community.general.ufw: 64 | rule: allow 65 | src: '{{ item }}' 66 | loop: "{{ (cluster_cidr + ',' + service_cidr) | split(',') }}" 67 | 68 | - name: Allow Firewalld Exceptions 69 | when: 70 | - ansible_facts.services['firewalld.service'] is defined 71 | - ansible_facts.services['firewalld.service'].state == 'running' 72 | block: 73 | - name: If firewalld enabled, open api port 74 | ansible.posix.firewalld: 75 | port: "{{ api_port }}/tcp" 76 | zone: internal 77 | state: enabled 78 | permanent: true 79 | immediate: true 80 | 81 | - name: If firewalld enabled, open etcd ports 82 | when: groups[server_group] | length > 1 83 | ansible.posix.firewalld: 84 | port: "2379-2381/tcp" 85 | zone: internal 86 | state: enabled 87 | permanent: true 88 | immediate: true 89 | 90 | - name: If firewalld enabled, open inter-node ports 91 | ansible.posix.firewalld: 92 | port: "{{ item }}" 93 | zone: internal 94 | state: enabled 95 | permanent: true 96 | immediate: true 97 | with_items: 98 | - 5001/tcp # Spegel (Embedded distributed registry) 99 | - 8472/udp # Flannel VXLAN 100 | - 10250/tcp # Kubelet metrics 101 | - 51820/udp # Flannel Wireguard (IPv4) 102 | - 51821/udp # Flannel Wireguard (IPv6) 103 | 104 | - name: If firewalld enabled, allow node CIDRs 105 | ansible.posix.firewalld: 106 | source: "{{ item }}" 107 | zone: internal 108 | state: enabled 109 | permanent: true 110 | immediate: true 111 | loop: >- 112 | {{ 113 | ( 114 | groups[server_group] | default([]) 115 | + groups[agent_group] | default([]) 116 | ) 117 | | map('extract', hostvars, ['ansible_default_ipv4', 'address']) 118 | | flatten | unique | list 119 | }} 120 | 121 | - name: If firewalld enabled, allow default CIDRs 122 | ansible.posix.firewalld: 123 | source: "{{ item }}" 124 | zone: trusted 125 | state: enabled 126 | permanent: true 127 | immediate: true 128 | loop: "{{ (cluster_cidr + ',' + service_cidr) | split(',') }}" 129 | 130 | - name: Add br_netfilter to /etc/modules-load.d/ 131 | ansible.builtin.copy: 132 | content: "br_netfilter" 133 | dest: /etc/modules-load.d/br_netfilter.conf 134 | mode: "u=rw,g=,o=" 135 | when: (ansible_os_family == 'RedHat' or ansible_distribution == 'Archlinux') 136 | 137 | - name: Load br_netfilter 138 | community.general.modprobe: 139 | name: br_netfilter 140 | state: present 141 | when: (ansible_os_family == 'RedHat' or ansible_distribution == 'Archlinux') 142 | 143 | - name: Set bridge-nf-call-iptables (just to be sure) 144 | ansible.posix.sysctl: 145 | name: "{{ item }}" 146 | value: "1" 147 | state: present 148 | reload: true 149 | when: (ansible_os_family == 'RedHat' or ansible_distribution == 'Archlinux') 150 | loop: 151 | - net.bridge.bridge-nf-call-iptables 152 | - net.bridge.bridge-nf-call-ip6tables 153 | 154 | - name: Check for Apparmor existence 155 | ansible.builtin.stat: 156 | path: /sys/module/apparmor/parameters/enabled 157 | register: apparmor_enabled 158 | 159 | - name: Check if Apparmor is enabled 160 | when: apparmor_enabled.stat.exists 161 | ansible.builtin.command: cat /sys/module/apparmor/parameters/enabled 162 | register: apparmor_status 163 | changed_when: false 164 | 165 | - name: Install Apparmor Parser [Suse] 166 | when: 167 | - ansible_os_family == 'Suse' 168 | - apparmor_status is defined 169 | - apparmor_status.stdout == "Y" 170 | ansible.builtin.package: 171 | name: apparmor-parser 172 | state: present 173 | 174 | - name: Install Apparmor Parser [Debian] 175 | when: 176 | - ansible_distribution == 'Debian' 177 | - ansible_facts['distribution_major_version'] == "11" 178 | - apparmor_status is defined 179 | - apparmor_status.stdout == "Y" 180 | ansible.builtin.package: 181 | name: apparmor 182 | state: present 183 | 184 | - name: Gather the package facts 185 | ansible.builtin.package_facts: 186 | manager: auto 187 | 188 | # Iptables v1.8.0-1.8.4 have a specific bug with K3s. https://github.com/k3s-io/k3s/issues/3117 189 | - name: If iptables v1.8.0-1.8.4, warn user # noqa ignore-errors 190 | when: 191 | - ansible_facts.packages['iptables'] is defined 192 | - ansible_facts.packages['iptables'][0]['version'] is version('1.8.5', '<') 193 | - ansible_facts.packages['iptables'][0]['version'] is version('1.7.9', '>') 194 | ansible.builtin.fail: 195 | msg: 196 | - "Warning: Iptables {{ ansible_facts.packages['iptables'][0]['version'] }} found." 197 | - "Add '--prefer-bundled-bin' to extra_server_args variable to use the bundled iptables binary." 198 | ignore_errors: true 199 | 200 | - name: Add /usr/local/bin to sudo secure_path 201 | ansible.builtin.lineinfile: 202 | line: 'Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin' 203 | regexp: "Defaults(\\s)*secure_path(\\s)*=" 204 | state: present 205 | insertafter: EOF 206 | path: /etc/sudoers 207 | validate: 'visudo -cf %s' 208 | when: ansible_os_family == 'RedHat' 209 | 210 | - name: Setup alternative K3s directory 211 | when: 212 | - k3s_server_location is defined 213 | - k3s_server_location != "/var/lib/rancher/k3s" 214 | block: 215 | - name: Make rancher directory 216 | ansible.builtin.file: 217 | path: "/var/lib/rancher" 218 | mode: "0755" 219 | state: directory 220 | - name: Create symlink 221 | ansible.builtin.file: 222 | dest: /var/lib/rancher/k3s 223 | src: "{{ k3s_server_location }}" 224 | force: true 225 | state: link 226 | 227 | - name: Setup extra manifests 228 | when: extra_manifests is defined 229 | block: 230 | - name: Make manifests directory 231 | ansible.builtin.file: 232 | path: "/var/lib/rancher/k3s/server/manifests" 233 | mode: "0700" 234 | state: directory 235 | - name: Copy manifests 236 | ansible.builtin.copy: 237 | src: "{{ item }}" 238 | dest: "/var/lib/rancher/k3s/server/manifests" 239 | mode: "0600" 240 | loop: "{{ extra_manifests }}" 241 | 242 | - name: Setup optional private registry configuration 243 | when: registries_config_yaml is defined 244 | block: 245 | - name: Make k3s config directory 246 | ansible.builtin.file: 247 | path: "/etc/rancher/k3s" 248 | mode: "0755" 249 | state: directory 250 | - name: Copy config values 251 | ansible.builtin.copy: 252 | content: "{{ registries_config_yaml }}" 253 | dest: "/etc/rancher/k3s/registries.yaml" 254 | mode: "0644" 255 | -------------------------------------------------------------------------------- /roles/prereq/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cluster_cidr: "{{ (server_config_yaml | from_yaml)['cluster-cidr'] | default('10.42.0.0/16') }}" # noqa var-naming[no-role-prefix] 3 | service_cidr: "{{ (server_config_yaml | from_yaml)['service-cidr'] | default('10.43.0.0/16') }}" # noqa var-naming[no-role-prefix] 4 | -------------------------------------------------------------------------------- /roles/raspberrypi/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot Pi 3 | ansible.builtin.reboot: 4 | post_reboot_delay: 10 5 | reboot_timeout: 60 6 | 7 | - name: Regenerate bootloader image 8 | ansible.builtin.command: ./mkscr 9 | args: 10 | chdir: /boot 11 | notify: Reboot Pi 12 | changed_when: true 13 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test for raspberry pi /proc/cpuinfo 3 | ansible.builtin.command: grep -E "Raspberry Pi|BCM2708|BCM2709|BCM2835|BCM2836" /proc/cpuinfo 4 | register: grep_cpuinfo_raspberrypi 5 | failed_when: false 6 | changed_when: false 7 | 8 | - name: Test for raspberry pi /proc/device-tree/model 9 | ansible.builtin.command: grep -E "Raspberry Pi" /proc/device-tree/model 10 | register: grep_device_tree_model_raspberrypi 11 | failed_when: false 12 | changed_when: false 13 | 14 | - name: Set raspberry_pi fact to true 15 | ansible.builtin.set_fact: 16 | raspberry_pi: true 17 | when: 18 | grep_cpuinfo_raspberrypi.rc == 0 or grep_device_tree_model_raspberrypi.rc == 0 19 | 20 | - name: Set detected_distribution to Raspbian 21 | ansible.builtin.set_fact: 22 | detected_distribution: Raspbian 23 | when: > 24 | raspberry_pi|default(false) and 25 | ( ansible_facts.lsb.id|default("") == "Raspbian" or 26 | ansible_facts.lsb.description|default("") is match("[Rr]aspbian.*") ) 27 | 28 | - name: Set detected_distribution to Debian 29 | ansible.builtin.set_fact: 30 | detected_distribution: Debian 31 | when: > 32 | raspberry_pi|default(false) and 33 | ( ansible_facts.lsb.id|default("") == "Debian" or 34 | ansible_facts.lsb.description|default("") is match("Debian") ) 35 | 36 | - name: Set detected_distribution to ArchLinux (ARM64) 37 | ansible.builtin.set_fact: 38 | detected_distribution: Archlinux 39 | when: 40 | - ansible_facts.architecture is search("aarch64") 41 | - raspberry_pi|default(false) 42 | - ansible_facts.os_family is match("Archlinux") 43 | 44 | - name: Execute OS related tasks on the Raspberry Pi 45 | ansible.builtin.include_tasks: "{{ item }}" 46 | with_first_found: 47 | - "prereq/{{ detected_distribution }}.yml" 48 | - "prereq/{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml" 49 | - "prereq/{{ ansible_distribution }}.yml" 50 | - "prereq/default.yml" 51 | when: 52 | - raspberry_pi|default(false) 53 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/prereq/Archlinux.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Enable cgroup via boot commandline if not already enabled 3 | ansible.builtin.replace: 4 | path: /boot/boot.txt 5 | regexp: '^(setenv bootargs console=ttyS1,115200 console=tty0 root=PARTUUID=\${uuid} rw rootwait smsc95xx.macaddr="\${usbethaddr}"(?!.*\b{{ cgroup_item }}\b).*)$' 6 | replace: '\1 {{ cgroup_item }}' 7 | with_items: 8 | - "cgroup_enable=cpuset" 9 | - "cgroup_memory=1" 10 | - "cgroup_enable=memory" 11 | loop_control: 12 | loop_var: cgroup_item 13 | notify: Regenerate bootloader image 14 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/prereq/CentOS.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Enable cgroup via boot commandline if not already enabled 3 | ansible.builtin.replace: 4 | path: /boot/cmdline.txt 5 | regexp: '^([\w](?!.*\b{{ cgroup_item }}\b).*)$' 6 | replace: '\1 {{ cgroup_item }}' 7 | with_items: 8 | - "cgroup_enable=cpuset" 9 | - "cgroup_memory=1" 10 | - "cgroup_enable=memory" 11 | loop_control: 12 | loop_var: cgroup_item 13 | notify: Reboot Pi 14 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/prereq/Debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if /boot/firmware/cmdline.txt exists 3 | ansible.builtin.stat: 4 | path: /boot/firmware/cmdline.txt 5 | register: boot_firmware_cmdline_txt 6 | 7 | - name: Enable cgroup via boot commandline if not already enabled 8 | ansible.builtin.replace: 9 | path: "{{ (boot_firmware_cmdline_txt.stat.exists) | ternary('/boot/firmware/cmdline.txt', '/boot/cmdline.txt') }}" 10 | regexp: '^([\w](?!.*\b{{ cgroup_item }}\b).*)$' 11 | replace: '\1 {{ cgroup_item }}' 12 | with_items: 13 | - "cgroup_enable=cpuset" 14 | - "cgroup_memory=1" 15 | - "cgroup_enable=memory" 16 | loop_control: 17 | loop_var: cgroup_item 18 | notify: Reboot Pi 19 | 20 | - name: Gather the package facts 21 | ansible.builtin.package_facts: 22 | manager: auto 23 | 24 | # IPtables versions 1.6.1 and older have problems with K3s, so we force the use of 25 | # iptables-legacy in that case. 26 | - name: If old iptables found, change to iptables-legacy 27 | when: 28 | - ansible_facts.packages['iptables'] is defined 29 | - ansible_facts.packages['iptables'][0]['version'] is version('1.6.2', '<') 30 | block: 31 | - name: Iptables version on node 32 | ansible.builtin.debug: 33 | msg: "iptables version {{ ansible_facts.packages['iptables'][0]['version'] }} found" 34 | 35 | - name: Flush iptables before changing to iptables-legacy 36 | ansible.builtin.iptables: 37 | flush: true 38 | changed_when: false # iptables flush always returns changed 39 | 40 | - name: Changing to iptables-legacy 41 | community.general.alternatives: 42 | path: /usr/sbin/iptables-legacy 43 | name: iptables 44 | register: ip4_legacy 45 | 46 | - name: Changing to ip6tables-legacy 47 | community.general.alternatives: 48 | path: /usr/sbin/ip6tables-legacy 49 | name: ip6tables 50 | register: ip6_legacy 51 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/prereq/Raspbian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if /boot/firmware/cmdline.txt exists 3 | ansible.builtin.stat: 4 | path: /boot/firmware/cmdline.txt 5 | register: boot_firmware_cmdline_txt 6 | 7 | - name: Enable cgroup via boot commandline if not already enabled 8 | ansible.builtin.replace: 9 | path: "{{ (boot_firmware_cmdline_txt.stat.exists) | ternary('/boot/firmware/cmdline.txt', '/boot/cmdline.txt') }}" 10 | regexp: '^([\w](?!.*\b{{ cgroup_item }}\b).*)$' 11 | replace: '\1 {{ cgroup_item }}' 12 | with_items: 13 | - "cgroup_enable=cpuset" 14 | - "cgroup_memory=1" 15 | - "cgroup_enable=memory" 16 | loop_control: 17 | loop_var: cgroup_item 18 | notify: Reboot Pi 19 | 20 | - name: Gather the package facts 21 | ansible.builtin.package_facts: 22 | manager: auto 23 | 24 | # IPtables versions 1.6.1 and older have problems with K3s, so we force the use of 25 | # iptables-legacy in that case. 26 | - name: If old iptables found, change to iptables-legacy 27 | when: 28 | - ansible_facts.packages['iptables'] is defined 29 | - ansible_facts.packages['iptables'][0]['version'] is version('1.6.2', '<') 30 | block: 31 | - name: Iptables version on node 32 | ansible.builtin.debug: 33 | msg: "iptables version {{ ansible_facts.packages['iptables'][0]['version'] }} found" 34 | 35 | - name: Flush iptables before changing to iptables-legacy 36 | ansible.builtin.iptables: 37 | flush: true 38 | changed_when: false # iptables flush always returns changed 39 | 40 | - name: Changing to iptables-legacy 41 | community.general.alternatives: 42 | path: /usr/sbin/iptables-legacy 43 | name: iptables 44 | register: ip4_legacy 45 | 46 | - name: Changing to ip6tables-legacy 47 | community.general.alternatives: 48 | path: /usr/sbin/ip6tables-legacy 49 | name: ip6tables 50 | register: ip6_legacy 51 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/prereq/Ubuntu.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Enable cgroup via boot commandline if not already enabled 3 | when: lookup('fileglob', '/boot/firmware/cmdline.txt', errors='warn') | length > 0 4 | ansible.builtin.replace: 5 | path: /boot/firmware/cmdline.txt 6 | regexp: '^([\w](?!.*\b{{ cgroup_item }}\b).*)$' 7 | replace: '\1 {{ cgroup_item }}' 8 | with_items: 9 | - "cgroup_enable=cpuset" 10 | - "cgroup_memory=1" 11 | - "cgroup_enable=memory" 12 | loop_control: 13 | loop_var: cgroup_item 14 | notify: Reboot Pi 15 | 16 | - name: Install Ubuntu Raspi Extra Packages 17 | ansible.builtin.apt: 18 | # Fixes issues in newer Ubuntu where VXLan isn't setup right. 19 | # See: https://github.com/k3s-io/k3s/issues/4234 20 | name: linux-modules-extra-raspi 21 | update_cache: true 22 | state: present 23 | when: "ansible_distribution_version is version('20.10', '>=') and ansible_distribution_version is version('24.04', '<')" 24 | -------------------------------------------------------------------------------- /roles/raspberrypi/tasks/prereq/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | --------------------------------------------------------------------------------