이 글은 "엔터프라이즈 JVM 애플리케이션의 Cloud-Native 전환기" 시리즈의 2편입니다.
프로젝트를 진행함에 따라, Atlassian 제품을 k8s 로 올리는 작업을 run-book으로 사용할 수 있는 템플릿 형태로 만들고 싶었습니다.
누구든 이 문서를 보고 동일한 환경을 재현할 수 있고, 테스트 환경에서 서비스를 빠르게 올리고 내리는 것을 자유롭게 반복할 수 있도록 하기 위해, Ansible을 선택하게 됐습니다.
수동 설치는 재현이 어렵고, 환경마다 결과가 달라질 수 있습니다. 플레이북으로 프로비저닝을 코드화해두면 노드를 날리고 다시 세워도 항상 동일한 환경이 보장됩니다. 테스트 환경에서 빠르게 서비스를 올리고, 내리는 것을 자유롭게 진행하기 위해 Ansible로 노드를 구성하게 되었습니다.
이번 편에서는 OrbStack으로 Rocky Linux 머신 3대를 생성하고, Ansible 플레이북으로 k3s 클러스터를 완성하는 전 과정을 다룹니다.
우선 인프라 아키텍처는 아래와 같습니다.

세 노드의 역할을 간단히 정리하면 이렇습니다.
infra-node는 PostgreSQL과 NFS 서버를 담당합니다. Atlassian 클러스터가 공유 파일 시스템으로 사용할 Shared Home이 여기에 올라갑니다.
master-node는 k3s 컨트롤 플레인이 올라가는 노드로, ArgoCD와 Prometheus 같은 운영 도구도 함께 구성합니다.
worker-node는 실제 Jira와 Confluence가 배포되는 워크로드 노드입니다.
환경 준비 — OrbStack과 Ansible 설치
가장 먼저 Mac에 OrbStack과 Ansible을 설치합니다. Homebrew가 설치되어 있다면 명령어 세 줄이면 끝납니다.
# Homebrew 업데이트
brew update
# OrbStack 설치 (초경량 MicroVM 엔진)
brew install --cask orbstack
# Ansible 설치
brew install ansible
OrbStack은 기존 VM(VirtualBox, VMware)이나 Docker Desktop의 대안으로 설계된 경량화된 가상화 플랫폼입니다. 일반 VM은 OS 전체를 부팅해야 해서 머신 하나 띄우는 데 수 분이 걸리지만, OrbStack은 최적화된 리눅스 커널 덕분에 빠르게 머신이 시작됩니다.
자원 관리 방식도 다릅니다. 일반 VM은 "8GB 할당"을 설정하면 실제 사용량이 적어도 호스트 메모리에서 8GB가 사라집니다. OrbStack은 필요할 때만 자원을 끌어다 쓰고, 사용하지 않을 때는 즉시 반환합니다. Atlassian 앱이 JVM 메모리를 많이 잡아먹는 환경에서, 가상화 레이어 자체를 가볍게 유지하는 건 선택이 아닌 필수였습니다.
네트워크와 파일 공유 면에서도 편합니다. 별도 브리지 설정 없이 localhost로 컨테이너 서비스에 바로 접속할 수 있고, macOS Finder에서 리눅스 파일 시스템에 직접 접근할 수 있습니다.
Rocky Linux 3대를 10초 만에 띄우기
도구 설치가 끝났다면 머신을 생성할 차례입니다. 이번 프로젝트는 역할에 따라 노드를 3개로 나눴습니다.
# Infra Node 생성 - PostgreSQL + NFS 담당
orb create -a arm64 rocky:9 infra-node
# Master Node 생성 - k3s 컨트롤 플레인
orb create -a arm64 rocky:9 master-node
# Worker Node 생성 - 실제 워크로드가 배포되는 노드
orb create -a arm64 rocky:9 worker-node
명령어를 실행하면 이미지를 다운로드하고 순식간에 머신이 생성됩니다.
orb list로 상태를 확인해볼 수 있습니다.
$ orb list
NAME STATE DISTRO VERSION ARCH IP
---- ----- ------ ------- ---- --
infra-node running rockylinux 9 arm64 192.168.139.120
master-node running rockylinux 9 arm64 192.168.139.219
worker-node running rockylinux 9 arm64 192.168.139.111
OrbStack의 또 하나의 장점은 SSH 설정을 자동으로 구성해준다는 겁니다.
별도의 IP 확인이나 키 등록 과정 없이, 생성 직후부터 orb -m master-node 로 바로 접속할 수 있습니다.
Ansible 인벤토리 작성 및 연동 테스트
머신이 준비됐다면, Ansible이 어떤 서버에 어떤 그룹으로 접속할지 알려주는 인벤토리 파일(hosts.ini)을 먼저 작성합니다.
Ansible은 구조적으로 두 파일을 분리해서 관리합니다.
| 파일 | 구분 | 역할 |
| hosts.ini (인벤토리) | WHERE - 어디에 실행할 것인가? | 서버 그룹과 접속 정보를 담은 주소록 |
| site.yml (플레이북) | WHAT - 어떤 상태로 만들 것인가? | 서버가 도달해야 할 상태를 정의한 작업 지시서 |
타겟과 로직을 완전히 분리하는 이 구조 덕분에, 나중에 AWS EKS로 환경이 바뀌어도 플레이북은 그대로 두고 인벤토리 파일만 교체하면 됩니다.
# 작업 폴더 생성
mkdir -p ~/atlassian-k8s-project/ansible
cd ~/atlassian-k8s-project/ansible
# hosts.ini 파일 작성
cat <<EOF > hosts.ini
[infra]
infra-node.orb.local
[k3s_master]
master-node.orb.local
[k3s_worker]
worker-node.orb.local
[all:vars]
ansible_connection=ssh
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
ansible_ssh_private_key_file=~/.orbstack/ssh/id_ed25519
EOF
# 연동 테스트
ansible -i hosts.ini all -m ping
⚠️ 여기서 막혔던 부분 — Permission denied (publickey)
처음에는 ansible_ssh_private_key_file 줄 없이 테스트를 했다가 아래 에러를 만났습니다.
eumhwiyeong@master-node.orb.local: Permission denied (publickey)원인은 간단했습니다.
OrbStack은 머신 생성 시 전용 SSH 키를 자동으로 만들어 두기 때문에, Ansible에게 "이 키로 접속해" 라고 명시해주지 않으면 기본 키를 찾다가 실패합니다. ~/.orbstack/ssh/id_ed25519 경로를 인벤토리에 추가해주면 바로 해결됩니다.
Ansible 플레이북 — 인프라를 코드로 정의한다는 것
플레이북을 작성하기 전에, Ansible을 단순 자동화 스크립트와 구분짓는 가장 중요한 개념 하나를 짚고 넘어갑니다.
멱등성(Idempotency)이란?
1번 실행하든 100번 실행하든, 서버의 결과 상태는 항상 동일해야 한다는 개념입니다. 말은 쉬운데, 실제 코드에서는 이렇게 차이가 납니다.
# ❌ 나쁜 예시 — 명령어 기반 (멱등성 보장 불가)
shell: systemctl start firewalld
# 이미 켜져 있으면 에러를 낼 수 있습니다.
# ✅ 좋은 예시 — 선언적 모듈 사용 (멱등성 완벽 보장)
systemd:
name: firewalld
state: started
# Ansible이 현재 상태를 먼저 체크합니다.
# 이미 켜져 있다면 아무것도 하지 않고 'OK'로 넘어갑니다.
선언적 모듈을 쓰면 Ansible이 "현재 서버 상태를 먼저 확인하고, 이미 원하는 상태라면 아무것도 하지 않는" 방식으로 동작합니다. 이 차이가 쌓이면 어떤 환경에서 실행해도 "항상 같은 결과"가 나오는 인프라가 만들어집니다.
플레이북 기본 구조
---
- name: 1. K3s Master Node 설치 # Play 이름 (터미널에 출력됨)
hosts: k3s_master # 타겟: hosts.ini의 그룹명과 매핑
become: yes # sudo 권한으로 실행
tasks:
- name: K3s 패키지 설치 # Task 이름 (무엇을 하는지 명시)
dnf: # 모듈: Ansible이 제공하는 자동화 도구
name: k3s-selinux
state: present # 선언: "이 패키지가 설치된 상태로 만들어라"
전체 플레이북 — site_orbstack.yml
이 플레이북 하나가 7가지 작업을 순서대로 처리합니다.
Play 1. 전체 노드 공통 패키지 및 방화벽
- name: 1. 필수 패키지 설치 및 방화벽 시작
hosts: all
become: yes
tasks:
- name: Firewalld 및 필수 유틸리티 설치
dnf:
name:
- firewalld
- curl
- tar
- nfs-utils # NFS 클라이언트 — 마운트용으로 전 노드에 필요
state: present
- name: Firewalld 서비스 활성화 및 시작
systemd:
name: firewalld
state: started
enabled: yes
nfs-utils를 전체 노드에 설치한 이유가 있습니다.
NFS 서버는 infra-node에만 올라가지만, 나중에 master-node와 worker-node가 Shared Home을 마운트하려면 NFS 클라이언트 패키지가 필요합니다. 처음부터 공통 패키지로 넣어두는 게 깔끔합니다.
Play 2. PostgreSQL 구축
- name: 2. PostgreSQL 구축
hosts: infra
become: yes
tasks:
- name: PostgreSQL 패키지 설치
dnf:
name: postgresql-server
state: present
- name: PostgreSQL 초기화 (최초 1회)
shell: postgresql-setup --initdb
args:
creates: /var/lib/pgsql/data/PG_VERSION # 이미 초기화됐다면 스킵
- name: 외부 접속 허용 — listen_addresses
lineinfile:
path: /var/lib/pgsql/data/postgresql.conf
regexp: "^#?listen_addresses"
line: "listen_addresses = '*'"
notify: Restart PostgreSQL
- name: 외부 접속 허용 — pg_hba.conf
lineinfile:
path: /var/lib/pgsql/data/pg_hba.conf
regexp: "^host.*all.*all.*192.168.0.0/16"
line: "host all all 192.168.0.0/16 md5"
notify: Restart PostgreSQL
- name: 방화벽 개방
firewalld:
service: postgresql
permanent: yes
immediate: yes
state: enabled
handlers:
- name: Restart PostgreSQL
systemd:
name: postgresql
state: restarted
enabled: yes
설정 파일을 수정하는 Task가 두 개인데, 각각 재시작하지 않고 notify/handlers 패턴을 씁니다. Play가 끝날 때 단 한 번만 재시작됩니다.
Play 3. NFS 서버 구축
- name: 3. NFS 서버 구축
hosts: infra
become: yes
tasks:
- name: NFS 서버 패키지 설치
dnf:
name: nfs-utils
state: present
- name: Shared Home 디렉토리 생성
file:
path: /data/shared-home/jira-shared-home
state: directory
mode: '0777'
- name: NFS Export 설정 적용
lineinfile:
path: /etc/exports
line: "/data/shared-home/jira-shared-home *(rw,sync,no_root_squash)"
state: present
notify: Restart NFS
- name: 방화벽 개방 (NFS, RPC-Bind, Mountd)
firewalld:
service: "{{ item }}"
permanent: yes
immediate: yes
state: enabled
loop:
- nfs
- rpc-bind
- mountd
handlers:
- name: Restart NFS
systemd:
name: nfs-server
state: restarted
enabled: yes
PostgreSQL과 NFS를 별도 Play로 분리한 이유는 생명주기가 다르기 때문입니다. DB 설정을 변경할 때 NFS가 재시작되면 안 되고, 반대도 마찬가지입니다.
Play 4. K3s 방화벽 설정
- name: 4. K3s 전용 방화벽 설정 (Master & Worker)
hosts: k3s_master, k3s_worker
become: yes
tasks:
- name: K3s 필수 포트 개방 (API Server, Kubelet, Flannel)
firewalld:
port: "{{ item }}"
permanent: yes
immediate: yes
state: enabled
loop:
- 6443/tcp # K8s API Server
- 10250/tcp # Kubelet
- 8472/udp # Flannel VXLAN (Pod 간 통신)
- name: K8s 내부 가상 네트워크를 Trusted 구역으로 허용
firewalld:
zone: trusted
source: "{{ item }}"
permanent: yes
immediate: yes
state: enabled
loop:
- 10.42.0.0/16 # Pod 서브넷
- 10.43.0.0/16 # Service 서브넷
- name: 컨테이너 인터넷 통신을 위한 Masquerade 허용
firewalld:
masquerade: yes
permanent: yes
immediate: yes
state: enabled
Play 5. K3s Master 설치
- name: 5. K3s Master 구성
hosts: k3s_master
become: yes
tasks:
- name: K3s 마스터 설치 스크립트 실행
shell: "curl -sfL <https://get.k3s.io> | sh -"
args:
creates: /usr/local/bin/k3s
- name: kubectl 심볼릭 링크 생성 (PATH 어디서나 실행 가능)
file:
src: /usr/local/bin/kubectl
dest: /usr/bin/kubectl
state: link
- name: .kube 디렉토리 생성
file:
path: "/home/{{ ansible_user }}/.kube"
state: directory
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0755'
- name: k3s.yaml을 사용자 kubeconfig로 복사
copy:
src: /etc/rancher/k3s/k3s.yaml
dest: "/home/{{ ansible_user }}/.kube/config"
remote_src: yes
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0600'
- name: Master 노드 토큰 읽어오기 (Worker 연결용)
slurp:
src: /var/lib/rancher/k3s/server/node-token
register: master_token
여기서 변수 하나를 바꿨습니다. 처음에는 ansible_facts['user_id']를 썼는데, become: yes 상태에서 이 변수는 root를 반환합니다.
SSH로 접속한 원래 사용자 경로에 kubeconfig를 만들려면 ansible_user를 써야 합니다. 직접 실행해보고 나서야 알게 된 부분입니다.
Play 6. K3s Worker 설치
- name: 6. K3s Worker 구성
hosts: k3s_worker
become: yes
tasks:
- name: K3s 워커 설치 및 마스터 연결
shell: >-
curl -sfL <https://get.k3s.io> |
K3S_URL=https://{{ hostvars[groups['k3s_master'][0]]['inventory_hostname'] }}:6443
K3S_TOKEN={{ hostvars[groups['k3s_master'][0]]['master_token']['content'] | b64decode | trim }}
sh -
args:
creates: /usr/local/bin/k3s
워커가 마스터의 토큰을 가져오는 부분입니다. 파일은 다른 Play에 있지만 Ansible 프로세스는 플레이북이 끝날 때까지 살아있습니다.
마스터 Play에서 register: master_token으로 저장한 값을 hostvars로 꺼내 씁니다.
Play 7. 최종 구성 검증
- name: 7-1. kubectl alias 설정
hosts: k3s_master, k3s_worker # infra-node에는 kubectl이 없으므로 제외
become: yes
tasks:
- name: .bashrc에 k alias 추가
blockinfile:
path: "/home/{{ ansible_user }}/.bashrc"
marker: "# {mark} ANSIBLE MANAGED BLOCK: kubectl alias"
block: |
alias k='kubectl'
source <(kubectl completion bash)
complete -o default -F __start_kubectl k
- name: 7-2. 인프라 전체 구성 검증
hosts: k3s_master
become: yes
tasks:
- name: 클러스터 노드 상태 확인
command: kubectl get nodes -o wide
environment:
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
register: nodes_output
- name: 노드 목록 출력
debug:
msg: "{{ nodes_output.stdout_lines }}"
- name: PostgreSQL 포트 응답 확인
wait_for:
host: infra-node.orb.local
port: 5432
timeout: 10
- name: NFS 포트 응답 확인
wait_for:
host: infra-node.orb.local
port: 2049
timeout: 10
- name: NFS 공유 디렉토리 확인
command: showmount -e infra-node.orb.local
register: nfs_check
- name: NFS 공유 목록 출력
debug:
msg: "{{ nfs_check.stdout_lines }}"
마지막 Play에서는 플레이북이 실행되고, 실제 설치한 패키지들이 실제로 동작하는지 확인하기 위해서 검증과정을 작성했습니다.
노드 상태를 직접 확인하고, PostgreSQL과 NFS 포트가 살아있는지 검증하고, NFS 공유 디렉터리가 노출되었는지를 확인하는 과정입니다.
OrbStack의 SELinux가 Disabled인 이유
Rocky Linux를 선택한 이유 중 하나가 엔터프라이즈 환경의 보안 정책을 직접 다뤄보기 위해서였습니다.
그런데 OrbStack에서 SELinux 상태를 확인하면 Disabled로 나옵니다.
$ sestatus
SELinux status: disabled
이유는 OrbStack의 구조 때문입니다. Mac의 메모리를 아끼기 위해 호스트와 가볍게 통신하는 컨테이너형 가상화를 사용하는데, 이 과정에서 SELinux가 커널 레벨에서 아예 비활성화된 채로 부팅됩니다.
로컬 테스트 환경에서는 k3s-selinux 정책 패키지 없이도 k3s가 충돌 없이 깔끔하게 동작하기 때문에, 훨씬 편하게 작업을 할 수 있었습니다.
실제 엔터프라이즈 환경에서는 SELinux를 Enforcing으로 유지하면서 k3s-selinux 패키지를 설치하고, 방화벽은 끄지 않고 필요한 포트만 핀포인트로 여는 방식으로 구성합니다. 이 부분은 AWS EKS 전환 편에서 더 깊게 다룰 예정입니다.
성공 로그 — 클러스터가 세워지는 순간
모든 Task가 에러 없이 마무리되면 이런 RECAP 메시지를 볼 수 있습니다.
PLAY RECAP
infra-node.orb.local : ok=5 changed=1 unreachable=0 failed=0
master-node.orb.local : ok=10 changed=4 unreachable=0 failed=0
worker-node.orb.local : ok=9 changed=4 unreachable=0 failed=0
failed=0을 확인했다면 Master 노드에 접속해서 클러스터 상태를 확인합니다.
orb -m master-node
sudo kubectl get nodes
노드 3대가 Ready 상태로 올라와 있다면 성공입니다.
마치며
이번 편에서 주요 과제는 k3s 클러스터를 만든 것 자체가 아니었습니다.
플레이북 한 번으로 노드 3대가 동일한 상태로 프로비저닝된다는 것, 그리고 노드를 날리고 다시 세워도 항상 같은 결과가 나온다는 것을 경험하고자 했습니다.
처음 run-book 템플릿을 만들겠다고 목표를 세웠을 때, 가장 먼저 해결해야 했던 문제가 "어떻게 하면 동일한 환경을 누구든 재현할 수 있게 만들까"였습니다. 이번 편에서 그 첫 번째 답을 찾은 셈입니다.
다음 편에서는 ArgoCD를 helm으로 배포하고 GitOps 배포 파이프라인을 구성하는 과정을 다루겠습니다.
'프로젝트 > 엔터프라이즈 JVM 애플리케이션의 Cloud-Native 전환기' 카테고리의 다른 글
| 3-2. ArgoCD로 GitOps 배포 파이프라인 구축하기 (0) | 2026.05.06 |
|---|---|
| 3-1. Helm으로 ArgoCD 설치하고, Ansible로 자동화하기 (1) | 2026.04.29 |
| 2-2. ansible-playbook 역할 기반으로 재구성하기 (0) | 2026.04.22 |
| 1. 엔터프라이즈 JVM 애플리케이션의 Cloud-Native 전환기 (0) | 2026.03.31 |