소개
Juniper & Oat의 두 카페에는 동일한 승인 음료 가격이 필요합니다. 각 지점의 이름, 영업시간과 특별 메뉴는 유지해야 합니다. 하나의 Ansible 템플릿과 플레이북으로 실제 SSH 관리 대상 두 곳을 업데이트한 다음 의도적으로 발생시킨 메뉴 드리프트를 복구합니다.
기본 터미널 탐색과 작은 Ansible 인벤토리 및 플레이북을 읽어 본 경험이 필요합니다. 호스트 변수, JSON 렌더링, 안전한 미리 보기, 관리 대상 콘텐츠와 로컬 콘텐츠의 차이는 사용하는 시점에 설명합니다. 도구와 웹사이트가 준비되어 있어 설치보다 자동화에 집중할 수 있습니다.
공유 가격과 지점 정보 분리하기
이 단계에서는 두 카페가 공유하는 것과 서로 달라야 하는 것을 구분하고, 재사용 가능한 하나의 템플릿에 사용할 승인 데이터를 선언합니다.
Juniper & Oat에는 두 동네 카페가 있습니다. 기본 음료와 가격은 같지만 영업시간, 특별 메뉴와 안내문은 지점마다 다릅니다. 여러분은 이 로컬 정보를 덮어쓰지 않고 공유 메뉴를 업데이트해야 합니다. 준비된 web1과 web2는 교육 VM의 커널을 공유하는 SSH 관리 컨테이너 두 개이며, 별도의 VM이 아닙니다. Controller에서 Ansible을 실행하고 각 대상에서는 실제 메뉴 서비스가 실행됩니다.
Menu Preview를 열고 First branch를 선택하여 메뉴, 영업시간과 특별 메뉴를 읽으세요. 브라우저의 뒤로 버튼으로 지점 목록에 돌아간 다음 Second branch를 선택하세요. 두 곳 모두 Flat white의 이전 가격을 표시하며 Park Café의 영업시간도 틀립니다. 각 페이지의 특별 메뉴와 안내문은 이미 올바릅니다. Visit은 같은 지점의 방문 안내를 열고, Download menu는 현재 실제 CSV를 다운로드합니다. 이들은 LabEx 탭이 아니라 페이지 링크입니다.
Controller 탭으로 전환하세요. 터미널은 프로젝트 디렉터리에서 시작합니다. 디렉터리를 잘못 이동했을 때도 다음 명시적인 명령으로 돌아올 수 있습니다.
cd /home/labex/project
인벤토리의 web1과 web2는 서로 다른 준비된 SSH 대상을 가리킵니다. 파일을 변경하지 않고 읽는 cat으로 인벤토리와 승인된 지점 입력을 확인하세요.
cat inventory.ini
cat host_vars/web1.yml host_vars/web2.yml
Harbor Café는 07:00–16:00, Park Café는 08:00–18:00에 영업해야 합니다. host_vars 디렉터리는 이름이 일치하는 인벤토리 호스트에 변수를 제공합니다. 이 입력과 준비된 SSH 설정을 변경하지 마세요. 차이는 의도된 것이며 제거할 오류가 아닙니다.
승인된 공유 메뉴는 시나리오 데이터로 제공됩니다. 변수를 작성하기 전에 읽으세요.
cat approved-menu.json
Espresso 3.20, Flat white 4.20, Tea 3.00이 나열되어 있습니다. 가격은 따옴표로 감싼 문자열로 저장하여 소수점 두 자리가 형식 변환 후에도 유지되게 합니다. menu_items는 두 호스트가 공유하는 이름/가격 객체의 YAML 목록이 됩니다.
준비된 group_vars와 templates 디렉터리는 비어 있습니다. group_vars/all.yml 파일은 모든 인벤토리 호스트에 변수를 제공합니다. 다음 here-document는 완전한 파일을 작성합니다. >는 정확히 그 대상 파일을 덮어쓰며, 따옴표로 감싼 YAML 구분자는 셸이 내용을 확장하지 못하게 합니다. 닫는 구분자는 줄 맨 앞에 단독으로 있어야 합니다. 내용을 잘못 입력했다면 전체 블록을 다시 실행하세요. 변수 파일만 덮어씁니다.
cat > group_vars/all.yml <<'YAML'
menu_items:
- name: Espresso
price: "3.20"
- name: Flat white
price: "4.20"
- name: Tea
price: "3.00"
YAML
Jinja 템플릿은 해석된 값을 관리 파일에 삽입합니다. 이중 중괄호는 표현식을 평가하고 파이프는 필터를 적용합니다. 여기서 to_nice_json은 비ASCII 문자를 포함한 객체 전체를 읽기 쉬운 유효한 JSON으로 직렬화합니다. 삽입 값마다 직접 따옴표를 처리할 필요가 없습니다. 두 지점은 같은 템플릿을 사용합니다. branch_name과 opening_hours는 각 호스트 입력에서 오고 menu_items는 공유합니다.
cat > templates/menu.json.j2 <<'JINJA'
{{ {'branch': branch_name, 'hours': opening_hours, 'items': menu_items} | to_nice_json }}
JINJA
Ansible을 실행하기 전에 작성한 두 소스를 읽어 빠진 따옴표, 잘못된 들여쓰기나 구분자를 찾으세요.
cat group_vars/all.yml templates/menu.json.j2
웹사이트는 아직 바뀌지 않았습니다. 원하는 데이터를 선언했지만 적용하지는 않았습니다. 다음 단계에서 Ansible이 각 호스트의 변수 문맥으로 이 소스를 렌더링합니다.
하나의 플레이북을 두 카페에 적용하기
이 단계에서는 같은 선언을 두 실제 SSH 대상에 미리 보고 배포한 다음 실제 메뉴와 다운로드를 사용합니다.
Controller의 /home/labex/project에 머무르세요. 플레이는 호스트를 선택하고 태스크는 관리 상태를 선언합니다. become: true는 파일이 root 소유가 되도록 준비된 대상의 승인된 권한 상승을 요청합니다. LabEx 로그인 계정은 변경하지 않습니다. file 모듈은 디렉터리를 선언하고 template 모듈은 공유 소스를 렌더링하여 menu.json만 교체합니다. 소유권과 따옴표로 감싼 권한을 명시하면 디렉터리 접근과 공개 메뉴 읽기를 예측할 수 있습니다. 디렉터리는 0755, 메뉴는 0644입니다.
로컬 파일 local-special.txt, local-notice.txt와 비공개 staff-notes.txt는 관리 메뉴 선언의 범위 밖입니다. 디렉터리 전체 내용을 교체하거나 이 파일들을 수정하지 마세요. 바이트, root 소유권과 원래 권한을 유지하세요. 특별 메뉴/안내문은 0644, 비공개 메모는 0600입니다. 서비스는 요청마다 menu.json을 읽으므로 데이터만 바꾸는 메뉴 업데이트에 인위적인 재시작이나 핸들러가 필요하지 않습니다.
완전한 플레이북을 작성하세요. YAML 목록 항목은 -를 사용하고 공백은 모듈 인수의 중첩을 나타냅니다. src 경로는 컨트롤러 소스를 가리키고 dest는 각 관리 대상에 있습니다.
cat > menus.yml <<'YAML'
- name: Keep both branch menus current
hosts: all
gather_facts: false
become: true
tasks:
- name: Keep the menu directory available
ansible.builtin.file:
path: /srv/branch-menu
state: directory
owner: root
group: root
mode: "0755"
- name: Render the shared menu with each branch's inputs
ansible.builtin.template:
src: templates/menu.json.j2
dest: /srv/branch-menu/menu.json
owner: root
group: root
mode: "0644"
YAML
먼저 적용 없이 --syntax-check로 플레이북 문법을 검사하세요.
ansible-playbook menus.yml --syntax-check
YAML 오류가 보고되면 지정된 파일과 줄을 읽으세요. 위의 전체 블록과 들여쓰기를 비교하여 블록을 다시 작성한 뒤 검사를 반복하세요. Ansible 문법 오류 때문에 VM을 초기화할 필요는 없습니다.
--check는 지원되는 모듈의 변경을 예측하고 --diff는 제안한 변경 전후 내용을 표시합니다. 비밀이 없는 이 메뉴 파일에 유용하지만 미리 보기는 실제 업무 검증이 아닙니다. 두 옵션을 함께 실행하세요.
ansible-playbook menus.yml --check --diff
두 호스트 모두 메뉴 변경을 예측해야 합니다. diff는 지점 이름의 차이를 유지하면서 가격과 Park Café 영업시간을 수정해야 합니다. 대상 파일은 아직 바뀌지 않았습니다. Menu Preview로 전환하여 지점을 열거나 새로 고치고 이전 값이 그대로 보이는지 확인하세요. Controller로 돌아가 미리 보기 옵션 없이 같은 선언을 적용하세요.
ansible-playbook menus.yml
각 호스트의 요약에는 연결 불가나 실패한 태스크가 없어야 합니다. 필요한 수정이 다르므로 정확한 changed 수는 다를 수 있습니다. 임시 명령으로 실제 관리 데이터를 읽으세요. all은 인벤토리의 두 호스트를 선택하고 -m은 모듈을 지정하며 -a는 인수를 전달합니다. command 모듈은 컨트롤러가 아닌 각 대상에서 cat을 실행합니다.
ansible all -m ansible.builtin.command -a 'cat /srv/branch-menu/menu.json'
둘 다 승인된 기본 음료와 가격을 포함해야 합니다. Harbor Café와 Park Café는 각각 다른 이름과 영업시간을 유지해야 합니다.
이제 Menu Preview로 전환하여 각 지점을 새로 고치세요. 세 가격, 지점 이름, 영업시간, 특별 메뉴와 안내문을 읽으세요. Visit을 열고 Menu로 돌아오세요. Download menu를 선택하고 CSV를 여세요. 해당 지점의 승인된 세 항목이 있어야 하고 모든 행에 그 지점 이름이 있어야 합니다. 홈페이지가 열린다는 사실만으로는 충분하지 않습니다.

왼쪽 공유 가격과 오른쪽 특별 메뉴를 비교하세요. Park Café는 같은 승인 음료 목록을 사용하면서 자신의 영업시간과 특별 메뉴를 유지합니다.
브라우저에 메뉴 누락/잘못된 메뉴 오류가 보이면 Controller로 돌아가 명령 출력을 읽고 JSON 템플릿을 1단계와 비교하세요. 컨트롤러 소스를 수정하고 플레이북을 다시 실행하세요. 대상에 연결할 수 없다면 inventory.ini를 읽고 원래 출력과 비교하세요. SSH 신뢰 검사를 끄거나 플랫폼 계정을 변경하지 마세요.
Controller로 돌아오세요. 변경 없이 재실행하면 대기 중인 변경이 없어야 합니다.
ansible-playbook menus.yml
두 요약 행을 찾으세요.
web1 ... changed=0 ... unreachable=0 ... failed=0 ...
web2 ... changed=0 ... unreachable=0 ... failed=0 ...
이것이 멱등적인 구성입니다. 원하는 파일과 이미 일치하므로 Ansible이 다시 쓰지 않습니다. 일반 요청이나 서비스 로그도 멈춘다는 뜻은 아닙니다.
로컬 콘텐츠를 지우지 않고 드리프트 복구하기
이 단계에서는 관리 메뉴 값 하나를 의도적으로 손상시키고, 로컬 콘텐츠를 유지한 채 변경하지 않은 컨트롤러 소스로 복구합니다.
Controller에 머무르세요. 구성 드리프트는 실제 관리 상태가 선언한 의도와 더 이상 일치하지 않는 것입니다. 승인된 지점별 영업시간의 차이는 드리프트가 아닙니다. 수동으로 바꾼 공유 가격은 드리프트입니다.
다음은 web1에만 한정된 의도적인 오류입니다. -b는 이 임시 태스크에 become을 요청합니다. copy의 content는 지정된 메뉴 JSON만 교체하며 디렉터리는 교체하지 않습니다. 구조는 올바르지만 승인되지 않은 Espresso 가격 0.01을 사용합니다. 이 임시 파일은 일부러 틀린 것이므로 가격을 group_vars에 복사하지 마세요.
ansible web1 -b -m ansible.builtin.copy -a 'dest=/srv/branch-menu/menu.json content={"branch":"Harbor Café","hours":"07:00–16:00","items":[{"name":"Espresso","price":"0.01"},{"name":"Flat white","price":"4.20"},{"name":"Tea","price":"3.00"}]} owner=root group=root mode=0644'
Menu Preview로 전환하고 First branch를 새로 고쳐 잘못된 Espresso 가격을 보세요. 두 번째 지점과 두 특별 메뉴는 영향을 받지 않아야 합니다. 이것은 빨간 상태 표시를 흉내 낸 것이 아니라 실제 대상 파일 변경입니다.
Controller로 돌아오세요. 원래 소스를 수정하지 마세요. 기존에 선언한 원하는 상태를 미리 보세요.
ansible-playbook menus.yml --check --diff
web1만 변경을 예측해야 합니다. Web2는 이미 승인된 상태입니다. 같은 플레이북을 적용하여 관리 메뉴를 복구하세요.
ansible-playbook menus.yml
Menu Preview로 전환하고 두 지점을 새로 고치세요. Espresso는 다시 3.20이어야 합니다. 두 특별 메뉴, 안내문, 영업시간과 메뉴 다운로드도 올바르게 유지되어야 합니다. 자동화가 소유하는 것은 구성 파일 하나이며 디렉터리의 모든 로컬 파일이 아닙니다.
Controller로 돌아가 마지막으로 재실행하세요.
ansible-playbook menus.yml
두 요약은 다시 changed=0, unreachable=0, failed=0이어야 합니다. 성공적인 복구는 대상을 한 번 수동으로 고치는 데서가 아니라 컨트롤러 데이터와 공유 템플릿의 재사용 가능한 의도에서 나옵니다. 나중에 승인된 공유 항목이 바뀌어도 같은 템플릿을 사용할 수 있어야 합니다.
요약
공유 가격, 지점 입력과 로컬 콘텐츠를 분리하고, 하나의 템플릿을 두 SSH 대상에 미리 보고 적용했습니다. 실제 페이지와 CSV 다운로드를 사용하고, 정당한 지점 차이를 지우지 않으면서 실제 구성 드리프트를 복구했습니다. 변경 없는 재실행으로 선언한 메뉴가 원하는 상태에 수렴했음을 확인했습니다.



