[Kubernetes] Cluster: Kubespray를 이용해 클러스터 구성하기 - 3.3.2. cluster.yml - boilerplate.yml
서종호(가시다)님의 On-Premise K8s Hands-on Study 4주차 학습 내용을 기반으로 합니다.
TL;DR
이번 글에서는 cluster.yml의 첫 번째 단계인 boilerplate.yml을 분석한다.
- ansible_version.yml: Ansible 버전, Python netaddr, Jinja 버전 검증
- dynamic_groups: 구 그룹명 호환성 유지,
k8s_cluster그룹 자동 생성 - validate_inventory: 인벤토리 설정값 검증 (etcd 홀수, 네트워크 범위 등)
전체 흐름에서의 위치

boilerplate.yml은 cluster.yml에서 가장 먼저 실행되는 플레이북으로, 본격적인 클러스터 설치 전에 환경 검증과 인벤토리 준비를 수행한다.
# cluster.yml
- name: Common tasks for every playbooks
import_playbook: boilerplate.yml # ← 첫 번째 실행
boilerplate.yml 구조
---
- name: Check ansible version
import_playbook: ansible_version.yml
- name: Inventory setup and validation
hosts: all
gather_facts: false
tags: always
roles:
- dynamic_groups
- validate_inventory
- name: Install bastion ssh config
hosts: bastion[0]
gather_facts: false
environment: "{{ proxy_disable_env }}"
roles:
- { role: kubespray_defaults }
- { role: bastion-ssh-config, tags: ["localhost", "bastion"] }
| 플레이 | 대상 | 역할 |
|---|---|---|
| Check ansible version | all (run_once) | Ansible/Python/Jinja 버전 검증 |
| Inventory setup and validation | all | 그룹 매핑, 인벤토리 검증 |
| Install bastion ssh config | bastion[0] | Bastion 호스트 SSH 설정 (선택) |
1. Check ansible version

ansible_version.yml 분석
---
- name: Check Ansible version
hosts: all
gather_facts: false
become: false
# 모든 호스트를 대상으로 설정되어 있지만, run_once 덕분에 실제로는 딱 한 번만 실행
# 버전 체크는 배포 서버에서 한 번만 하면 됨
run_once: true
vars:
minimal_ansible_version: 2.17.3
maximal_ansible_version: 2.18.0
# 다른 특정 태그를 지정해서 실행하더라도, 이 버전 체크 과정은 항상 포함
tags: always
# 목적: Kubespray 소스 코드와 Ansible 버전 간의 호환성 보장
tasks:
# Ansible 버전 체크: 2.17.3 <= version < 2.18.0
- name: "Check {{ minimal_ansible_version }} <= Ansible version < {{ maximal_ansible_version }}"
assert:
msg: "Ansible must be between {{ minimal_ansible_version }} and {{ maximal_ansible_version }} exclusive"
that:
- ansible_version.string is version(minimal_ansible_version, ">=")
- ansible_version.string is version(maximal_ansible_version, "<")
tags: check
# Python netaddr 라이브러리 설치 확인 (pip3 list | grep netaddr로도 확인 가능)
# ipaddr 필터로 127.0.0.1을 처리해보고, 에러 발생 시 미설치로 판단
- name: "Check that python netaddr is installed"
assert:
msg: "Python netaddr is not present"
that: "'127.0.0.1' | ansible.utils.ipaddr"
tags: check
# Jinja2 템플릿 엔진 버전 확인
# 최신 Jinja2 문법({% set ... %})을 실행해보고 정상 작동 여부 확인
- name: "Check that jinja is not too old (install via pip)"
assert:
msg: "Your Jinja version is too old, install via pip"
that: "{% set test %}It works{% endset %}{{ test == 'It works' }}"
tags: check
주요 설정 분석
| 설정 | 값 | 설명 |
|---|---|---|
hosts |
all |
모든 호스트 대상 |
gather_facts |
false |
시스템 정보 수집 안 함 (버전 체크에 불필요) |
run_once |
true |
한 번만 실행 (버전 체크는 배포 서버에서 한 번이면 충분) |
tags |
always |
--tags 옵션과 관계없이 항상 실행 |
vars 분석
vars:
minimal_ansible_version: 2.17.3
maximal_ansible_version: 2.18.0
play vars로 정의된 변수다. 이 플레이 내에서만 유효하며, Kubespray가 지원하는 Ansible 버전 범위를 명시한다.
assert 모듈
assert는 조건이 만족하지 않으면 플레이북을 중단시키는 모듈이다:
- name: "Check Ansible version"
assert:
msg: "에러 메시지" # 실패 시 출력할 메시지
that: # 검증할 조건 목록 (모두 true여야 통과)
- 조건1
- 조건2
검증 항목
| 검증 | 조건 | 목적 |
|---|---|---|
| Ansible 버전 | 2.17.3 <= version < 2.18.0 |
호환되는 Ansible 버전만 허용 |
| Python netaddr | ipaddr 필터 동작 여부 |
IP 주소 처리에 필요한 라이브러리 |
| Jinja 버전 | {% set %} 문법 동작 여부 |
템플릿 렌더링에 필요 |
2. Inventory setup and validation
플레이 구조
- name: Inventory setup and validation
hosts: all
gather_facts: false
tags: always
roles:
- dynamic_groups
- validate_inventory
두 개의 롤이 순차적으로 실행된다.
dynamic_groups 롤

역할
구 그룹명 호환성 유지와 k8s_cluster 그룹 자동 생성을 담당한다.
| 기능 | 설명 |
|---|---|
| 이름 표준화 | 사용자가 하이픈(kube-master)을 쓰든 언더바(kube_control_plane)를 쓰든, 내부적으로 표준화된 이름 사용 |
| 자동 재분류 | 과거 방식 이름으로 그룹을 만들어도 최신 이름으로 서버들을 자동 재분류 |
| 계층적 구조 형성 | 워커/마스터 노드만 정의해도 k8s_cluster라는 통합 그룹 자동 생성 |
코드 분석
---
- name: Match needed groups by their old names or definition
vars:
# "A라는 이름은 B라는 이름을 포함한다"는 매핑 규칙 정의
group_mappings:
kube_control_plane: # 과거 이름인 kube-master를 포함
- kube-master
kube_node: # 과거 이름인 kube-node를 포함
- kube-node
calico_rr:
- calico-rr
no_floating:
- no-floating
k8s_cluster: # kube_node, kube_control_plane, calico_rr를 하나로 묶음
- kube_node
- kube_control_plane
- calico_rr
# 실행 중에(In-memory) 새로운 Ansible 그룹 생성
# 조건에 맞는 서버들을 key 값으로 명명된 그룹에 실시간 할당
group_by:
key: "{{ item.key }}"
# 겹치는 이름이 하나라도 있다면 group_by 실행
when: group_names | intersect(item.value) | length > 0
loop: "{{ group_mappings | dict2items }}"
동작 원리
| 요소 | 설명 |
|---|---|
group_mappings |
신규 그룹명과 구 그룹명/하위 그룹의 매핑 규칙 |
group_by |
호스트를 지정한 그룹에 동적으로 추가 (In-memory) |
when |
호스트의 그룹과 매핑 값이 겹칠 때만 실행 |
loop |
group_mappings 딕셔너리를 순회 |
실제 동작 예시
인벤토리에 kube_node와 kube_control_plane 그룹이 정의되어 있으면:
k8s-w1이 kube_node에 속함
→ k8s_cluster 매핑의 value에 kube_node가 있음
→ group_names | intersect(['kube_node', ...]) | length > 0 → true
→ k8s-w1을 k8s_cluster 그룹에 추가
결과적으로 k8s_cluster 그룹이 자동으로 생성되어, 이후 작업에서 “모든 클러스터 노드”를 한 번에 지칭할 수 있다.
validate_inventory 롤

역할
인벤토리와 변수 설정의 유효성을 검증한다. 클러스터 구성 전에 잘못된 설정을 사전에 차단한다.
주요 검증 항목
---
# 그룹 검증
- name: Stop if kube_control_plane group is empty
assert:
that: groups.get('kube_control_plane')
- name: Stop if etcd group is empty in external etcd mode
assert:
that: groups.get('etcd') or etcd_deployment_type == 'kubeadm'
- name: Stop if even number of etcd hosts
assert:
that: groups.get('etcd', groups.kube_control_plane) | length is not divisibleby 2
# 네트워크 검증
- name: Guarantee that enough network address space is available for all pods
assert:
that: "{{ (kubelet_max_pods | default(110)) | int <= (2 ** (32 - kube_network_node_prefix | int)) - 2 }}"
- name: Check that kube_pods_subnet does not collide with kube_service_addresses
assert:
that:
- kube_pods_subnet | ansible.utils.ipaddr(kube_service_addresses) | string == 'None'
# 옵션 검증
- name: Stop if unsupported options selected
assert:
that:
- kube_network_plugin in ['calico', 'flannel', 'cloud', 'cilium', ...]
- dns_mode in ['coredns', 'coredns_dual', 'manual', 'none']
- kube_proxy_mode in ['iptables', 'ipvs', 'nftables']
- container_manager in ['docker', 'crio', 'containerd']
검증 항목 정리
| 카테고리 | 검증 항목 | 설명 |
|---|---|---|
| 그룹 | kube_control_plane 비어있음 |
컨트롤플레인 노드 필수 |
| 그룹 | etcd 호스트 수가 짝수 | 홀수여야 quorum 형성 가능 |
| 네트워크 | Pod/Service 서브넷 충돌 | kube_pods_subnet과 kube_service_addresses 중복 불가 |
| 네트워크 | kubelet_max_pods 초과 |
노드당 Pod 수가 할당된 네트워크 범위 초과 |
| 네트워크 | 전체 IP 대역 부족 | 클러스터 노드 수 대비 IP 범위 검증 |
| 호환성 | K8s 버전 | Kubespray 릴리스에서 지원하는 최소 버전 이상인지 |
| 호환성 | 컨테이너 매니저 조합 | Docker/Containerd/CRI-O와 Kata/gVisor 조합 유효성 |
| 변수 | Boolean 타입 | Boolean 값이 문자열로 잘못 설정되지 않았는지 |
| 변수 | 상호 의존성 | RBAC/Cloud Provider/Dashboard 등 설정 짝 검증 |
참고: 이러한 사전 검증 덕분에 클러스터 설치 중간에 실패하는 것을 방지할 수 있다. 설정 오류는 설치 시작 전에 발견하는 것이 좋다.
검증 공식 분해
etcd 홀수 검증:
groups.get('etcd', groups.kube_control_plane) | length is not divisibleby 2
etcd는 Raft 합의 알고리즘을 사용하며, 과반수(quorum)가 동의해야 데이터를 쓸 수 있다.
| 노드 수 | quorum | 허용 장애 수 | 비고 |
|---|---|---|---|
| 2 | 2 | 0 | 1대만 죽어도 쓰기 불가 |
| 3 | 2 | 1 | 1대 장애 허용 |
| 4 | 3 | 1 | 3대와 동일한 장애 허용 (비효율) |
| 5 | 3 | 2 | 2대 장애 허용 |
짝수는 홀수 대비 장애 허용 수가 동일하면서 비용만 증가하므로 비효율적이다.
네트워크 주소 공간 검증:
(kubelet_max_pods | default(110)) | int <= (2 ** (32 - kube_network_node_prefix | int)) - 2
각 노드는 kube_network_node_prefix 크기의 서브넷을 할당받는다.
| 요소 | 의미 |
|---|---|
kube_network_node_prefix |
노드별 서브넷 크기 (예: /24) |
32 - prefix |
호스트 비트 수 (예: 32 - 24 = 8) |
2 ** (32 - prefix) |
총 IP 수 (예: 2^8 = 256) |
- 2 |
네트워크/브로드캐스트 주소 제외 |
kubelet_max_pods |
노드당 최대 Pod 수 (기본 110) |
예시 (/24 서브넷):
사용 가능 IP = 2^(32-24) - 2 = 254개
kubelet_max_pods = 110
110 <= 254 → 검증 통과
3. Install bastion ssh config
플레이 구조
- name: Install bastion ssh config
hosts: bastion[0]
gather_facts: false
environment: "{{ proxy_disable_env }}"
roles:
- { role: kubespray_defaults }
- { role: bastion-ssh-config, tags: ["localhost", "bastion"] }
역할
Bastion 호스트를 통해 프라이빗 네트워크의 노드에 접근해야 할 때 SSH 설정을 자동으로 구성한다.
| 설정 | 값 | 설명 |
|---|---|---|
hosts |
bastion[0] |
bastion 그룹의 첫 번째 호스트 |
environment |
proxy_disable_env |
프록시 비활성화 |
bastion-ssh-config 롤
---
- name: Set bastion host IP and port
set_fact:
bastion_ip: "{{ hostvars[groups['bastion'][0]]['ansible_host'] }}"
bastion_port: "{{ hostvars[groups['bastion'][0]]['ansible_port'] | d(22) }}"
delegate_to: localhost
- name: Create ssh bastion conf
become: false
delegate_to: localhost
template:
src: "{{ ssh_bastion_confing__name }}.j2"
dest: "{{ playbook_dir }}/{{ ssh_bastion_confing__name }}"
Bastion 호스트의 IP와 포트를 가져와 SSH 설정 파일을 생성한다.
참고: bastion 그룹이 인벤토리에 정의되어 있지 않으면 이 플레이는 스킵된다.
실행 로그 분석
실제 ansible-playbook cluster.yml 실행 시 boilerplate.yml 부분의 로그를 분석한다.
Check Ansible version
PLAY [Check Ansible version] ***************************************************
TASK [Check 2.17.3 <= Ansible version < 2.18.0] ********************************
ok: [k8s-ctr] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [Check that python netaddr is installed] **********************************
ok: [k8s-ctr] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [Check that jinja is not too old (install via pip)] ***********************
ok: [k8s-ctr] => {
"changed": false,
"msg": "All assertions passed"
}
- 모든 검증이
ok로 통과 changed: false- 시스템 상태를 변경하지 않음 (검증만 수행)
Inventory setup and validation
PLAY [Inventory setup and validation] ******************************************
TASK [dynamic_groups : Match needed groups by their old names or definition] ***
changed: [k8s-ctr] => (item={'key': 'k8s_cluster', 'value': ['kube_node', 'kube_control_plane', 'calico_rr']})
k8s_cluster그룹이 동적으로 생성됨changed: true- 호스트가 새 그룹에 추가됨
TASK [validate_inventory : Stop if kube_control_plane group is empty] **********
ok: [k8s-ctr] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [validate_inventory : Stop if even number of etcd hosts] ******************
ok: [k8s-ctr] => {
"changed": false,
"msg": "All assertions passed"
}
TASK [validate_inventory : Guarantee that enough network address space is available for all pods] ***
ok: [k8s-ctr] => {
"changed": false,
"msg": "All assertions passed"
}
- 인벤토리 검증 항목들이 모두 통과
- etcd 홀수 검증, 네트워크 범위 검증 등
Install bastion ssh config
PLAY [Install bastion ssh config] **********************************************
skipping: no hosts matched
bastion그룹이 인벤토리에 없으므로 스킵- 일반적인 단일 노드/프라이빗 네트워크 없는 환경에서는 정상
결과
boilerplate.yml의 역할을 정리하면 다음과 같다:
| 단계 | 플레이 | 역할 |
|---|---|---|
| 1 | Check ansible version | 실행 환경 검증 (Ansible, Python, Jinja) |
| 2 | Inventory setup | 그룹 호환성 유지, k8s_cluster 자동 생성 |
| 3 | Inventory validation | 설정 오류 사전 차단 |
| 4 | Bastion ssh config | (선택) Bastion 호스트 SSH 설정 |
boilerplate.yml은 클러스터 설치의 사전 준비 단계로, 잘못된 환경이나 설정을 미리 걸러내어 설치 중 실패를 방지한다.
다음 글에서는 OS 부트스트랩과 fact 수집을 담당하는 internal_facts.yml을 분석한다.
댓글남기기