LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


프로비저닝 자동화를 위한 Ansible AWX, 설치부터 엔터프라이즈 환경 적용까지 - 2

1부에 이어서

이번 글에서는 고급 사용자를 위한 내용을 소개하려고 합니다. 1부에서 기본 설치를 해보셨나요? 크게 어렵지 않게 설치할 수 있었을 거라고 생각합니다. 문제는 기업 환경에서 실제 운영하기 위해서는 장애 발생에 대한 대비와 대량의 작업을 수행하기 위한 클러스터링 환경 등을 제공해야 하는데 아시다시피 이러한 부분에 대해서는 정보가 많지 않습니다.

개인적으로 이해할 수 있는 부분은, Ansible Tower라는 상용 제품이 있는데 이 제품에 탑재된 모든 기능을 제공한다면 아무리 오픈소스라고 하더라도 상충되는 부분이 있지 않을까 생각합니다. 그렇다고 기능을 일부러 빼버리거나 사용할 수 없도록 막아놓은 것은 아니기 때문에 우리가 삽질(?)을 조금 더 하면 대부분의 기능을 사용할 수 있습니다. 본문에서 LINE의 AWX 운영 환경과 수정한 내용 중 일부를 소개하겠습니다.

좀 더 씹어보기

AWX의 고가용성(High Availability - HA) 구조

  • AWX_WEB: NGINX가 동작합니다. 사용자 UI를 제공합니다. 
  • AWX_TASK: Ansible을 fork합니다. 실제 작업을 수행합니다. 
  • AWX_MQ: RabbitMQ가 동작합니다. 생성되는 모든 작업은 일단 MQ로 투입된 뒤 AWX_TASK에서 수행합니다. 클러스터 구조를 지원하기 위해 필요합니다. 
  • Cache: Memcached가 동작합니다. 내부적인 캐쉬입니다.
  • PGPOOL: PostgreSQL의 장애 처리를 담당하는 별도의 소프트웨어입니다.  마스터 노드DB에 장애가 발생했을 때 슬레이브DB로 페일 오버(fail over)를 지원하기 위해 구성하였습니다. 
  • DB: PostgreSQL이 동작합니다. AWX에서 작업한 모든 내용은 이곳에 저장되고, 기본 설치 시 Docker 컨테이너로 구동되며, 별도 DB를 구축하여 연결하는 것을 허용합니다.

아래는 구조도입니다(Seoul, Busan은 이해를 돕기 위해 설정한 임의의 지역입니다).

AWX 고가용성 구성을 위한 서비스들

  • WEB LB: Verda L7 LB(load balance)
    • LINE에서는 자체적인 IaaS(Infrastructure as a Service) 환경인 Verda라는 서비스가 있습니다. 이 서비스를 통하여 HTTP와 HTTPS에 대한 L7 부하 분산을 수행합니다. 
    • AWX를 설치하는 서버와 DB 서버도 모두 Verda의 VM(Virtual Machine)을 사용했습니다.
  • TASK + MQ(MQ 클러스터)
    • AWX 기본 설치는 MQ의 클러스터를 구성해 주지 않습니다. 수동으로 MQ에 노드를 추가하거나 설치 Playbook에 내용을 추가할 수 있습니다.
  • PostgreSQL
    • PostgreSQL(이하 pgsql)은 자체적인 복제 기능을 제공하는데 이를 'Stream Replication'이라고 말합니다. 이 기능은 마스터 DB의 수정 사항이 슬레이브(slave) DB에 동일하게 저장되는 것을 보장하기 위해 사용됩니다.
  • Pgpool-II 
    • Pgpool은 SQL 처리 요청을 받아서 부하를 분산(쓰기는 마스터 DB로 분기, 읽기는 round-robin 방식으로 마스터, 슬레이브 분산)하고 마스터 DB의 장애 확인 및 장애 확인 시 복구를 위한 스크립트 실행을 담당합니다. 가상 IP를 사용하여 IP를 이동시켜 페일 오버하는 기능도 제공되지만, 앞서 LINE의 Verda나 Amazon AWS 같은 클라우드 환경에서 임의로 IP를 할당하거나 이동시키는 게 쉽지 않기 때문에 해당 기능은 사용하지 않았습니다. 
  • AWX-HA 설치하기
    • AWX 기본 설치는 고가용성 구성을 지원하지 않습니다.
    • AWX 설치는 Ansible로 수행되기 때문에 필요한 작업을 추가할 수 있고, Git에 이미 AWX 버전 별로 HA 설치가 가능한 palybook을 작성하여 공개하고 있습니다(AWX 최신 버전은 8.0.0이지만 이 글을 작성 중인 19년 10월 현재 6.1.0까지 제공되고 있습니다).
      • Git 프로젝트: https://github.com/sujiar37/AWX-HA-InstanceGroup
      • 개인이 작성 후 공개한 코드입니다(집이 가까우면 밥이라도 사드리고 싶은데 말입니다 :)).
      • 라이선스: MIT 라이선스입니다(참고).
      • 개인적인 소견으로 AWX 프로젝트에서는 별도의 고가용성 구성을 위한 코드를 제공할 의사가 없는 것으로 보입니다. 
    • 제공되는 playbook에서 일부 필요한 내용을 수정하여 사용하고 있습니다. 

코드 작성 및 수정 내용(원본 프로젝트: AWX-HA-InstanceGroup

  1. SSH public key 배포 playbook 작성 
    이 내용은 AWX 설치와는 관계없이 추가한 내용입니다. Ansible을 처음 사용하면 public key를 대상 서버에 넣는 작업부터 난제가 됩니다. 다행히 LINE은 내부적으로 Kerberos 인증을 수행하기 때문에 'keytab'이라는 인증 토큰을 생성해서 대상 서버에 접속할 수 있습니다. 아래 코드는 Kerberos 환경에서 AWX를 사용하여 사용자 ID와 PASSWORD를 입력받아서 대상 서버에 SSH public key를 배포하는 playbook 콘셉트를 설명합니다.
    dist_ssh_pubkey.yml
    # SSH Public Key를 대상 서버에 추가합니다.
    # AWX가 설치된 서버에서 실행되어 각 서버에 Key를 추가하고, 대상 서버에서 실제 작업이 실행되지는 않습니다.
    - hosts: awx-server-host 
     
      vars:
        keytab_dir_path: /home/awx/krbkeytab
    #  SSH public key는 공개를 전제로 합니다. 해당 파일을 Ansible role에 포함시켰습니다. 해당 파일의 내부 내용을 변수로 읽어냅니다.
        ssh_pub_awx_dev_key: "{{ lookup('file', './files/ssh/awx_dev_public.key') }}"
     
      tasks:
    # create directory if deleted (or if it didn't exist at all)
        - name: create keytab directory
          file:
            state: directory
            path: "{{ keytab_dir_path }}"
          ignore_errors: True
     
    # 아래 KRB_ID와 KRB_PWD는 AWX의 Survey 기능을 사용하여 실행 시 개인 사용자의 계정 정보를 입력받습니다.
    # 계정 정보를 저장했을 때 발생할 수 있는 보안 이슈를 해결합니다.
        - name: Run expect to wait for a successful gen keytab
          shell: |
            set timeout 10
            spawn /usr/nhnkrb5/sbin/ktutil
     
            expect "ktutil: "
            send "addent -password -p {{KRB_ID}} -k 1 -e des3-cbc-sha1
    "
     
            expect "Password for {{KRB_ID}}@CORP.COM: "
            send "{{KRB_PWD}}
    "
     
            expect "ktutil: "
            send "wkt {{ keytab_dir_path }}/krb5auth_{{KRB_ID}}
    "
     
            expect "ktutil: "
            send "quit
    "
     
            exit 0
          args:
            executable: /usr/bin/expect
          environment:
            PATH: /usr/nhnkrb5/sbin:{{ Ansible_env.PATH }}
     
        # 사용자가 패스워드를 잘못 입력하면 생성된 파일이 계속 남아서 문제를 일으킵니다.
        - name: make kerboros ticket use keytab file
          command: kinit -V -f {{KRB_ID}}@CORP.COM -k -t {{ keytab_dir_path }}/krb5auth_{{KRB_ID}}
          register: command_result
          ignore_errors: true
     
        # 사용자가 계정 정보를 잘못 입력해서 인증 결과가 오류라면 잘못 생성된 keytab 파일을 삭제합니다.
        - name: delete keytab file
          file:
            state: absent
            path: "{{ keytab_dir_path }}/krb5auth_{{KRB_ID}}"
          when: "command_result.rc != 0"
       # Dry run 실행의 경우에  command module을 지원하지 않기 때문에 에러가 발생하지만 무시합니다.
          ignore_errors: "{{ Ansible_check_mode }}"
     
        - name: delete kerboros ticket
          command: kdestroy
          when: "command_result.rc != 0"
          ignore_errors: "{{ Ansible_check_mode }}"
     
        - fail:
            msg: 'make kerboros ticket failed, Check kerboros ID/PASSWD, UID is Not employ number'
          when: "command_result.rc != 0"
          ignore_errors: "{{ Ansible_check_mode }}"
     
          #기존 파일 백업
        - name: authorized_keys2 Backup - rsh command loop  (distribute ssh public key and key Deduplication)
          command: rsh {{item}} "cp ~/.ssh/authorized_keys2 ~/.ssh/authorized_keys2.backup"
          with_inventory_hostnames:
            - "{{ __GROUP_NAME }}"
     
          #이상 없으면 SSH key를 대상 서버에 추가합니다.
          #Ansible authorized_key module은 SSH key가 없으면 사용 불가능 -> uniq 명령으로 key 중복 정리, 여러 번 실행 시 키가 계속 추가되는 것을 방지합니다.
        - name: distribut ssh public key and key Deduplication clear
          command: rsh {{item}} "echo {{ ssh_pub_awx_dev_key }} >> ~/.ssh/authorized_keys2 ; cat ~/.ssh/authorized_keys2 | uniq > ~/.ssh/authorized_keys2.uniq ; mv ~/.ssh/authorized_keys2{.uniq,} ; chmod 0600 ~/.ssh/authorized_keys2"
    # __GROUP_NAME 은 AWX에서 수행 시 ssh public key를 설치할 대상 서버의 목록을 정의한 Inventory Group 이름입니다.
          with_inventory_hostnames:
            - "{{ __GROUP_NAME }}"
     
          #정상적으로 끝났으면 keytab 파일을 삭제합니다.
        - name: delete keytab file
          file:
            state: absent
            path: "{{ keytab_dir_path }}/krb5auth_{{KRB_ID}}"
  2. SSL 인증서 설치 추가
    AWX 설치 시에 HTTPS 접속을 위한 SSL 인증서를 배포해 주지 않습니다. 당연히 제가 가진 인증서를 배포해야겠지요. 여러 서버에 인증서 파일을 넣는 작업이 번거로워서 설치 스크립트에 추가했습니다. AWX-HA-InstanceGroup 프로젝트를 로컬에 복제하여 작업했고, 회사 Git에서 관리하고 있습니다(AWX 4.0 버전 기준입니다).
    1. main.yml 파일(참고)을 수정합니다.
      main.yml
      # SSL 설치 yml을 호출합니다.
      - import_tasks: install_ssl.yml
        when: task == "all" or task == "setup_env"
       
      - import_tasks: config_dockerlog.yml
        when: task == "all" or task == "setup_env"
       
      - import_tasks: setup_env.yml
        when: task == "all" or task == "setup_env"
       
      - import_tasks: build_env.yml
        when: task == "all" or task == "build_env"
       
      - import_tasks: run_env.yml
        when: task == "all" or task == "run_env"
    2. AWX-HA-InstanceGroup/roles/awx_ha_v4/tasks/ 폴더(참고)에 파일을 추가합니다. 
      install_ssl.yml
      # 서버에 폴더를 만듭니다.
      - name: Create ssl certfile store directory
        file:
          path: "{{ ssl_certificate }}"
          state: directory
       
      # awxweb.pem 파일은 git 프로젝트 내 지정한 위치에 있어야 합니다.
      - name: Copy SSL Cert file
        copy:
          src: "{{ role_path }}/files/awxweb.pem"
          dest: "{{ ssl_certificate }}"
  3. HA 설치 시 MQ 구성
    AWX-HA-InstanceGroup 프로젝트에서 WEB과 TASK 컨테이너 설치 시 WEB 컨테이너가 설치되는 서버의 MQ는 클러스터에 포함되지 않도록 되어 있습니다. 웹 서비스를 하는 서버는 제외하도록 한 것 같습니다. 하지만 저희는 WEB만 따로 구성하지 않고 모든 AWX 노드에 WEB, TASK를 동시에 구성하므로 모두 포함시켜야 합니다. 별 내용이 아닌데 이것 때문에 MQ에 투입된 작업이 종종 수행하는 데 실패하는 문제가 발생해서 오랜 시간 고생했습니다.
    1. join_rmq_cluster.yml 파일(참고)에서 조건을 변경하여 모든 MQ를 클러스터에 포함하도록 변경합니다. When 절을 주석 처리합니다.
      - name: Set Primary RabbitMQ hostname
        set_fact:
          rabbitmq_primary_host: "{{ groups['awx_instance_group_web'][0] }}"
          rabbitmq_primary_host_dns: "{{ hostvars[groups['awx_instance_group_web'][0]].Ansible_hostname }}"
       
      - name: Check whether the agent nodes are clustered already
        command: /sbin/rabbitmqctl cluster_status
        register: check_rmq_cluster
      #  when: inventory_hostname not in groups['awx_instance_group_web']
        check_mode: no
       
      - name: Stop RabbitMQ app in the agent nodes
        command: /sbin/rabbitmqctl stop_app
        when:
          - check_rmq_cluster.stdout is not search(rabbitmq_primary_host_dns)
      #    - inventory_hostname not in groups['awx_instance_group_web']
      - name: Join agent nodes under RabbitMQ cluster
        command: /sbin/rabbitmqctl join_cluster rabbit@{{ rabbitmq_primary_host_dns }}
        when:
          - check_rmq_cluster.stdout is not search(rabbitmq_primary_host_dns)
      #    - inventory_hostname not in groups['awx_instance_group_web']
        register: cluster_status
       
      - name: Start RabbitMQ app in the agent nodes
        command: /sbin/rabbitmqctl start_app
        when:
          - check_rmq_cluster.stdout is not search(rabbitmq_primary_host_dns)
          - cluster_status is changed
      #    - inventory_hostname not in groups['awx_instance_group_web']
  4. DB 고가용성을 위한 지역별 변수
    LINE은 IDC에서 장애 발생 시의 DR(Disaster Recovery)을 위해 서로 다른 지역에서 IDC를 각각 운영 중입니다. AWX 환경을 구축할 때 LINE의 다른 서비스들의 DR 작업을 자동화할 수 있게, IDC 장애 발생 시에도 지속적으로 운영될 수 있도록 설계했습니다. 각 지역의 AWX 노드(AWX 작업을 수행하는 TASK 서버)는 각자 자신이 위치한 지역의 DB에 접속하도록 구성해야 하는데 두 개 지역을 하나의 설정으로 같이 설치하면 요건을 충족할 수 없었습니다. 그래서 설치 시 대상 서버가 어떤 지역의 서버인지 명시하고 해당 설정에 따라 DB에 접속하도록 수정했습니다. Ansible의 템플릿, Jinja2 사용 방법과 조건문 적용을 이해하는 데 참고할 수 있습니다.
    1. 대상 서버 목록 작성 시 지역 정보를 추가합니다(호스트 명과 seoulbusan은 임의의 지역입니다 오해하지 말아주세요 :) ).
      hosts(원본)
      [all]
       
      [awx_instance_group_web]
      awx_region1_001 region=seoul
      awx_region1_002 region=seoul
      awx_region1_003 region=seoul
      awx_region2_001 region=busan
      awx_region2_002 region=busan
      awx_region2_003 region=busan
       
       
      [awx_instance_group_task]
      awx_region1_001 region=seoul
      awx_region1_002 region=seoul
      awx_region1_003 region=seoul
      awx_region2_001 region=busan
      awx_region2_002 region=busan
      awx_region2_003 region=busan
    2. 설치 시 환경 변수에 원래는 1개의 DB 정보만 들어 있습니다. 우리는 2개 지역의 DB를 구분해야 하므로 별도의 변수(pg_db_host_master/pg_db_host_slave)를 추가합니다. 아래에 추가된 다른 변수들은 앞서 설명한 HTTPS 접속을 위한 정보와 파일의 저장 위치를 임의의 위치로 변경한 설정입니다.
      all.yml(원본)
      ---
      ### AWX Default Settings
      awx_unique_secret_key: xxxx
      awx_admin_default_pass: xxxx
       
      ### Postgre DB details
      #Multi db
      pg_db_host_master: "database-region1"
      pg_db_host_slave: "database-region2"
      #pgpool port
      pg_db_port: "xxxxx"
      #Single db
      #pg_db_port: "xxxxx"
       
      pg_db_pass: "xxxxx"
      pg_db_user: "xxxxx"
      pg_db_name: "xxxxx"
       
      ###  RabbitMQ default settings
      rabbitmq_cookie: "xxxx"
      rabbitmq_username: "xxxx"
      rabbitmq_password: "xxxx"
       
      ## ADD SSL Certinfo
      host_port_ssl: 443
      ssl_certificate: /work/ssl_cert
       
      docker_compose_dir: /work/awxcompose
      project_data_dir: /work/awx/projects
    3. 다음 두 개의 파일에서 지역에 따라 분기 처리를 추가합니다. 원본 playbook에서 WEB 컨테이너와 TASK 컨테이너를 구분하여 구성하기 때문에 양쪽 설정에 모두 조건을 추가해야 합니다.
      1. docker-compose.yml.agent.j2(원본)
        #jinja2: trim_blocks: True, lstrip_blocks: True
        version: '2'
        services:
         
          task:
            image: awx_task_ha:{{ awx_task_tag }}
        ...
        ...
              DATABASE_NAME: {{ pg_db_name }}
              DATABASE_USER: {{ pg_db_user }}
              DATABASE_PASSWORD: {{ pg_db_pass }}
        {% if 'seoul' ==  region  %}
              DATABASE_HOST: {{ pg_db_host_master }}
        {% elif 'busan' ==  region %}
              DATABASE_HOST: {{ pg_db_host_slave }}
        {% else %}
              DATABASE_HOST: {{ pg_db_host_master }}
        {% endif %}
              DATABASE_PORT: {{ pg_db_port }}
        ...
        ...
      2. docker-compose.yml.j2(원본)
        #jinja2: trim_blocks: True, lstrip_blocks: True
        version: '2'
        services:
         
          web:
            image: awx_web_ha:{{ awx_web_tag }}
        ...
        ...
              DATABASE_NAME: {{ pg_db_name }}
              DATABASE_USER: {{ pg_db_user }}
              DATABASE_PASSWORD: {{ pg_db_pass }}
              DATABASE_PORT: {{ pg_db_port }}
        {% if 'seoul' ==  region  %}
              DATABASE_HOST: {{ pg_db_host_master }}
        {% elif 'busan' ==  region %}
              DATABASE_HOST: {{ pg_db_host_slave }}
        {% else %}
              DATABASE_HOST: {{ pg_db_host_master }}
        {% endif %}
        ...
        ...

즐기기

2018년 11월 1.0.7로 시작한 AWX는 2019년 11월 현재 4.0.0. 버전으로 운영 중입니다. 초기 팀 내부 업무와 다수의 서버에 설정을 반영하기 위한 작업의 생산성을 높이기 위해 시작하여 지금은 약 5천 대의 서버가 등록되어 60여 명의 사용자가 웹 UI로 Ansible을 사용하고 있습니다.

앞서 말씀드렸듯, AWX는 Ansible을 운영할 수 있는 환경을 제공할 뿐이므로 AWX를 설치하는 것은 단지 도구를 준비하는 수준, 그 이상도 이하도 아닙니다. LINE의 AWX 사용자들은 이미 CLI 환경에서 Ansible을 사용하고 있었으며, 사내 Git에서 playbook을 잘 관리하고 있었습니다. 사내 Git을 검색해 본 결과 약 200개의 Ansible 프로젝트가 등록되어 있어 아직 AWX 사용 환경이 크게 확대된 것은 아닌 것으로 보입니다. 다만 기대하는 바는, 앞으로 클라우드 환경이 IaaS에서 PaaS로 고도화할수록 자동화의 필요성은 더욱 커질 것이고, 그때마다 셸 스크립트로 작업을 자동화하는 것에는 한계가 있을 거라고 생각한다는 점입니다. 또한 고 수준의 언어로 자동화를 수행하기에는 OPS 업무를 담당하는 엔지니어들의 학습 장벽이 너무 높을 수도 있습니다. 따라서 학습 비용이 낮고 다양한 환경을 제어할 수 있는 모듈을 제공하고 있는 Ansible이 괜찮은 대안이 되지 않을까 생각합니다.

  • Red Hat은 PaaS 솔루션인 OpenShift의 내부 제어를 모두 Ansible로 구현했고, 향후 RHEL 8(Red Hat Enterprise Linux)에서 추가되는 자동화 기능인 Cockpit 역시 Ansible을 통한 제어를 제공합니다. 
  • Amazon AWS에서는 Ansible playbook 실행을 지원하는 AWS Systems Manager를 2019년 9월부터 제공합니다(참고).
  • 기업 네트워크 장비 회사인 CISCO에서도 자사 장비 제어를 Ansible로 할 수 있도록 모듈을 제공하고 있고, 교육과 자격증 과정도 운영하고 있습니다(참고).

이와 같이 여러 IT 기업에서 Ansible에 대한 지원을 점차 확대하고 있어서 향후에는 상당한 수준의 통합된 형태로 제공되지 않을까 생각합니다. 기업의 필요와 시장의 공급이 잘 맞아떨어지는 경우가 그리 흔하지 않기 때문에 엔지니어라면 한 번쯤 사용해 볼 만하다고 생각하며, 회사 내에 관리 작업을 위한 셸 스크립트가 수십여 개가 넘고 하나의 셸이 여러 버전으로 관리되는 등의 문제가 있거나, 하나의 작업으로 웹 서버 네트워크 장비 등을 통합해서 자동화해야 하는 필요 등이 있는 경우 Ansible을 검토해 보시는 걸 추천합니다.

또한 Ansible을 사용하고 Git에 잘 관리할 수 있는 환경까지 준비되었다면, 반복 작업을 사람의 손으로 수행하기보다는 정형화된 작업(AWX의 UI)으로 장애를 줄이고 민감한 작업에 대한 권한을 제어하는 등의 목적으로 AWX나 Ansible Tower를 검토해 볼 만하다고 생각합니다. 물론 AWX는 오픈소스이므로 회사 내에서 자체적인 구성과 운영을 위한 준비를 해야 하겠고, 지원이 필요하다면 Red Hat의 지원을 받아 Ansible Tower를 도입하는 방법도 있겠습니다(Red Hat 광고 아닙니다. 오해는 하지 말아주세요 :) )

길고 난해한 글 읽느라 수고 많으셨습니다. 아직 Ansible을 접하지 않았다면 더욱 읽기 어렵지 않았을까 생각합니다. 쉽게 쓰려고 노력은 했으나... 필자의 내공이 부족했던 점 두루 양해를 바라며, 어디선가 인터넷의 바다에서 삽질을 하고 있을 엔지니어에게 작은 도움이 되기를 기대해 봅니다.