0. 서버별 전제 조건

  1. 방화벽 기본 상태 - off

  2. selinux - off

  3. ansible 유저 생성 - bootstrap_ansible_user.sh

    #!/bin/bash
    set -e
    
    ########################################
    # Config
    ########################################
    ANSIBLE_USER="ansible"
    KEY_PATH="$HOME/.ssh/ansible_id_rsa"
    COMMENT="ansible-control-node"
    
    ########################################
    # Host Groups
    ########################################
    
    EXTERNAL_HOSTS=(
      nginx1
      nginx2
      was1
      was2
    )
    
    OPS_HOSTS=(
      monitoring
      dr_ha
      dr2
    )
    
    BASTION_HOST=bastion
    
    INTERNAL_HOSTS=(
      redis1
      redis2
      ai_worker
      video_storage
      ansible_job
      db_vip
      db_lb_01
      db_lb_02
    )
    
    DB_HOSTS=(
      dba
      dbs
      dbb
    )
    
    ########################################
    # SSH Key 준비
    ########################################
    echo "🔐 SSH key 확인 중..."
    if [ ! -f "$KEY_PATH" ]; then
      echo "👉 SSH key 생성"
      ssh-keygen -t ed25519 -f "$KEY_PATH" -C "$COMMENT" -N ""
    else
      echo "✅ SSH key 이미 존재"
    fi
    
    ########################################
    # Functions
    ########################################
    bootstrap_host() {
      local host=$1
      echo "🚀 [$host] ansible 유저 bootstrap"
    
      ssh root@"$host" bash <<EOF
    set -e
    
    # 1. ansible 유저 생성
    id ansible &>/dev/null || useradd ansible
    
    # 2. sudo 권한 부여
    echo "ansible ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/ansible
    chmod 440 /etc/sudoers.d/ansible
    
    # 3. SSH 디렉토리 준비
    mkdir -p /home/ansible/.ssh
    chmod 700 /home/ansible/.ssh
    chown ansible:ansible /home/ansible/.ssh
    EOF
    
      # 4. SSH key 복사
      ssh-copy-id -i "${KEY_PATH}.pub" ansible@"$host" || {
        echo "⚠️  [$host] 키 복사 실패 (이미 존재 / 접근 불가)"
      }
    
      echo "✅ [$host] 완료"
      echo
    }
    
    ########################################
    # Execute
    ########################################
    
    echo "==============================="
    echo "🔹 External Zone"
    echo "==============================="
    for host in "${EXTERNAL_HOSTS[@]}"; do
      bootstrap_host "$host"
    done
    
    echo "==============================="
    echo "🔹 OPS / DR Zone"
    echo "==============================="
    for host in "${OPS_HOSTS[@]}"; do
      bootstrap_host "$host"
    done
    
    echo "==============================="
    echo "🔹 Bastion"
    echo "==============================="
    bootstrap_host "$BASTION_HOST"
    
    echo "==============================="
    echo "🔹 Internal Service Zone"
    echo "==============================="
    for host in "${INTERNAL_HOSTS[@]}"; do
      bootstrap_host "$host"
    done
    
    echo "==============================="
    echo "🔹 DB Zone"
    echo "==============================="
    for host in "${DB_HOSTS[@]}"; do
      bootstrap_host "$host"
    done
    
    echo "🎉 모든 서버에 ansible 유저 bootstrap 완료"
    
    chmod +x bootstrap_ansible_user.sh
    ./bootstrap_ansible_user.sh
    
    
  4. 시간 동기화 - Seoul

    1. Asia/Seoul
    timedatectl set-timezone Asia/Seoul
    
  5. ssh key 배포

    1. ~/.ssh/config 파일
    ########################################
    # 공통 설정
    ########################################
    Host *
      User ansible
      IdentityFile ~/.ssh/ansible_id_rsa
      ServerAliveInterval 60
      ServerAliveCountMax 3
      StrictHostKeyChecking no
      UserKnownHostsFile /dev/null
    
    ########################################
    # External Zone (Direct)
    ########################################
    Host nginx1
      HostName 172.16.6.51
    
    Host nginx2
      HostName 172.16.6.52
    
    Host was1
      HostName 172.16.6.53
    
    Host was2
      HostName 172.16.6.54
    
    Host monitoring
      HostName 172.16.6.57
    
    Host dr_ha
      HostName 172.16.6.58
    
    Host dr2
      HostName 172.16.6.59
    
    ########################################
    # Keepalived VIP (Bastion)
    ########################################
    Host bastion
      HostName 10.1.1.1
    
    ########################################
    # Internal Zone (via VIP)
    ########################################
    Host redis1
      HostName 10.1.1.5
      proxyjump bastion
    
    Host redis2
      HostName 10.1.1.10
      ProxyJump bastion
    
    Host ai_worker
      HostName 10.1.1.20
      ProxyJump bastion
    
    Host ansible_job
      HostName 10.1.1.30
      ProxyJump bastion
    
    Host db_vip
      HostName 10.1.1.40
      ProxyJump bastion
    
    Host db_lb_01
      HostName 10.1.1.41
      ProxyJump bastion
    
    Host db_lb_02
      HostName 10.1.1.42
      ProxyJump bastion
    
    Host dba
      HostName 10.1.5.10
    
    Host dbs
      HostName 10.1.5.20
    
    Host dbb
      HostName 10.1.5.30
    
    

    b. ansible 서버에서 실행 - key 배포 =⇒ deploy_ssh_key.sh ./deploy_ssh_key.sh

    #!/bin/bash
    set -e
    
    KEY_PATH="$HOME/.ssh/ansible_id_rsa"
    COMMENT="ansible-control-node"
    
    echo "🔐 SSH key 확인 중..."
    
    if [ ! -f "$KEY_PATH" ]; then
      echo "👉 SSH key 생성"
      ssh-keygen -t ed25519 -f "$KEY_PATH" -C "$COMMENT" -N ""
    else
      echo "✅ SSH key 이미 존재"
    fi
    
    echo
    echo "🚀 SSH 키 배포 시작"
    echo "==========================="
    
    ########################################
    # External Zone (Direct)
    ########################################
    EXTERNAL_HOSTS=(
      nginx1
      nginx2
      was1
      was2
    )
    
    ########################################
    # OPS / DR Zone (Direct)
    ########################################
    OPS_HOSTS=(
      monitoring
      dr_ha
      dr2
    )
    
    ########################################
    # Bastion
    ########################################
    BASTION_HOST=bastion
    
    ########################################
    # Internal Service Zone (via Bastion)
    ########################################
    INTERNAL_HOSTS=(
      redis1
      redis2
      ai_worker
      video_storage
      ansible_job
      db_vip
      db_lb_01
      db_lb_02
    )
    
    ########################################
    # DB Zone (Direct via NIC 10.1.5.x)
    ########################################
    DB_HOSTS=(
      dba
      dbs
      dbb
    )
    
    ########################################
    # Functions
    ########################################
    copy_key() {
      local host=$1
      echo "  → $host"
      ssh-copy-id -i "${KEY_PATH}.pub" ansible@"$host" || {
        echo "    ⚠️  $host 실패 (이미 키 있음 / 서버 꺼짐 / 접근 불가)"
      }
    }
    
    ########################################
    # Execute
    ########################################
    
    echo
    echo "🔹 External Zone"
    for host in "${EXTERNAL_HOSTS[@]}"; do
      copy_key "$host"
    done
    
    echo
    echo "🔹 OPS / DR Zone"
    for host in "${OPS_HOSTS[@]}"; do
      copy_key "$host"
    done
    
    echo
    echo "🔹 Bastion"
    copy_key "$BASTION_HOST"
    
    echo
    echo "🔹 Internal Service Zone (via Bastion)"
    for host in "${INTERNAL_HOSTS[@]}"; do
      copy_key "$host"
    done
    
    echo
    echo "🔹 DB Zone (10.1.5.x direct)"
    for host in "${DB_HOSTS[@]}"; do
      copy_key "$host"
    done
    
    echo
    echo "🎉 완료!"
    echo "👉 이제 모든 서버에 대해 비밀번호 없이 ssh / ansible 접속 가능"
    
    

    c. ansible 서버에서 실행 - ansible 로그인 제한 해제 deploy_sudoers.sh

    #!/bin/bash
    set -e
    
    ANSIBLE_USER="ansible"
    SUDOERS_FILE="/etc/sudoers.d/ansible"
    
    echo "🔐 ansible sudoers 설정 (TTY + sudo -S)"
    echo "========================================"
    
    # sudo 비밀번호 1회 입력
    read -s -p "👉 ansible sudo 비밀번호 입력: " SUDOPASS
    echo
    echo
    
    ########################################
    # 대상 서버
    ########################################
    HOSTS=(
      nginx1
      nginx2
      was1
      was2
      monitoring
      dr_ha
      dr2
      bastion
      ai_worker
      db_vip
      db_lb_01
      db_lb_02
      video_storage
      ansible_job
      dba
      dbs
      dbb
      redis1
      redis2
    )
    
    ########################################
    # 함수
    ########################################
    setup_sudoers () {
      local host=$1
      echo "👉 $host"
    
      ssh -tt "${ANSIBLE_USER}@${host}" <<EOF
    echo "${SUDOPASS}" | sudo -S bash -c '
      echo "ansible ALL=(ALL) NOPASSWD:ALL" > ${SUDOERS_FILE}
      chmod 440 ${SUDOERS_FILE}
      visudo -cf ${SUDOERS_FILE}
    '
    exit
    EOF
    }
    
    ########################################
    # 실행
    ########################################
    for host in "${HOSTS[@]}"; do
      setup_sudoers "$host"
    done
    
    echo
    echo "🎉 완료!"
    echo "👉 이제 ansible sudo 비밀번호 없이 사용 가능"
    
    

통신 테스트

ansible all -m ping
ansible internal -m ping
ansible internal_common -m ping
ansible storage -m ping
ansible web -m ping

1. 전체 ANSIBLE 구조 - TREE

[ansible@ansible project_ansible]$ tree
.
├── ansible.cfg
├── inventories
│   └── prod
│       ├── group_vars
│       │   ├── ai_processing.yml
│       │   ├── all.yml
│       │   ├── dr.yml
│       │   ├── monitoring.yml
│       │   ├── postgres.yml
│       │   ├── redis.yml
│       │   ├── storage.yml
│       │   ├── was.yml
│       │   └── web.yml
│       ├── hosts.yml
│       └── host_vars
│           ├── ai_worker.yml
│           └── README.md
├── playbooks
│   ├── dr_ha.yml
│   ├── dr_pgbackrest.yml
│   ├── dr.yml
│   ├── grafana.yml
│   ├── loki.yml
│   ├── node_exporter.yml
│   ├── pgsql-etcd.yml
│   ├── pgsql-haproxy.yml
│   ├── pgsql-keepalived.yml
│   ├── pgsql-patroni.yml
│   ├── pgsql-postgresql.yml
│   ├── primary_pgbackrest.yml
│   ├── prometheus.yml
│   ├── promtail.yml
│   ├── reset.yml
│   └── site.yml
├── README.md
├── roles
│   ├── ai_processing
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── files
│   │   ├── handlers
│   │   ├── tasks
│   │   │   ├── cleanup.yml
│   │   │   ├── ffmpeg.yml
│   │   │   ├── layout.yml
│   │   │   ├── main.yml
│   │   │   ├── runtime.yml
│   │   │   ├── scripts.yml
│   │   │   ├── tts.yml
│   │   │   └── worker.yml
│   │   └── templates
│   │       ├── ai-worker.service.j2
│   │       ├── cleanup_ai.sh.j2
│   │       ├── process_video.sh.j2
│   │       ├── redis_worker.sh.j2
│   │       ├── status.json.j2
│   │       └── tts_generate.sh.j2
│   ├── common
│   │   ├── files
│   │   ├── handlers
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   ├── dr
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── files
│   │   ├── handlers
│   │   ├── tasks
│   │   │   ├── config.yml
│   │   │   ├── install.yml
│   │   │   ├── main.yml
│   │   │   ├── restore.yml
│   │   │   └── stanza.yml
│   │   └── templates
│   │       └── pgbackrest.conf.j2
│   ├── etcd
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── etcd.yml.j2
│   ├── fastapi
│   │   ├── files
│   │   ├── handlers
│   │   ├── tasks
│   │   └── templates
│   ├── grafana
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── files
│   │   │   └── dashboards
│   │   │       ├── disk-io-13022.json
│   │   │       ├── linux-system-13978.json
│   │   │       ├── loki-logs.json
│   │   │       ├── network-12693.json
│   │   │       ├── node-exporter-full-1860.json
│   │   │       └── prometheus-stats-3662.json
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── meta
│   │   │   └── main.yml
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   │   ├── grafana.ini.j2
│   │   │   └── provisioning
│   │   │       ├── dashboards
│   │   │       │   └── provider.yml.j2
│   │   │       └── datasources
│   │   │           ├── loki.yml.j2
│   │   │           └── prometheus.yml.j2
│   │   ├── tests
│   │   │   ├── inventory
│   │   │   └── test.yml
│   │   └── vars
│   │       └── main.yml
│   ├── haproxy_pgsql
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── haproxy.cfg.j2
│   ├── keepalived
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── keepalived.conf.j2
│   ├── loki
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── meta
│   │   │   └── main.yml
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   │   ├── loki.service.j2
│   │   │   └── loki.yml.j2
│   │   ├── tests
│   │   │   ├── inventory
│   │   │   └── test.yml
│   │   └── vars
│   │       └── main.yml
│   ├── monitoring
│   │   ├── files
│   │   ├── handlers
│   │   ├── tasks
│   │   └── templates
│   ├── nginx
│   │   ├── files
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       ├── fastpi.conf.j2
│   │       └── keepalived.conf.j2
│   ├── node_exporter
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── meta
│   │   │   └── main.yml
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   │   └── node_exporter.service.j2
│   │   ├── tests
│   │   │   ├── inventory
│   │   │   └── test.yml
│   │   └── vars
│   │       └── main.yml
│   ├── patroni
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── patroni.yml.j2
│   ├── postgresql
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   └── tasks
│   │       └── main.yml
│   ├── prometheus
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── meta
│   │   │   └── main.yml
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   │   ├── prometheus.service.j2
│   │   │   └── prometheus.yml.j2
│   │   ├── test_indent.yml
│   │   ├── tests
│   │   │   ├── inventory
│   │   │   └── test.yml
│   │   └── vars
│   │       └── main.yml
│   ├── promtail
│   │   ├── defaults
│   │   │   └── main.yml
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── meta
│   │   │   └── main.yml
│   │   ├── README.md
│   │   ├── tasks
│   │   │   └── main.yml
│   │   ├── templates
│   │   │   ├── promtail.service.j2
│   │   │   └── promtail.yml.j2
│   │   ├── tests
│   │   │   ├── inventory
│   │   │   └── test.yml
│   │   └── vars
│   │       └── main.yml
│   ├── redis
│   │   ├── defaults
│   │   ├── files
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   └── main.yml
│   │   └── templates
│   │       └── redis.conf.j2
│   ├── storage
│   │   ├── files
│   │   ├── handlers
│   │   ├── tasks
│   │   └── templates
│   └── was
│       ├── files
│       │   └── app
│       │       ├── main.py
│       │       └── requirements.txt
│       ├── handlers
│       │   └── main.yml
│       ├── tasks
│       │   └── main.yml
│       └── templates
│           └── was.service.j2
├── scripts
│   ├── backup.sh
│   ├── healthcheck.sh
│   └── restore.sh
└── vault

2. 네트워크 구조 - 각 서버들간의 관계도

ansible-inventory -i inventories/prod/hosts.yml --graph

@all:
  |--@ungrouped:
  |--@web:
  |  |--nginx1
  |  |--nginx2
  |--@was:
  |  |--was1
  |  |--was2
  |--@ops:
  |  |--monitoring
  |  |--dr_ha
  |  |--dr_repo
  |--@internal:
  |  |--@internal_common:
  |  |  |--redis1
  |  |  |--redis2
  |  |  |--ai_worker
  |  |--@vs:
  |  |  |--video_storage
  |  |--@pgsql_vip:
  |  |  |--db_vip
  |  |--@pgsql_lb:
  |  |  |--db_lb_01
  |  |  |--db_lb_02
  |--@pgsql_db:
  |  |--dba
  |  |--dbs
  |  |--dbb

ansible-galaxy collection install ansible.posix

3. ANSIBLE - ansible.cfg 구성

[defaults]
inventory = inventories/prod/hosts.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
forks = 10
interpreter_python = auto_silent
remote_user = ansible
allow_world_readable_tmpfiles = True

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False