|
| 1 | +--- |
| 2 | +- name: Renew Cluster Certificates |
| 3 | + hosts: k8s_cluster |
| 4 | + gather_facts: true |
| 5 | + become: true |
| 6 | + any_errors_fatal: "{{ any_errors_fatal | default(true) }}" |
| 7 | + vars: |
| 8 | + valid_days: 3650 |
| 9 | + k8s_path: /etc/kubernetes |
| 10 | + pki_path: "{{ k8s_path }}/pki" |
| 11 | + k8s_backup_path: "{{ k8s_path }}-backup-{{ ansible_date_time.iso8601_basic_short }}" |
| 12 | + tasks: |
| 13 | + - name: Create k8s backup path |
| 14 | + file: |
| 15 | + path: "{{ k8s_backup_path }}" |
| 16 | + state: directory |
| 17 | + mode: '0755' |
| 18 | + |
| 19 | + # use synchronize need install sshpass |
| 20 | + - name: Backup current kubeconfig |
| 21 | + ansible.posix.synchronize: |
| 22 | + src: "{{ k8s_path }}/{{ item }}" |
| 23 | + dest: "{{ k8s_backup_path }}" |
| 24 | + loop: |
| 25 | + - admin.conf |
| 26 | + - kubelet.conf |
| 27 | + - scheduler.conf |
| 28 | + - controller-manager.conf |
| 29 | + delegate_to: "{{ inventory_hostname }}" |
| 30 | + |
| 31 | + - name: Backup current certificates |
| 32 | + ansible.posix.synchronize: |
| 33 | + src: "{{ pki_path }}/" |
| 34 | + dest: "{{ k8s_backup_path }}/pki/" |
| 35 | + delegate_to: "{{ inventory_hostname }}" |
| 36 | + |
| 37 | + - name: Pre-check expiration date of certificate |
| 38 | + command: kubeadm certs check-expiration |
| 39 | + register: expiration_info |
| 40 | + - debug: var=expiration_info.stdout_lines |
| 41 | + |
| 42 | + - name: Generate k8s certificates |
| 43 | + vars: |
| 44 | + # https://kubernetes.io/docs/setup/best-practices/certificates/#all-certificates |
| 45 | + _certs_opt_mapping: |
| 46 | + - name: admin.conf |
| 47 | + kind: [clientAuth] |
| 48 | + target: "{{ pki_path }}/admin.conf" |
| 49 | + parent_ca: ca |
| 50 | + - name: controller-manager.conf |
| 51 | + kind: [clientAuth] |
| 52 | + target: "{{ pki_path }}/controller-manager.conf" |
| 53 | + parent_ca: ca |
| 54 | + - name: scheduler.conf |
| 55 | + kind: [clientAuth] |
| 56 | + target: "{{ pki_path }}/scheduler.conf" |
| 57 | + parent_ca: ca |
| 58 | + - name: apiserver |
| 59 | + kind: [serverAuth] |
| 60 | + target: "{{ pki_path }}/apiserver" |
| 61 | + parent_ca: ca |
| 62 | + - name: apiserver-kubelet-client |
| 63 | + kind: [clientAuth] |
| 64 | + target: "{{ pki_path }}/apiserver-kubelet-client" |
| 65 | + parent_ca: ca |
| 66 | + - name: front-proxy-client |
| 67 | + kind: [clientAuth] |
| 68 | + target: "{{ pki_path }}/front-proxy-client" |
| 69 | + parent_ca: front-proxy-ca |
| 70 | + - name: apiserver-etcd-client |
| 71 | + kind: [clientAuth] |
| 72 | + target: "{{ pki_path }}/apiserver-etcd-client" |
| 73 | + parent_ca: etcd/ca |
| 74 | + - name: etcd/healthcheck-client |
| 75 | + kind: [clientAuth] |
| 76 | + target: "{{ pki_path }}/etcd/healthcheck-client" |
| 77 | + parent_ca: etcd/ca |
| 78 | + - name: etcd/peer |
| 79 | + kind: [serverAuth, clientAuth] |
| 80 | + target: "{{ pki_path }}/etcd/peer" |
| 81 | + parent_ca: etcd/ca |
| 82 | + - name: etcd/server |
| 83 | + kind: [serverAuth, clientAuth] |
| 84 | + target: "{{ pki_path }}/etcd/server" |
| 85 | + parent_ca: etcd/ca |
| 86 | + |
| 87 | + block: |
| 88 | + - name: Create certs_opt_mapping fact |
| 89 | + block: |
| 90 | + - name: Set default certs_opt_mapping fact value |
| 91 | + set_fact: |
| 92 | + certs_opt_mapping: "{{ _certs_opt_mapping }}" |
| 93 | + certs_renewal_list: "{{ _certs_opt_mapping | map(attribute='name') }}" |
| 94 | + |
| 95 | + - name: Ensure necessary directories exist |
| 96 | + file: |
| 97 | + path: "{{ item }}" |
| 98 | + state: directory |
| 99 | + owner: root |
| 100 | + group: root |
| 101 | + mode: u=rw |
| 102 | + loop: |
| 103 | + - "{{ pki_path }}/csr" |
| 104 | + - "{{ pki_path }}/ext" |
| 105 | + - "{{ pki_path }}/ext/etcd" |
| 106 | + |
| 107 | + - name: Generate new CSR by kubeadm |
| 108 | + command: |- |
| 109 | + kubeadm certs generate-csr \ |
| 110 | + --cert-dir=csr \ |
| 111 | + --kubeconfig-dir=csr \ |
| 112 | + --config=/etc/kubernetes/kubeadm-config.yaml |
| 113 | + args: |
| 114 | + chdir: "{{ pki_path }}" |
| 115 | + |
| 116 | + - name: Register SAN extension for all CSR files |
| 117 | + shell: |- |
| 118 | + openssl req -text -noout \ |
| 119 | + -reqopt no_subject,no_header,no_version,no_serial,no_signame,no_validity,no_issuer,no_pubkey,no_sigdump,no_aux \ |
| 120 | + -in csr/{{ item.name }}.csr \ |
| 121 | + | sed '1,3d;s/ Address//g;s/^[[:blank:]]*//;s/[[:blank:]]*$//' |
| 122 | + args: |
| 123 | + chdir: "{{ pki_path }}" |
| 124 | + register: csr_info |
| 125 | + loop: "{{ certs_opt_mapping }}" |
| 126 | + loop_control: |
| 127 | + label: "{{ item.name }}" |
| 128 | + |
| 129 | + - name: Generate extension files |
| 130 | + vars: |
| 131 | + cert_v3_ext: |- |
| 132 | + keyUsage = critical, digitalSignature, keyEncipherment |
| 133 | + extendedKeyUsage = {{ item.0.kind | join(',') }} |
| 134 | + {% if item.1.stdout %} |
| 135 | + subjectAltName = {{ item.1.stdout }} |
| 136 | + {% endif %} |
| 137 | + copy: |
| 138 | + content: "{{ cert_v3_ext }}" |
| 139 | + dest: "{{ pki_path }}/ext/{{ item.0.name }}.ext" |
| 140 | + mode: u=rw,g=r,o= |
| 141 | + loop: "{{ certs_opt_mapping|zip(csr_info.results)|list }}" |
| 142 | + loop_control: |
| 143 | + label: "{{ item.0.name }}" |
| 144 | + |
| 145 | + - name: Create new signed certificates |
| 146 | + command: |- |
| 147 | + openssl x509 -req -days {{ valid_days }} \ |
| 148 | + -in csr/{{ item.name }}.csr \ |
| 149 | + -extfile ext/{{ item.name }}.ext \ |
| 150 | + -CA {{ item.parent_ca }}.crt \ |
| 151 | + -CAkey {{ item.parent_ca }}.key \ |
| 152 | + -CAcreateserial \ |
| 153 | + -out {{ item.target }}.crt |
| 154 | + args: |
| 155 | + chdir: "{{ pki_path }}" |
| 156 | + loop: "{{ certs_opt_mapping }}" |
| 157 | + loop_control: |
| 158 | + label: "{{ item.name }}" |
| 159 | + |
| 160 | + - name: Copy keys to certificates location and ensure that permissions are strict |
| 161 | + vars: |
| 162 | + _conf_files: "{{ certs_opt_mapping | selectattr('name', 'search', '.conf') }}" |
| 163 | + copy: |
| 164 | + src: "{{ pki_path }}/csr/{{ item.name }}.key" |
| 165 | + remote_src: true |
| 166 | + dest: "{{ item.target }}.key" |
| 167 | + owner: root |
| 168 | + group: root |
| 169 | + mode: u=rw |
| 170 | + loop: "{{ certs_opt_mapping | difference(_conf_files) }}" |
| 171 | + loop_control: |
| 172 | + label: "{{ item.name }}" |
| 173 | + |
| 174 | + - name: Update conf files with embedded certs |
| 175 | + vars: |
| 176 | + _kubeconf_cn_mapping: |
| 177 | + admin.conf: kubernetes-admin |
| 178 | + scheduler.conf: system:kube-scheduler |
| 179 | + controller-manager.conf: system:kube-controller-manager |
| 180 | + block: |
| 181 | + - name: Slurp kubeconfig files |
| 182 | + slurp: |
| 183 | + src: "{{ pki_path }}/csr/{{ item }}" |
| 184 | + register: kubeconfig_files_content |
| 185 | + loop: "{{ _kubeconf_cn_mapping.keys() | intersect(certs_renewal_list) }}" |
| 186 | + |
| 187 | + - name: Create kubeconfig key files |
| 188 | + vars: |
| 189 | + _content: "{{ config_file.content | b64decode | from_yaml }}" |
| 190 | + copy: |
| 191 | + content: "{{ _content.users.0.user['client-key-data'] | b64decode }}" |
| 192 | + dest: "{{ pki_path }}/{{ config_file.item }}.key" |
| 193 | + owner: root |
| 194 | + group: root |
| 195 | + mode: u=rw |
| 196 | + loop: "{{ kubeconfig_files_content.results }}" |
| 197 | + loop_control: |
| 198 | + loop_var: config_file |
| 199 | + label: "{{ config_file.item }}" |
| 200 | + |
| 201 | + - name: Update conf files with embedded certs |
| 202 | + environment: |
| 203 | + KUBECONFIG: "/etc/kubernetes/{{ item.key }}" |
| 204 | + command: |- |
| 205 | + kubectl config set-credentials {{ item.value }} \ |
| 206 | + --client-key {{ item.key }}.key \ |
| 207 | + --client-certificate {{ item.key }}.crt --embed-certs |
| 208 | + args: |
| 209 | + chdir: "{{ pki_path }}" |
| 210 | + loop: "{{ _kubeconf_cn_mapping | dict2items | selectattr('key', 'in', certs_renewal_list) }}" |
| 211 | + |
| 212 | + - name: Updating kubeconfig in root path |
| 213 | + copy: |
| 214 | + src: "{{ k8s_path }}/admin.conf" |
| 215 | + remote_src: true |
| 216 | + dest: /root/.kube/config |
| 217 | + owner: root |
| 218 | + group: root |
| 219 | + mode: u=rw |
| 220 | + |
| 221 | + - name: Remove conf certificates and temporary directories |
| 222 | + file: |
| 223 | + path: "{{ pki_path }}/{{ item }}" |
| 224 | + state: absent |
| 225 | + loop: |
| 226 | + - admin.conf.crt |
| 227 | + - admin.conf.key |
| 228 | + - scheduler.conf.crt |
| 229 | + - scheduler.conf.key |
| 230 | + - controller-manager.conf.crt |
| 231 | + - controller-manager.conf.key |
| 232 | + - csr |
| 233 | + - ext |
| 234 | + |
| 235 | + rescue: |
| 236 | + - name: Restore certificates |
| 237 | + ansible.posix.synchronize: |
| 238 | + src: "{{ k8s_backup_path }}/" |
| 239 | + dest: "{{ pki_path | regex_replace('\\/$', '') }}" |
| 240 | + delegate_to: "{{ inventory_hostname }}" |
| 241 | + |
| 242 | + - name: Fail certificates generation |
| 243 | + fail: |
| 244 | + msg: Certificates generation failed, restored an initial state |
| 245 | + |
| 246 | + - name: Restart kubelet systemd services |
| 247 | + block: |
| 248 | + - name: Restart services |
| 249 | + systemd: |
| 250 | + name: kubelet |
| 251 | + state: restarted |
| 252 | + |
| 253 | + - name: Wait until cluster is available |
| 254 | + command: kubectl cluster-info |
| 255 | + retries: 60 |
| 256 | + delay: 1 |
| 257 | + register: result |
| 258 | + until: result is succeeded and "running" in result.stdout |
| 259 | + |
| 260 | + - name: Run check-expiration for control-plane |
| 261 | + command: kubeadm certs check-expiration |
| 262 | + register: expiration_info |
| 263 | + - debug: var=expiration_info.stdout_lines |
0 commit comments