[OpenStack Project] VMware 없이 노트북 서버로 kolla-ansible OpenStack 설치 [2]

KH55S·2025년 12월 15일

OpenStack Project

목록 보기
2/8

서론

  • Ansible을 이용한 리소스 프로비저닝
  • kolla-ansible은 오픈스택이라는 거대한 인프라를 Ansible Playbook 형태로 코드로 정의하여 배포하는 도구이다.
  • 오픈스택 구성 요소가 어떻개 배포되는지 이해하기 위해서는 Ansible 문법과 동작 원리를 알아야 한다.
  • 리소스를 생성하는 Playbook을 작성해보는 것은 Ansible의 Module, Task, Role 구조를 익히는 가장 빠른 방법이다.

  • 플랫폼 엔지니어의 주 업무는 클라우드 인프라 구축 및 유지보수이다.
  • 구축 후 기능이 정상 동작하는지 확인해야 한다. 매번 대시보드에서 마우스로 클릭하여 VM을 만드는 것은 비효율적이다.
  • 내가 구축한 오픈스택이 정상인지 확인하기 위해 VM 생성부터 삭제까지의 테스트 시나리오를 코드로 작성한다.

환경 구성

  • Ansible로 오픈스택을 제어하려면, Ansible이 오픈스택 API와 통신할 수 있도록 라이브러리(SDK)와 컬렉션을 설치해야 한다.
    • openstacksdk는 이미 설치되어 있으니, Ansible OpenStack Collection만 설치

  • Ansible OpenStack Collection 설치 : Ansible 2.9 이후부터는 모듈들이 'Collection' 단위로 분리되었다. 오픈스택 관련 모듈(os_server, os_network 등)을 사용하기 위해 설치한다.
ansible-galaxy collection install openstack.cloud

  • 인증 정보 설정 (clouds.yaml)

    • Ansible이 오픈스택에 접속하려면 인증 정보가 필요하다. 환경변수(source admin-openrc.sh) 보다 clouds.yml 파일을 사용하는 것이 표준이며 관리가 용이하다.
    • 위치 : ~/.config/openstack/clouds.yaml
    • /etc/kolla/admin-openrc.sh 파일의 내용을 참고해서 작성
    # clouds.yaml
    
    clouds:
    my-openstack:
      auth:
        auth_url: "http://<admin-openrc.sh의 OS_AUTH_URL>/v3"
        username: "admin"
        password: "<비밀번호>"
        project_name: "admin"
        project_domain_name: "Default"
        user_domain_name: "Default"
      region_name: "RegionOne"
      interface: "public"
      identity_api_version: 3
    • auth_ur에서 끝에 /v3를 붙여야 하는 이유
      • Keystone API 엔드포인트 : 오픈스택의 인증 서비스인 Keystone은 http://IP:5000/v3라는 특정 경로에서 인증 토큰 발행 요청을 받는다.

  • Ansible Playbook 작성 (provision_vm.yml)
    • 프로젝트 생성 > 네트워크 생성 > 라우터 생성 > 보안 그룹 설정 > 키페어 생성 > VM 생성의 과정을 수행하는 Playbook 코드
  # 해당 코드는 에러가 발생함. 이유는 아래에서 설명
  ---
  - name: OpenStack Resource Provisioning
    hosts: localhost
    connection: local
    gather_facts: false
    vars:
      cloud_name: "my-openstack"  # clouds.yaml에 정의한 이름
      vm_name: "ansible-test-vm"
      image_name: "cirros"
      flavor_name: "m1.tiny"
      key_name: "my-key"
      net_name: "private_net_ansible"
      subnet_name: "private_subnet_ansible"
      router_name: "router_ansible"
      ext_net_name: "ext_net"  # 대시보드를 통해 만든 외부 네트워크 이름
      cidr: "10.0.1.0/24"
      dns_servers: ["8.8.8.8"]

    tasks:
      - name: 1. Create Private Network (VXLAN)
        openstack.cloud.network:
          cloud: "{{ cloud_name }}"
          name: "{{ net_name }}"
          state: present
          external: false

      - name: 2. Create Subnet
        openstack.cloud.subnet:
          cloud: "{{ cloud_name }}"
          name: "{{ subnet_name }}"
          network_name: "{{ net_name }}"
          cidr: "{{ cidr }}"
          dns_nameservers: "{{ dns_servers }}"
          state: present

      - name: 3. Create Router and Connect to External Network
        openstack.cloud.router:
          cloud: "{{ cloud_name }}"
          name: "{{ router_name }}"
          state: present
          network: "{{ ext_net_name }}"  # Gateway 설정
          interfaces:
            - "{{ subnet_name }}"

      - name: 4. Create Keypair
        openstack.cloud.keypair:
          cloud: "{{ cloud_name }}"
          name: "{{ key_name }}"
          state: present
          public_key_file: "<본인 SSH 키 경로>"

      - name: 5. Allow ICMP (Ping) in Security Group
        openstack.cloud.security_group_rule:
          cloud: "{{ cloud_name }}"
          security_group: default
          protocol: icmp
          remote_ip_prefix: 0.0.0.0/0
          state: present

      - name: 6. Allow SSH in Security Group
        openstack.cloud.security_group_rule:
          cloud: "{{ cloud_name }}"
          security_group: default
          protocol: tcp
          port_range_min: 22
          port_range_max: 22
          remote_ip_prefix: 0.0.0.0/0
          state: present

      - name: 7. Create VM Instance
        openstack.cloud.server:
          cloud: "{{ cloud_name }}"
          name: "{{ vm_name }}"
          state: present
          image: "{{ image_name }}"
          flavor: "{{ flavor_name }}"
          key_name: "{{ key_name }}"
          network: "{{ net_name }}"
          wait: yes
          auto_ip: yes  # Floating IP 자동 할당 옵션
        register: server_info

      - name: 8. Print VM IP Address
        debug:
          msg: "VM Created! IP: {{ server_info.server.accessIPv4 }}"
  • public_key_file에서 SSH 키 경로 확인 방법

     # .pub로 끝나는 파일이 있다면 그 파일의 경로가 SSH 키 경로
     $ ls -al ~/.ssh/*.pub
     # 파일이 없다면 다음의 명령어로 생성
     $ ssh-keygen -t rsa -b 4096
    • 원인 : 오픈스택에서는 프로젝트가 생성될 때마다 자동으로 default라는 이름의 Security Group이 하나씩 생긴다. Ansible 모듈(openstack.cloud.security_group_rule)이 이름이 default인 그룹을 찾아달라고 API에 요청했는데, 현재 사용 중인 admin 계정에서는 다른 프로젝트들에도 default 그룹이 있어서 에러가 발생했다.
    • 해결 : default 그룹을 건드리지 않고, 전용 보안그룹을 새로 생성해서 사용한다. ( provision_vm.yml 파일 수정 )
vars:
    sec_group_name: "ansible-secgroup"  # 새로 만들 보안 그룹 이름

  tasks:

    # 보안 그룹 생성
    - name: 5. Create Security Group
      openstack.cloud.security_group:
        cloud: "{{ cloud_name }}"
        name: "{{ sec_group_name }}"
        description: "Security group for Ansible test"
        state: present

    # 새로 만든 그룹에 ICMP/SSH 규칙 추가
    - name: 6. Allow ICMP and SSH
      openstack.cloud.security_group_rule:
        cloud: "{{ cloud_name }}"
        security_group: "{{ sec_group_name }}" # default가 아니라 위에서 만든 그룹 지정
        protocol: "{{ item.protocol }}"
        port_range_min: "{{ item.port | default(omit) }}"
        port_range_max: "{{ item.port | default(omit) }}"
        remote_ip_prefix: 0.0.0.0/0
        state: present
      loop:
        - { protocol: icmp }
        - { protocol: tcp, port: 22 }

    # 보안 그룹 연결 부분 수정
    - name: 7. Create VM Instance
      openstack.cloud.server:
        cloud: "{{ cloud_name }}"
        name: "{{ vm_name }}"
        state: present
        image: "{{ image_name }}"
        flavor: "{{ flavor_name }}"
        key_name: "{{ key_name }}"
        network: "{{ net_name }}"
        security_groups:
          - "{{ sec_group_name }}"
        wait: yes
        auto_ip: yes
      register: server_info
    • 원인 : 오픈스택이 반환한 Dictionary 데이터 구조 안에 accessIPv4라는 키가 없기 때문에 발생.
    • 해결 : 변수 구조 직접 확인
    - name: 8. Print VM IP Address
      debug:
        var: server_info
  • server_info.server.addresses.private_net_ansible[0].addr


  • 실행
$ ansible-playbook provision_vm.yml

  • 리소스 삭제 (delete_resources.yml)
    • 의존성 문제를 피하기 위해 삭제는 생성의 역순으로 진행한다
    • 생성 순서 : 네트워크 → 서브넷 → 라우터 → VM
    • 삭제 순서 : VM → 라우터(인터페이스 분리 선행) → 서브넷 → 네트워크
  • 변수들만 모아놓은 YAML 파일을 따로 만들고 Playbook이 이 파일을 참조하도록 만든다.
    • 이점
      • DRY (Don't Repeat Yourself) 원칙 적용 : 생성 코드와 삭제 코드 간의 변수 불일치로 인한 휴먼 에러를 방지하기 위해 vars_files를 사용하여 설정값을 중앙에서 관리하도록 개선
      • 유지보수성 : 나중에 VM 이름을 바꾸고 싶을 때, common_vars.yml 하나만 수정하면 생성과 삭제 로직 모두에 적용 가능
# common_vars.yml

cloud_name: "my-openstack"
vm_name: "ansible-test-vm"
key_name: "my-key"
net_name: "private_net_ansible"
subnet_name: "private_subnet_ansible"
router_name: "router_ansible"
sec_group_name: "ansible-secgroup"
image_name: "cirros"
flavor_name: "m1.tiny"
ext_net_name: "ext_net"
cidr: "10.0.1.0/24"
dns_servers: ["8.8.8.8"]
# delete_resources.yml
---
- name: OpenStack Resource Clean-up
  hosts: localhost
  connection: local
  gather_facts: false
  
  vars_files:
    - common_vars.yml

  tasks:
    - name: 1. Delete VM Instance
      openstack.cloud.server:
        cloud: "{{ cloud_name }}"
        name: "{{ vm_name }}"
        state: absent
        wait: yes  # VM이 완전히 사라질 때까지 대기

    - name: 2. Delete Security Group
      openstack.cloud.security_group:
        cloud: "{{ cloud_name }}"
        name: "{{ sec_group_name }}"
        state: absent

    - name: 3. Delete Keypair
      openstack.cloud.keypair:
        cloud: "{{ cloud_name }}"
        name: "{{ key_name }}"
        state: absent

    - name: 4. Detach Subnet from Router (Interface 제거)
      openstack.cloud.router:
        cloud: "{{ cloud_name }}"
        name: "{{ router_name }}"
        state: present
        interfaces: []  # 인터페이스 목록을 비워서 강제로 분리

    - name: 5. Delete Router
      openstack.cloud.router:
        cloud: "{{ cloud_name }}"
        name: "{{ router_name }}"
        state: absent

    - name: 6. Delete Subnet
      openstack.cloud.subnet:
        cloud: "{{ cloud_name }}"
        name: "{{ subnet_name }}"
        state: absent

    - name: 7. Delete Network
      openstack.cloud.network:
        cloud: "{{ cloud_name }}"
        name: "{{ net_name }}"
        state: absent

학습

  • 위의 Playbook을 실행할 때, Ansible은 API 클라이언트로서 동작한다.
  • 흐름도 : Ansible Playbook -> Ansible Modules (Collection) -> OPenStack SDK (Python Library) -> REST API Request (HTTP/HTTPS) -> OpenStack Services
  • 실행 위치 : (hosts: localhost)
    • Playbook의 hosts가 localhost인 이유는 Ansible이 파이썬 코드를 컨트롤 노드(노트북)에서 실행해야 하기 때문이다.
    • 노트북에서 Python SDK가 작동하여 원격지에 있는 오픈스택 API 앤드포인트로 JSON 데이터를 날리는 방식이다.

모듈별 동작 분석

  • 📌 리소스 생성
  • Task 1 : 네트워크 생성 (openstack.cloud.network)
    • API 호출 대상 : Neutron API (POST /v2.0/networks)
      • 상세 동작
        • cloud: my-openstack 정보를 이용해 인증 토큰을 발급받는다. (Keystone)
        • state: present를 확인하고, private_net_ansible이라는 이름의 네트워크가 이미 존재하는지 조회(GET)한다. (멱등성)
        • 없다면 생성을 요청한다.
        • external: false 옵션에 의해 이 네트워크는 Tenant Network로 설정된다. Kolla-Ansible의 기본 설정에 따라 VXLAN 타입으로 생성되며, VNI(VXLAN ID)가 자동으로 할당된다.
  • Task 2 : 서브넷 생성 (openstack.cloud.subnet)
    • API 호출 대상: Neutron API (POST /v2.0/subnets)
      • 상세 동작
        • network_name 변수를 참조하여, 앞서 만든 네트워크의 UUID를 조회한다. (API는 이름 대신 UUID를 요구하기 때문)
        • 지정된 CIDR(10.0.1.0/24)와 DNS 서버 정보를 담아 서브넷 생성을 요청한다.
        • 이때 생성된 서브넷은 논리적으로 VXLAN 네트워크 안에 캡슐화된다.
  • Task 3 : 라우터 생성 및 인터페이스 연결 (openstack.cloud.router)
    • API 호출 대상 : Neutron API
    • 상세 동작 (복합 작업) : 이 모듈은 편의를 위해 세 가지 API 작업을 순차적으로 수행한다.
      • 라우터 생성 : POST /v2.0/routers를 호출하여 라우터 객체를 만든다.
      • 외부 게이트웨이 설정 : PUT /v2.0/routers/{router_id}를 호출하여 ext_net(외부망)을 라우터의 Gateway로 설정한다. (이때 SNAT 기능이 활성화된다.)
      • 내부 인터페이스 연결 : PUT /v2.0/routers/{router_id}/add_router_interface를 호출하여 private_subnet_ansible을 라우터에 연결한다.
  • Task 4 : 키페어 등록 (openstack.cloud.keypair)
    • API 호출 대상 : Nova API (POST /os-keypairs)
      • 상세 동작
        • 사용자 노트북의 로컬 경로(~/.ssh/id_ecdsa.pub)에 있는 파일 내용을 읽는다.
        • 이 공개키 문자열을 my-key라는 이름으로 Nova DB에 등록한다.
        • 이후 VM이 생성될 때, Nova는 이 공개키를 VM 내부의 /home/cirros/.ssh/authorized_keys 파일에 주입(Injection)한다.
  • Task 5 : 보안 그룹 생성 (openstack.cloud.security_group)
    • API 호출 대상 : Neutron API (POST /v2.0/security-groups)
      • 상세 동작
        • ansible-secgroup이라는 빈 껍데기(Container)를 만든다. 기본적으로 아웃바운드 허용 규칙만 포함되어 있다.
  • Task 6 : 보안 그룹 규칙 추가 (openstack.cloud.security_group_rule)
    • API 호출 대상 : Neutron API (POST /v2.0/security-group-rules)
      • 상세 동작
        • loop 구문에 의해 모듈이 두 번 실행된다.
        • 첫 번째 루프 : ICMP(Ping) 프로토콜에 대해 Ingress를 허용하는 규칙을 추가
        • 두 번째 루프 : TCP 22번 포트(SSH)에 대해 Ingress를 허용하는 규칙을 추가
        • remote_ip_prefix : 0.0.0.0/0은 모든 소스 IP에서의 접근을 허용
  • Task 7 : 인스턴스(VM) 생성 (openstack.cloud.server)
    • API 호출 대상 : Nova API (POST /servers)
      • 상세 동작 (오케스트레이션) : Ansible 모듈이 VM 생성을 위해 여러 서비스의 정보를 취합한다.
        • ID 조회 (Resolution) : 사용자가 입력한 이름(cirros, m1.tiny, private_net_ansible)을 가지고 Glance, Nova, Neutron API를 조회하여 각각의 UUID를 찾아낸다.
        • VM 생성 요청 : 찾아낸 UUID들과 Keypair, Security Group 정보를 조합하여 Nova에게 생성을 요청한다.
        • 대기 (wait: yes) : VM 상태가 BUILD에서 ACTIVE로 변할 때까지 API를 주기적으로 Polling한다.
        • Floating IP 처리 (auto_ip: yes)
          • VM 생성이 완료되면, Neutron API에 사용 가능한 Floating IP가 있는지 묻는다.
          • 없다면 할당 가능한 Pool(ext_net)에서 IP를 하나 새로 생성
          • 그 IP를 방금 만든 VM의 포트와 연결한다.
  • Task 8 : IP 출력
    • 동작
      • 오픈스택 API를 호출하지 않는다.
      • Task 7에서 register: server_info로 받아온 메모리 상의 JSON 데이터 중 server.addresses.private_net_ansible[0].addr 값을 파싱 하여 화면에 출력한다.

  • 📌 리소스 삭제
    • state: absent
      • Ansible에서 리소스를 삭제할 때 사용하는 공통 파라미터
      • 이 리소스가 없어야 한다(Absent)는 상태를 정의하면, Ansible이 존재 여부를 확인하고 있을 경우에만 삭제 API를 호출한다.
    • wait: yes (VM 삭제 시 필수)
      • VM 삭제 요청(DELETE /servers/{id})을 보내면 API는 즉시 응답하지만, 실제 삭제 작업은 백그라운드에서 몇 초간 진행된다.
      • 이 옵션을 끄면, VM이 아직 다 안 지워졌는데 Ansible이 다음 단계(네트워크 삭제 등)로 넘어간다. 그러면 VM이 사용 중이라 네트워크를 못 지운다는 에러가 발생한다.
      • wait: yes는 VM 상태가 완전히 사라질 때까지 기다린다.
    • 라우터 인터페이스 분리 (interfaces: [])
      • 라우터에 서브넷이 연결된 상태에서는 라우터를 바로 삭제할 수 없는 경우가 많다.
      • state: present와 함께 interfaces: []를 주면, Ansible은 이 라우터에는 아무 인터페이스도 없어야 한다고 판단하고 기존에 연결된 서브넷을 Detach 한다. (IaC의 선언적 특징 활용)

  • 멱등성 (Idempotency)
    • Playbook의 가장 큰 특징은 멱등성이다.
    • 코드를 몇 번 실행해도 시스템의 상태가 항상 동일해야 한다.
    • Task 1을 다시 실행하면, Ansible은 이미 private_net_ansible이 존재함을 감지하고 OK 상태를 반환하며 넘긴다.
    • 실수로 코드를 중복 실행하더라도 리소스가 중복 생성되거나 에러가 나는 것을 방지한다. (IaC의 핵심 원칙)

0개의 댓글