/ azure

Ansible로 Azure 관리하기 #2 - Provision

Ansible로 Azure 관리하기 1편 Ansible 기초에서 기본 개념을 살펴봤다. 2편에서는 Azure에 VM을 생성하는 방법에 대해서 애기해보자. 1편에서 살펴본 azure_rm_virtualnetwork 과 같은 모듈을 사용해서 Azure에 필요한 리소스를 만들 수 있다. 하지만 이 방법은 단점이 있는데 빠르게 발전하고 변화하는 Azure의 여러가지 기능을 빠르게 따라오지를 못한다. 새로나온 기능에 대한 상세한 설정이 빠져 있는 경우도 있다. 이를 극복하기 위해서 ARM Template과 Ansible를 함께 사용하는 방법을 추천한다.

ARM 템플릿

ARM 템플릿 (Azure Resource Manager Template)은 Azure의 리소스와 설정들을 Json 으로 표현해 놓고 Azure에 배포하면 템플릿에 설정한 그대로 리소스를 배포 할 수 있는 방법이다. 템플릿의 구조와 구문은 문서를 통해 확인 할 수 있다.

Gitbhu의 Azure Quick Start template 프로젝트에 다양한 예제를 볼 수 있다.

또한 ARM 템플릿은 파라미터를 받을 수 있다. 파라미터로 VM이름 Admin ID 등을 받아서 Azure에 VM을 생성할 수 있다.

azure_rm_deployment 모듈

azure_rm_deployment 모듈은 ARM Template를 읽어와서 Azure에 배포해주는 모듈이다. 이 모듈에 ARM 템플릿의 위치를 알려주고 파라미터를 전달하면 Azure에 배포한다.

Github에 샘플코드를 올려놨다. 샘플 코드에 있는 ARM 템플릿은 Load Balancer, 두 개의 웹서버, 하나의 데이터 베이스 서버를 생성한다. azuredeploy.json의 일부 코드를 살펴보자.

VM을 생성하는 부분에서 tags 도 붙여주는 것을 볼 수 있다. 이 tag를 이용해서 다이나믹 인벤토리 결과를 필터링 할 수 있다.

또한 Custom Script Extension을 이용하여 윈도우 머신에 WinRM을 설정하는 부분도 볼 수 있다. 이렇게 하면 VM이 생성되고 처음 부팅 될 때 ConfigureRemotingForAnsible.ps1 파워쉘 스크립트가 실행되면서 WinRM을 설정해주기 때문에 Ansible로 관리할 수 있게 된다. 윈도우 머신의 경우 이렇게 설정해주면 되고 리눅스는 SSH로 연결하기 때문에 이 과정이 필요없다.

{
"apiVersion": "2016-04-30-preview",
"type": "Microsoft.Compute/virtualMachines",
"name": "[concat(parameters('vmNamePrefix'), copyindex())]",
"copy": {
  "name": "virtualMachineLoop",
  "count": "[variables('numberOfInstances')]"
},
"tags": {
    "service": "web"
},
"location": "[resourceGroup().location]",
"dependsOn": [
  "[concat('Microsoft.Storage/storageAccounts/', parameters('storageAccountName'))]",
  "[concat('Microsoft.Network/networkInterfaces/', parameters('nicNamePrefix'), copyindex())]",
  "[concat('Microsoft.Compute/availabilitySets/', variables('availabilitySetName'))]"
],
"resources": [
    {
        "type": "Microsoft.Compute/virtualMachines/extensions",
        "name": "[concat(concat(parameters('vmNamePrefix'), copyindex()), concat('/WinRMCustomScriptExtension', copyindex()))]",
        "apiVersion": "2016-04-30-preview",
        "location": "[resourceGroup().location]",
        "dependsOn": [
          "[concat('Microsoft.Compute/virtualMachines/', concat(parameters('vmNamePrefix'), copyindex()))]"
        ],
        "properties": {
          "publisher": "Microsoft.Compute",
          "type": "CustomScriptExtension",
          "typeHandlerVersion": "1.4",
          "settings": {
            "fileUris": [
              "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
            ],
            "commandToExecute": "powershell -ExecutionPolicy Unrestricted -file ConfigureRemotingForAnsible.ps1"
          }
        }
    }
],
"properties": {
  "availabilitySet": {
    "id": "[resourceId('Microsoft.Compute/availabilitySets',variables('availabilitySetName'))]"
  },
  "hardwareProfile": {
    "vmSize": "[parameters('vmSize')]"
  },
  "osProfile": {
    "computerName": "[concat(parameters('vmNamePrefix'), copyIndex())]",
    "adminUsername": "[parameters('adminUsername')]",
    "adminPassword": "[parameters('adminPassword')]"
  },
  "storageProfile": {
    "imageReference": {
      "publisher": "[parameters('imagePublisher')]",
      "offer": "[parameters('imageOffer')]",
      "sku": "[parameters('imageSKU')]",
      "version": "latest"
    },
    "osDisk": {
      "createOption": "FromImage"
    }
  },
  "networkProfile": {
    "networkInterfaces": [
      {
        "id": "[resourceId('Microsoft.Network/networkInterfaces',concat(parameters('nicNamePrefix'),copyindex()))]"
      }
    ]
  },
  "diagnosticsProfile": {
    "bootDiagnostics": {
      "enabled": "true",
      "storageUri": "[concat('http://',parameters('storageAccountName'),'.blob.core.windows.net')]"
    }
  }
}
},

ARM 템플릿 배포

ARM 템플릿을 실행하는 Ansible 롤을 살펴보자.

배포 지역과 리소스 그룹 이름을 지정하고 바로 파라미터가 나온다. ARM 템플릿에 정의되어 있는 파라미터를 이런식으로 전달 가능하다. ARM 템플릿은 azuredeploy.json 파일을 읽어서 json 포멧으로 붙인다. 그 표현식은 "{{ lookup('file','../armtemplate/azuredeploy.json') | from_json }}" 이다. 현재 플레이북의 상대적인 위치를 지정해서 가져올 수 있다.

lookup 과 같은 플러그인을 이용하여 위치를 지정했다.

같은 플레이북에 Create와 Destroy가 같이 있으므로 tag 를 이용해서 구분해줬다.

---
# Create or update a template deployment based on uris using parameter and template links
- name: Create Azure Deploy
  azure_rm_deployment:
    state: present
    location: koreasouth
    resource_group_name: AnsibleVMGroup
    parameters:
      storageAccountName:
        value: "avmstoragedig"
      adminUsername: 
        value: "ansibleuser"
      adminPassword:
        value: "Ansible!2345678"
      dnsNameforLBIP:
        value: "ansibleweb123"
      vmNamePrefix: 
        value: "WebVM"
      lbName: 
        value: "WebLB"
      nicNamePrefix:
        value: "webservernic"
      publicIPAddressName: 
        value: "WebPublicIP"
      vnetName: 
        value: "AnsibleVNet"
      vmSize: 
        value: "Standard_D2s_v3"
    template: "{{ lookup('file','../armtemplate/azuredeploy.json') | from_json }}"
  tags:
    - create
  delegate_to: 127.0.0.1

# Destroy a template deployment
- name: Destroy Azure Deploy
  azure_rm_deployment:
    state: absent
    resource_group_name: AnsibleVMGroup
  tags:
    - destroy
  delegate_to: 127.0.0.1

github의 디렉토리 구조를 보면 위의 role 은 ansible/roles/tasks/main.yml 에 있고 실제 플레이북은 ansible/provision.yml 파일이다. 플레이북을 보면 hosts가 127.0.0.1로 표현되어 있는데 이는 Ansible 컨트롤 머신에서 실행되라는 얘기다. Azure에 VM을 만드는 과정은 특정 리모트 호스트와 관련이 없고 로컬에서 실행되면 되기 때문에 아래와 같이 hosts와 connection을 지정하면 된다.

---
- hosts: 127.0.0.1
  connection: local
  roles: 
    - role: provision

이 플레이북을 실행하면 아래와 같은 결과를 볼 수 있다.

ansible-playbook 명령을 실행할 때 생성 태스크만 실행되도록 --tags crate를 지정한다. 이 명령은 inventory가 필요없기 때문에 -i 옵션이 없다. 플레이북을 실행하면 실제 Azure에 3개의 VM과 Load Balancer 등이 만들어진다.

$ cd ansible
$ ansible-playbook provision.yml --tags create
 [WARNING]: Unable to parse /etc/ansible/hosts as an inventory source

 [WARNING]: No inventory was parsed, only implicit localhost is available

 [WARNING]: Could not match supplied host pattern, ignoring: all

 [WARNING]: provided hosts list is empty, only localhost is available


PLAY [127.0.0.1] **************************************************

TASK [Gathering Facts] ********************************************************************
ok: [127.0.0.1]

TASK [provision : Create Azure Deploy] ******************************************************************
changed: [127.0.0.1 -> 127.0.0.1]

PLAY RECAP *******************************************************
127.0.0.1                  : ok=2    changed=1    unreachable=0    failed=0

Static 인벤토리로 실행

provision.yml 에서 만든 윈도우 VM은 아무것도 설치되어 있지 않은 VM이다. 이제 VM에 웹서버, 데이터베이스 서버라는 역할을 할 수 있도록 설정해야 한다. inventory.yml 에 생성된 VM의 IP를 넣어놓고 webserver.yml 플레이북을 실행해보자.

ansible/group_vars/all.yml 에 WinRM으로 접속을 위한 정보가 들어 있다.

webserver.yml 플레이북은 common, webserver 롤을 차례로 수행한다.

---
- hosts: web
  roles:
    - common
    - webserver 

ansible-azure/ansible/roles/webserver/tasks/main.yml

---
  - name: Ensure IIS webserver is installed
    win_feature:
      name: Web-Server
      state: present

  - name: Deploy default iisstart.htm file
    template:
      src: iisstart.j2
      dest: c:\inetpub\wwwroot\iisstart.htm

  - name: Ensure IIS service is running
    win_service: 
      name: W3SVC
      start_mode: auto
      state: started

ansible-azure/ansible/roles/common/tasks/main.yml

---
- name: Install Chocolatey
  win_shell: "Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))"
    
- name: Ensure .NET Framework 4.7.1 is installed using chocolatey
  win_chocolatey:
    name: dotnet4.7.1
    state: present

실행결과

$ ansible-playbook -i inventory.yml webservers.yml

PLAY [web] *********************************************************************************************

TASK [Gathering Facts] *********************************************************************************
ok: [10.10.0.5]
ok: [10.10.0.4]

TASK [common : Install Chocolatey] *********************************************************************
changed: [10.10.0.5]
changed: [10.10.0.4]

TASK [common : Ensure .NET Framework 4.7.1 is installed using chocolatey] ******************************
ok: [10.10.0.4]
ok: [10.10.0.5]

TASK [webserver : Ensure IIS webserver is installed] ***************************************************
ok: [10.10.0.4]
ok: [10.10.0.5]

TASK [webserver : Deploy default iisstart.htm file] ****************************************************
ok: [10.10.0.4]
ok: [10.10.0.5]

TASK [webserver : Ensure IIS service is running] *******************************************************
ok: [10.10.0.4]
ok: [10.10.0.5]

PLAY RECAP *********************************************************************************************
10.10.0.4                  : ok=6    changed=1    unreachable=0    failed=0
10.10.0.5                  : ok=6    changed=1    unreachable=0    failed=0

다이나믹 인벤토리로 실행

다이나믹 인벤토리로 Azure에 있는 VM들을 조회해서 플레이북을 실행할 수 있다. 다이나믹 인벤토리에 대해서는 Ansible로 Azure 관리하기 #1 - Ansible 기초에 나와있다.

실행 명령

$ ansible-playbook -i azure_rm.py webserver_dynamic.yml

$ ansible-playbook -i azure_rm.py database_dynamic.yml

실행결과의 내용은 크게 다르지 않지만 Azure에 있는 VM들의 내용을 가져오기 위한 시간이 걸린다. 이 명령을 데이터베이스 서버가 설정 되었다.

여기까지 ARM 템플릿을 이용해서 Azure에 필요한 리소스들을 만들고 VM들을 만들었다. 고정된 서버를 운영한다면 Static 인벤토리를 이용하고 VM의 갯수가 늘었다 줄어다 하는 상황이면 다이나믹 인벤토리를 이용할 수 있다. Ansible을 실행해서 웹서버와 데이터베이스 서버를 설정 할 수 있었다.

Azure 다이나믹 인벤토리의 상세

다이나믹 인벤토리의 실체는 파이썬으로 구현된 azure_rm.py라는 파일이다. 이 파이썬 코드가 Azure 파이썬 SDK를 이용하여 Azure에 있는 VM들의 정보를 조회해서 인벤토리로 제공하는 것이다.

azure_rm.py를 사용할 대 수정한 부분이 있다. 다이나믹 인벤토리는 ansible_host라는 변수로 호스트 이름을 주는데 코드는 Publid IP를 주기로 되어 있다. 나는 Private IP로 받아서 내부 통신으로 수행하고 싶었기 때문에 아래처럼 코드를 수정했다. 수정한 부분은 줄번호 674이다.

host_vars['private_ip_alloc_method'] = ip_config.private_ip_allocation_method
host_vars['ansible_host'] = ip_config.private_ip_address
if ip_config.public_ip_address:

azure_rm.py의 테스트

azure_rm.py이 잘 작동하는지 테스트 할 수 있다.

$ ./azure_rm.py --resource-groups=AnsibleVMGroup --pretty
{
  "KoreaSouth": [
    "DatabaseVM",
    "WebVM0",
    "WebVM1"
  ],
  "_meta": {
    "hostvars": {
      "DatabaseVM": {
        "ansible_connection": "winrm",
        "ansible_host": "10.10.1.4",
        "computer_name": "WebVMdatabase",
        "fqdn": null,
        "id": "/subscriptions/e47f0bbb-cd59-41dc-86b7-2e239d536c04/resourceGroups/AnsibleVMGroup/providers/Microsoft.Compute/virtualMachines/DatabaseVM",
        "image": {
          "offer": "WindowsServer",
...

테스트를 위한 상세 옵션은 azure_rm.py --help 명령으로 알 수 있다. 이 명령을 통해서 결과를 필터링 하는 방법을 테스트 해보면 좋다. 5가지 필터링을 제공한다. 리소스그룹 이름과 태그를 이용하는 것이 유용했다.

  • 전체
  • 지역(Azure Region)
  • 리소스그룹 이름
  • Security Group 이름(정확히 모르겠음)
  • 태그 키 이름
  • 태그 키_값
  • 전원 상태

azure_rm.ini

azure_rm.ini는 다이나믹 인벤토리가 사용하는 파일인데 이 파일을 이용해서 다이나믹 인벤토리가 가져오는 VM리스트를 필터링 할 수 있다. 하지만 정적인 설정이기 때문에 불편할 수 있다. 동적으로 적용하기 위해서는 환경변수를 이용해야 하는데 Ansible 명령 앞에 환경변수를 설정하는 방법이 있다.

$ AZURE_TAGS=service:database ansible-playbook -i azure_rm.py database_dynamic.yml

ansible-playbook을 실행하기전에 AZURE_TAGS를 설정하는 명령이다. 이렇게 사용할 수 있는 환경변수는 총 5가지다.

  • AZURE_RESOURCE_GROUPS
  • AZURE_TAGS
  • AZURE_LOCATIONS
  • AZURE_TAGS
  • AZURE_INCLUDE_POWERSTATE

정리하면 다이나믹 인벤토리를 실행할 때 전체 리스트가 아닌 선택된 VM 리스트를 가져오고 싶다면 azure_rm.ini에 5가지 필터링을 사용하거나 환경변수를 사용해서 필터링 할 수 있다.

Playbook에서 필터링

다이나믹 인벤토리가 가져온 리스트를 다시 Playbook에서 필터링 할 수 있다. hosts에 service_database라는 표현은 다이나믹 인벤토리가 가져온 리스트에서 태그가 service:database 인 호스트만 적용한다는 의미다.

---
# VMs that is tagged by service:database
- hosts: service_database
  roles:
    - common
    - database

hosts: azure 는 다이나믹 인벤토리가 가져오 호스트 전체를 의미한다.

Azure의 Tag를 잘 사용하면 쉽게 필터링을 할 수 있다.

참고자료

Azure+Ansible 따라하기
Ansible Azure 공식문서
Azure의 Ansible 관련 문서
채널 9 동영상