소개
Riverlight Weekend의 홈페이지는 준비되었지만 방문자는 프로그램과 장소 페이지를 열 수 없습니다. 두 소스 서비스 자체는 이미 작동합니다. Ansible로 진입점을 선택한 인벤토리 구성원에 연결하고, 설정이 변경될 때만 다시 읽게 하며, 부적절한 대체 소스로부터 작동 중인 경로를 보호합니다.
기본 터미널 탐색과 작은 Ansible 인벤토리, 변수, 플레이북을 읽어 본 경험이 필요합니다. 이 랩은 HTTP 서비스 역할, 그룹 구성원, 다른 호스트의 변수, JSON, 읽기 전용 확인 요청과 핸들러를 사용 전에 설명합니다. 소프트웨어와 행사 서비스는 준비되어 있습니다. 소요 시간은 읽기와 일반적인 수정을 포함한 저자의 추정치입니다.
프로그램과 방문 페이지 연결
이 단계에서는 템플릿에 주소를 고정하는 대신 인벤토리 구성원을 사용하여 행사 사이트를 준비된 프로그램 및 장소 소스에 연결합니다.
Riverlight Weekend에는 작동하는 홈페이지와 방문자 안내가 있습니다. 현재 프로그램과 장소 링크는 페이지를 사용할 수 없다는 메시지를 표시합니다. 진입점은 준비된 작은 HTTP 역방향 프록시로, 요청을 다른 서비스에 전달하고 실제 응답을 돌려줍니다. 프로그램 데이터를 홈페이지에 복사하지 않습니다. 진입점은 컨트롤러에서 실행되며, 두 소스 노드는 이 실습 VM의 커널을 공유하는 컨테이너입니다. 세 개의 독립 VM이 아닙니다. 소프트웨어와 서비스는 준비되었으며 진입점 연결만 관리합니다.
Event Site Tab을 엽니다. 홈페이지에는 **A little closer to the river.**가 표시되어야 합니다. Explore the program을 누르면 **This page is taking a break.**가 나타납니다. 아직 경로가 연결되지 않아 예상되는 HTTP 502 응답입니다. 페이지의 Back to the weekend로 돌아간 다음 탐색 메뉴의 Visitor guide를 엽니다. 이 페이지는 이미 작동하며 계속 유지되어야 합니다.
Controller Tab으로 전환합니다. 시작 디렉터리는 /home/labex/project입니다. pwd는 현재 디렉터리를 출력합니다.
pwd
작업 디렉터리를 잃었다면 cd /home/labex/project로 돌아옵니다. 파일 이름을 잘못 쓰면 “No such file or directory”가 표시됩니다. 철자를 확인하고 필요하면 ls templates로 해당 디렉터리 목록을 본 뒤 다시 시도하세요. VM을 초기화할 필요는 없습니다.
cat은 파일을 출력합니다. 승인된 서비스 선택, 인벤토리와 호스트별 데이터를 읽습니다.
cat approved-services.txt
cat inventory.ini
cat host_vars/web1.yml
cat host_vars/web2.yml
인벤토리의 [program]과 [venue] 그룹은 각각 하나의 소스를 선택합니다. web1은 현재 프로그램을, web2는 장소를 제공합니다. 호스트 파일에는 실제 service_address와 서비스 포트가 있습니다. 주소는 VM마다 다를 수 있습니다. 아카이브 서비스를 제공하는 노드도 있지만, 접근 가능하다는 이유만으로 승인된 현재 프로그램이 되는 것은 아닙니다.
[gateway] 그룹은 entry를 포함합니다. ansible_connection=local은 SSH를 추가로 거치지 않고 컨트롤러에서 진입점을 관리한다는 뜻입니다. /usr/bin/python3은 준비된 로컬 Python 인터프리터입니다. HTTP 포트는 특정 주소에서 요청을 기다리는 서비스를 식별합니다. 한 노드가 여러 서비스를 서로 다른 포트에서 실행할 수 있습니다.
진입점을 바꾸기 전에 선택한 소스를 직접 읽습니다. 준비된 curl 설정 파일에 실제 URL이 있습니다. --config는 파일에서 curl 옵션을 읽습니다. fail은 HTTP 오류를 명령 실패로 처리하고, silent는 진행 표시를 숨기며, show-error는 유용한 오류를 남깁니다. 이 파일에는 비밀번호나 토큰이 없습니다.
cat requests/web1.conf
curl --config requests/web1.conf
cat requests/web2.conf
curl --config requests/web2.conf
JSON 응답은 program과 venue, 사이트 riverlight와 각 판본을 식별합니다. JSON은 이름이 있는 필드로 구성된 구조화된 텍스트입니다. 소스 요청이 성공하면 그 소스에 직접 접근할 수 있음을 알 수 있지만, 아직 사이트 경로의 동작을 증명하지는 못합니다.
이제 경로 템플릿을 만듭니다. Ansible의 groups는 그룹 이름을 구성원 목록에 연결합니다. [0]은 구성원이 하나인 그룹에서 첫 구성원을 선택합니다. hostvars는 해당 구성원의 변수를 가져옵니다. 아래 플레이북에서는 이를 program_host와 venue_host라고 부릅니다. Jinja 표현식의 ~는 숫자 포트까지 텍스트로 이어 붙입니다. | to_json은 따옴표를 위험하게 직접 조합하지 않고 올바르게 인용된 JSON 문자열을 만듭니다.
아래 셸 명령은 >로 이 파일 하나를 교체합니다. <<'EOF'는 뒤따르는 줄을 그대로 cat에 전달합니다. 작은따옴표는 셸이 Jinja 중괄호를 해석하지 않도록 합니다. 단독 줄의 종료 표시 EOF까지 붙여 넣으세요. 구분자를 빠뜨려 셸이 계속 입력을 기다리면 Ctrl+C를 누른 뒤 전체 명령을 다시 붙여 넣습니다.
cat > templates/routes.json.j2 <<'EOF'
{
"program": {{ ('http://' ~ hostvars[program_host].service_address ~ ':' ~ hostvars[program_host].program_port) | to_json }},
"venue": {{ ('http://' ~ hostvars[venue_host].service_address ~ ':' ~ hostvars[venue_host].venue_port) | to_json }}
}
EOF
진입점 플레이북을 작성합니다. gather_facts: false는 쓰지 않는 머신 정보 수집을 생략합니다. vars는 현재 인벤토리로 평가되는 표현식을 정의합니다. template은 파일을 만들고 모드 0644(소유자는 쓰기 가능, 모두 읽기 가능)를 설정한 뒤 경로 파일이 달라질 때만 핸들러에 알립니다. 핸들러는 알림 뒤 실행되는 작업으로, 보통 play의 마지막에 실행됩니다.
진입점은 경로를 메모리에 보관하므로 파일 교체만으로는 부족합니다. 준비된 다시 읽기 도우미는 이 진입점의 기존 프로세스에만 실제 SIGHUP 신호를 보내고 확인을 기다립니다. 핸들러의 command는 개별 인수 목록인 argv를 사용합니다. playbook_dir는 플레이북이 있는 디렉터리로, 관리하는 진입점 경로를 이 프로젝트에 묶어 둡니다. sudo나 서비스 재시작은 필요하지 않습니다.
cat > connect.yml <<'EOF'
- name: Connect the event services
hosts: gateway
gather_facts: false
vars:
program_host: "{{ groups['program'][0] }}"
venue_host: "{{ groups['venue'][0] }}"
tasks:
- name: Write the inventory-derived routes
ansible.builtin.template:
src: templates/routes.json.j2
dest: "{{ playbook_dir }}/entry/routes.json"
mode: '0644'
notify: Reload entry
handlers:
- name: Reload entry
ansible.builtin.command:
argv:
- python3
- /opt/labex/service-links/reload.py
- "{{ playbook_dir }}/entry"
EOF
먼저 YAML 구조를 검사하고 예상되는 파일 변경을 미리 봅니다. --syntax-check는 작업을 적용하지 않습니다. --check는 지원되는 변경을 예측하고, --diff는 이전 내용과 제안된 내용을 보여 줍니다. 이 미리 보기는 게이트웨이에 신호를 보내거나 최종 사이트의 동작을 증명하지 않습니다.
ansible-playbook connect.yml --syntax-check
ansible-playbook connect.yml --check --diff
차이에는 연결되지 않은 127.0.0.1:9 경로가 현재 소스의 주소와 포트로 바뀌는 내용이 보여야 합니다. 플레이북을 정상 적용합니다.
ansible-playbook connect.yml
템플릿 변경과 **RUNNING HANDLER [Reload entry]**가 보여야 합니다. Event Site로 돌아가 탐색 메뉴의 Program을 열고 현재 세 세션인 Paper and ink, Acoustic afternoon, Shared supper를 읽습니다. Download the schedule을 눌러 schedule.csv를 다운로드합니다. 시간, 세션과 장소는 표와 일치해야 합니다. Visit에서 Riverside Yard를 열고 Visitor guide로 이동합니다. 홈페이지와 안내는 계속 작동해야 합니다.
연결된 프로그램에는 이 세 세션이 표시되고 표 바로 아래에 Download the schedule이 있어야 합니다.

Controller로 돌아갑니다. VM 안에서도 실제 다운로드를 저장하고 살펴볼 수 있습니다. curl의 -f는 HTTP 오류에서 실패하고, -sS는 진행 표시를 숨기면서 오류를 표시하며, -o는 출력 파일 이름을 정합니다.
curl -fsS http://127.0.0.1:8081/program/schedule.csv -o schedule.csv
cat schedule.csv
예상 내용:
time,session,place
10:30,Paper and ink,Making table
13:00,Acoustic afternoon,River stage
16:00,Shared supper,Long table
127.0.0.1은 이 컨트롤러를 뜻하며 진입점은 포트 8081에서 대기합니다. 웹사이트의 공개 브라우저 주소도 같은 서비스로 전달됩니다.
같은 플레이북을 다시 실행합니다.
ansible-playbook connect.yml
요약에는 changed=0과 failed=0이 나타나고 핸들러는 실행되지 않아야 합니다. 이것이 수렴입니다. 현재 선언이 이미 파일과 일치합니다. 실제 페이지와 다운로드가 사용 가능한 결과를 증명하며 실행 요약만으로는 충분하지 않습니다.
교체 전에 작동 중인 경로 보호
이 단계에서는 경로 쓰기 전에 실제 HTTP 식별 정보 검사를 추가하고, 작동하는 사이트를 유지하면서 접근 가능한 아카이브 서비스를 거부합니다.
Controller의 /home/labex/project에서 계속합니다. HTTP 200은 서버가 성공적으로 응답했다는 뜻이며 서비스 종류, 행사나 판본까지 확인해 주지는 않습니다. 현재 프로그램과 장소는 /identity로 자신을 식별합니다. 준비된 group_vars/all.yml에 승인된 사이트와 판본이 있습니다.
cat group_vars/all.yml
변경 이전의 검사인 사전 확인을 추가합니다. assert는 조건이 거짓이면 play를 멈춥니다. 첫 검사는 서비스 그룹마다 정확히 한 구성원을 요구합니다. 각 uri 작업은 컨트롤러에서 실제 HTTP GET을 보냅니다. return_content: true는 본문을 보관하고 JSON 응답은 .json으로도 읽을 수 있습니다. register는 작업 결과를 변수 이름에 저장하여 다음 작업이 쓰게 합니다. 마지막 검사들은 실제 식별 정보와 승인 값을 비교합니다.
HTTP 작업은 의도적으로 check_mode: false를 사용합니다. 읽기 전용 요청은 미리 보기에서도 실행되어야 하기 때문입니다. 기본 uri 작업은 check mode에서 건너뛸 수 있습니다. 이 설정은 읽기 전용 확인 요청에만 속하며, 미리 보기 중 템플릿 적용이나 다시 읽기를 허용하지 않습니다. 3초 요청 제한은 없는 소스 때문에 실습이 오래 막히는 것을 방지합니다.
플레이북을 아래의 완전한 검사 포함 버전으로 교체합니다. 같은 템플릿을 유지하고 모든 식별 정보 검사를 템플릿 작업보다 앞에 둡니다.
cat > connect.yml <<'EOF'
- name: Connect the event services
hosts: gateway
gather_facts: false
vars:
program_host: "{{ groups['program'][0] }}"
venue_host: "{{ groups['venue'][0] }}"
tasks:
- name: Require one member for each service
ansible.builtin.assert:
that:
- groups['program'] | length == 1
- groups['venue'] | length == 1
- name: Read the selected program identity
ansible.builtin.uri:
url: "http://{{ hostvars[program_host].service_address }}:{{ hostvars[program_host].program_port }}/identity"
return_content: true
timeout: 3
check_mode: false
register: program_probe
- name: Read the selected venue identity
ansible.builtin.uri:
url: "http://{{ hostvars[venue_host].service_address }}:{{ hostvars[venue_host].venue_port }}/identity"
return_content: true
timeout: 3
check_mode: false
register: venue_probe
- name: Check the actual service identities
ansible.builtin.assert:
that:
- program_probe.json.service == 'program'
- program_probe.json.site == event_site
- program_probe.json.edition == program_edition
- venue_probe.json.service == 'venue'
- venue_probe.json.site == event_site
- venue_probe.json.edition == venue_edition
- name: Write the inventory-derived routes
ansible.builtin.template:
src: templates/routes.json.j2
dest: "{{ playbook_dir }}/entry/routes.json"
mode: '0644'
notify: Reload entry
handlers:
- name: Reload entry
ansible.builtin.command:
argv:
- python3
- /opt/labex/service-links/reload.py
- "{{ playbook_dir }}/entry"
EOF
ansible-playbook connect.yml --syntax-check
이제 작동 중인 경로 파일의 지문을 저장합니다. sha256sum은 내용 해시를 계산하고 >는 나중에 비교할 수 있도록 별도 파일에 출력을 저장합니다.
sha256sum entry/routes.json > routes-before.sha256
준비된 대체 인벤토리는 프로그램으로 아카이브를 선택합니다. 내용을 읽고, 이번 명령에서만 인벤토리를 정하는 -i로 의도적으로 시도합니다. 일반 인벤토리 파일은 바뀌지 않습니다.
cat inventory-wrong-service.ini
ansible-playbook -i inventory-wrong-service.ini connect.yml
이 명령은 0이 아닌 종료 상태와 failed=1로 실패해야 합니다. 아카이브는 접근 가능하고 JSON을 돌려주지만 판본이 승인되지 않았습니다. 식별 정보 검사는 Write the inventory-derived routes보다 앞에서 실패하고, 요약은 changed=0이며 핸들러는 실행되지 않아야 합니다. 이는 uri가 응답 자체를 얻지 못하는 연결 거부와 다릅니다. 두 경우 모두 쓰기를 중단해야 합니다.
저장한 지문을 확인합니다. -c는 sha256sum에 현재 파일과 기록된 해시를 비교하도록 지시합니다.
sha256sum -c routes-before.sha256
entry/routes.json: OK가 예상됩니다. Event Site에서 Program을 열고 현재 일정을 다시 다운로드합니다. 제안이 거부된 뒤에도 파일이 존재하는 데 그치지 않고 현재 프로그램을 이용할 수 있어야 합니다.
Controller로 돌아가 일반 인벤토리를 사용합니다.
ansible-playbook connect.yml --check --diff
ansible-playbook connect.yml
선택한 두 식별 정보가 모두 통과해야 합니다. 이미 올바른 경로는 변경할 필요가 없으므로 요약에는 changed=0이 나오고 핸들러는 실행되지 않습니다. YAML 들여쓰기 오류가 나오면 전체 블록을 비교하고 탭 대신 공백으로 connect.yml을 다시 씁니다. 문법 오류는 사전 검사가 소스를 올바르게 거부했다는 증거가 아닙니다.
관리 범위는 connect.yml, templates/routes.json.j2, 해당 플레이북을 통한 진입점 경로입니다. 제공된 인벤토리, 호스트 데이터, 승인된 판본, curl 요청 파일, 백엔드 내용, 주최자 메모, 홈페이지와 방문자 안내를 보존합니다. 다시 읽기는 제공된 /opt/labex/service-links/reload.py와 이 프로젝트의 진입점 디렉터리를 사용합니다. 재사용할 템플릿이나 확인 요청에 현재 노드 이름과 주소를 고정하지 마세요. 소스 페이지를 진입점에 복사하거나, 실패를 숨기거나, 소스를 재시작하거나, 준비된 게이트웨이를 고치지 마세요. 인벤토리가 바뀌어도 설정된 구성원과 포트가 실제 연결을 결정해야 합니다.
요약
인벤토리에서 도출한 경로로 실제 프로그램과 장소 서비스를 연결하고, 웹사이트와 정확한 CSV 다운로드를 살펴보았으며, 핸들러로 변경된 설정만 다시 읽게 했습니다. 이어서 접근할 수 있지만 승인되지 않은 소스를 경로 쓰기 전에 거부하여 현재 사이트를 보존했습니다. 다음 독립 랩에서는 이전 소스와 데이터를 남기면서 서비스 구성원을 변경합니다.



