지난 글에서 Promox와 Terraform으로 홈랩에서 사용할 네대의 VM을 프로비저닝 했습니다. 제 Bastion과 Kubernetes 클러스터를 구성하기 위해 노드별로 필요한 환경 설정을 진행하려고 합니다.
VM마다 직접 접속해서 설정할까도 생각했지만, 앞으로 VM을 다시 만들거나 구성을 바꿀 일이 있을 것 같았습니다. 그때마다 이전에 어떤 설정을 했는지 찾아가며 작업하기보다는 코드로 남겨두고 싶었고, 여러 노드의 공통 설정도 함께 관리하고 싶었습니다. 그래서 VM 생성 이후의 작업은 Ansible로 구성하기로 했습니다.
다만 Ansible에서 처리할 작업이 생각보다 많았습니다. Bastion과 Tailscale 설정부터 Kubernetes 클러스터 설치, Helm과 Argo CD 부트스트랩까지 포함하다 보니, 하나의 Playbook에 모두 작성하면 나중에 필요한 부분을 찾아 수정하거나 특정 단계만 다시 실행하기 불편할 것 같았습니다.
그래서 site.yml을 진입점으로 두고 작업을 Role로 나눈 뒤, 실행 명령은 Makefile에 묶었습니다. 이번 글에서는 이 구조를 어떻게 나누고 실행하도록 구성했는지 정리해보겠습니다.
site.yml과 Role 구성
site.yml에는 전체 작업의 실행 순서를 작성했습니다. 각 Play에서 대상 호스트와 Role을 지정하고, 일부 작업만 실행할 수 있도록 태그를 붙였습니다.
- name: 1. Bastion SSH 포트/방화벽/SELinux 구성
hosts: bastion
become: true
roles: [bastion_ssh]
tags: [bastion, bastion_ssh]
- name: 3. Tailscale 설치 및 Tailnet 조인
hosts: tailscale_nodes
become: true
roles: [tailscale]
tags: [tailscale, management]
- name: 4. Kubespray 실행
hosts: localhost
connection: local
gather_facts: false
roles: [kubespray_runner]
tags: [kubespray]
위 코드는 site.yml의 일부입니다. 실제 파일에는 Bastion 호스트 매핑과 설치 후 노드 설정, Helm 설치, SOPS 키 주입, Argo CD 구성도 포함되어 있습니다. 각 Play에는 태그를 붙여두어서, Tailscale 설정만 적용하고 싶을 때는 --tags tailscale로 해당 단계를 호출하도록 했습니다.
세부 작업은 각 Role 안에 작성했습니다. bastion_ssh에는 SSH 포트와 SELinux, 방화벽 설정을, tailscale에는 설치와 설정, Tailnet 조인 작업을 모았습니다. Role 내부에서도 설치와 설정처럼 구분이 필요한 작업은 파일을 나눠두었고, 주요 디렉터리 구조는 아래와 같습니다.
ansible/
├── site.yml
├── Makefile
├── inventories/homelab/
│ ├── hosts.yml.example
│ └── group_vars/
├── roles/
│ ├── bastion_ssh/
│ ├── tailscale/
│ ├── kubespray_runner/
│ ├── k8s_node_config/
│ └── argocd/
└── scripts/
├── prepare-bastion.sh
├── ansible-env.sh
└── run-ansible.sh
이 중 kubespray_runner는 Bastion에서 Kubespray를 호출하는 Role입니다. 앞의 Play에서 실행 대상을 localhost로 지정한 것도 이 때문입니다. Ansible을 실행하는 Bastion에서 Kubespray를 호출하고, Kubespray가 각 Kubernetes 노드에 접속해 설치를 진행하도록 구성했습니다.
Bastion에서 Ansible을 실행하기 위한 준비
이 흐름으로 실행하려면 먼저 infra-bastion에 Ansible과 Python 환경이 준비되어 있어야 했습니다. 아직 Ansible을 사용할 수 없는 단계이기 때문에, 실행 환경을 설치하는 작업은 scripts/prepare-bastion.sh에 작성했습니다.
스크립트에서는 필요한 패키지를 설치하고 Python 가상환경인 .venv를 만든 뒤, Ansible 컨트롤러와 Kubespray의 의존성을 설치합니다. Kubespray 저장소를 설정한 버전으로 체크아웃하는 작업도 여기에 포함했습니다.
cd /path/to/homelab/ansible
./scripts/prepare-bastion.sh
설치 이후 Playbook을 호출할 때는 run-ansible.sh를 거치도록 했습니다. 가상환경 활성화와 경로 설정을 매번 따로 하지 않도록 ansible-env.sh에 모아두고, 실행 스크립트에서 먼저 읽어오는 방식입니다.
# run-ansible.sh의 핵심 부분
. "${SCRIPT_DIR}/ansible-env.sh"
cd "${HOMELAB_ANSIBLE_ROOT}"
exec ansible-playbook "$@"
ansible-env.sh에서는 .venv를 활성화하고 ANSIBLE_CONFIG, 인벤토리 경로, Role 경로를 설정합니다. 이 설정을 읽은 뒤 프로젝트 디렉터리로 이동해서 ansible-playbook을 실행하므로, 어느 위치에서 호출하더라도 같은 실행 환경을 사용하도록 했습니다.
실행에 필요한 인증 정보 준비
실행 환경 외에도 Tailscale 인증 키와 SOPS의 age 비공개키처럼 Playbook에 전달할 값이 필요했습니다. 이 값들은 로컬 secrets.yml에 넣고 Ansible Vault로 암호화한 뒤, 필요한 단계에서 -e @secrets.yml --ask-vault-pass 옵션으로 전달하도록 했습니다. 실제 IP와 접속 사용자 정보가 포함된 hosts.yml도 공개 저장소에서는 제외하고 hosts.yml.example만 남겼습니다.
여기서 age 비공개키는 이후 GitOps에서 사용할 SOPS 파일을 복호화하기 위한 키입니다. SOPS로 암호화한 파일은 GitOps 저장소에서 관리하지만, 그 파일을 읽을 키는 먼저 클러스터에 준비되어 있어야 해서 Ansible의 초기 구성에 포함했습니다.
따라서 Ansible Vault에는 Tailscale 인증 키와 age 비공개키 등 부트스트랩에 필요한 값을 보관하고, SOPS는 GitOps 저장소에 올릴 시크릿 파일을 암호화하는 데 사용했습니다. sops_bootstrap Role에서 Vault로 전달받은 age 비공개키를 Kubernetes Secret으로 등록하고, Argo CD 측의 복호화 과정에서 이 키를 참조하도록 연결하는 흐름입니다.
이때 키를 Secret으로 등록하는 작업 외에 Argo CD에서 복호화 도구와 키를 사용할 수 있도록 하는 설정도 필요합니다. 해당 설정은 Argo CD 구성 내용을 다룰 때 함께 정리하겠습니다.
Makefile로 실행 순서와 점검 명령 정리
실행 환경과 변수 파일을 준비하고 나니, 각 단계에서 사용할 명령도 한곳에 정리하고 싶었습니다. Playbook을 호출할 때마다 태그와 변수 파일 옵션을 입력하는 대신, 필요한 사전 작업까지 make 명령으로 묶어서 실행하도록 했습니다.
아래는 Makefile의 주요 부분입니다. sync-kubespray와 ssh-check의 세부 정의는 생략했습니다.
all: prepare inventory syntax tailscale bootstrap
prepare:
./scripts/prepare-bastion.sh
inventory:
./scripts/run-inventory.sh --graph
syntax:
./scripts/run-ansible.sh site.yml --syntax-check
tailscale:
./scripts/run-ansible.sh site.yml --tags tailscale -e @secrets.yml --ask-vault-pass
bootstrap: sync-kubespray ssh-check
./scripts/run-ansible.sh site.yml -e @secrets.yml --ask-vault-pass
kubespray: sync-kubespray ssh-check
./scripts/run-ansible.sh site.yml --tags kubespray
전체 구성은 make all로 실행하며, 병렬 옵션 없이 실행했을 때 prepare → inventory → syntax → tailscale → bootstrap 순서로 진행하도록 작성했습니다. Bastion의 실행 환경을 준비한 다음 인벤토리와 Playbook 문법을 확인하고, Tailscale 설정을 거쳐 나머지 구성을 진행하는 순서입니다.
bootstrap 직전에는 sync-kubespray로 Kubespray 체크아웃을 맞추고, ssh-check로 Kubernetes 노드에 접속할 수 있는지 확인합니다. 이 연결 확인에 필요한 접속 환경을 준비하기 위해 Tailscale을 앞에서 실행하도록 했습니다. 이후 bootstrap에서는 태그 제한 없이 site.yml 전체를 호출하므로, Tailscale 작업도 다시 실행 대상에 포함됩니다.
실행 전에 사용하는 점검 명령은 필요할 때 따로 호출할 수도 있습니다.
make inventory
make syntax
make ssh-check
make inventory에서는 노드가 의도한 그룹에 들어가 있는지 확인하고, make syntax에서는 Playbook 문법을 검사합니다. make ssh-check는 Ansible ping 모듈로 SSH 접속과 대상 서버의 Python 실행 가능 여부를 확인하도록 했습니다. 일반적인 ICMP ping과는 다른 확인이며, 처음 구성할 때는 Tailscale 등 노드 접근에 필요한 설정을 마친 뒤 실행합니다.
점검 이후 전체 구성이 아닌 Kubespray 단계만 호출하고 싶다면 아래 명령을 사용합니다.
make kubespray
이 경우에도 sync-kubespray와 ssh-check를 먼저 수행한 뒤 kubespray 태그가 지정된 작업을 호출합니다. Makefile에 사용한 Ansible 옵션은 아래와 같습니다.
| 옵션 | 용도 |
| --syntax-check | Playbook 문법 검사 |
| --tags tailscale | 해당 태그를 기준으로 실행할 작업 선택 |
| -e @secrets.yml | secrets.yml의 값을 추가 변수로 전달 |
| --ask-vault-pass | Vault로 암호화된 내용을 읽기 위한 비밀번호 입력 |
이렇게 일부 단계만 다시 호출할 수 있도록 하면서, 이미 설치된 상태에서의 처리도 추가했습니다. kubespray_runner에서는 k8s-master의 /etc/kubernetes/admin.conf가 있으면 설치 작업을 건너뛰도록 했습니다. 그래서 make kubespray를 실행하더라도 이 조건에 따라 실제 설치는 진행되지 않을 수 있습니다.
우선 현재는 초기 설치 과정이기 때문에, 간단하게 파일의 존재 여부만 확인하는 정도로 조건을 구성했습니다. 설치 도중 실패해서 파일만 남아 있는 경우나, 설치 후 변경한 설정을 다시 반영해야 하는 경우까지 판단하지는 못합니다. 우선 초기 설치의 중복 실행을 피하기 위한 조건으로 넣어두었고, 재적용이 필요할 때는 클러스터 상태와 Role의 건너뛰기 조건을 함께 확인하도록 했습니다.
Kubespray 설치 과정과 관련 설정은 이후 글에서 자세히 다루겠습니다.
마치며
VM을 만든 뒤 어떤 설정을 적용했는지 남겨두려던 작업이 Ansible Role과 실행 스크립트, Makefile 구성으로 이어졌습니다. 나중에 VM을 다시 만들거나 일부 설정을 바꿀 때도 이 구성을 바탕으로 필요한 작업을 진행하려고 합니다.
이번 글에서는 전체 구조와 실행 흐름을 정리했고, 다음 글에서는 그 안에 작성한 Tailscale과 Bastion 설정을 살펴보겠습니다. 외부에서 Bastion에 접속하고, Bastion을 통해 Kubernetes 노드에 접근하도록 구성한 내용을 정리할 예정입니다.
참고 자료
'프로젝트 > 홈랩 구성기' 카테고리의 다른 글
| 미니 PC 한 대를 여러 서버로 - Promox로 시작하기 (0) | 2026.09.23 |
|---|---|
| 토이 프로젝트가 돌아갈 공간을 만들어보자 (0) | 2026.09.14 |