13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] Ansible

  • Ansible로 홈랩 자동화 입문 — 인벤토리와 첫 플레이북

    홈랩에 서버가 몇 대만 넘어가도 “하나하나 SSH 접속해서 똑같은 설정” 반복이 지옥이 됩니다. 저는 그래서 Ansible을 씁니다. 제어 노드 한 대에서 여러 서버를 동시에, 일관되게 설정하는 자동화 도구죠. 홈랩에 Ansible 학습 환경을 꾸리고 쓰는 법을 입문자 눈높이로 정리합니다.

    1. Ansible이 좋은 이유

    • 에이전트 불필요: 관리 대상에 뭘 설치할 필요 없이 SSH만 되면 됩니다.
    • 멱등성(idempotent): 같은 플레이북을 몇 번 돌려도 결과가 같습니다(이미 된 건 건너뜀).
    • YAML: 사람이 읽는 선언형 문법이라 진입장벽이 낮습니다.

    2. 홈랩 학습 토폴로지

    저는 제어 노드 1대 + 관리 대상 여러 대로 학습 랩을 구성했습니다.

    역할 설명
    제어 노드(control) Ansible 설치, 여기서 명령 실행
    관리 대상(node1~3) SSH로 제어받는 서버들

    Proxmox에서 cloud-init 템플릿으로 노드 몇 대를 찍어내면 학습 환경이 몇 분 만에 완성됩니다.

    3. 설치와 인벤토리

    # 제어 노드에 설치
    sudo apt install -y ansible
    
    # SSH 키를 관리 대상에 배포(비밀번호 없이 접속)
    ssh-copy-id user@node1

    인벤토리는 “누구를 관리할지” 목록입니다.

    # inventory.ini
    [webservers]
    node1 ansible_host=10.0.0.11
    node2 ansible_host=10.0.0.12
    
    [all:vars]
    ansible_user=user
    # 연결 확인 (전체에 ping)
    ansible -i inventory.ini all -m ping

    4. 첫 플레이북

    플레이북은 “무엇을 할지”를 YAML로 적은 것입니다. 패키지 설치 + 서비스 보장 예시입니다.

    # site.yml
    - hosts: webservers
      become: true
      tasks:
        - name: nginx 설치
          apt:
            name: nginx
            state: present
            update_cache: true
    
        - name: nginx 실행 보장
          service:
            name: nginx
            state: started
            enabled: true
    ansible-playbook -i inventory.ini site.yml

    이 한 번으로 모든 webservers에 nginx가 설치·실행됩니다. 다시 돌려도 “이미 됨”으로 건너뛰는 게 멱등성입니다.

    5. 실전에서 자주 쓰는 모듈

    모듈 용도
    apt/dnf 패키지 관리
    copy/template 파일·설정 배포(변수 치환)
    service/systemd 서비스 제어
    user 계정 관리
    lineinfile 설정 파일 한 줄 수정

    6. 홈랩에서의 활용

    • 초기 세팅 표준화: 새 VM마다 계정·SSH·방화벽·패키지를 플레이북 하나로.
    • 일괄 업데이트: apt upgrade를 전 서버에 동시에.
    • 설정 드리프트 방지: 플레이북이 곧 “원하는 상태” 문서이자 복구 수단.

    7. 정리

    Ansible은 인벤토리(누구를) + 플레이북(무엇을), 이 두 가지가 전부입니다. 에이전트 없이 SSH만으로, 멱등하게, 여러 서버를 한 번에 — 홈랩이 2~3대만 넘어가도 체감 효과가 큽니다. cloud-init으로 노드를 찍고 Ansible로 설정하는 조합이면, 서버를 “손으로” 만지는 일이 확 줄어듭니다.

  • [인프라] Ansible을 활용한 프라이빗 네트워킹 자동화 구축 사례

    [인프라] Ansible을 활용한 프라이빗 네트워킹 자동화 구축 사례

    [인프라] Ansible 프라이빗 네트워킹 자동화 구축 사례

    Ansible 프라이빗 네트워킹 작업을 손으로 해보신 분들은 아마 비슷한 경험 있으실 겁니다. 서버 2~3대일 때는 상관없었는데, 노드가 늘어나고 서브넷이 여러 개로 갈라지기 시작하면 어느 순간부터 설정이 서로 조금씩 달라지더라고요. 저도 홈랩에서 서비스용 VM, 모니터링 노드, 백업 서버를 따로 굴리면서 프라이빗 네트워킹을 수동으로 맞추다가 꽤 크게 삽질했습니다. 처음엔 이게 뭔가 싶었는데, 결국 답은 Ansible(앤서블, 에이전트 없이 설정을 배포하는 자동화 도구)로 네트워크 상태를 코드로 관리하는 쪽이었어요. 이번 글에서는 제가 직접 구축했던 흐름을 바탕으로, Ansible 프라이빗 네트워킹을 어떻게 정리했고 어떤 문제를 만났는지, 그리고 왜 이 방식이 네트워크 자동화와 IaC 구축 사례로 꽤 실용적인지 풀어보겠습니다.

    핵심은 단순합니다. 네트워크 장비를 거창하게 바꾸는 게 아니라, 리눅스 서버들 사이의 프라이빗 네트워킹 규칙을 플레이북으로 통일하고, 터널 인터페이스와 라우팅, 방화벽 정책을 반복 가능하게 만드는 거거든요. 말은 쉬운데 실제로 해보면 변수 설계, 템플릿 관리, 검증 순서에서 많이 흔들립니다. 여기서 중요한 포인트! 처음부터 완벽하게 만들려고 하기보다, 재현 가능한 최소 구성부터 잡는 게 훨씬 중요해요.

    Ansible 프라이빗 네트워킹 전체 아키텍처를 보여주는 홈랩 다이어그램

    홈랩 서버, 관리 노드, 프라이빗 터널 구간이 한눈에 보이는 전체 구성 예시입니다.

    Ansible로 프라이빗 네트워킹을 관리하면 편한 이유

    쉽게 말해 수동 설정의 정반대 방식입니다. 예전에는 서버 한 대마다 <code>/etc 아래 설정 파일을 열고, 라우팅을 넣고, 방화벽 규칙을 맞추고, 서비스 재시작까지 사람이 기억에만 의존했거든요. 근데 이 방식은 꼭 한 군데가 빠집니다. 저도 실제로 써보니 어떤 노드는 MTU가 다르고, 어떤 노드는 AllowedIPs가 빠져 있고, 또 어떤 노드는 재부팅 후 설정이 안 살아나는 식으로 문제가 생겼더라고요.

    이걸 Infrastructure as Code(IaC, 인프라를 코드로 관리하는 방식)로 바꾸면 상황이 달라집니다.

    • Inventory(인벤토리, 관리 대상 목록)에 어떤 서버가 있는지 정의해요.
    • Variables(변수)로 사설 대역, 터널 주소, 허용 라우트를 분리하죠.
    • Template(템플릿)으로 노드별 설정 파일을 자동 생성합니다.
    • Playbook(플레이북)으로 같은 절차를 모든 노드에 반복 적용하죠.
    • Idempotency(멱등성, 여러 번 실행해도 결과가 같음)를 활용해 drift를 줄여요.

    즉, 네트워크 자동화를 하겠다는 말이 대단한 솔루션 도입이 아니라, 사람이 기억하던 설정을 코드로 옮기는 과정일 뿐입니다. 저도 처음엔 네트워크 자동화라고 하면 스위치나 라우터 자동화부터 떠올렸는데, 막상 운영에서는 서버 간 프라이빗 네트워킹 통일만 해도 체감 효과가 엄청 컸거든요.

    이번 IaC 구축 사례: 전제 조건과 구성

    이번 예시는 제가 홈랩에서 자주 쓰는 구조를 단순화한 형태입니다. 관리 노드 한 대에서 Ansible을 실행하고, 여러 리눅스 서버에 WireGuard(와이어가드, 경량 VPN 터널) 기반의 사설 통신 구간을 배포하는 방식이에요. 특정 벤더 장비에 종속되지 않아서 연습용으로도 좋고, 나중에 클라우드 VM과 온프레미스 서버를 함께 묶을 때도 응용하기 편하더라고요.

    항목 수동 구성 Ansible 활용
    터널 설정 배포 서버마다 직접 작성 템플릿으로 일괄 생성
    라우팅 반영 명령어 수동 입력 변수 기반 반복 적용
    방화벽 정책 누락 가능성 높음 태스크로 표준화
    검증 접속 후 개별 확인 애드혹 명령과 플레이북으로 점검
    변경 추적 문서 의존 Git 기반 이력 관리

    제가 실제로 정리한 목표는 아래 네 가지였습니다.

    1. 사설 터널 인터페이스 설정을 노드별로 자동 생성할 것
    2. 서브넷 광고와 허용 라우트를 변수로 분리할 것
    3. 방화벽 정책을 최소 허용 원칙으로 맞출 것
    4. 검증 명령까지 플레이북 운용 흐름에 포함할 것

    사실 여기까지만 정리해도 운영 난이도가 확 내려갑니다. 특히 장애가 났을 때 “이 서버는 누가 어떻게 바꿨지?”가 아니라 “현재 Git에 있는 정의와 실제 상태가 같은가?”로 질문이 바뀌는 게 큽니다.

    실전 구현 1: 디렉터리와 Inventory 설계

    제가 직접 해보니 제일 먼저 잡아야 할 게 파일 구조였습니다. 플레이북보다 구조가 먼저더라고요. 구조가 흔들리면 변수 이름이 꼬이고, 나중에는 어디를 고쳐야 하는지 찾느라 시간이 더 들어가거든요.

    ansible-private-network/
    ├── inventory/
    │   └── hosts.yml
    ├── group_vars/
    │   └── private_nodes.yml
    ├── host_vars/
    │   ├── node-a.yml
    │   ├── node-b.yml
    │   └── node-c.yml
    ├── templates/
    │   └── wg0.conf.j2
    └── playbooks/
        └── private-network.yml

    인벤토리는 이렇게 시작했습니다.

    all:
      children:
        private_nodes:
          hosts:
            node-a:
              ansible_host: 10.10.0.11
            node-b:
              ansible_host: 10.10.0.12
            node-c:
              ansible_host: 10.10.0.13

    그룹 변수에는 공통 속성을 넣어요.

    private_network_interface: wg0
    private_network_port: 51820
    private_network_cidr: 10.200.0.0/24
    private_network_mtu: 1380
    private_network_service_name: wg-quick@wg0
    private_network_allowed_udp_port: 51820

    그리고 호스트 변수에는 노드별 정보만 남깁니다.

    wireguard_address: 10.200.0.1/24
    wireguard_private_key: "{{ vault_node_a_private_key }}"
    wireguard_peers:
      - name: node-b
        public_key: "{{ vault_node_b_public_key }}"
        endpoint: "node-b.example.internal:51820"
        allowed_ips:
          - 10.200.0.2/32
      - name: node-c
        public_key: "{{ vault_node_c_public_key }}"
        endpoint: "node-c.example.internal:51820"
        allowed_ips:
          - 10.200.0.3/32

    여기서 중요한 포인트는 공통 값과 개별 값을 철저히 분리하는 거예요. 처음엔 저도 peers까지 그룹 변수에 밀어 넣었다가, 특정 노드만 광고해야 하는 라우트가 달라지면서 구조를 다시 뜯었거든요. 처음 설계가 깔끔하면 이후 네트워크 자동화가 훨씬 편해집니다.

    Ansible 프라이빗 네트워킹 인벤토리와 변수 구조를 설명하는 구성도

    인벤토리, 그룹 변수, 호스트 변수의 역할 분리를 설명하는 구성도입니다.

    실전 구현 2: 템플릿과 플레이북으로 프라이빗 네트워킹 배포

    이제 실제 설정을 만듭니다. 템플릿은 Jinja2(진자2, 변수 치환 템플릿 엔진)를 사용하고, 노드마다 다른 값만 렌더링되게 구성하죠.

    [Interface]
    Address = {{ wireguard_address }}
    ListenPort = {{ private_network_port }}
    PrivateKey = {{ wireguard_private_key }}
    MTU = {{ private_network_mtu }}
    
    {% for peer in wireguard_peers %}
    [Peer]
    PublicKey = {{ peer.public_key }}
    Endpoint = {{ peer.endpoint }}
    AllowedIPs = {{ peer.allowed_ips | join(', ') }}
    PersistentKeepalive = 25
    {% endfor %}

    플레이북은 아래처럼 구성했습니다.

    - name: Configure private networking with Ansible
      hosts: private_nodes
      become: true
      vars:
        wireguard_config_path: "/etc/wireguard/{{ private_network_interface }}.conf"
      tasks:
        - name: Install WireGuard package on Debian or Ubuntu
          ansible.builtin.apt:
            name: wireguard
            state: present
            update_cache: true
          when: ansible_facts['os_family'] == 'Debian'
    
        - name: Ensure wireguard config directory exists
          ansible.builtin.file:
            path: /etc/wireguard
            state: directory
            owner: root
            group: root
            mode: '0700'
    
        - name: Render wireguard configuration
          ansible.builtin.template:
            src: ../templates/wg0.conf.j2
            dest: "{{ wireguard_config_path }}"
            owner: root
            group: root
            mode: '0600'
          notify: restart wireguard
    
        - name: Allow WireGuard UDP port
          ansible.builtin.iptables:
            chain: INPUT
            protocol: udp
            destination_port: "{{ private_network_allowed_udp_port }}"
            jump: ACCEPT
            comment: Allow WireGuard
    
        - name: Enable IP forwarding for routed nodes
          ansible.posix.sysctl:
            name: net.ipv4.ip_forward
            value: '1'
            state: present
            reload: true
          when: routed_node | default(false)
    
        - name: Enable and start wireguard service
          ansible.builtin.systemd:
            name: "{{ private_network_service_name }}"
            enabled: true
            state: started
    
      handlers:
        - name: restart wireguard
          ansible.builtin.systemd:
            name: "{{ private_network_service_name }}"
            state: restarted

    실행은 단순합니다.

    ansible-playbook -i inventory/hosts.yml playbooks/private-network.yml --ask-vault-pass

    여기서 저는 Ansible Vault(앤서블 볼트, 민감정보 암호화 기능)를 꼭 같이 쓰는 편입니다. 프라이빗 키를 평문으로 저장해 두면 언젠가 반드시 문제가 돼거든요. 특히 홈랩이라고 방심하면 안 되더라고요. Git에 한 번 잘못 올라가면 정리도 번거롭고요.

    또 하나, 라우팅이 필요한 노드라면 단순 터널 생성만으로 끝나지 않습니다. 예를 들어 특정 노드가 192.168.50.0/24 대역 뒤에 있는 내부 자원을 광고해야 한다면, peer의 allowed_ips에 그 대역을 넣고 해당 노드에는 포워딩을 켜야 하죠. 이 부분이 빠지면 터널은 올라왔는데 실제 통신은 안 되는, 가장 답답한 상태가 돼요. 저도 여기서 한참 막혔었습니다.

    템플릿에서 실제 설정 파일이 생성되고 서비스가 재시작되는 흐름을 보여주는 다이어그램입니다.

    실전 구현 3: 운영 관점에서 꼭 넣어야 할 검증 절차

    제가 처음 만든 플레이북은 배포까지만 자동화하고 검증은 손으로 했는데, 그러면 반쪽짜리더라고요. 배포보다 더 중요한 게 결과 확인이거든요. 그래서 아래처럼 최소 검증 루틴을 꼭 넣었습니다.

    1. 서비스 상태 확인
    2. 인터페이스 주소 확인
    3. 피어 간 핑 테스트
    4. 필요 시 라우팅된 내부 대역까지 도달성 확인
    ansible private_nodes -i inventory/hosts.yml -m ansible.builtin.shell -a "systemctl is-active wg-quick@wg0"
    ansible private_nodes -i inventory/hosts.yml -m ansible.builtin.shell -a "ip addr show wg0"
    ansible private_nodes -i inventory/hosts.yml -m ansible.builtin.shell -a "wg show"
    ansible private_nodes -i inventory/hosts.yml -m ansible.builtin.shell -a "ping -c 2 10.200.0.1"

    조금 더 확실하게 하고 싶다면 검증용 태스크를 별도 태그로 분리하는 것도 좋습니다.

    - name: Validate private networking status
      hosts: private_nodes
      become: true
      tasks:
        - name: Check wireguard service state
          ansible.builtin.command: systemctl is-active wg-quick@wg0
          register: wg_service
          changed_when: false
    
        - name: Print wireguard service state
          ansible.builtin.debug:
            var: wg_service.stdout

    이렇게 해두면 변경 배포 직후에 같은 도구로 배포와 검증을 이어서 실행할 수 있어요. 운영에서는 이 연결감이 꽤 중요합니다. “설정은 들어갔습니다”와 “통신이 됩니다”는 완전히 다른 말이거든요.

    ⚠️ 트러블슈팅: 제가 실제로 막혔던 포인트들

    이 섹션은 꼭 넣고 싶었어요. 블로그 글은 대개 예쁘게 완성된 결과만 보여주는데, 실제 운영은 그 전에 삽질이 있거든요 ㅎㅎ 저도 처음엔 금방 끝날 줄 알았는데, 프라이빗 네트워킹은 생각보다 자잘한 함정이 많았습니다.

    1. CIDR(사이더, IP 대역 표기) 중복

    가장 먼저 만난 문제였습니다. 터널용 대역과 기존 내부망 대역이 겹치면 라우팅이 애매해져요. 특히 홈랩에서는 예전에 대충 잡아둔 10.x 대역이 여기저기 숨어 있는 경우가 많거든요. 해결은 단순하지만 중요합니다. 터널 대역은 기존 VLAN, 내부 서브넷과 절대 겹치지 않게 별도로 예약해야 하죠.

    2. MTU 불일치

    터널은 올라오는데 큰 패킷에서만 통신이 불안정한 경우가 있었습니다. 처음엔 방화벽 문제인 줄 알았는데, 실제로는 MTU가 맞지 않아서 단편화 이슈가 생긴 거였어요. 이럴 때는 템플릿에 MTU를 명시해서 노드별 편차를 없애는 게 편합니다. 환경마다 정답 값은 다를 수 있어서, 확신 없는 수치를 무작정 복붙하기보다는 현재 경로 MTU를 기준으로 조정하시는 게 좋습니다.

    3. AllowedIPs 설계 실수

    WireGuard에서 AllowedIPs는 단순 허용 목록이 아니라 사실상 라우팅 의도까지 담아요. 저는 처음에 모든 대역을 넓게 넣었다가 의도치 않은 경로 선점 때문에 통신이 꼬인 적이 있습니다. 여기서 중요한 포인트! peer마다 정말 필요한 대역만 최소한으로 선언해야 합니다.

    4. 키 관리

    프라이빗 키와 퍼블릭 키를 어떻게 배포할지 초반에 기준이 없으면 금방 혼란스러워집니다. 제가 추천하는 방식은 이겁니다.

    • 프라이빗 키는 Vault로 암호화
    • 퍼블릭 키는 host_vars 또는 별도 데이터 파일에 저장
    • 운영 환경과 실험 환경의 키를 분리
    • 키 재생성 절차를 문서화

    이걸 안 해두면 나중에 노드 교체할 때 “어느 키가 어디서 쓰였더라?” 하고 다시 추적하게 돼요. 실제로 한번 겪어보니 이거 진짜 번거롭더라고요.

    5. 서비스 재시작 타이밍

    설정 파일이 바뀔 때마다 바로 재시작하는 건 편하지만, 여러 태스크가 연달아 변경될 때 중간 상태로 서비스가 흔들릴 수 있어요. 그래서 handler로 재시작을 뒤로 모아두는 구조가 안정적이었습니다. 작은 차이 같아 보여도 운영 중엔 꽤 큽니다.

    Ansible 프라이빗 네트워킹 트러블슈팅 포인트를 보여주는 운영 분석 이미지

    CIDR 중복, MTU, 라우팅 설정 오류 같은 대표 장애 포인트를 정리한 체크리스트 이미지입니다.

    검증 결과: 수동 작업이 줄어든 체감이 확실했습니다

    구성을 정리하고 나서 가장 먼저 달라진 게 신규 노드 추가 속도였습니다. 예전에는 서버 한 대를 네트워크에 편입하려면 체크리스트를 보면서 한 줄씩 넣어야 했는데, 지금은 host_vars 추가하고 플레이북 돌린 뒤 검증만 하면 되거든요. 드디어 됐다! 싶은 순간이 여기였어요.

    또 좋았던 점은 운영 표준화였습니다. 이전에는 같은 역할 서버인데도 설정 파일이 미묘하게 달랐어요. 누가 언제 손봤는지 애매한 부분도 있었고요. 지금은 인벤토리와 변수 파일이 기준점이 되니, 장애 대응할 때도 시선이 한 군데로 모입니다. 이게 생각보다 큽니다.

    • 신규 노드 편입 절차가 단순해졌습니다.
    • 방화벽 규칙 누락 가능성이 줄었습니다.
    • 사설 통신 경로를 코드로 검토할 수 있게 됐습니다.
    • 변경 이력 추적이 쉬워졌습니다.
    • 다른 환경으로 복제하기 편해졌습니다.

    특히 Ansible 활용 관점에서 좋았던 게, 네트워크 설정과 운영 문서가 따로 놀지 않게 됐다는 점이에요. 플레이북 자체가 운영 절차 문서 역할을 하거든요. 이것도 꽤 실용적인 IaC 구축 사례라고 느꼈습니다.

    배포 완료 후 서비스 활성 상태와 피어 연결 상태를 검증하는 운영 화면 예시입니다.

    정리와 다음 단계

    이번 Ansible 프라이빗 네트워킹 구축에서 제가 배운 게 명확했어요. 네트워크 자동화는 거창한 출발이 아니라, 반복되는 수동 설정을 코드로 옮기는 것부터 시작한다는 점 말이죠. 처음엔 변수 분리나 템플릿 구조가 좀 귀찮게 느껴질 수 있는데, 노드가 늘어날수록 그 차이가 확실히 벌어집니다. 저도 처음엔 헷갈렸는데, 한 번 구조를 잡아두니 이후 확장은 훨씬 편했어요.

    만약 지금 수동으로 사설 터널, 라우팅, 방화벽을 맞추고 계시다면 이렇게 시작해 보세요.

    1. 대상 노드를 인벤토리로 정리합니다.
    2. 공통 변수와 개별 변수를 분리합니다.
    3. 설정 파일을 템플릿으로 바꿉니다.
    4. 서비스 적용과 검증을 플레이북에 함께 넣습니다.
    5. 민감정보는 Vault로 분리합니다.

    이 정도만 해도 운영 피로도가 꽤 줄어듭니다. 다음 글에서는 이번 구성을 이어서 멀티 서브넷 라우팅이나 Git 기반 변경 이력 관리 쪽까지 확장하는 방법도 다뤄볼 예정입니다. 이전 글에서 다뤘던 서버 표준화나 모니터링 자동화와 연결해서 보시면 흐름이 더 잘 보이실 겁니다.

    수동 구성과 자동화 구성의 차이, 운영 이점, 다음 확장 방향을 요약한 인포그래픽입니다.

    자주 묻는 질문

    Q1. 꼭 WireGuard를 써야 하나요?

    아니에요. 핵심은 특정 제품이 아니라 프라이빗 네트워킹 설정을 Ansible로 일관되게 관리하는 방식입니다. 다만 예제 설명에는 구조가 비교적 단순한 WireGuard가 잘 맞았거든요.

    Q2. 네트워크 장비 자동화와는 다른가요?

    조금 달라요. 이번 글은 서버 간 사설 통신 구성에 초점을 맞춘 사례입니다. 하지만 접근 방식 자체는 네트워크 자동화의 좋은 출발점이 되죠.

    Q3. 테스트 환경 없이 바로 운영에 넣어도 될까요?

    개인적으로는 권하지 않습니다. 저도 홈랩에서 먼저 검증하고 운영에 반영하는 편이거든요. 특히 라우팅과 방화벽은 예상과 다르게 동작할 수 있어서, 작은 테스트 환경이 큰 사고를 막아줘요.

    Q4. 어떤 부분부터 문서화해야 하나요?

    인벤토리 구조, 변수 규칙, 키 관리 방법, 검증 명령 순서부터 문서화하세요. 이 네 가지가 흔들리면 나머지도 금방 흔들립니다.

  • [Proxmox] Ansible로 Proxmox 환경 1년 자동화 회고: 효율과 함정

    [Proxmox] Ansible로 Proxmox 환경 1년 자동화 회고: 효율과 함정

    Ansible로 Proxmox 환경 1년 자동화 회고: 효율과 함정

    안녕하세요, 13년차 서버실 지킴이입니다. 인프라 엔지니어로 일하다 보면 반복적인 작업에 지칠 때가 많죠. 특히 홈랩(Homelab)을 운영하는 저 같은 경우는 작은 규모라도 손이 많이 가는 일들이 부지기수입니다. 새로운 가상 머신(Virtual Machine, VM)을 만들고, 네트워크를 설정하고, 백업을 돌리는 일들은 매번 마우스 클릭과 타이핑의 연속이었거든요. ‘이걸 좀 더 효율적으로 할 수 없을까?’ 하는 고민은 늘 저를 따라다녔습니다. 그러다 1년 전, 제 Proxmox(프록스목스) 환경에 Ansible(앤서블)을 도입하기로 결심했고, 지금까지 정말 많은 것을 배우고 경험했습니다.

    오늘은 제가 지난 1년간 Ansible Proxmox 자동화를 구축하고 운영하면서 느꼈던 효율성과, 예상치 못했던 함정들, 그리고 그 해결 과정까지 솔직하게 풀어보려고 합니다. 혹시 저처럼 수동 작업의 굴레에서 벗어나고 싶은 인프라 엔지니어 분들이 계시다면, 이 글이 작은 힌트가 되기를 바랍니다.

    Ansible과 Proxmox를 활용한 홈랩 자동화 아키텍처 다이어그램

    Proxmox와 Ansible이 어떻게 연동되어 홈랩 인프라를 자동화하는지 보여주는 추상적인 아키텍처 다이어그램입니다. Ansible 컨트롤러가 SSH를 통해 Proxmox 노드와 통신하며, Proxmox API를 활용하여 VM, LXC 컨테이너, 스토리지, 네트워크 등의 리소스를 관리하는 모습을 나타냅니다.

    Ansible과 Proxmox, 왜 찰떡궁합일까요?

    먼저, 두 기술의 핵심 개념을 짧게 짚고 넘어가죠.

    • Ansible (앤서블): 에이전트리스(Agentless) 방식의 IT 자동화 도구입니다. 관리 대상 서버에 별도의 에이전트(Agent)를 설치할 필요 없이 SSH(Secure Shell) 연결을 통해 명령을 실행하고 작업을 수행하더라고요. YAML(야믈) 기반의 플레이북(Playbook)으로 인프라의 상태를 코드화(Infrastructure as Code, IaC)할 수 있다는 게 가장 큰 장점입니다.
    • Proxmox VE (프록스목스 가상 환경): 오픈소스 기반의 강력한 가상화 플랫폼입니다. 리눅스 컨테이너(LXC)와 KVM(Kernel-based Virtual Machine) 기반의 가상 머신을 모두 지원하며, 웹 기반 UI를 통해 쉽게 관리할 수 있죠. 엔터프라이즈급 기능을 홈랩에서도 무료로 활용할 수 있다는 점에서 인기가 많습니다.

    Ansible은 Proxmox가 제공하는 풍부한 API(Application Programming Interface)를 활용해 VM 생성, 네트워크 설정, 스냅샷 관리 등 거의 모든 작업을 자동화할 수 있더라고요. Ansible Proxmox 모듈 덕분에 복잡한 API 호출 없이도 YAML 플레이북으로 직관적인 작업이 가능합니다. 말 그대로 인프라를 코드로 관리하는 거죠.

    1년 간의 Ansible Proxmox 자동화, 실전 구현 경험

    처음엔 저도 ‘이게 진짜 될까?’ 싶었거든요. 그런데 막상 해보니 생각보다 강력하더라고요. 기본적인 VM 생성부터 복잡한 네트워크 설정, 백업 관리까지 Ansible 플레이북 하나로 뚝딱 해결되는 걸 보고 감탄했습니다.

    기본적인 VM 생성 플레이북

    가장 많이 사용하는 작업이죠. 새로운 VM을 만들고, OS 이미지를 마운트하고, 기본적인 리소스(CPU, RAM, 디스크)를 할당하는 플레이북입니다. 저는 주로 community.general.proxmox 컬렉션의 모듈들을 활용했습니다.

    # vm_create.yml
    ---
    - name: Proxmox에 새로운 VM 생성 및 설정
      hosts: proxmox_host
      gather_facts: no
      vars:
        vm_id: 101
        vm_name: my-test-vm
        vm_memory: 2048 # MB
        vm_cores: 2
        vm_disk_size: 30 # GB
        vm_iso: local:iso/ubuntu-22.04-live-server-amd64.iso # Proxmox ISO 스토리지 경로
        vm_bridge: vmbr0
        vm_ip: 192.168.1.101/24
        vm_gw: 192.168.1.1
        vm_dns: 8.8.8.8
    
      tasks:
        - name: VM 생성
          community.general.proxmox_kvm:
            api_host: "{{ inventory_hostname }}"
            api_user: root@pam
            api_password: "{{ proxmox_root_password }}" # ansible vault로 관리
            vmid: "{{ vm_id }}"
            name: "{{ vm_name }}"
            memory: "{{ vm_memory }}"
            cores: "{{ vm_cores }}"
            scsihw: virtio-scsi-pci
            bootdisk: scsi0
            iso: "{{ vm_iso }}"
            net:
              - name: net0
                model: virtio
                bridge: "{{ vm_bridge }}"
            state: present
          register: create_vm_result
    
        - name: 디스크 추가 (VM 생성 시 디스크 옵션이 없는 경우)
          community.general.proxmox_disk:
            api_host: "{{ inventory_hostname }}"
            api_user: root@pam
            api_password: "{{ proxmox_root_password }}"
            vmid: "{{ vm_id }}"
            disk: scsi0
            size: "{{ vm_disk_size }}G"
            storage: local-lvm # Proxmox 스토리지 이름
            format: qcow2
            state: present
          when: create_vm_result.changed # VM이 새로 생성되었을 때만 디스크 추가
    
        - name: VM 부팅
          community.general.proxmox_kvm:
            api_host: "{{ inventory_hostname }}"
            api_user: root@pam
            api_password: "{{ proxmox_root_password }}"
            vmid: "{{ vm_id }}"
            state: started
          when: create_vm_result.changed

    위 플레이북은 특정 Proxmox 호스트(proxmox_host)에 새로운 VM을 만들고 시작하는 예시입니다. proxmox_root_password 같은 민감 정보는 나중에 설명할 Ansible Vault(볼트)로 암호화해서 관리하는 게 좋습니다. 그리고 VM 생성 시 디스크 옵션을 한 번에 주기 어려운 모듈 버전도 있어서, 저는 디스크 추가 태스크를 따로 분리하기도 했어요.

    네트워크 설정 자동화 (Bridge, VLAN)

    Proxmox 노드의 네트워크 인터페이스 설정도 Ansible로 자동화하더라고요. 특히 브릿지(Bridge)나 VLAN(Virtual Local Area Network) 태그를 적용할 때 정말 유용했습니다.

    # network_config.yml
    ---
    - name: Proxmox 노드 네트워크 설정
      hosts: proxmox_host
      become: yes
      tasks:
        - name: 네트워크 인터페이스 백업
          ansible.builtin.copy:
            src: /etc/network/interfaces
            dest: /etc/network/interfaces.bak_{{ ansible_date_time.iso8601_basic_short }}
            remote_src: yes
    
        - name: 새로운 네트워크 설정 적용
          ansible.builtin.template:
            src: interfaces.j2
            dest: /etc/network/interfaces
            owner: root
            group: root
            mode: '0644'
          notify: 네트워크 서비스 재시작
    
      handlers:
        - name: 네트워크 서비스 재시작
          ansible.builtin.systemd:
            name: networking
            state: restarted
    # templates/interfaces.j2
    # /etc/network/interfaces - Proxmox Host Network Configuration
    
    auto lo
    iface lo inet loopback
    
    auto vmbr0
    iface vmbr0 inet static
      address {{ vmbr0_ip }}
      netmask {{ vmbr0_netmask }}
      gateway {{ vmbr0_gateway }}
      bridge-ports {{ physical_nic }}
      bridge-stp off
      bridge-fd 0
    
    # VLAN 10 for Management
    auto vmbr0.10
    iface vmbr0.10 inet static
      address {{ vmbr0_10_ip }}
      netmask {{ vmbr0_10_netmask }}
      vlan-raw-device vmbr0

    template 모듈을 사용해서 Jinja2 템플릿 파일(interfaces.j2)로 Proxmox 호스트의 /etc/network/interfaces 파일을 관리했습니다. 이렇게 하면 네트워크 구성이 코드화되어 버전 관리도 쉽고, 여러 노드에 동일한 설정을 적용하기도 편리하죠. notify 핸들러를 사용해서 변경 사항 적용 후 네트워크 서비스를 자동으로 재시작하도록 했습니다.

    백업 및 스냅샷 관리

    데이터는 소중하니까요. 자동화된 백업과 스냅샷 관리는 필수입니다. 특히 홈랩에서는 실수로 날려버리는 경우가 많아서 꼭 필요하더라고요.

    # backup_snapshot.yml
    ---
    - name: Proxmox VM 백업 및 스냅샷 관리
      hosts: proxmox_host
      gather_facts: no
      vars:
        vmid_to_manage: 101
        backup_storage: backup_pool # Proxmox 백업 스토리지 이름
    
      tasks:
        - name: VM 스냅샷 생성
          community.general.proxmox_snap:
            api_host: "{{ inventory_hostname }}"
            api_user: root@pam
            api_password: "{{ proxmox_root_password }}"
            vmid: "{{ vmid_to_manage }}"
            snapname: "daily-snapshot-{{ ansible_date_time.iso8601_basic_short }}"
            description: "Daily snapshot by Ansible"
            state: present
    
        - name: VM 백업 실행 (shell 명령 사용)
          ansible.builtin.shell: |
            vzdump {{ vmid_to_manage }} \
              --storage {{ backup_storage }} \
              --compress zstd \
              --mode stop
          register: backup_result
          async: 3600
          poll: 60

    proxmox_snap 모듈로 특정 VM의 스냅샷을 생성했습니다. 백업의 경우, Proxmox 환경에 따라 직접 vzdump 유틸리티를 호출하는 방식도 많이 사용합니다. 이렇게 하면 더 세밀한 제어가 가능하더라고요. async: 3600과 poll: 60을 사용해서 백업이 완료될 때까지 Ansible이 기다려줍니다.

    Ansible 플레이북 실행 성공 결과 터미널 화면

    Ansible 플레이북이 성공적으로 실행되어 Proxmox 환경에 VM이 생성되고 네트워크 설정이 적용되는 과정을 보여주는 터미널 화면입니다. 각 태스크의 성공 여부와 변경 사항이 녹색으로 표시되어 자동화가 잘 작동하고 있음을 나타냅니다.

    ⚠️ 삽질 대잔치! Proxmox Ansible 자동화의 함정과 해결책

    자동화가 늘 쉬웠던 건 아닙니다. 저도 꽤 많은 삽질을 했거든요. 특히 Proxmox는 업데이트 주기가 빠르고, Ansible 모듈도 계속 발전하다 보니 버전 호환성이나 예상치 못한 동작으로 골머리를 앓기도 했습니다.

    모듈 버전 호환성 문제

    초기에는 community.general 컬렉션의 proxmox 모듈이 자주 업데이트되면서, 특정 옵션이 사라지거나 이름이 바뀌는 경우가 많았습니다. 제가 작성했던 플레이북이 갑자기 에러를 뿜어낼 때마다 당황했죠.

    • 해결책: `ansible-galaxy collection install community.general:==X.Y.Z` 명령으로 특정 버전의 컬렉션을 설치하고 고정했습니다. 프로덕션 환경이라면 더더욱 버전을 명확히 관리하는 것이 중요하더라고요.

    인증 방식의 복잡함

    Proxmox API에 접근하려면 인증이 필요합니다. 처음엔 root 계정의 비밀번호를 직접 플레이북에 넣었는데, 보안상 너무 위험하다는 걸 깨달았죠.

    • 해결책: Proxmox의 API 토큰(API Token) 기능을 활용했습니다. 특정 사용자에게만 권한을 부여한 API 토큰을 발급받아 사용하고, 이 토큰 정보는 Ansible Vault (앤서블 볼트)로 암호화해서 관리했습니다. ansible-vault encrypt_string 'MY_PROXMOX_API_TOKEN' --name 'proxmox_api_token' 같은 명령어로 쉽게 암호화할 수 있습니다.

    네트워크 인터페이스 이름 충돌

    VM을 생성할 때 네트워크 인터페이스를 여러 개 추가하다 보면, Proxmox 내부적으로 할당되는 인터페이스 이름(예: net0, net1) 때문에 예상치 못한 문제가 발생하기도 했습니다. 특히 net 배열 인덱스를 잘못 지정하면 원하는 브릿지에 연결이 안 되는 경우가 있었어요.

    • 해결책: 플레이북 내에서 net 옵션을 사용할 때 인덱스 순서를 명확히 하고, Proxmox 웹 UI에서 VM 생성 후 실제 할당된 인터페이스 이름과 비교하며 디버깅했습니다. 그리고 네트워크 설정 변경 후 Proxmox 호스트 재부팅이 필요한 경우도 있어서, reboot 모듈을 활용하기도 했습니다.

    비동기 작업 처리

    VM 생성이나 백업 같은 작업은 시간이 오래 걸릴 수 있습니다. Ansible은 기본적으로 동기(Synchronous) 방식으로 동작하기 때문에, Proxmox에서 작업이 완료되기 전에 Ansible 태스크가 타임아웃(Timeout)되거나 다음 태스크로 넘어가 버리는 문제가 있었습니다.

    • 해결책: async와 poll 옵션을 활용했습니다. 예를 들어, async: 300은 해당 태스크를 백그라운드에서 최대 300초(5분) 동안 실행하고, poll: 10은 10초마다 작업 완료 여부를 확인하는 방식입니다. 이렇게 하면 Ansible이 Proxmox의 비동기 작업을 기다려줍니다.
    Proxmox 웹 UI에서 Ansible로 자동화된 VM 리스트 확인

    Ansible 자동화로 생성 및 설정된 Proxmox VM들의 리스트를 Proxmox 웹 UI에서 보여주는 화면입니다. 플레이북으로 지정된 이름과 ID, 그리고 현재 상태(Running)가 명확하게 표시되어 자동화가 잘 작동하고 있음을 나타냅니다.

    🎉 1년 간의 성과와 얻은 교훈 (검증 및 결과)

    지난 1년간 Ansible과 Proxmox를 함께 사용하면서 얻은 가장 큰 성과는 역시 효율성입니다. 이제는 새로운 서버를 구축하거나 기존 서버 환경을 재구성할 때 클릭 몇 번이 아니라, 플레이북 실행 한 번으로 모든 것이 해결됩니다. 삽질도 많이 했지만, 그만큼 얻은 것도 많아요.

    압도적인 효율성 증가

    예전에는 새 VM 하나 만드는데 10분 이상 걸렸지만, 이제는 플레이북 실행부터 VM 부팅까지 2~3분이면 충분합니다. 특히 여러 대의 VM을 동시에 배포할 때는 그 효과가 정말 크더라고요. 재현 가능한(Idempotent) 인프라를 구축했다는 점도 중요합니다. 언제든 동일한 상태로 인프라를 복원하거나 재구축할 수 있거든요.

    인프라 문서화 효과

    플레이북 자체가 인프라의 현재 상태를 설명하는 가장 정확한 문서가 됩니다. 누가 봐도 이 플레이북이 어떤 VM을 어떤 설정으로 만들고 있는지 한눈에 들어오더라고요. 팀원들과의 협업이나 나중에 인프라를 다시 파악할 때 정말 큰 도움이 됩니다.

    홈랩 운영의 즐거움

    무엇보다 홈랩 운영이 훨씬 즐거워졌습니다. 새로운 기술을 실험하고 싶을 때, 클릭 몇 번으로 환경을 만들고 망가뜨려도 부담이 없거든요. 실패해도 플레이북만 다시 돌리면 그만이니까요. 이게 진짜 홈랩 자동화 경험의 핵심 아닐까요?

    Ansible Proxmox 자동화의 장점과 단점을 비교하는 표

    Ansible을 활용한 Proxmox 자동화의 주요 장점과 고려해야 할 단점을 비교하여 보여주는 표입니다. 효율성, 재현성, 문서화 등의 장점과 함께 학습 곡선, 복잡성 관리 등의 단점을 요약합니다.

    마무리하며: 다음 단계와 독자에게 전하는 말

    Ansible과 Proxmox의 조합은 저의 인프라 관리 방식을 완전히 바꿔놓았습니다. 물론 완벽하지는 않았지만, 수동 작업으로 인한 피로감을 줄이고, 더 본질적인 문제 해결에 집중할 수 있게 해주었죠. Ansible 플레이북과 Proxmox 인프라 관리에 대한 경험은 저에게 큰 자산이 되었습니다.

    혹시 아직 자동화를 시작하지 않으셨다면, 작은 부분부터라도 꼭 도전해보시길 권합니다. 처음엔 어렵게 느껴질 수 있지만, 한 번 손에 익으면 인프라 관리의 새로운 지평이 열릴 겁니다. Ansible 외에도 Terraform(테라폼) 같은 다른 IaC 도구들도 많으니, 여러분의 환경과 필요에 맞는 도구를 찾아보는 것도 좋은 방법입니다. 저는 다음 단계로 Proxmox 클러스터 관리나 더 복잡한 스토리지 연동 자동화에 도전해볼 생각입니다. 여러분의 자동화 여정도 응원할게요! 궁금한 점이나 공유하고 싶은 경험이 있다면 언제든 댓글로 남겨주세요.

  • [Cloud] AWX를 활용한 클라우드 인프라 자동화 1년 회고: 운영 효율성과 도전 과제

    [Cloud] AWX를 활용한 클라우드 인프라 자동화 1년 회고: 운영 효율성과 도전 과제

    AWX 클라우드 인프라 자동화 1년 회고: 운영 효율성과 도전 과제

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 제가 지난 1년간 AWX를 활용한 클라우드 인프라 자동화를 경험하며 느꼈던 점들을 솔직하게 회고해 보려고 해요. 클라우드 환경이 복잡해질수록 수동 작업의 한계를 뼈저리게 느끼곤 하잖아요? 저도 처음엔 정말 막막했습니다. 혹시 여러분도 이런 고민 해보신 적 있으신가요? “수동 작업 줄이고 싶은데, Ansible 플레이북은 많아지고 관리도 어렵고…” 딱 제가 그랬거든요. 그래서 이 녀석, AWX를 도입하게 됐습니다.

    AWX 클라우드 자동화 아키텍처 개요 다이어그램

    AWX 클라우드 자동화 아키텍처 개요 다이어그램

    AWX, 너는 누구니? (핵심 개념)

    자, 그럼 AWX가 뭔지부터 간단히 짚고 넘어갈게요. AWX는 Ansible Tower의 오픈소스 버전이라고 생각하시면 됩니다. 쉽게 말해, Ansible(앤서블) 플레이북(Playbook)을 웹 UI(User Interface)로 관리하고 실행할 수 있게 해주는 도구예요. 단순히 플레이북 실행뿐만 아니라, 인벤토리(Inventory, 관리 대상 호스트 목록), 크리덴셜(Credential, 자격 증명), 프로젝트(Project, 플레이북 저장소), 작업 템플릿(Job Template, 실행 단위), 그리고 워크플로우(Workflow, 여러 작업 템플릿 연결)까지 체계적으로 관리할 수 있게 해줍니다. 💡 특히 팀 단위로 Ansible을 활용할 때, 누가 어떤 작업을 언제 실행했는지, 성공 여부는 어땠는지 한눈에 파악할 수 있어서 정말 편하더라고요.

    AWX 도입 및 초기 셋업 경험

    저도 처음엔 “이거 설치부터 만만치 않겠는데?” 하고 살짝 긴장했었는데, 생각보다 어렵지 않았어요. 저는 주로 Docker Compose(도커 컴포즈)를 이용해서 컨테이너 환경에 AWX를 배포했습니다. 공식 문서에 잘 나와 있어서 따라 하는 데 큰 무리는 없었죠. 하지만 역시 초반 삽질은 피해 갈 수 없더라고요. 😅

    • 인벤토리 구성: 클라우드 환경이다 보니, 정적인 인벤토리보다는 동적 인벤토리(Dynamic Inventory) 스크립트를 활용해야 했습니다. AWS EC2 같은 경우는 플러그인을 활용해서 현재 떠 있는 인스턴스 목록을 자동으로 가져오게 설정했죠. 처음엔 필터링 규칙 잡는 게 좀 헷갈렸는데, 몇 번 해보니 감이 오더라고요.
    • 자격 증명(Credential) 관리: SSH 키나 클라우드 API 키 같은 민감 정보를 안전하게 관리하는 게 중요했습니다. AWX의 크리덴셜 기능을 사용해서 암호화된 형태로 저장하고, 필요한 작업 템플릿에만 연결해서 사용했습니다.
    • Git(깃) 연동: 저희 팀은 모든 Ansible 플레이북을 Git 저장소에 관리하고 있었거든요. AWX 프로젝트와 Git을 연동해서, Git에 푸시(Push)만 하면 AWX가 자동으로 최신 플레이북을 가져오도록 설정했습니다. 이 부분이 정말 편리했어요!

    클라우드 인프라 자동화, 이렇게 해봤습니다

    AWX를 도입하고 나서 저희 팀의 클라우드 인프라 자동화는 날개를 달았습니다. 지난 1년간 다양한 작업들을 AWX를 통해 자동화했는데요, 몇 가지 대표적인 사례를 소개해 드릴게요.

    1. EC2(혹은 VM) 프로비저닝 및 초기 설정 자동화: 새로운 서버를 띄울 때마다 수동으로 OS 설정, 보안 설정, 기본 에이전트 설치 등을 하는 게 큰일이었거든요. AWX에 작업 템플릿을 만들어두고, 인스턴스 ID만 입력하면 모든 초기 설정이 자동으로 완료되도록 했습니다.
    2. 보안 그룹(Security Group) 변경 자동화: 개발자들이 특정 IP를 열어달라고 요청할 때마다 일일이 AWS 콘솔에 들어가서 작업했었는데, 이젠 AWX 작업 템플릿에 IP와 설명을 입력하고 실행하면 끝! 휴먼 에러도 줄고, 작업 속도도 훨씬 빨라졌죠.
    3. 로드밸런서(Load Balancer) 대상 그룹(Target Group) 등록/해제: 배포 시 서비스 인스턴스를 로드밸런서에서 빼고 다시 넣는 작업을 AWX 워크플로우로 묶어 자동화했습니다.
    4. 주기적인 서버 패치 및 재부팅 관리: 매달 특정 요일에 정해진 서버 그룹에 OS 패치를 적용하고 필요시 재부팅하는 작업을 스케줄러(Scheduler) 기능을 활용해서 자동화했습니다.

    간단한 Ansible 플레이북 예시를 하나 보여드릴게요. 특정 EC2 인스턴스의 특정 포트를 보안 그룹에 추가하는 플레이북입니다.

    - name: Add a specific IP to security group
      hosts: localhost
      connection: local
      gather_facts: no
    
      vars:
        instance_id: 'i-xxxxxxxxxxxxxxxxx'
        port_to_open: 8080
        ip_to_allow: '192.168.1.1/32'
        description: 'Allow access from specific IP'
    
      tasks:
        - name: Get security group ID from instance
          amazon.aws.ec2_instance_info:
            instance_ids: "{{ instance_id }}"
          register: ec2_info
    
        - name: Set security group ID fact
          set_fact:
            sg_id: "{{ ec2_info.instances[0].security_groups[0].group_id }}"
    
        - name: Authorize ingress rule
          amazon.aws.ec2_group:
            group_id: "{{ sg_id }}"
            region: ap-northeast-2
            rules:
              - proto: tcp
                ports:
                  - "{{ port_to_open }}"
                cidr_ip: "{{ ip_to_allow }}"
                rule_desc: "{{ description }}"
            purge_rules: no
    

    이런 플레이북을 AWX에 등록하고, instance_id, port_to_open, ip_to_allow 같은 변수들만 작업 템플릿에서 설정하면 되는 거죠. 정말 편하더라고요! 🚀

    AWX 작업 템플릿 설정 화면 예시

    AWX 작업 템플릿 설정 화면 예시

    1년 회고: AWX가 가져온 운영 효율성

    지난 1년간 AWX를 운영하면서 가장 크게 체감한 건 바로 운영 효율성의 극대화였습니다. 🎉

    • 수동 작업 시간 획기적으로 감소: 과거에 1시간씩 걸리던 수동 설정 작업들이 이제는 AWX 작업 템플릿 한 번 클릭으로 5분 안에 끝나는 마법을 경험했습니다.
    • 휴먼 에러(Human Error) 감소: 정형화된 작업 템플릿을 사용하니, 사람이 실수할 여지가 거의 없어졌습니다. 특히 야간 작업이나 긴급 상황에서 빛을 발하더라고요.
    • 작업 표준화 및 가시성 확보: 모든 자동화 작업이 AWX를 통해 이루어지면서, 어떤 팀원이든 동일한 절차로 작업을 수행할 수 있게 되었고, 대시보드에서 모든 작업 이력을 한눈에 볼 수 있게 되었습니다.
    • 팀원 간 협업 용이: Ansible 플레이북을 공유하고, 각자의 권한에 맞춰 작업 템플릿을 실행할 수 있으니 팀원 간의 협업이 훨씬 부드러워졌어요.

    AWX 대시보드를 보면 성공률이 압도적으로 높은 걸 확인할 수 있는데, 정말 뿌듯하더라고요. 👍

    AWX 대시보드 자동화 작업 성공률 통계

    AWX 대시보드 자동화 작업 성공률 통계

    마주했던 도전 과제와 삽질 (Feat. 해결 과정)

    물론 AWX와 함께한 1년이 장밋빛만 있었던 건 아닙니다. 저도 꽤 많은 삽질을 했습니다. ⚠️ 특히 다음과 같은 부분에서 도전 과제가 있었어요.

    1. 자격 증명(Credential) 관리의 복잡성: 초기에는 AWS IAM(Identity and Access Management) 역할(Role)을 AWX에 연동하는 데 애를 먹었습니다. AWX가 EC2 인스턴스에서 실행될 때 IAM 역할을 상속받아 사용하는 방식과, 직접 AWS Access Key를 등록하는 방식 중 보안과 편리성을 저울질하는 데 시간이 걸렸죠. 결국, IAM 역할 기반 인증을 적극 활용하는 방향으로 정착했습니다.
    2. 동적 인벤토리(Dynamic Inventory) 스크립트 작성의 어려움: 클라우드 환경은 인스턴스가 수시로 뜨고 꺼지기 때문에, 항상 최신 인벤토리 정보를 유지하는 게 중요합니다. AWX에 내장된 클라우드 플러그인들을 최대한 활용했지만, 특정 태그(Tag)나 조건에 맞는 인스턴스만 필터링하는 복잡한 스크립트를 작성할 때는 시행착오가 많았습니다. 파이썬(Python)으로 직접 스크립트를 짜면서 디버깅하는 과정이 꽤 힘들었네요.
    3. 워크플로우(Workflow) 설계의 난이도: 여러 작업 템플릿을 연결해서 복잡한 배포 파이프라인(Pipeline)이나 운영 절차를 자동화할 때, 조건부 실행이나 실패 시 롤백(Rollback) 같은 로직을 AWX 워크플로우로 구현하는 게 생각보다 쉽지 않았습니다. 처음에는 너무 복잡하게 얽혀서 디버깅도 어려웠는데, 점차 작은 단위의 작업 템플릿으로 쪼개고, 명확한 성공/실패 조건을 정의하면서 해결해 나갔습니다.
    4. AWX 업그레이드의 험난함: 오픈소스 프로젝트이다 보니, 새로운 버전이 나올 때마다 업그레이드 가이드라인이 명확하지 않거나, 의존성 문제로 애를 먹는 경우가 있었습니다. 특히 컨테이너 환경에서 볼륨(Volume)이나 데이터베이스 마이그레이션(Migration) 과정에서 여러 번 백업/복구를 반복하며 땀 좀 흘렸습니다. 💦

    이 모든 과정들이 저를 더 단단하게 만들어주었네요. 역시 인프라는 삽질하면서 배우는 게 최고인 것 같습니다! ㅎㅎ

    AWX, 앞으로의 방향은? (마무리)

    지난 1년간 AWX 클라우드 자동화를 경험하면서 정말 많은 것을 배우고, 팀의 운영 효율성을 크게 향상시킬 수 있었습니다. 수동 작업을 줄이고, 휴먼 에러를 방지하며, 표준화된 절차를 통해 안정적인 서비스 운영을 할 수 있게 된 것이 가장 큰 성과라고 생각합니다.

    물론 아직 가야 할 길이 멀지만, 앞으로는 AWX를 CI/CD(Continuous Integration/Continuous Deployment) 파이프라인에 더욱 깊숙이 통합하고, GitOps(깃옵스) 철학을 도입하여 모든 인프라 변경 사항을 Git을 통해 관리하는 시스템을 구축하고 싶습니다. IaC(Infrastructure as Code, 코드형 인프라)를 넘어, 모든 것이 코드로 정의되고 자동으로 배포되는 이상적인 환경을 꿈꾸고 있거든요.

    혹시 여러분도 AWX를 활용한 클라우드 자동화 경험이 있으시다면, 어떤 점이 좋았고 어떤 어려움이 있었는지 댓글로 공유해 주시면 좋겠습니다. 다음 글에서는 AWX와 CI/CD 연동에 대한 좀 더 구체적인 내용을 다뤄볼까 합니다. 기대해주세요! 😊

    AWX와 클라우드 인프라 자동화의 미래 비전 인포그래픽

    AWX와 클라우드 인프라 자동화의 미래 비전 인포그래픽

  • [Proxmox] Ansible로 Proxmox 자동화, 실패 없는 10가지 베스트 프랙티스

    [Proxmox] Ansible로 Proxmox 자동화, 실패 없는 10가지 베스트 프랙티스

    안녕하세요! 13년차 인프라 엔지니어, ’13년차의 서버실’ 운영자입니다. 오늘은 제가 홈랩과 실무 환경에서 늘 고민하고 적용해왔던 주제, 바로 Ansible로 Proxmox 자동화에 대해 이야기해보려고 합니다. 많은 분들이 Proxmox VE(Virtual Environment)를 사용하시면서 가상 머신(VM)이나 컨테이너(LXC)를 수동으로 생성하고 설정하는 데 시간을 많이 쏟으셨을 거예요. 저도 처음엔 그랬습니다. 매번 클릭하고 타이핑하고… 그러다 어느 순간 ‘이거 자동화할 수 없을까?’ 하는 생각이 머리를 스치더군요. 그래서 Ansible을 들고 Proxmox에 뛰어들었고, 수많은 삽질 끝에 얻은 귀한 경험들을 여러분과 나누고자 합니다.

    오늘 제가 준비한 내용은 ‘실패 없는 Proxmox 자동화를 위한 10가지 베스트 프랙티스’입니다. 단순한 기능 소개를 넘어, 제가 직접 부딪히고 해결했던 문제들과 그 과정에서 배운 노하우를 멘토처럼 알려드릴게요. 이 글을 읽고 나면 여러분의 홈랩 자동화는 물론, 실제 인프라 관리 효율도 한층 더 높아질 거라고 확신합니다. 자, 그럼 시작해볼까요? 🎉

    Ansible과 Proxmox 연동 아키텍처 다이어그램

    Ansible 컨트롤러가 Proxmox 호스트들을 관리하며 VM, LXC, 스토리지, 네트워크 등을 자동화하는 아키텍처 다이어그램입니다.

    Proxmox와 Ansible, 왜 함께해야 할까요? (개념 설명)

    Proxmox VE(프로모스 VE)는 강력한 오픈소스 가상화 플랫폼입니다. KVM(Kernel-based Virtual Machine) 기반의 가상 머신과 LXC(Linux Container) 컨테이너를 지원하며, 웹 기반 GUI로 손쉽게 관리할 수 있죠. 하지만 아무리 GUI가 편해도 수십, 수백 대의 VM을 일일이 설정하는 건 비효율적이거든요. 여기서 Ansible(앤서블)이 등장합니다. Ansible은 IT 자동화를 위한 오픈소스 도구로, SSH 기반의 Agentless(에이전트리스) 방식이라 별도의 에이전트 설치 없이도 원격 서버를 제어할 수 있더라고요. YAML(야믈) 기반의 플레이북(Playbook)으로 인프라의 ‘상태’를 정의하면, Ansible이 그 상태를 맞춰주죠.

    쉽게 말해, Proxmox가 튼튼한 서버실이라면 Ansible은 이 서버실을 알아서 척척 관리해주는 유능한 비서인 셈입니다. VM 생성, 네트워크 설정, 스토리지 연결 등 반복적이고 오류 발생 가능성이 높은 작업들을 Ansible로 자동화하면, 시간을 절약하고 휴먼 에러(human error)를 크게 줄일 수 있거든요. 특히 저처럼 홈랩 자동화에 관심이 많은 분들에게는 정말 최고의 조합이라고 할 수 있습니다.

    실패 없는 Proxmox 자동화를 위한 10가지 베스트 프랙티스 (실전 구현)

    제가 13년 동안 쌓아온 경험과 숱한 삽질 끝에 얻은 Proxmox 자동화 팁들을 공유합니다. 이 원칙들을 지키면 여러분의 Ansible Proxmox 자동화 여정이 훨씬 수월할 거예요.

    1. 동적 인벤토리(Dynamic Inventory) 활용하기

    가장 먼저 강조하고 싶은 건 바로 동적 인벤토리입니다. VM이 수시로 생성되고 삭제되는 환경에서는 정적 인벤토리(Static Inventory) 파일을 매번 수정하기가 어렵거든요. Proxmox의 API를 활용해서 현재 떠 있는 VM 목록을 동적으로 가져오는 스크립트를 사용하면 정말 편합니다. community.general 컬렉션에 Proxmox 동적 인벤토리 플러그인이 있으니 활용해보세요. 저는 주로 Python 스크립트를 직접 짜서 사용했는데, 이게 훨씬 유연하더라고요.

    # ansible.cfg 예시
    [inventory]
    enable_plugins = proxmox
    
    # inventory/proxmox.yml
    plugin: community.proxmox.proxmox
    base_url: https://your-proxmox-host:8006/api2/json
    api_user: root@pam
    api_password: your_password_or_token
    validate_certs: False # 실제 환경에서는 True 권장
    

    이렇게 설정하면 ansible-inventory -i inventory/proxmox.yml --list 명령으로 현재 Proxmox에 있는 VM들을 Ansible 인벤토리로 불러올 수 있습니다. 💡 팁: 보안을 위해 API 토큰을 사용하는 것을 강력히 추천합니다.

    2. 항상 멱등성(Idempotency)을 지키세요

    Ansible의 핵심 철학은 멱등성입니다. 즉, 플레이북을 여러 번 실행해도 항상 동일한 결과를 내고, 이미 목표 상태에 도달했다면 아무 작업도 하지 않아야 한다는 뜻이거든요. Proxmox VM 생성/수정 시 이 멱등성을 지키는 것이 매우 중요합니다. 예를 들어, VM이 이미 존재하면 생성하지 않거나, 설정이 동일하면 변경하지 않도록 플레이북을 작성해야 하더라고요. Proxmox 모듈들은 대부분 멱등성을 지원하지만, 스크립트나 커맨드를 직접 사용할 때는 주의가 필요합니다. 저도 초기에 이 부분을 간과해서 이미 생성된 VM에 또 생성 명령이 들어가 오류가 나던 경험이 많았거든요.

    3. 변수 관리, 이렇게 해야 깔끔합니다

    플레이북 내에 하드코딩된 값은 피하고, 변수(Variables)를 적극적으로 활용하세요. 특히 group_vars와 host_vars 디렉토리를 사용하면 VM의 종류나 호스트별로 유연하게 설정을 관리할 수 있더라고요. 민감한 정보(API 키, 비밀번호 등)는 Ansible Vault(앤서블 볼트)를 사용해서 암호화하세요. 보안은 아무리 강조해도 지나치지 않습니다. 저도 한때 vars 파일에 비밀번호를 그냥 넣었다가 식겁한 적이 있습니다. 😅

    # group_vars/all.yml
    pve_api_user: 'root@pam'
    pve_api_token_id: 'ansible-token'
    pve_api_token_secret: '{{ vault_pve_api_token_secret }}' # vault로 관리
    
    # host_vars/my-vm.yml
    vm_id: 101
    vm_name: 'web-server-01'
    vm_memory: 2048
    vm_cores: 2
    vm_net_bridge: 'vmbr0'
    vm_ip: '192.168.1.101'
    

    4. Proxmox 전용 모듈, 똑똑하게 쓰기

    Ansible은 community.proxmox 컬렉션을 통해 Proxmox를 위한 다양한 모듈을 제공합니다. proxmox_kvm, proxmox_lxc, proxmox_storage 등 목적에 맞는 모듈을 사용하세요. 이 모듈들은 Proxmox API를 활용하여 안정적이고 멱등성 있는 작업을 수행할 수 있도록 설계되었더라고요. 셸 스크립트(shell script)로 API를 직접 호출하는 것보다 훨씬 안전하고 관리하기 편합니다. 예전에는 uri 모듈로 직접 HTTP 요청을 날리기도 했는데, 전용 모듈이 훨씬 편리하더라고요.

    - name: Create a new KVM virtual machine
      community.proxmox.proxmox_kvm:
        api_host: "{{ pve_api_host }}"
        api_user: "{{ pve_api_user }}"
        api_token_id: "{{ pve_api_token_id }}"
        api_token_secret: "{{ pve_api_token_secret }}"
        vmid: "{{ vm_id }}"
        name: "{{ vm_name }}"
        memory: "{{ vm_memory }}"
        cores: "{{ vm_cores }}"
        net:
          net0: 'model=virtio,bridge={{ vm_net_bridge }}'
        state: present
      delegate_to: localhost
    

    5. 오류 핸들링(Error Handling), 미리미리 대비하기

    자동화 작업은 예상치 못한 문제로 실패할 수 있습니다. 네트워크 문제, Proxmox API 응답 지연, 잘못된 변수 값 등 다양한 원인이 있죠. failed_when, ignore_errors, block/rescue를 활용하여 견고한 플레이북을 만드세요. 특히 중요한 작업은 block으로 묶고, 실패 시 rescue 블록에서 정리 작업을 하도록 구성하면 정말 안전하더라고요. 저도 처음에 에러가 나면 그냥 멈추게 만들었는데, 나중에는 중단된 상태를 수동으로 정리하는 게 더 힘들더라고요. ⚠️

    - name: Critical VM creation block
      block:
        - name: Create VM
          community.proxmox.proxmox_kvm:
            # ... VM 생성 설정 ...
            state: present
          delegate_to: localhost
        - name: Configure VM network
          ansible.builtin.shell: 'some_network_config_command {{ vm_ip }}'
      rescue:
        - name: Clean up VM if creation failed
          community.proxmox.proxmox_kvm:
            api_host: "{{ pve_api_host }}"
            api_user: "{{ pve_api_user }}"
            api_token_id: "{{ pve_api_token_id }}"
            api_token_secret: "{{ pve_api_token_secret }}"
            vmid: "{{ vm_id }}"
            state: absent
          delegate_to: localhost
    

    6. 태그(Tags)로 원하는 작업만 골라 실행하기

    플레이북이 점점 커지면 특정 작업만 실행하거나 건너뛰고 싶을 때가 많습니다. 이때 태그(Tags)를 사용하면 정말 유용하더라고요. 각 태스크(task)에 의미 있는 태그를 부여하고, --tags 또는 --skip-tags 옵션으로 플레이북 실행을 제어할 수 있습니다. 예를 들어, ‘network’ 태그를 부여한 태스크만 실행하거나, ‘cleanup’ 태그가 붙은 태스크는 건너뛰는 식이죠. 저도 처음엔 플레이북 전체를 매번 돌렸는데, 태그를 사용하니 개발 및 테스트 시간이 엄청 단축되더라고요. ✅

    # 'create_vm' 태그가 붙은 작업만 실행
    ansible-playbook -i inventory/proxmox.yml playbook.yml --tags create_vm
    
    # 'cleanup' 태그가 붙은 작업만 건너뛰고 실행
    ansible-playbook -i inventory/proxmox.yml playbook.yml --skip-tags cleanup
    

    7. 역할(Roles) 기반으로 플레이북 구조화하기

    복잡한 Proxmox 스크립트와 플레이북은 역할(Roles) 기반으로 구조화하는 것이 좋습니다. 역할은 변수, 태스크, 핸들러(handler), 파일, 템플릿(template) 등을 논리적으로 묶어 재사용성을 높여주거든요. 예를 들어, ‘proxmox-vm-create’ 역할, ‘proxmox-net-config’ 역할 등으로 나누어 관리하면 가독성이 높아지고 유지보수가 훨씬 쉬워집니다. 저의 홈랩에서도 VM 생성, 네트워크 설정, 특정 애플리케이션 설치 등을 각각의 역할로 만들어서 사용하고 있습니다.

    # roles/proxmox-vm-create/tasks/main.yml
    - name: Create VM
      community.proxmox.proxmox_kvm:
        # ... VM 생성 설정 ...
    
    # playbook.yml
    - name: Deploy web server VM on Proxmox
      hosts: localhost
      connection: local
      roles:
        - role: proxmox-vm-create
          vars:
            vm_name: 'web-server-02'
            vm_id: 102
            # ... 기타 변수 ...
        - role: proxmox-net-config
          vars:
            vm_ip: '192.168.1.102'
    

    8. 플레이북 실행 전 반드시 테스트하기 (–check –diff)

    실제 변경 사항을 적용하기 전에 --check (dry run, 드라이 런) 모드로 플레이북을 실행하여 어떤 변경이 일어날지 미리 확인하세요. --diff 옵션을 함께 사용하면 변경될 내용을 자세히 볼 수 있더라고요. 이 두 옵션은 실제 환경에 적용하기 전에 발생할 수 있는 잠재적인 문제를 미리 발견하는 데 정말 중요한 역할을 합니다. 저의 수많은 ‘대형 사고’를 막아준 고마운 기능입니다. 😅

    # 실제로 변경하지 않고, 어떤 변경이 일어날지 미리 확인
    ansible-playbook -i inventory/proxmox.yml playbook.yml --check --diff
    

    9. 버전 관리 시스템(Git)은 필수입니다

    모든 플레이북, 인벤토리, 역할 파일은 Git과 같은 버전 관리 시스템에 저장해야 합니다. 변경 이력을 추적하고, 여러 사람이 협업하며, 필요할 경우 이전 버전으로 되돌릴 수 있게 해주거든요. Ansible Proxmox 자동화 스크립트는 인프라의 ‘코드’와 같으므로, 코드 관리의 모범 사례를 따르는 것이 중요합니다. 저도 처음엔 그냥 로컬 폴더에 저장해두고 관리했는데, 실수로 파일을 날리거나 변경 이력을 잃어버려서 다시 처음부터 작성했던 뼈아픈 경험이 있습니다.

    10. AWX/Tower(자동화 컨트롤러)로 더 강력하게

    Ansible 자동화가 복잡해지고 규모가 커지면, AWX(앤서블 워크스)나 Ansible Tower(앤서블 타워)와 같은 자동화 컨트롤러의 도입을 고려해보세요. 웹 기반 UI를 통해 플레이북 실행, 인벤토리 관리, 사용자 권한 제어, 스케줄링, 로깅 등 모든 자동화 작업을 중앙에서 관리할 수 있거든요. AWX Proxmox 연동은 특히 강력합니다. 저는 홈랩에서 AWX를 구축해서 복잡한 VM 배포 파이프라인을 만들었는데, 이젠 버튼 하나로 모든 게 되니 정말 편하더라고요. 이젠 홈랩 자동화의 끝판왕이라고 부르고 싶을 정도입니다.

    AWX 대시보드에서 성공적으로 실행된 Proxmox 자동화 작업 목록

    AWX 대시보드에서 Proxmox 관련 자동화 작업이 성공적으로 실행된 모습을 보여주는 스크린샷입니다.

    ⚠️ 삽질 경험담: 제가 겪었던 문제들 (주의사항/트러블슈팅)

    제가 Ansible Proxmox 자동화를 하면서 가장 많이 겪었던 문제들을 몇 가지 공유해 드릴게요. 여러분은 저처럼 삽질하지 마시라고요! ㅎㅎ

    권한 문제 (Permission Denied)

    Proxmox API를 사용할 때 가장 많이 만나는 에러 중 하나가 바로 권한 문제입니다. root@pam 계정을 직접 사용하는 것은 보안상 좋지 않고, 특정 권한만 가진 API 토큰을 생성해서 사용하는 것이 모범 사례거든요. Proxmox의 ‘Datacenter -> Permissions -> API Tokens’에서 필요한 권한(예: VM.Audit, VM.PowerMgmt, VM.Allocate)만 부여된 토큰을 만들고, 이 토큰 ID와 시크릿(secret)을 Ansible Vault로 안전하게 관리해야 합니다. 처음엔 root 권한으로 대충 했는데, 나중에 보안 감사 때 혼쭐이 날 뻔했습니다. 😱

    네트워크 설정 복잡성

    Proxmox의 네트워크 설정은 처음엔 좀 복잡하게 느껴질 수 있습니다. 특히 브리지(bridge) 설정이나 VLAN(Virtual Local Area Network) 태깅 같은 부분이요. Ansible로 VM을 생성하고 네트워크를 붙일 때, Proxmox 호스트의 네트워크 설정(/etc/network/interfaces)과 정확히 일치하는지 확인해야 합니다. 잘못된 브리지 이름을 사용하거나, VLAN ID를 놓치면 VM이 네트워크에 연결되지 않아 한참을 헤맸던 적이 많습니다. 네트워크 자동화는 항상 신중하게 테스트하는 게 정말 중요해요.

    이 외에도 Proxmox API 응답 지연, 모듈 인자(argument) 오류, 템플릿(Template) 이미지 경로 문제 등 다양한 변수들이 있었습니다. 중요한 건 에러 메시지를 꼼꼼히 읽고, Proxmox 공식 문서와 Ansible 모듈 문서를 참고하며 차분하게 해결해나가는 인내심입니다. 저도 처음엔 ‘이게 왜 안 돼!’ 하면서 키보드를 던질 뻔한 적이 한두 번이 아니었거든요. 하지만 포기하지 않고 해결했을 때의 쾌감은 정말 짜릿합니다! ✨

    Ansible로 자동화된 Proxmox 가상 머신 및 컨테이너 목록

    Ansible로 자동 생성 및 설정된 Proxmox 가상 머신(VM) 및 컨테이너(LXC) 목록을 보여주는 Proxmox 웹 인터페이스 화면입니다.

    마치며: 자동화, 결국 시간을 벌어주는 일 (마무리)

    오늘은 13년차 인프라 엔지니어의 시선으로 Ansible Proxmox 자동화의 베스트 프랙티스 10가지를 자세히 소개해 드렸습니다. 동적 인벤토리, 멱등성, 변수 관리, Proxmox 모듈 활용, 오류 핸들링, 태그, 역할, 테스트, 버전 관리, 그리고 AWX/Tower까지, 이 모든 것이 여러분의 홈랩 자동화와 실무 인프라 관리에 큰 도움이 될 거라고 생각합니다.

    처음에는 복잡해 보일 수 있지만, 한 번 구축해두면 반복적인 작업에서 정말 완전히 해방될 수 있습니다. Proxmox 스크립트를 일일이 작성하고 실행하는 시간 대신, 더 중요하고 창의적인 일에 집중할 수 있게 되는 거죠. 자동화는 결국 우리에게 ‘시간’이라는 가장 소중한 자원을 벌어다 줍니다. 여러분도 오늘 배운 내용을 바탕으로 자신만의 Ansible Proxmox 자동화 시스템을 구축해보시고, 그 편리함을 직접 경험해보시길 바랍니다. 다음 글에서는 AWX를 활용한 Proxmox VM 배포 파이프라인 구축에 대해 더 자세히 다뤄볼 예정이니 기대해주세요! 😉

    Ansible과 Proxmox 연동의 이점을 요약한 인포그래픽

    Ansible과 Proxmox 로고를 중심으로 자동화, 효율성, 확장성, 홈랩, 인프라 관리 등 주요 이점을 시각적으로 요약한 인포그래픽입니다.

  • [k8s] K3s와 AWX 통합 사례 연구: 경량 Kubernetes 자동화 워크플로우 구축

    [k8s] K3s와 AWX 통합 사례 연구: 경량 Kubernetes 자동화 워크플로우 구축

    안녕하세요, 13년차의 서버실입니다. 오늘은 제가 홈랩에서 직접 경험하고 삽질하면서 얻은 지식 하나를 여러분과 공유해볼게요. 바로 K3s(케이쓰리쓰)와 AWX(에이더블유엑스)를 통합하여 경량 Kubernetes(쿠버네티스) 환경에서 자동화 워크플로우를 구축하는 사례입니다.

    작은 규모의 인프라나 홈랩에서 Kubernetes를 운영하다 보면, 리소스 제약 때문에 고민이 많아지죠. 게다가 반복적인 배포나 설정 변경 작업을 수동으로 하다 보면 시간 낭비는 물론이고 휴먼 에러 발생 확률도 높아지고요. 저도 “이걸 어떻게 하면 좀 더 효율적으로 관리할 수 있을까?” 하는 고민에 빠져있었거든요. 그러다가 경량 Kubernetes인 K3s와 Ansible(앤서블) 기반의 자동화 플랫폼인 AWX의 조합을 떠올리게 됐습니다.

    이 둘을 잘 엮으면 리소스 효율적인 환경에서 강력한 자동화 시스템을 만들 수 있겠다는 확신이 들었죠. 그래서 직접 부딪혀가며 구축해봤고, 그 과정과 삽질 경험을 솔직하게 풀어보려 합니다. 혹시 여러분도 비슷한 고민을 하고 계셨다면, 이 글이 좋은 가이드가 될 수 있을 거예요. 💡

    K3s와 AWX를 통합한 자동화 워크플로우의 전체 아키텍처 다이어그램

    K3s 클러스터 위에 AWX가 배포되어 외부 Git 리포지토리의 Ansible 플레이북을 가져와 다른 K3s/Kubernetes 클러스터에 배포하는 통합 아키텍처 다이어그램입니다.

    K3s와 AWX, 왜 이 조합일까요?

    먼저, 이 두 가지 기술이 무엇인지, 그리고 왜 함께 사용하면 시너지가 나는지 간단하게 짚고 넘어가겠습니다.

    K3s: 가볍지만 강력한 경량 Kubernetes

    K3s(케이쓰리쓰)는 Rancher Labs(랜처 랩스)에서 만든 경량 Kubernetes(쿠버네티스) 배포판입니다. 이름에서 알 수 있듯이 ‘K8s(케이츠) – 5 = K3s’라는 유머러스한 의미를 가지고 있어요. 그만큼 바이너리 크기가 작고, 메모리 사용량도 적으니까요. 일반적인 Kubernetes는 컨트롤 플레인(Control Plane) 구성 요소들이 많은 리소스를 요구하는데, K3s는 SQLite를 기본 데이터베이스로 사용하고 불필요한 기능을 제거해서 엣지 컴퓨팅(Edge Computing), IoT(사물 인터넷), 또는 저사양 환경에 정말 딱 맞습니다. 제가 홈랩에서 운영하는 라즈베리 파이(Raspberry Pi) 클러스터 같은 곳에 정말 찰떡이더라고요. ✅

    AWX: Ansible 자동화 플랫폼의 중앙 허브

    AWX(에이더블유엑스)는 Red Hat(레드햇)의 Ansible Tower(앤서블 타워)의 오픈소스 업스트림 프로젝트입니다. Ansible(앤서블)은 강력한 IT 자동화 도구인데, AWX는 이 Ansible 플레이북(Playbook)들을 웹 UI를 통해 중앙에서 관리하고 실행할 수 있게 해줍니다. 그냥 복잡한 커맨드 라인(Command Line) 없이도 프로젝트, 인벤토리(Inventory), 자격 증명(Credential) 등을 관리하고, 잡 템플릿(Job Template)을 만들어 반복적인 작업을 예약하거나 실행 결과를 쉽게 확인할 수 있죠. 특히 팀 단위로 자동화 작업을 공유하고 접근 제어를 해야 할 때 진짜 빛을 발합니다. 🎉

    통합의 시너지: 경량 K3s 위의 자동화 플랫폼

    저는 K3s의 가벼움 위에 AWX를 올려서 경량 Kubernetes 환경을 위한 자동화 허브를 구축하고 싶었습니다. K3s에서 돌아가는 애플리케이션의 배포나 설정을 AWX를 통해 관리하려는 거였거든요. 예를 들어, 새로운 컨테이너 이미지를 빌드하면 AWX가 자동으로 K3s 클러스터에 배포하도록 하거나, 특정 서비스의 스케일링(Scaling) 작업을 AWX 잡 템플릿으로 만들어서 필요할 때마다 클릭 한 번으로 실행하는 식입니다. 이렇게 하면 인프라 관리가 정말 수월해지더라고요. 💡

    실전 구현: K3s 위에 AWX 배포하기

    이제 제가 직접 K3s 클러스터에 AWX를 배포했던 과정을 단계별로 설명해 드릴게요. 저는 이미 K3s 클러스터가 구성되어 있다는 가정하에 진행했습니다. 아직 K3s 설치가 안 되어 있다면, 다음 명령어로 간단하게 설치할 수 있습니다.

    curl -sfL https://get.k3s.io | sh -

    1. AWX Operator 설치

    AWX는 Kubernetes 환경에 AWX Operator(오퍼레이터)를 통해 배포하는 것이 가장 일반적입니다. Operator는 특정 애플리케이션(여기서는 AWX)을 Kubernetes의 컨트롤러처럼 관리해주는 도구라고 생각하시면 돼요. 배포부터 업데이트, 스케일링까지 알아서 처리해주니까요.

    AWX Operator를 설치하려면 공식 AWX Operator 저장소에서 최신 설치 가이드를 확인하시고, 다음 명령어로 설치합니다.

    kubectl apply -f https://raw.githubusercontent.com/ansible/awx-operator/devel/deploy/operator.yaml

    이 명령어가 AWX Operator와 필요한 CRD(Custom Resource Definition, 사용자 정의 리소스 정의)를 모두 설치해줍니다. CRD는 Kubernetes가 AWX라는 새로운 리소스 타입을 이해할 수 있도록 해주는 설정 파일이거든요. 이 과정을 거치면 awx-operator 파드(Pod)가 실행되면서 AWX 인스턴스를 배포할 준비가 완료됩니다.

    2. AWX 인스턴스 배포

    Operator가 준비되었다면, 이제 우리가 원하는 AWX 인스턴스를 생성할 차례입니다. AWX라는 Custom Resource(CR) 객체를 생성하면, Operator가 이 CR을 감지하고 실제 AWX 애플리케이션을 배포해줍니다. 저는 awx.yaml이라는 파일을 만들어서 다음과 같이 설정했습니다.

    apiVersion: awx.ansible.com/v1beta1
    kind: AWX
    metadata:
      name: my-awx
    spec:
      # AWX Operator가 배포할 AWX 인스턴스의 이름
      # 여기서는 my-awx로 지정했습니다.
    
      # ingressType: Ingress를 사용하여 Kubernetes Ingress를 통해 AWX에 접근하도록 설정합니다.
      # K3s는 기본적으로 Traefik Ingress Controller를 포함하고 있어 편리합니다.
      ingressType: Ingress
      # ingress_host: AWX에 접근할 호스트 이름을 지정합니다.
      # 실제 환경에서는 DNS에 이 호스트를 등록해야 합니다.
      ingress_host: awx.myhomelab.local
      
      # 기타 설정 (필요에 따라 주석 해제 및 수정)
      # postgres_storage_class: AWX 데이터베이스를 위한 Persistent Volume Claim(PVC)의 StorageClass를 지정합니다.
      # 홈랩 환경에서는 hostPath나 local-storage 등을 사용할 수 있습니다.
      # postgres_storage_class: standard
      
      # web_extra_volume_mounts: AWX 웹 파드에 추가 볼륨을 마운트할 때 사용합니다.
      # e.g., 인증서 마운트 등
      # tasks_extra_volume_mounts: AWX 태스크 파드에 추가 볼륨을 마운트할 때 사용합니다.
      
      # image_version: 사용할 AWX 이미지 버전 (특정 버전을 고정할 경우)
      # image_version: "23.3.0"
      
      # resource_requirements: AWX 파드들의 리소스 요청/제한을 설정합니다.
      # 작은 환경에서는 이 부분을 적절히 조절해야 합니다.
      # web_resource_requirements:
      #   requests:
      #     cpu: 500m
      #     memory: 1Gi
      #   limits:
      #     cpu: 1000m
      #     memory: 2Gi
    

    이 파일을 저장하고 kubectl apply -f awx.yaml 명령어를 실행하면 AWX Operator가 AWX 인스턴스에 필요한 Deployment(디플로이먼트), Service(서비스), Ingress(인그레스), Persistent Volume Claim(PVC) 등을 자동으로 생성하기 시작합니다.

    K3s 클러스터에 AWX 파드들이 정상적으로 실행 중임을 나타내는 터미널 화면

    AWX 웹 UI의 로그인 화면 또는 kubectl get pods -n awx 명령어를 실행했을 때 AWX 관련 파드들이 모두 Running 상태인 것을 보여주는 스크린샷입니다.

    ⚠️ 삽질 경험과 트러블슈팅

    제가 이 과정을 진행하면서 겪었던 몇 가지 삽질 경험과 해결 방법을 공유합니다. 여러분은 저처럼 헤매지 않으시길 바라요! 😂

    1. 리소스 부족 문제

    K3s는 가볍지만, AWX는 생각보다 많은 리소스를 필요로 합니다. 특히 데이터베이스(PostgreSQL)와 웹, 태스크 파드들이 동시에 돌아가기 때문에, 2GB RAM 이하의 시스템에서는 버벅이거나 파드가 계속 재시작되는 문제가 발생할 수 있어요. 최소 4GB RAM, 2코어 CPU 이상을 권장합니다. 저도 처음엔 라즈베리 파이 4(4GB 모델)에서 돌렸는데, 다른 서비스들과 함께 돌리니 정말 아슬아슬하더라고요. 결국 8GB 모델로 업그레이드하거나, AWX 전용 노드를 따로 두는 것을 고려하게 됐습니다.

    해결책: awx.yaml 파일의 resource_requirements 섹션을 통해 각 파드의 리소스 요청(requests)과 제한(limits)을 조절할 수 있습니다. 하지만 너무 낮추면 성능 문제가 발생하니, 하드웨어 사양을 충분히 확보하는 것이 최고의 방법입니다.

    2. Persistent Volume(영구 볼륨) 설정

    AWX는 데이터베이스(PostgreSQL)와 작업 실행 로그 등을 저장하기 위해 Persistent Volume(PV, 영구 볼륨)이 필요합니다. K3s는 기본적으로 local-path-provisioner(로컬 패스 프로비저너)를 내장하고 있어서 별도의 StorageClass(스토리지 클래스) 설정 없이도 PVC(Persistent Volume Claim)를 생성하면 자동으로 PV가 프로비저닝(Provisioning)됩니다. 하지만 이 방식은 특정 노드에 데이터가 종속되기 때문에 노드 장애 시 데이터 손실 위험이 있습니다.

    해결책: 홈랩 환경에서는 간단하게 hostPath 타입을 사용하여 특정 경로에 데이터를 저장할 수 있지만, 좀 더 안정적인 환경을 원한다면 NFS(네트워크 파일 시스템) 서버를 구축하고 NFS CSI Driver(드라이버)를 설치하여 공유 스토리지를 사용하는 것이 좋습니다. 저도 NFS를 사용해서 여러 노드에서 AWX 파드들이 유연하게 움직일 수 있도록 구성했거든요.

    3. Ingress(인그레스) 접근 문제

    ingress_host를 설정했지만, 웹 브라우저에서 접속이 안 되는 경우가 있었습니다. K3s는 기본적으로 Traefik(트래픽) Ingress Controller(인그레스 컨트롤러)를 내장하고 있어 편리합니다. 하지만 제 경우에는 다음과 같은 문제가 있었습니다.

    • DNS 설정: awx.myhomelab.local과 같은 호스트 이름을 사용하려면, 홈랩 내 DNS 서버에 해당 호스트를 등록하거나, 접속하려는 PC의 /etc/hosts (Linux/macOS) 또는 C:\Windows\System32\drivers\etc\hosts (Windows) 파일에 IP 주소와 호스트 이름을 직접 매핑해주어야 합니다.
      # /etc/hosts 예시
      192.168.1.100 awx.myhomelab.local # K3s 마스터 노드의 IP 주소
    • Traefik 설정: 가끔 Traefik이 외부 트래픽을 제대로 라우팅(Routing)하지 못하는 경우가 있는데, K3s 설치 시 Traefik 관련 옵션을 확인하거나, Traefik 대시보드(보통 http://<k3s_master_ip>:8080/dashboard/)에서 Ingress Route(인그레스 라우트)가 제대로 생성되었는지 확인해야 합니다.

    해결책: 저는 /etc/hosts 파일을 수정하고, kubectl get ingress -n awx 명령어로 AWX Ingress 리소스가 정상적으로 생성되었는지 확인하면서 문제를 해결했습니다.

    검증 및 결과: AWX로 K3s 자동화하기

    여러 삽질 끝에 드디어 AWX가 K3s 클러스터 위에 성공적으로 배포되었습니다! 🎉 이제 웹 브라우저를 열고 설정했던 ingress_host로 접속하면 AWX 로그인 화면이 보일 겁니다. 초기 관리자 비밀번호는 AWX Operator가 Secret(시크릿)으로 생성해두는데, 다음 명령어로 확인할 수 있습니다.

    kubectl get secret my-awx-admin-password -o jsonpath='{.data.password}' | base64 --decode

    로그인 후, 저는 간단한 Ansible 플레이북을 만들어서 K3s 클러스터의 노드 목록을 조회하는 작업을 자동화해봤습니다. AWX에서 Project(프로젝트), Inventory(인벤토리), Credential(자격 증명)을 설정하고 Job Template(잡 템플릿)을 생성한 뒤 실행하니, 깔끔하게 K3s 노드 정보가 출력되는 것을 확인할 수 있었습니다. 이 화면을 보는 순간, 그동안의 고생이 눈 녹듯 사라지더라고요! 정말 뿌듯했어요. 😄

    AWX 웹 UI에서 K3s 노드 목록을 조회하는 Ansible 잡이 성공적으로 실행된 결과 화면

    AWX 웹 UI에서 Ansible 플레이북이 성공적으로 실행되어 K3s 클러스터의 노드 목록을 출력하는 잡 실행 결과 화면 스크린샷입니다. 초록색 성공 메시지와 함께 결과가 표시됩니다.

    마무리: 경량 Kubernetes 자동화, 여러분도 해보세요!

    K3s와 AWX를 통합하여 경량 Kubernetes 환경에서 자동화 워크플로우를 구축한 경험은 저에게 정말 값진 시간이었습니다. 비록 중간중간 삽질도 많이 했지만, 그 과정에서 얻은 지식과 해결 능력은 어떤 책에서도 배울 수 없는 것이었거든요.

    주요 배운 점:

    • K3s는 가볍지만, AWX처럼 리소스를 많이 쓰는 애플리케이션을 올릴 때는 충분한 하드웨어 리소스 계획이 필요하다는 것.
    • Persistent Volume 설정은 안정적인 운영을 위한 핵심이라는 것. (홈랩이라도 NFS는 정말 중요합니다.)
    • Kubernetes Ingress와 DNS 설정은 항상 꼼꼼하게 확인해야 한다는 것.
    • AWX Operator를 사용하면 복잡한 AWX 배포를 Kubernetes 스타일로 정말 간단하게 할 수 있다는 것.

    이 조합은 특히 저처럼 홈랩을 운영하거나, 소규모 팀에서 효율적인 인프라 자동화를 목표로 할 때 정말 강력한 솔루션이 될 수 있다고 확신합니다. 여러분도 직접 K3s와 AWX를 설치하고 이것저것 만져보면서 자신만의 자동화 워크플로우를 구축해보시길 강력히 추천합니다. 직접 해보면서 얻는 경험만큼 값진 건 없으니까요! 💪

    다음 글에서는 AWX를 이용한 좀 더 복잡한 GitOps(깃옵스) 파이프라인 구축에 대해 더 깊이 다뤄볼 예정입니다. 기대해주세요! 😄

    K3s와 AWX 통합으로 얻을 수 있는 주요 장점을 시각적으로 요약한 인포그래픽

    K3s와 AWX 통합의 주요 장점 (예: 리소스 효율성, 중앙 집중식 자동화, 빠른 배포, 간편한 관리)을 시각적으로 요약한 인포그래픽입니다.

  • [k8s] AWX로 쿠버네티스 워크플로우 자동화: 실전 가이드

    [k8s] AWX로 쿠버네티스 워크플로우 자동화: 실전 가이드

    안녕하세요, 13년차 인프라 엔지니어 ’13년차의 서버실’ 주인장입니다. 오늘은 많은 분들이 고민하시는 쿠버네티스(Kubernetes) 워크플로우 자동화에 대해 이야기해보려고 해요. 특히 AWX를 활용해서 어떻게 복잡한 배포 과정을 효율적으로 관리할 수 있는지, 제가 직접 겪었던 경험과 실전 사례를 중심으로 풀어볼까 합니다. Ansible과 DevOps 문화에 관심 있는 분들이라면 이번 글이 정말 도움이 될 거예요.

    저도 처음엔 쿠버네티스 클러스터에 애플리케이션을 배포하는 과정이 꽤나 번거롭더라고요. 매번 kubectl apply -f 명령어를 입력하고, 버전 관리하고, 환경별로 설정 바꾸고… 반복적인 작업은 언제나 실수의 여지를 남기기 마련이잖아요. 그러다 보니 자연스럽게 자동화의 필요성을 느끼게 됐고, 홈랩에서 AWX(Ansible Workflow eXecutor)를 활용한 솔루션을 모색하게 됐습니다. 직접 삽질해가며 구축했던 과정, 지금부터 솔직하게 공유해볼게요.

    AWX와 쿠버네티스 연동을 통한 워크플로우 자동화 개요 아키텍처 다이어그램, AWX가 Git에서 Ansible Playbook을 가져와 쿠버네티스에 배포하는 과정을 보여줍니다.

    AWX와 쿠버네티스 연동을 통한 워크플로우 자동화 개요 아키텍처 다이어그램. AWX가 Git 저장소에서 Ansible Playbook을 가져와 쿠버네티스 API를 통해 리소스를 배포하는 과정을 보여줍니다.

    AWX와 쿠버네티스, 왜 함께 써야 할까요?

    먼저 핵심 개념부터 짚고 넘어갈게요. AWX는 Ansible Automation Platform의 업스트림 오픈소스 프로젝트로, 웹 기반 UI를 통해 Ansible Playbook을 실행하고 관리해주는 도구예요. 쉽게 말해, 터미널에서 명령어로 실행하던 Ansible 작업을 UI로 편하게 스케줄링하고, 권한을 관리하고, 실행 결과를 모니터링할 수 있다는 거죠. 제가 홈랩에서 다양한 자동화 실험을 할 때 정말 유용하게 써먹고 있는 녀석입니다.

    그리고 쿠버네티스(Kubernetes, K8s)는 컨테이너화된 워크로드와 서비스를 자동으로 배포, 스케일링 및 관리해주는 오픈소스 시스템이에요. 컨테이너 오케스트레이션(Container Orchestration)의 사실상 표준이 되었죠. 선언적(Declarative) 방식으로 원하는 상태를 정의하면, 쿠버네티스가 그 상태를 유지하도록 알아서 관리해줍니다. 저는 주로 Deployment(디플로이먼트)와 Service(서비스), Ingress(인그레스, 외부 트래픽 진입점) 같은 리소스들을 YAML 파일로 정의해서 사용하고 있어요.

    이 둘을 함께 쓰면 어떤 시너지가 생길까요? 바로 선언적 인프라(Declarative Infrastructure) 관리가 가능해진다는 거예요. Ansible Playbook으로 쿠버네티스 리소스의 최종 상태를 정의하고, AWX가 이 Playbook을 실행해서 클러스터에 배포하는 방식이죠. 이렇게 하면 수동 작업으로 인한 오류를 줄이고, 배포 과정을 표준화하며, 지속적인 통합/배포(CI/CD) 워크플로우를 구축하는 데 정말 큰 도움이 돼요. 실제 운영 환경에서 이 조합은 정말 강력하더라고요.

    실전 구현: AWX로 쿠버네티스 워크플로우 자동화하기

    자, 그럼 이제 제가 직접 구축했던 방법을 단계별로 보여드릴게요. 목표는 AWX를 통해 간단한 Nginx 웹 서버를 쿠버네티스 클러스터에 배포하고, 외부에 노출시키는 거예요.

    1단계: AWX 환경 설정

    1. 인벤토리(Inventory) 생성: AWX에서 Playbook을 실행할 대상(Host)을 정의합니다. 여기서는 쿠버네티스 클러스터의 API 서버에 접근해야 하므로, 로컬 호스트를 대상으로 하거나, 쿠버네티스 API 엔드포인트를 지정해요. 저는 주로 localhost를 대상으로 하고, kubeconfig 파일을 통해 클러스터에 접근하는 방식을 선호합니다.
    2. 자격 증명(Credential) 생성: 쿠버네티스 클러스터에 접근하기 위한 자격 증명이 필요해요. kubeconfig 파일을 AWX에 등록하는 방법이 가장 일반적입니다.
    # AWX Credential 설정 예시 (Kubeconfig Type)
    # --- Kubeconfig 내용 --- (실제 파일 내용)
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority-data: ...
        server: https://your-kubernetes-api-server:6443
      name: my-cluster
    contexts:
    - context:
        cluster: my-cluster
        user: my-user
      name: my-context
    current-context: my-context
    kind: Config
    preferences: {}
    users:
    - name: my-user
      user:
        client-certificate-data: ...
        client-key-data: ...
    

    2단계: Ansible Playbook 작성

    쿠버네티스 리소스를 배포하기 위한 Playbook을 작성해요. Ansible이 제공하는 kubernetes.core.k8s 모듈로 쿠버네티스 리소스 관리가 정말 쉬워진다니까요. 저는 Nginx Deployment와 Service를 배포하는 Playbook을 만들었어요.

    # playbook.yaml
    ---
    - name: Deploy Nginx to Kubernetes
      hosts: localhost
      connection: local
      collections:
        - kubernetes.core
    
      vars:
        namespace: default
        app_name: my-nginx
        image_version: 1.25.3
    
      tasks:
        - name: Ensure Namespace exists
          kubernetes.core.k8s:
            api_version: v1
            kind: Namespace
            name: "{{ namespace }}"
            state: present
    
        - name: Deploy Nginx Deployment
          kubernetes.core.k8s:
            state: present
            definition: |
              apiVersion: apps/v1
              kind: Deployment
              metadata:
                name: "{{ app_name }}-deployment"
                namespace: "{{ namespace }}"
                labels:
                  app: "{{ app_name }}"
              spec:
                replicas: 2
                selector:
                  matchLabels:
                    app: "{{ app_name }}"
                template:
                  metadata:
                    labels:
                      app: "{{ app_name }}"
                  spec:
                    containers:
                    - name: "{{ app_name }}"
                      image: nginx:"{{ image_version }}"
                      ports:
                      - containerPort: 80
    
        - name: Expose Nginx Service
          kubernetes.core.k8s:
            state: present
            definition: |
              apiVersion: v1
              kind: Service
              metadata:
                name: "{{ app_name }}-service"
                namespace: "{{ namespace }}"
                labels:
                  app: "{{ app_name }}"
              spec:
                selector:
                  app: "{{ app_name }}"
                ports:
                  - protocol: TCP
                    port: 80
                    targetPort: 80
                type: NodePort # 또는 LoadBalancer, ClusterIP 등 환경에 맞게
    

    이 Playbook을 Git 저장소(예: GitHub, GitLab)에 커밋해요. AWX가 이 저장소에서 Playbook을 가져와 실행할 거거든요.

    3단계: AWX 프로젝트 및 작업 템플릿(Job Template) 설정

    AWX UI에서 다음을 설정합니다.

    1. 프로젝트(Project) 생성: Git 저장소를 연결하여 Playbook을 가져와요.
    2. 작업 템플릿(Job Template) 생성: 이 템플릿에 위에서 만든 인벤토리, 자격 증명, 프로젝트, 그리고 실행할 Playbook 파일(playbook.yaml)을 연결합니다.
    AWX 작업 템플릿 설정 화면 스크린샷, Git 프로젝트, 인벤토리, 쿠버네티스 자격 증명, 실행할 Playbook 파일을 지정하는 모습

    AWX 작업 템플릿 설정 화면 스크린샷. Git 프로젝트, 인벤토리, 쿠버네티스 자격 증명, 실행할 Playbook 파일을 지정하는 모습을 보여줍니다.

    ⚠️ 삽질 경험 & 워크플로우 자동화 트러블슈팅

    제가 이 AWX와 쿠버네티스 자동화 과정을 진행하면서 겪었던 몇 가지 삽질과 그 해결책을 공유할게요. 혹시 비슷한 문제를 겪는 분들이 있다면 도움이 될 거예요.

    • 인증 오류 (Authentication Error): 가장 흔한 문제 중 하나예요. kubeconfig 파일의 권한 문제, 또는 파일 내용 자체가 잘못된 경우가 많았어요. 특히 client-certificate-data와 client-key-data가 정확한 base64 인코딩 값인지 여러 번 확인했거든요. AWX가 실행되는 컨테이너 환경에서 kubeconfig 파일에 접근 권한이 없어서 생기는 경우도 있었고요. 💡 팁: AWX 컨테이너 내부에서 kubectl get pods를 직접 실행해보며 인증 문제를 디버깅해보세요.
    • YAML 문법 오류: 쿠버네티스 리소스 정의는 YAML 파일로 하는데, 들여쓰기나 오타 하나로도 워크플로우가 실행 안 돼요. 특히 Ansible Playbook 내 definition 블록 안에 YAML을 넣을 때는 더욱 주의해야 합니다. ⚠️ 경고: definition: | 다음 줄부터는 반드시 두 칸 이상 들여쓰기 해야 합니다!
    • Ansible kubernetes.core.k8s 모듈 문제: 처음에는 이 모듈을 쓰는 게 익숙하지 않아서 헤맸어요. 특히 state: present로 리소스를 생성하고, state: absent로 삭제하는 방식에 익숙해지는 데 시간이 좀 걸렸거든요. 그리고 kubeconfig 파일 경로를 명시적으로 지정해야 하는 경우도 있었습니다 (kubeconfig: /path/to/kubeconfig).
    • 네트워크 연결 문제: AWX가 쿠버네티스 API 서버에 접근할 수 없는 네트워크 환경일 경우 당연히 실패하죠. 방화벽 규칙이나 네트워크 정책을 다시 확인해야 해요.

    ✅ 결과 검증 및 자동화의 힘!

    모든 설정이 끝나면, AWX UI에서 작업 템플릿을 실행(Launch)해요. Playbook이 성공적으로 실행되면 AWX 작업 로그에서 초록색 SUCCESS 메시지를 확인할 수 있어요. 저도 이 메시지를 처음 봤을 때, “드디어 됐다!” 하고 쾌재를 불렀던 기억이 생생해요.

    이제 쿠버네티스 클러스터에서 실제로 리소스가 배포되었는지 확인해볼 차례입니다.

    
    kubectl get deployments -n default
    # NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
    # my-nginx-deployment   2/2     2            2           2m
    
    kubectl get services -n default
    # NAME              TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
    # my-nginx-service   NodePort   10.96.123.456   <none>        80:3xxxx/TCP   2m
    

    정상적으로 Nginx Deployment와 Service가 생성된 걸 확인할 수 있어요. 이제 NodePort로 할당된 포트를 통해 Nginx 웹 서버에 접속하면 “Welcome to Nginx!” 페이지를 볼 수 있을 거예요. 🎉 정말 편리하지 않나요? 몇 번의 클릭만으로 쿠버네티스에 애플리케이션을 배포하는 워크플로우 자동화가 완성된 거죠.

    AWX 작업 성공 로그와 kubectl get pods/deployments 명령 결과 비교 화면, AWX의 성공적인 Task 완료와 쿠버네티스 배포 리소스 확인

    AWX 작업 성공 로그와 kubectl get pods/deployments 명령 결과 비교 화면. AWX의 Job Output에서 Task가 성공적으로 완료된 것을 보여주고, 터미널에서 kubectl 명령어로 배포된 리소스들을 확인하는 모습을 나란히 보여줍니다.

    마무리하며: 자동화, 멈추지 않는 여정

    이번 AWX와 쿠버네티스 연동 사례를 통해 워크플로우 자동화가 얼마나 강력한지 다시 한번 느꼈어요. 단순한 배포를 넘어, IaC(Infrastructure as Code, 코드형 인프라)의 개념을 실현하고, DevOps 파이프라인의 핵심 구성 요소로 활용할 수 있다는 것이 핵심이죠.

    인프라 엔지니어로서 제가 얻은 가장 큰 교훈은 “반복되는 작업은 무조건 자동화하라”는 거예요. 처음엔 자동화 스크립트 작성에 시간이 들지언정, 장기적으로는 시간과 노력을 아끼고, 휴먼 에러를 줄여준다는 걸 뼈저리게 경험했거든요. 이 경험이 여러분의 인프라 관리에도 작은 영감이 되기를 바랍니다.

    다음 글에서는 이 워크플로우를 확장해서 Ingress 컨트롤러를 배포하고, 외부 도메인으로 서비스에 접근하는 방법을 다뤄볼까 해요. 기대해주세요!

    AWX, Ansible, Kubernetes, DevOps 로고들이 원형으로 배치된 인포그래픽. 각 로고들이 서로 연결되어 워크플로우 자동화를 이루는 모습을 시각적으로 보여줍니다.

  • [Cloud] Ansible로 멀티 클라우드 인프라 자동화: AWS, Azure, GCP 통합 관리 가이드

    [Cloud] Ansible로 멀티 클라우드 인프라 자동화: AWS, Azure, GCP 통합 관리 가이드

    Ansible로 멀티 클라우드 인프라 자동화: AWS, Azure, GCP 통합 관리 가이드

    안녕하세요, 13년차 서버실 지킴이입니다. 요즘 인프라 환경을 보면 단순히 한 클라우드에만 갇혀 있지 않죠? 멀티 클라우드(Multi-Cloud)는 이제 선택이 아니라 필수가 되어가는 시대인 것 같습니다. 저도 처음엔 AWS만 주력으로 썼었는데, 프로젝트가 커지고 요구사항이 다양해지면서 Azure나 GCP도 함께 다루게 되더라고요. 근데 이게 여러 클라우드를 동시에 관리하려니 여간 복잡한 게 아니었습니다. 각 클라우드마다 CLI도 다르고, API도 다르고… 수동으로 관리하다가는 퇴근은커녕 야근만 늘겠다 싶었죠. 😭

    이런 고민을 하던 중에 저의 든든한 동반자, Ansible(앤서블)을 떠올렸습니다. Ansible은 이미 온프레미스 환경에서 서버 자동화에 요긴하게 써왔던 도구거든요. ‘이걸로 멀티 클라우드도 통합 관리할 수 있지 않을까?’ 하는 생각에 홈랩에서 이것저것 실험해봤습니다. 결과는 대만족이었습니다! 오늘은 제가 직접 경험하며 삽질했던 내용과 함께, Ansible로 AWS, Azure, GCP 인프라를 한 번에 자동화하는 방법을 여러분께 알려드리려고 합니다. 이 글을 통해 멀티 클라우드 관리의 복잡성을 확 줄여보시길 바랍니다. 자, 그럼 시작해볼까요? 🎉

    Ansible을 활용한 멀티 클라우드 통합 관리 아키텍처 다이어그램.

    Ansible, 왜 멀티 클라우드 자동화에 최적일까요?

    먼저 Ansible이 어떤 녀석인지, 그리고 왜 멀티 클라우드 환경에 딱 맞는지 간단히 짚고 넘어가죠. 쉽게 말해 Ansible(앤서블)은 에이전트리스(Agentless) 기반의 자동화 도구(Automation Tool)입니다. 관리 대상 서버에 별도의 에이전트를 설치할 필요 없이 SSH(Secure Shell)나 WinRM(Windows Remote Management) 프로토콜을 이용해 명령을 실행하죠. 이게 멀티 클라우드 환경에서 엄청난 강점입니다.

    • 단일 제어 플레인(Single Control Plane): 각 클라우드 콘솔이나 CLI를 오갈 필요 없이, Ansible 컨트롤러 노드(Control Node) 하나에서 모든 클라우드 인프라를 관리할 수 있습니다.
    • 모듈(Modules) 기반의 추상화: Ansible은 각 클라우드 프로바이더(Cloud Provider)별로 수많은 모듈을 제공합니다. 예를 들어, AWS EC2 인스턴스를 만들 때는 amazon.aws.ec2_instance 모듈을, Azure VM을 만들 때는 azure.azcollection.azure_rm_virtualmachine 모듈을 사용하죠. 각 클라우드 API의 복잡성을 몰라도 모듈만 잘 사용하면 됩니다.
    • 멱등성(Idempotency): 플레이북(Playbook)을 여러 번 실행해도 항상 동일한 최종 상태(Desired State)를 보장합니다. 이미 생성된 리소스는 건드리지 않고, 필요한 변경사항만 적용되니 안심하고 재실행할 수 있습니다.
    • YAML(YAML Ain’t Markup Language) 기반의 쉬운 학습 곡선: 복잡한 프로그래밍 언어 대신 사람이 읽고 쓰기 쉬운 YAML 문법으로 플레이북을 작성합니다. 인프라 엔지니어에게는 정말 친숙한 방식이죠.

    결론적으로 Ansible은 코드로서의 인프라(Infrastructure as Code, IaC)를 구현하기에 아주 적합하며, 특히 이종 클라우드 환경을 통합 관리하는 데 강력한 성능을 발휘합니다.

    실전 구현: AWS, Azure, GCP 인프라 자동화

    자, 이제 직접 Ansible로 멀티 클라우드 인프라를 구축해볼 차례입니다. 목표는 각 클라우드에 웹 서버 역할을 할 가상 머신(Virtual Machine)을 하나씩 생성하고, 간단한 웹 서비스(Nginx)를 설치하는 것입니다.

    1. Ansible 및 클라우드 연동 환경 설정

    먼저 Ansible 컨트롤러 노드에 필요한 도구들을 설치해야 합니다. 저는 Ubuntu 환경에서 진행했습니다.

    #!/bin/bash
    # Ansible 설치
    sudo apt update && sudo apt install -y ansible python3-pip
    
    # AWS 연동을 위한 boto3 라이브러리 설치
    pip3 install boto3 botocore
    
    # Azure 연동을 위한 Azure CLI 및 모듈 설치
    sudo apt install -y azure-cli
    ansible-galaxy collection install azure.azcollection
    
    # GCP 연동을 위한 Google Cloud SDK 및 모듈 설치
    echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -
    sudo apt update && sudo apt install -y google-cloud-sdk
    ansible-galaxy collection install google.cloud
    

    각 클라우드 인증 정보 설정도 중요합니다:

    • AWS: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 환경 변수 설정 또는 ~/.aws/credentials 파일 설정
    • Azure: az login 명령으로 로그인 또는 서비스 주체(Service Principal) 생성 후 환경 변수 설정
    • GCP: 서비스 계정(Service Account) 키 파일 다운로드 후 GOOGLE_APPLICATION_CREDENTIALS 환경 변수 설정

    ⚠️ 인증 정보 관리 중요: 실제 운영 환경에서는 AWS Secrets Manager, Azure Key Vault, GCP Secret Manager 같은 비밀 관리 서비스(Secret Management Service)를 활용하거나 Ansible Vault를 사용하여 인증 정보를 안전하게 보관해야 합니다. 절대 민감한 정보를 플레이북에 직접 넣지 마세요!

    2. 동적 인벤토리(Dynamic Inventory) 설정

    클라우드 환경은 리소스가 수시로 생성/삭제되기 때문에 고정된 인벤토리 파일보다는 동적 인벤토리를 사용하는 것이 효율적입니다. Ansible은 각 클라우드 프로바이더를 위한 동적 인벤토리 플러그인을 제공하거든요.

    # aws_ec2.yml (AWS EC2 인스턴스 동적 인벤토리)
    plugin: amazon.aws.ec2
    regions:
      - ap-northeast-2
    keyed_groups:
      - key: tags.env
        prefix: env_
    
    # azure_rm.yml (Azure VM 동적 인벤토리)
    plugin: azure.azcollection.azure_rm
    include_vm_resource_groups:
      - my-ansible-rg
    
    # gcp_compute.yml (GCP Compute Engine 동적 인벤토리)
    plugin: google.cloud.gcp_compute
    projects:
      - your-gcp-project-id
    zones:
      - asia-northeast3-a
    

    이제 ansible-inventory -i aws_ec2.yml --list 등으로 각 클라우드의 인벤토리를 확인할 수 있습니다.

    Ansible으로 생성된 각 클라우드 가상 머신 목록.

    3. 클라우드 리소스 생성 플레이북 작성

    각 클라우드에 가상 머신을 생성하고 Nginx를 설치하는 플레이북입니다. 하나의 플레이북에서 여러 클라우드를 대상으로 할 수 있다는 점이 핵심입니다.

    ---
    # multi_cloud_infra.yml
    # AWS, Azure, GCP에 동시에 웹 서버를 배포하는 플레이북
    
    - name: AWS 인프라 준비
      hosts: localhost
      connection: local
      gather_facts: no
    
      vars:
        aws_region: ap-northeast-2
        ssh_key_path: ~/.ssh/id_rsa.pub
    
      tasks:
        # AWS Security Group 먼저 생성
        - name: AWS 보안 그룹 생성
          amazon.aws.ec2_security_group:
            name: ansible-sg-aws
            description: "Ansible managed AWS webserver security group"
            region: "{{ aws_region }}"
            rules:
              - proto: tcp
                ports: 22
                cidr_ip: 0.0.0.0/0
              - proto: tcp
                ports: 80
                cidr_ip: 0.0.0.0/0
            rules_egress:
              - proto: all
                cidr_ip: 0.0.0.0/0
          register: aws_sg_output
    
        # AWS EC2 인스턴스 생성
        - name: AWS EC2 인스턴스 생성
          amazon.aws.ec2_instance:
            name: ansible-aws-webserver
            image_id: ami-0c802847a7dd848c0  # Ubuntu 22.04 LTS (ap-northeast-2)
            instance_type: t2.micro
            region: "{{ aws_region }}"
            security_group: ansible-sg-aws
            key_name: your_aws_keypair
            tags:
              env: dev
              project: ansible-multi-cloud
            state: running
            wait: yes
          register: aws_ec2_output
    
    - name: Azure 인프라 준비
      hosts: localhost
      connection: local
      gather_facts: no
    
      vars:
        azure_resource_group: my-ansible-rg
        azure_location: koreacentral
        ssh_key_path: ~/.ssh/id_rsa.pub
    
      tasks:
        # Azure Virtual Network 생성 (필수 선행 작업)
        - name: Azure Virtual Network 생성
          azure.azcollection.azure_rm_virtualnetwork:
            resource_group: "{{ azure_resource_group }}"
            name: ansible-vnet
            location: "{{ azure_location }}"
            address_prefixes:
              - "10.0.0.0/16"
          register: azure_vnet_output
    
        # Azure Subnet 생성
        - name: Azure Subnet 생성
          azure.azcollection.azure_rm_subnet:
            resource_group: "{{ azure_resource_group }}"
            name: default
            address_prefix: "10.0.0.0/24"
            virtual_network: ansible-vnet
          register: azure_subnet_output
    
        # Azure NSG 생성
        - name: Azure 네트워크 보안 그룹 생성
          azure.azcollection.azure_rm_networksecuritygroup:
            resource_group: "{{ azure_resource_group }}"
            name: ansible-azure-nsg
            location: "{{ azure_location }}"
            rules:
              - name: AllowSSH
                protocol: Tcp
                destination_port_range: 22
                access: Allow
                priority: 100
                direction: Inbound
              - name: AllowHTTP
                protocol: Tcp
                destination_port_range: 80
                access: Allow
                priority: 110
                direction: Inbound
          register: azure_nsg_output
    
        # Azure VM 생성
        - name: Azure VM 생성
          azure.azcollection.azure_rm_virtualmachine:
            resource_group: "{{ azure_resource_group }}"
            name: ansible-azure-webserver
            vm_size: Standard_B1s
            admin_username: azureuser
            ssh_password_enabled: false
            ssh_public_keys:
              - path: /home/azureuser/.ssh/authorized_keys
                key_data: "{{ lookup('file', ssh_key_path) }}"
            image:
              offer: 0001-com-ubuntu-server-jammy
              publisher: Canonical
              sku: 22_04-lts-gen2
              version: latest
            location: "{{ azure_location }}"
          register: azure_vm_output
    
    - name: GCP 인프라 준비
      hosts: localhost
      connection: local
      gather_facts: no
    
      vars:
        gcp_project: your-gcp-project-id
        gcp_zone: asia-northeast3-a
        gcp_network: default
        ssh_key_path: ~/.ssh/id_rsa.pub
        gcp_user: ansible
    
      tasks:
        # GCP Firewall 규칙 생성
        - name: GCP 방화벽 규칙 생성
          google.cloud.gcp_compute_firewall:
            name: ansible-gcp-allow-http-ssh
            project: "{{ gcp_project }}"
            allowed:
              - IPProtocol: tcp
                ports: [ '22', '80' ]
            source_ranges: [ '0.0.0.0/0' ]
            target_tags: [ 'http-server' ]
            state: present
          register: gcp_fw_output
    
        # GCP Compute Engine 인스턴스 생성
        - name: GCP Compute Engine 인스턴스 생성
          google.cloud.gcp_compute_instance:
            name: ansible-gcp-webserver
            project: "{{ gcp_project }}"
            zone: "{{ gcp_zone }}"
            machine_type: e2-micro
            disks:
              - auto_delete: 'true'
                boot: 'true'
                initialize_params:
                  source_image: projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts
                  disk_size_gb: 20
            network_interfaces:
              - name: "{{ gcp_network }}"
                access_configs:
                  - name: "External NAT"
                    type: "ONE_TO_ONE_NAT"
            metadata:
              ssh-keys: "{{ gcp_user }}:{{ lookup('file', ssh_key_path) }}"
            tags:
              items:
                - http-server
            state: present
          register: gcp_instance_output
    
    - name: 모든 클라우드 웹서버에 Nginx 설치
      hosts: all
      gather_facts: yes
    
      pre_tasks:
        - name: SSH 연결 대기
          ansible.builtin.wait_for_connection:
            timeout: 300
    
      tasks:
        - name: Nginx 설치
          ansible.builtin.apt:
            name: nginx
            state: present
            update_cache: yes
          become: yes
    
        - name: Nginx 서비스 시작
          ansible.builtin.service:
            name: nginx
            state: started
            enabled: yes
          become: yes
    

    위 플레이북은 각 클라우드별로 분리된 구조로 작성되어 있습니다. 첫 번째부터 세 번째까지는 각각 AWS, Azure, GCP 인프라를 생성하는 hosts: localhost 플레이북입니다. 마지막 플레이북은 동적 인벤토리에서 수집된 모든 호스트에 Nginx를 설치합니다.

    플레이북 실행은 간단합니다:

    ansible-playbook -i aws_ec2.yml -i azure_rm.yml -i gcp_compute.yml multi_cloud_infra.yml
    

    ⚠️ 삽질 경험 및 트러블슈팅

    제가 이 과정을 진행하면서 겪었던 몇 가지 삽질 경험과 해결 방법을 공유합니다. 여러분은 저처럼 헤매지 마시길 바랍니다. ㅎㅎ

    1. 클라우드 인증 실패

    문제: 플레이북 실행 시 Unauthorized, Access Denied, Credential Error 같은 메시지가 뜨면서 클라우드 리소스 생성이 안 되는 경우가 많았습니다.

    해결:

    • AWS: ~/.aws/credentials 파일의 내용이 정확한지, 또는 환경 변수(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)가 올바르게 설정되었는지 확인했습니다. 그리고 해당 IAM 사용자에게 EC2 생성, Security Group 관리 등 필요한 권한이 제대로 부여되었는지 IAM 정책을 검토했습니다.
    • Azure: az login으로 로그인 세션이 유효한지 확인하거나, 서비스 주체(Service Principal)를 사용한다면 AZURE_CLIENT_ID, AZURE_SECRET, AZURE_TENANT, AZURE_SUBSCRIPTION_ID 환경 변수가 정확한지 확인했습니다. 해당 서비스 주체에 기여자(Contributor) 또는 그에 준하는 역할이 할당되어야 합니다.
    • GCP: 서비스 계정 키 파일(JSON)의 경로가 GOOGLE_APPLICATION_CREDENTIALS 환경 변수에 올바르게 지정되었는지 확인했습니다. 또한, 서비스 계정에 Compute Engine Instance Admin (v1), Service Account User 등 필요한 역할이 부여되어 있는지 GCP IAM 콘솔에서 확인했습니다.

    2. SSH 접속 문제 (Nginx 설치 단계)

    문제: 인스턴스는 잘 생성됐는데, Nginx 설치 단계에서 SSH Connection refused 또는 Timeout 에러가 발생했습니다.

    해결:

    • 보안 그룹/방화벽 규칙: 각 클라우드에서 생성된 가상 머신의 보안 그룹(AWS), 네트워크 보안 그룹(Azure), 방화벽 규칙(GCP)에 Ansible 컨트롤러 노드의 IP 주소 또는 0.0.0.0/0(테스트용)에서 22번 포트(SSH) 접속을 허용하는 규칙이 있는지 확인했습니다.
    • SSH 키페어: 플레이북에서 지정한 ssh_key_path의 퍼블릭 키가 각 클라우드 인스턴스에 올바르게 등록되었는지 확인했습니다. AWS는 키페어 이름을, Azure/GCP는 직접 퍼블릭 키 내용을 전달하는 방식이므로 차이에 유의했습니다.
    • 사용자 이름: 각 클라우드 인스턴스의 기본 사용자 이름이 다릅니다. AWS Ubuntu는 ubuntu, Azure Ubuntu는 azureuser, GCP Ubuntu는 ubuntu로 설정했습니다. 플레이북에 ansible_user: 사용자명으로 명시하면 더 안전합니다.
    • 인스턴스 부팅 시간: 가끔 인스턴스가 완전히 부팅되기 전에 Ansible이 SSH 접속을 시도하여 실패하는 경우가 있습니다. ansible.builtin.wait_for_connection 모듈을 사용해 SSH 포트가 열릴 때까지 기다리도록 플레이북에 추가했습니다. 이건 정말 중요해요!

    3. Azure 네트워크 구성 누락

    문제: Azure VM을 생성하려니 Virtual Network와 Subnet이 없다고 에러가 나왔습니다.

    해결: Azure는 AWS나 GCP와 달리 기본 네트워크 구조가 없으므로, VM 생성 전에 Virtual Network와 Subnet을 명시적으로 생성해야 합니다. 위의 플레이북에서 이를 추가했으니 참고하세요.

    검증 및 결과 확인 ✅

    플레이북 실행이 성공적으로 완료되었다면, 이제 각 클라우드 콘솔에 접속하여 리소스가 잘 생성되었는지 확인해볼 차례입니다. AWS EC2, Azure VM, GCP Compute Engine 목록에서 ansible-aws-webserver, ansible-azure-webserver, ansible-gcp-webserver라는 이름의 인스턴스가 실행 중인 것을 볼 수 있을 겁니다.

    각 인스턴스의 퍼블릭 IP 주소로 웹 브라우저에서 접속해보세요. Nginx 기본 페이지가 보인다면 성공입니다! 드디어 됐다! 🎉

    이 과정에서 여러분이 직접 각 클라우드의 콘솔을 열어 인스턴스를 생성하고, 보안 그룹을 설정하고, SSH로 접속해서 Nginx를 설치하는 수고를 덜 수 있었다는 것을 체감할 수 있을 겁니다. 자동화의 힘은 정말 대단하죠.

    성공적으로 배포된 Nginx 웹 서버 페이지.

    마무리하며: 클라우드 여정의 다음 단계

    오늘은 Ansible을 활용해서 AWS, Azure, GCP 멀티 클라우드 환경에 인프라를 자동화하고 웹 서버를 배포하는 과정을 함께 해봤습니다. 제가 직접 해보니, 처음엔 각 클라우드 모듈과 인증 방식에 적응하는 데 시간이 좀 걸렸지만, 한 번 체계를 잡아두니 그 이후부터는 정말 편하더라고요. 삽질 끝에 얻은 귀한 경험이었습니다. 💡

    이 글이 여러분의 멀티 클라우드 여정에 작은 등대가 되었기를 바랍니다. 여기서 멈추지 않고, 더 나아가 다음과 같은 내용들을 탐구해보시면 좋을 것 같습니다:

    • Ansible Vault: 민감한 정보를 안전하게 관리하는 방법.
    • CI/CD 파이프라인 통합: GitOps 워크플로우를 통해 변경 사항이 자동으로 배포되도록 구성.
    • Terraform과의 연동: Terraform으로 인프라를 프로비저닝하고, Ansible로 프로비저닝된 인스턴스에 소프트웨어를 구성하는 하이브리드 접근 방식.
    • 삭제 플레이북: 생성된 리소스를 깔끔하게 정리하는 삭제 플레이북 작성 (state: absent 활용).

    저도 홈랩에서 이런저런 실험을 계속하면서 새로운 인사이트를 얻고 있습니다. 다음번에는 더 재미있고 유익한 경험담으로 찾아뵙겠습니다. 혹시 이 글을 읽으시면서 궁금한 점이나 공유하고 싶은 삽질 경험이 있으시다면 언제든지 댓글로 남겨주세요! 감사합니다!

    Ansible을 통한 멀티 클라우드 자동화 요약.

  • [Cloud] Ansible Playbook 디버깅: 흔한 오류 해결 및 실전 팁

    [Cloud] Ansible Playbook 디버깅: 흔한 오류 해결 및 실전 팁

    목차

    Ansible Playbook 디버깅, 처음엔 저도 막막했습니다

    자동화 스크립트를 짜놓고 실행했는데 갑자기 빨간 글씨가 쭉 올라올 때… 그 순간의 당혹감, 혹시 공감되시나요? 저도 처음 Ansible을 도입했을 때 Playbook이 중간에 뻗어버리면 어디서부터 봐야 할지 몰라서 진짜 한참 헤맸거든요. 에러 메시지는 길고, 어디가 문제인지는 모르겠고, 설상가상으로 운영 서버에서 터지기라도 하면… 식은땀이 흘렀죠.

    13년 동안 인프라 엔지니어로 일하면서 Ansible을 본격적으로 쓴 게 벌써 7년이 넘었는데, 그 사이에 정말 별의별 Ansible 오류를 다 겪어봤습니다. 오늘은 그 삽질의 결정체를 모아서, Ansible Playbook 디버깅을 어떻게 체계적으로 접근하면 되는지 실전 경험 기반으로 정리해드릴게요. 처음 보시는 분도, 어느 정도 써보셨는데 아직도 에러 앞에서 막히시는 분도 모두 도움이 됐으면 합니다.

    Ansible Playbook 디버깅 전체 흐름도 — 오류 발생부터 해결까지 단계별 프로세스

    ▲ Ansible Playbook 디버깅의 전체 흐름 — 오류 발생부터 해결까지 단계별 접근 방법


    Ansible Playbook 디버깅이란? 기본 개념부터 잡고 가기

    쉽게 말해, Ansible Playbook 디버깅은 자동화 스크립트(Playbook)가 의도한 대로 동작하지 않을 때 원인을 찾고 수정하는 과정이에요. 일반적인 코드 디버깅이랑 비슷하긴 한데, Ansible만의 특성이 있어서 접근 방식이 좀 달라요.

    Ansible은 기본적으로 YAML(야믈, 사람이 읽기 쉬운 데이터 직렬화 형식) 기반의 선언형 자동화 도구거든요. 그래서 오류가 크게 세 가지 층위에서 발생합니다.

    • YAML 문법 오류: 들여쓰기 하나 잘못되면 바로 터집니다
    • Ansible 모듈 오류: 모듈(Module, Ansible의 기능 단위) 파라미터가 잘못되거나 버전이 맞지 않을 때
    • 원격 호스트 오류: 대상 서버에서 실제로 실행했을 때 발생하는 문제

    이 세 가지를 구분할 수 있어야 Ansible Playbook 디버깅이 빨라져요. 저도 처음엔 이게 다 뒤섞여 보여서 엄청 헤맸는데, 익숙해지면 에러 메시지만 봐도 어느 층위 문제인지 바로 감이 오더라고요.


    디버깅 전에 먼저 해야 할 것들: 사전 점검

    1. –syntax-check로 문법 먼저 확인

    실행하기 전에 문법 검사를 먼저 돌리는 게 기본 중의 기본입니다. 이거 안 하고 바로 돌리다가 원격 서버에서 절반쯤 실행되다 터지면 더 골치 아파요.

    # Playbook 문법 검사
    ansible-playbook site.yml --syntax-check
    
    # 인벤토리 파일 지정하는 경우
    ansible-playbook -i inventory/production site.yml --syntax-check

    문법 오류가 있으면 이렇게 나와요:

    ERROR! Syntax Error while loading YAML.
      found character that cannot start any token
    
    The error appears to be in '/home/user/playbooks/site.yml': line 15, column 3
    
      13: tasks:
      14:   - name: Install nginx
      15:    apt:   # <--- 여기 들여쓰기 문제
            ^ here

    라인 번호까지 알려주니까 찾기는 어렵지 않아요. 근데 YAML 들여쓰기는 진짜... 탭(Tab)이랑 스페이스(Space)를 섞으면 절대 안 됩니다. 이거 때문에 저도 초반에 얼마나 삽질했는지 ㅎㅎ

    2. --check 모드로 드라이런(Dry-run) 실행

    실제로 변경을 가하지 않고 "만약 실행하면 어떻게 될까"를 미리 보는 방법이에요. Check Mode(체크 모드)라고도 하고, Dry-run(드라이런, 실제 적용 없이 테스트 실행)이라고도 해요.

    # 드라이런 실행
    ansible-playbook site.yml --check
    
    # 드라이런 + 변경 예정 내용 상세 확인
    ansible-playbook site.yml --check --diff

    💡 팁: --diff 옵션을 같이 쓰면 파일이 어떻게 변경될지 diff 형식으로 보여줍니다. 설정 파일 배포 전에 꼭 써보세요. 진짜 유용해요.


    Ansible Playbook 디버깅 핵심 도구: 상세 출력과 debug 모듈

    Verbosity(상세 출력) 레벨 활용

    이게 제가 가장 자주 쓰는 방법이에요. -v 옵션을 붙이면 더 자세한 출력이 나오는데, 최대 4개까지 붙일 수 있습니다.

    # -v: 기본 상세 출력 (task 결과)
    ansible-playbook site.yml -v
    
    # -vv: 더 자세히 (파일/디렉토리 작업 포함)
    ansible-playbook site.yml -vv
    
    # -vvv: 연결 정보까지 (SSH 연결 디버깅에 유용)
    ansible-playbook site.yml -vvv
    
    # -vvvv: 가장 상세 (네트워크 패킷 수준)
    ansible-playbook site.yml -vvvv

    저는 보통 -vv나 -vvv를 주로 써요. -vvvv는 너무 많이 나와서 오히려 찾기 어려울 때도 있거든요.

    debug 모듈로 변수 값 확인하기

    Ansible 문제 해결에서 제일 많이 쓰는 게 바로 debug 모듈이에요. 변수 값이 의도한 대로 들어가 있는지 확인할 때 필수입니다.

    ---
    - name: 변수 디버깅 예제
      hosts: webservers
      vars:
        app_version: "1.2.3"
        deploy_path: "/opt/myapp"
    
      tasks:
        - name: 변수 값 확인
          debug:
            msg: "앱 버전: {{ app_version }}, 경로: {{ deploy_path }}"
    
        - name: 전체 변수 목록 확인 (hostvars)
          debug:
            var: hostvars[inventory_hostname]
    
        - name: 특정 변수만 확인
          debug:
            var: app_version
            verbosity: 2  # -vv 이상일 때만 출력

    verbosity 파라미터가 있는 게 포인트예요. 평소엔 안 보이다가 디버깅할 때만 출력되게 설정할 수 있거든요. 프로덕션 Playbook에 debug 태스크를 남겨둘 때 이렇게 해두면 깔끔합니다.

    register로 태스크 결과 캡처하기

    태스크(Task, Ansible의 개별 작업 단위) 실행 결과를 변수에 저장해서 다음 단계에서 활용하거나 디버깅할 수 있어요.

      tasks:
        - name: 서비스 상태 확인
          command: systemctl status nginx
          register: nginx_status
          ignore_errors: yes  # 오류가 나도 계속 진행
    
        - name: 결과 출력
          debug:
            var: nginx_status
    
        - name: 특정 필드만 확인
          debug:
            msg: |
              Return Code: {{ nginx_status.rc }}
              stdout: {{ nginx_status.stdout }}
              stderr: {{ nginx_status.stderr }}
    Ansible debug 모듈과 register를 활용한 Playbook 디버깅 설정 코드 예시

    ▲ debug 모듈과 register를 조합한 Ansible Playbook 디버깅 — 태스크 결과를 변수에 저장하고 단계별로 확인하는 방법


    Ansible 오류 유형별 해결 방법: 실전 트러블슈팅

    ⚠️ 오류 1: UNREACHABLE — 호스트에 접근 불가

    이거 처음 보면 당황하는 분들이 많은데, 대부분 SSH(시큐어 쉘, 원격 접속 프로토콜) 문제예요.

    TASK [Gathering Facts] *****
    fatal: [192.168.1.100]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh", "unreachable": true}

    체크리스트:

    1. SSH 키 등록 여부 확인: ssh -i ~/.ssh/id_rsa [email protected]
    2. 인벤토리(Inventory, 관리 대상 호스트 목록) 파일의 IP/호스트명 확인
    3. ansible_user, ansible_port 변수 확인
    4. 방화벽 규칙 확인
    # SSH 연결 직접 테스트
    ansible all -m ping -i inventory/hosts -vvv
    
    # 특정 호스트만 테스트
    ansible webserver01 -m ping -i inventory/hosts

    ⚠️ 오류 2: 변수 undefined — 변수를 찾을 수 없음

    Jinja2(진자2, Ansible의 템플릿 엔진) 템플릿에서 변수를 참조했는데 정의가 안 되어 있을 때 발생해요. 저도 이거 때문에 한참 헤맸습니다.

    fatal: [server01]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'db_password' is undefined"}

    해결 방법:

      tasks:
        # 방법 1: default 필터로 기본값 지정
        - name: DB 설정
          template:
            src: db.conf.j2
            dest: /etc/myapp/db.conf
          vars:
            db_password: "{{ db_password | default('changeme') }}"
    
        # 방법 2: vars_prompt로 실행 시 입력 받기
      vars_prompt:
        - name: db_password
          prompt: "DB 패스워드를 입력하세요"
          private: yes  # 입력 내용 숨김
    
        # 방법 3: ansible-vault로 암호화된 변수 파일 사용
        # ansible-playbook site.yml --ask-vault-pass

    ⚠️ 오류 3: 권한 오류 — Permission Denied

    원격 서버에서 sudo(슈퍼유저 권한 실행) 권한이 필요한 작업을 할 때 자주 만나는 Ansible 오류예요.

    ---
    - name: 웹서버 설정
      hosts: webservers
      become: yes          # sudo 권한으로 실행
      become_user: root    # root로 전환
    
      tasks:
        - name: nginx 설치
          apt:
            name: nginx
            state: present
    
        # 특정 태스크만 권한 상승
        - name: 로그 파일 수정
          file:
            path: /var/log/myapp
            mode: '0755'
          become: yes
          become_user: www-data
    # sudo 비밀번호 입력이 필요한 경우
    ansible-playbook site.yml --ask-become-pass

    ⚠️ 오류 4: 조건부 실행 문제 — when 절 오작동

    When(조건절) 설정이 잘못되면 실행되어야 할 태스크가 건너뛰어지거나, 반대로 실행되면 안 되는 게 실행되는 상황이 생겨요. 이거 진짜 찾기 어렵습니다.

      tasks:
        - name: OS 정보 수집
          setup:
            gather_subset:
              - distribution
    
        - name: 변수 타입 확인 (디버깅용)
          debug:
            msg: |
              OS Family: {{ ansible_os_family }}
              Distribution: {{ ansible_distribution }}
              Version: {{ ansible_distribution_version }}
    
        # 잘못된 예 — 문자열 비교 실수
        - name: Ubuntu에서만 실행 (잘못된 방법)
          apt:
            name: nginx
          when: ansible_distribution == 'ubuntu'  # 대소문자 주의!
    
        # 올바른 예
        - name: Ubuntu에서만 실행 (올바른 방법)
          apt:
            name: nginx
            state: present
          when: ansible_distribution == 'Ubuntu'  # 대문자 U
    
        # 복잡한 조건 — 가독성 높게 작성
        - name: 특정 환경에서만 실행
          command: /opt/deploy.sh
          when:
            - ansible_distribution == 'Ubuntu'
            - ansible_distribution_version is version('20.04', '>=')
            - env == 'production'

    ⚠️ 오류 5: 모듈 파라미터 오류

    Ansible 버전이 올라가면서 deprecated(더 이상 사용 안 하는) 파라미터가 생기거나, 파라미터명이 바뀌는 경우가 있어요. 이런 Ansible 오류는 메시지가 꽤 친절하게 나옵니다.

    [DEPRECATION WARNING]: The 'include' module is deprecated, use 'import_tasks' or 'include_tasks' instead.
    
    # 또는
    [WARNING]: Module did not set no_log for password
      tasks:
        # 구식 방법 (deprecated)
        - include: tasks/setup.yml
    
        # 새로운 방법
        - import_tasks: tasks/setup.yml   # 정적 포함 (컴파일 타임)
        - include_tasks: tasks/setup.yml  # 동적 포함 (런타임)

    고급 디버깅 기법: 실무에서 정말 유용한 것들

    특정 태스크만 실행하기 — tags 활용

    Playbook 전체를 돌리지 않고 문제가 있는 태스크만 콕 집어서 실행할 수 있어요. 태그(Tag) 기능인데, Ansible Playbook 디버깅할 때 진짜 유용합니다.

      tasks:
        - name: 패키지 설치
          apt:
            name: "{{ item }}"
            state: present
          loop:
            - nginx
            - git
            - curl
          tags:
            - packages
            - install
    
        - name: 설정 파일 배포
          template:
            src: nginx.conf.j2
            dest: /etc/nginx/nginx.conf
          tags:
            - config
            - nginx
    # 특정 태그만 실행
    ansible-playbook site.yml --tags "config"
    
    # 특정 태그 제외하고 실행
    ansible-playbook site.yml --skip-tags "packages"
    
    # 여러 태그 지정
    ansible-playbook site.yml --tags "config,nginx"

    특정 태스크부터 시작하기 — --start-at-task

    중간에 실패했을 때 처음부터 다시 돌리기 싫을 때 쓰는 방법이에요. 저 이거 알고 나서 삽질 시간이 확 줄었어요.

    # 특정 태스크명부터 실행
    ansible-playbook site.yml --start-at-task "설정 파일 배포"
    
    # step 모드: 태스크마다 실행 여부 물어봄
    ansible-playbook site.yml --step

    ansible-lint로 Playbook 품질 검사

    ansible-lint(앤서블 린트)는 Playbook의 코드 품질을 자동으로 검사해주는 도구예요. 문법 오류뿐 아니라 Best Practice(베스트 프랙티스, 모범 사례) 위반도 잡아줍니다.

    # 설치
    pip install ansible-lint
    
    # 검사 실행
    ansible-lint site.yml
    
    # 특정 규칙 무시하고 실행
    ansible-lint site.yml --exclude-path .ansible-lint
    # .ansible-lint 설정 파일
    skip_list:
      - yaml[line-length]  # 줄 길이 규칙 무시
      - name[casing]       # 이름 대소문자 규칙 무시
    
    warn_list:
      - experimental       # 실험적 규칙은 경고만

    Callback Plugin으로 출력 가독성 높이기

    기본 출력이 너무 지저분하다 싶으면 Callback Plugin(콜백 플러그인, 출력 형식 변환 도구)을 바꿔보세요.

    # ansible.cfg 설정
    [defaults]
    stdout_callback = yaml     # YAML 형식으로 출력 (가독성 좋음)
    # stdout_callback = dense  # 간결하게 출력
    # stdout_callback = debug  # 디버깅용 상세 출력
    
    callback_whitelist = profile_tasks  # 각 태스크 실행 시간 표시

    저는 yaml 콜백이랑 profile_tasks 조합을 제일 좋아해요. 어느 태스크가 오래 걸리는지 한눈에 보이거든요.

    Ansible yaml callback plugin 적용 전후 출력 화면 비교 — 가독성 향상으로 문제 해결 효율 증가

    ▲ yaml callback plugin 적용 전후 비교 — 출력 가독성이 크게 향상되어 Ansible 문제 해결이 훨씬 수월해집니다


    Ansible 오류 유형별 빠른 참조 표

    자주 만나는 오류들을 정리해봤습니다. 북마크해두고 쓰세요 ✅

    오류 유형 주요 증상 원인 해결 방법
    UNREACHABLE 호스트 연결 실패 SSH 설정 문제, 네트워크 오류 SSH 키 확인, 인벤토리 점검, -vvv로 연결 추적
    FAILED (문법) Playbook 시작 전 종료 YAML 들여쓰기, 문법 오류 --syntax-check, ansible-lint
    FAILED (변수) undefined variable 에러 변수 미정의, 오타 debug 모듈로 변수 확인, default 필터
    FAILED (권한) Permission denied sudo 설정 미흡 become/become_user 설정, sudoers 확인
    SKIPPED (예상치 못한) 태스크가 건너뜀 when 조건 오류 debug로 변수 값 확인, 조건식 재검토
    CHANGED (의도치 않은) 멱등성(Idempotency) 깨짐 모듈 사용 오류, command 모듈 남용 전용 모듈 사용, changed_when 설정
    MODULE ERROR 모듈 실행 실패 파라미터 오류, 버전 불일치 공식 문서 확인, -vv로 상세 확인

    멱등성(Idempotency) 확인: 자동화 스크립트의 핵심

    멱등성(Idempotency, 동일한 작업을 여러 번 실행해도 결과가 같아야 하는 성질)은 Ansible의 핵심 철학이에요. 이게 깨지면 Playbook을 두 번 돌렸을 때 뭔가 이상해지는 상황이 생기더라고요.

      tasks:
        # 나쁜 예: command 모듈은 멱등성이 없음
        - name: 디렉토리 생성 (나쁜 방법)
          command: mkdir /opt/myapp
    
        # 좋은 예: file 모듈은 멱등성 보장
        - name: 디렉토리 생성 (좋은 방법)
          file:
            path: /opt/myapp
            state: directory
            mode: '0755'
            owner: www-data
    
        # command/shell을 써야 할 때는 changed_when으로 제어
        - name: 애플리케이션 초기화 (한 번만 실행)
          command: /opt/myapp/init.sh
          args:
            creates: /opt/myapp/.initialized  # 이 파일 있으면 건너뜀
    
        # 또는 register + when 조합
        - name: 초기화 여부 확인
          stat:
            path: /opt/myapp/.initialized
          register: init_check
    
        - name: 초기화 실행
          command: /opt/myapp/init.sh
          when: not init_check.stat.exists

    자주 묻는 질문 (FAQ)

    Q. Ansible Playbook 실행 중에 특정 호스트만 실패했을 때 어떻게 하나요?

    A. --limit 옵션으로 특정 호스트만 재실행할 수 있어요. 실패한 호스트 목록은 .retry 파일에 자동 저장되기도 합니다.

    # 특정 호스트만 실행
    ansible-playbook site.yml --limit webserver01
    
    # retry 파일 활용
    ansible-playbook site.yml --limit @site.retry

    Q. 변수 우선순위가 헷갈려요. 어떻게 확인하나요?

    A. Ansible 변수 우선순위(Variable Precedence)는 복잡한데, ansible-config dump나 debug 모듈로 실제 적용된 값을 확인하는 게 제일 빠릅니다. 공식 문서에 22단계 우선순위가 나와 있는데... 저도 다 외우진 못해요 ㅎㅎ

    Q. ansible-playbook 실행이 너무 느린데 디버깅 말고 속도 개선 방법은요?

    A. 이건 별도로 다룰 주제인데, Forks(병렬 실행 수) 조정, Fact Caching(팩트 캐싱), Pipelining(파이프라이닝) 활성화가 주요 방법이에요. 다음 글에서 Ansible 성능 최적화를 다룰 예정입니다.


    마무리: 디버깅도 실력이다

    Ansible Playbook 디버깅 핵심 방법 5가지 요약 인포그래픽 — syntax-check부터 ansible-lint까지

    ▲ Ansible Playbook 디버깅 핵심 방법 요약 — syntax-check부터 debug 모듈, 태그 활용까지 단계별 접근법

    오늘 다룬 내용을 간단히 정리하면요:

    • ✅ 실행 전: --syntax-check로 문법 검사, --check --diff로 드라이런
    • ✅ 실행 중: -v ~ -vvv 상세 출력, debug 모듈로 변수 확인
    • ✅ 범위 좁히기: --tags, --limit, --start-at-task 활용
    • ✅ 코드 품질: ansible-lint로 사전 검사, 멱등성 확인
    • ✅ 가독성: yaml callback plugin + profile_tasks로 출력 개선

    솔직히 말씀드리면, 처음에 Ansible 오류를 만났을 때 저도 막막했어요. 에러 메시지가 길고 복잡해 보이는데, 결국 패턴이 있더라고요. 오늘 소개한 접근 방법들을 익히면 웬만한 Ansible 문제 해결은 훨씬 수월해질 거예요.

    🎉 핵심은 체계적인 접근이에요. 무턱대고 코드 고치지 말고, 문법 → 연결 → 변수 → 모듈 순서로 하나씩 좁혀가는 거죠. 그리고 debug 모듈은 정말 친한 친구처럼 자주 써보세요. 저도 매일 씁니다.

    다음 글에서는 Ansible Vault(앤서블 볼트)를 활용한 시크릿(Secret, 민감한 정보) 관리 방법을 다뤄볼 예정이에요. 패스워드나 API 키 같은 민감한 정보를 Playbook에 안전하게 넣는 방법인데, 이것도 실무에서 꼭 알아야 할 내용이라 기대해주세요.

    궁금한 점이나 여러분이 겪었던 특이한 Ansible 오류 경험이 있으시면 댓글로 남겨주세요. 같이 해결해봐요! 😊