자동화 전용 제어 노드 사용자와 pipx·Python 3 설치 권한
AUTOMATION CONTROL NODE
Ansible 설치·인벤토리·SSH 연결
전용 제어 노드에 pipx로 Ansible을 격리 설치하고 YAML 인벤토리, 검증된 SSH 호스트 키, 전용 자동화 계정과 최소 sudo 권한을 구성해 읽기 전용 작업부터 안전하게 시작합니다.BEFORE YOU START
시작 전에 준비하세요
각 대상 호스트의 콘솔에서 확인한 SSH 호스트 키 지문
공개키가 등록된 최소 권한 자동화 계정과 필요한 경우 제한된 sudoers 정책
첫 실행 범위를 제한할 테스트 호스트 한 대와 변경 승인 절차
권장 대상 여러 Linux 서버를 자동화하되 인벤토리에 비밀번호를 넣거나 호스트 키 검증을 끄지 않고 첫 연결부터 안전하게 구성하려는 운영자
FOLLOW THE RECIPE
8단계 구성·점검 레시피
제어 노드와 대상 범위 확정
Ansible은 제어 노드에서 실행되고 대상에는 일반적으로 Python과 SSH만 필요합니다. 운영·검증 환경을 같은 그룹에 섞지 않고 첫 실행은 한 대로 제한합니다.
cat /etc/os-release
uname -m
python3 --versionid
ssh -Vtimedatectl status
ip route get <STAGING_PRIVATE_IP>이 주소는 아래 inventory의 app-stg-01 ansible_host와 같아야 합니다.- 제어 노드가 개인 PC가 아닌 관리되는 전용 환경입니다.
- 대상 호스트 목록과 환경별 그룹 경계를 검토했습니다.
- SSH 실패 때 사용할 대상 콘솔 경로가 있습니다.
제어 노드가 침해되면 관리 대상 전체가 영향을 받습니다. 개인용 키와 자동화 키를 분리하고 제어 노드 자체를 중요 관리 서버로 취급합니다.
기존 Ansible·SSH 설정과 호스트 지문을 변경 전 기준으로 기록합니다.
기존 설치·SSH 호스트 키 사전 점검
기존 ansible 실행 경로와 설정 우선순위를 확인하고, SSH 첫 접속 전에 대상 콘솔의 지문과 클라이언트가 본 지문을 대조합니다. StrictHostKeyChecking을 끄지 않습니다.
command -v ansible
ansible --version없다는 메시지는 신규 설치입니다.ansible-config dump --only-changed설정에 비밀값이 있다면 외부 공유 전에 가립니다.sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub관리 대상의 콘솔에서 실행해 승인된 자산 기록에 남깁니다. 접속 프롬프트 자체를 신뢰 근거로 사용하지 않습니다.ssh-keygen -F <STAGING_PRIVATE_IP>
ssh-keygen -F '[<STAGING_PRIVATE_IP>]:<SSH_PORT>'기본 포트와 비표준 포트 표기 모두 확인합니다. 키가 바뀌었다면 먼저 콘솔에서 변경 사유와 새 지문을 승인받습니다.ssh -p <SSH_PORT> -o StrictHostKeyChecking=ask <AUTOMATION_USER>@<STAGING_PRIVATE_IP> 'id && python3 --version'콘솔에서 미리 기록한 지문과 프롬프트의 지문이 문자 단위로 일치할 때만 yes를 입력합니다. 확인할 수 없으면 no로 중단하며 accept-new 또는 host key 검증 해제를 사용하지 않습니다.- 기존 Ansible의 설치 주체와 버전을 기록했습니다.
- 대상 호스트 키 지문을 별도 신뢰 경로로 대조했습니다.
- 자동화 계정이 공개키로 로그인하고 Python 3을 실행할 수 있습니다.
REMOTE HOST IDENTIFICATION HAS CHANGED가 나오면 known_hosts를 즉시 지우지 않습니다. 정상 재설치인지 중간자 공격인지 자산 기록·콘솔에서 먼저 판정합니다.
pipx로 Ansible 실행 환경을 사용자 단위로 격리합니다.
pipx 격리 환경에 Ansible 설치
배포판 Python을 직접 변경하지 않고 pipx로 최신 안정 Ansible 패키지를 사용자 단위 설치합니다. 조직이 ansible-core만 표준화했다면 전체 패키지 대신 해당 패키지를 선택합니다.
apt-cache policy pipx openssh-client python3sudo apt install pipx openssh-client python3pipx ensurepath새 로그인 셸에서 PATH가 반영되는지 확인합니다.pipx install --include-deps ansible이미 설치됐다면 덮어쓰지 말고 pipx list와 조직 버전 정책을 먼저 확인합니다.ansible --version
ansible-community --version
pipx list- 시스템 Python에 sudo pip를 사용하지 않았습니다.
- ansible 실행 파일과 Python 인터프리터가 pipx 환경을 가리킵니다.
- 설치된 버전이 지원 중인 안정 릴리스입니다.
명령이 보이지 않으면 재설치보다 먼저 새 셸을 열고 pipx ensurepath가 안내한 사용자 bin 경로를 확인합니다.
권한이 제한된 YAML 인벤토리와 ansible.cfg를 작성합니다.
YAML 인벤토리와 연결 기본값 구성
인벤토리에는 호스트·포트·사용자 같은 비밀이 아닌 연결 정보만 둡니다. 비밀번호, become 비밀번호, 개인키 본문은 넣지 않고 ssh-agent 또는 Vault·외부 비밀 저장소를 사용합니다.
sudo install -d -m 0750 -o root -g <ANSIBLE_GROUP> /etc/ansiblesudo cp --archive --no-clobber /etc/ansible/inventory.yml /etc/ansible/inventory.yml.<BACKUP_SUFFIX>.bak기존 파일이 있을 때만 실행합니다.sudoedit /etc/ansible/inventory.ymlsudo cp --archive --no-clobber /etc/ansible/ansible.cfg /etc/ansible/ansible.cfg.<BACKUP_SUFFIX>.bak기존 파일이 있을 때만 실행합니다.sudoedit /etc/ansible/ansible.cfgsudo chown root:<ANSIBLE_GROUP> /etc/ansible/inventory.yml /etc/ansible/ansible.cfg
sudo chmod 640 /etc/ansible/inventory.yml /etc/ansible/ansible.cfgall:
children:
staging:
hosts:
app-stg-01:
ansible_host: <STAGING_PRIVATE_IP>
ansible_user: <AUTOMATION_USER>
ansible_port: <SSH_PORT>
production:
hosts:
app-prd-01:
ansible_host: <PRODUCTION_PRIVATE_IP>
ansible_user: <AUTOMATION_USER>
ansible_port: <SSH_PORT>실제 비밀번호·개인키 본문·Vault 비밀번호는 인벤토리에 넣지 않습니다.[defaults]
inventory = /etc/ansible/inventory.yml
host_key_checking = True
retry_files_enabled = False
interpreter_python = auto_silent
[ssh_connection]
pipelining = Falsepipelining은 sudoers 정책을 별도 검증한 뒤에만 고려합니다.- 운영·검증 호스트가 별도 그룹입니다.
- ansible_password와 ansible_become_password를 평문으로 저장하지 않았습니다.
- host_key_checking을 끄지 않았습니다.
- 그룹 쓰기 권한 없이 승인된 운영자만 파일을 읽습니다.
YAML은 들여쓰기와 자료형이 중요합니다. 비밀번호를 넣어 연결 문제를 우회하지 말고 inventory parser와 SSH를 각각 검증합니다.
인벤토리를 파싱하고 한 대씩 SSH·Python 연결을 시작합니다.
파싱·SSH·Python 연결 단계적 확인
전체 그룹에 모듈을 실행하기 전에 인벤토리 문법, 호스트 매핑, 한 대의 SSH 연결을 순서대로 확인합니다. 첫 검증은 상태를 바꾸지 않는 ping과 setup 일부만 사용합니다.
ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-config dump --only-changedANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-inventory --list
ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-inventory --graphANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-inventory --host app-stg-01ansible_host·ansible_user·ansible_port가 사전 검증한 <STAGING_PRIVATE_IP>·<AUTOMATION_USER>·<SSH_PORT>와 정확히 일치하지 않으면 중단합니다.ssh -p <SSH_PORT> -o StrictHostKeyChecking=yes <AUTOMATION_USER>@<STAGING_PRIVATE_IP> 'id && python3 --version'ask 단계에서 승인해 known_hosts에 저장한 동일 endpoint만 사용합니다. 이 명령이 실패하면 Ansible을 실행하지 않습니다.ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible app-stg-01 -m ansible.builtin.pingANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible app-stg-01 -m ansible.builtin.setup -a 'filter=ansible_distribution*'ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible app-stg-01 -b --ask-become-pass -m ansible.builtin.command -a 'id'become 비밀번호는 프롬프트에만 입력하고 저장하지 않습니다.- 인벤토리 그래프가 예상 그룹과 호스트만 포함합니다.
- app-stg-01의 유효 host·user·port가 콘솔에서 지문을 검증한 endpoint와 정확히 같습니다.
- StrictHostKeyChecking=yes 직접 SSH가 성공한 뒤에만 Ansible 모듈을 실행했습니다.
- 테스트 호스트 ping과 최소 facts 수집이 성공했습니다.
- become 실행 결과가 승인된 계정·권한과 일치합니다.
UNREACHABLE은 Ansible 모듈 문제가 아니라 DNS·SSH·키·호스트 지문 문제일 수 있습니다. 동일 계정의 직접 SSH부터 재검증합니다.
두 번째 호스트와 limit을 사용해 반복성을 검증합니다.
limit과 check mode로 반복 검증
대상 범위를 항상 --limit으로 눈에 보이게 제한하고, 임시 읽기 전용 playbook을 check mode와 diff로 검증합니다. ping 성공만으로 sudo·파일 권한·Python 호환이 모두 보장되지는 않습니다.
ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible <SECOND_TEST_HOST> -m ansible.builtin.pingANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible staging --limit app-stg-01 -m ansible.builtin.command -a 'uptime'ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-inventory --host app-stg-01출력에 비밀이 포함되지 않았는지 확인한 뒤 공유합니다.ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-playbook --syntax-check <READ_ONLY_PLAYBOOK>.ymlANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-playbook --check --diff --limit app-stg-01 <READ_ONLY_PLAYBOOK>.yml모든 모듈이 check mode를 완전히 지원하는 것은 아니므로 결과를 실제 변경 없음의 절대 보증으로 해석하지 않습니다.- 두 개 이상의 테스트 호스트에서 공개키 연결이 반복 성공했습니다.
- limit 대상과 예상 hosts count를 실행 전에 확인했습니다.
- 구문 검사와 check mode 결과에 의도하지 않은 변경이 없습니다.
- facts·diff 출력에 민감정보가 노출되지 않았습니다.
check mode는 모듈 구현에 따라 실제 명령을 생략하거나 정확한 차이를 예측하지 못할 수 있습니다. 첫 실제 변경은 별도 승인과 직렬 실행으로 제한합니다.
키·sudo·로그·비밀 관리 정책을 고정합니다.
자동화 계정과 비밀·감사 범위 잠금
대상마다 필요한 명령만 sudoers에 허용하고 개인키·Vault 비밀번호를 저장소와 인벤토리에서 분리합니다. no_log는 로그 노출을 줄이지만 이미 유출된 비밀을 보호하지 못합니다.
find ~/.ssh -maxdepth 1 -type f -printf '%m %u %g %p
'ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible app-stg-01 -m ansible.builtin.command -a 'sudo -l'결과에 내부 경로가 있으므로 외부 공유 전 가립니다.find <PROJECT_DIR> -maxdepth 3 -type f -name '*.vault.yml' -printf '%m %u %g %p
'내용은 출력하지 않습니다.grep -RIlE 'ansible_(password|become_password)[[:space:]]*:' /etc/ansible값이 발견되면 출력 자체를 외부 공유하지 말고 즉시 Vault·비밀 저장소로 이동합니다.- 개인키는 사용자만 읽고 자동화 전용 키는 개인용 키와 분리했습니다.
- 대상 sudoers는 필요한 명령과 계정에만 제한했습니다.
- Vault 비밀번호 파일을 Git·인벤토리·명령 인자에 넣지 않았습니다.
- 운영 playbook은 limit·serial·check_mode 지원 여부와 롤백을 검토합니다.
- SSH 호스트 키 변경은 자동 수락하지 않고 자산 기록과 대조합니다.
NOPASSWD가 무조건 안전하거나 위험한 것은 아닙니다. 허용 명령·인자 우회·파일 쓰기 경로까지 검토하지 않은 광범위 NOPASSWD가 핵심 위험입니다.
문제가 생기면 인벤토리와 설정만 비활성화하고 키·로그를 보존합니다.
인벤토리 비활성화와 기존 설정 복원
잘못된 자동화가 의심되면 실행 중 playbook을 새로 시작하지 않고 대상 변경을 서비스별 롤백 절차로 처리합니다. 인벤토리·키·로그를 삭제해 증거를 잃지 않습니다.
pgrep -a -u "$(id -u)" ansiblesudo cp --preserve=all /etc/ansible/inventory.yml.<BACKUP_SUFFIX>.bak /etc/ansible/inventory.yml검증한 기존 백업이 있는 분기에서만 실행합니다.sudo cp --preserve=all /etc/ansible/ansible.cfg.<BACKUP_SUFFIX>.bak /etc/ansible/ansible.cfg같은 변경 시점의 백업인지 대조합니다.sudo mv --no-clobber /etc/ansible/inventory.yml /etc/ansible/inventory.yml.disabled.<BACKUP_SUFFIX>기존 백업이 없는 최초 구성 분기에서만 실행합니다.sudo mv --no-clobber /etc/ansible/ansible.cfg /etc/ansible/ansible.cfg.disabled.<BACKUP_SUFFIX>기존 백업이 없는 최초 구성 분기에서만 실행합니다.ANSIBLE_CONFIG=/etc/ansible/ansible.cfg ansible-inventory --graph기존 설정 복원 분기에서만 실행하고 실제 작업은 시작하지 않습니다.- 추가 playbook 실행을 중지하고 영향 호스트를 기록했습니다.
- 기존 구성은 같은 변경 시점의 백업만 복원했습니다.
- 신규 파일은 삭제 대신 .disabled로 이동했습니다.
- 키 오용 가능성이 있으면 대상 authorized_keys와 비밀 저장소에서 별도 회전했습니다.
- 대상 서비스의 실제 변경은 각 서비스 롤백 가이드로 복구했습니다.
Ansible 제거만으로 이미 대상에 적용된 변경은 돌아가지 않습니다. 대상별 변경 기록과 백업을 기준으로 별도 복구해야 합니다.
원인 분석 후 테스트 인벤토리 한 대에서 check mode와 실제 변경을 다시 승인합니다.
SECURITY CHECK
운영 전 마지막 보안 점검
- StrictHostKeyChecking을 끄지 않고 대상 콘솔의 지문과 대조한다.
- 인벤토리에 SSH·become 비밀번호나 개인키 본문을 저장하지 않는다.
- 자동화 계정의 sudoers를 필요한 명령과 경로로 제한한다.
- 첫 실행은 --limit과 테스트 호스트로 제한하고 check mode 한계를 이해한다.
- 제어 노드·키·Vault 비밀번호를 중요 관리 자산으로 패치·감사한다.
COMMON ERRORS
자주 막히는 지점
UNREACHABLE
- 증상
- ping 모듈이 SSH 연결 전후에 실패합니다.
- 가능한 원인
- DNS, 방화벽, 사용자, 공개키, 호스트 키 또는 Python 경로 문제입니다.
- 확인 순서
- 같은 계정의 직접 SSH와 python3 --version을 먼저 확인하고 호스트 키 검증을 우회하지 않습니다.
YAML 인벤토리 파싱 실패
- 증상
- ansible-inventory가 그룹이나 호스트를 읽지 못합니다.
- 가능한 원인
- 들여쓰기·자료형·plugin 선택 또는 잘못된 파일 경로가 원인입니다.
- 확인 순서
- ansible-inventory --list와 --graph로 문법과 실제 설정 우선순위를 확인합니다.
become 실패
- 증상
- SSH 연결은 되지만 sudo 단계에서 실패합니다.
- 가능한 원인
- 자동화 계정의 sudoers 범위, TTY 정책 또는 비밀번호 전달 방식이 맞지 않습니다.
- 확인 순서
- 대상 콘솔에서 sudo -l을 검토하고 평문 become 비밀번호를 인벤토리에 추가하지 않습니다.
PRIMARY REFERENCES
공식 문서
- Ansible Community Documentation — Ansible 설치
- Ansible Community Documentation — 인벤토리 구성
- Ansible Community Documentation — SSH 연결 방식
- Ansible Community Documentation — Vault로 비밀 보호
설치 저장소와 지원 버전은 바뀔 수 있습니다. 검토일 이후에는 링크된 공식 문서와 현재 서버의 패키지 후보 버전을 함께 확인하세요.