Ansible과 Windows Host
이전에 Windows 환경에서 Ansible을 사용하는게 불가능하다는 것을 여러 삽질을 거쳐 확인했다.
이렇게 Ansible의 Control Node로는 Windows를 사용할 수 없지만,
Managed Node로는 사용할 수 있기에 Ansible을 사용해 Windows 빌드 서버를 관리할 수 있게 구성하는 방법을 테스트 해보았다.
하지만 Windows는 생각보다 쉽지는 않았는데 구성을 하며 알게된 1주일 간의 테스트 내용을 아래에 작성했다.
Managed Node로서의 Windows 환경
Windows 환경에 대한 설정이나 모듈이 생각보다 많긴 했지만 이슈가 생기거나 잘 동작하지 않을 때 이에 대한 글이나 해결법은 찾기 다소 어려웠다.
기본적으로 Linux에서 사용하는 모듈은 대부분 동작하지 않으며, Windows 전용으로 생성된 모듈을 찾아서 사용해야 한다.
https://docs.ansible.com/ansible/latest/collections/ansible/windows/index.html#modules
테스트 환경은
- Control Node : Ubuntu 22.04 LTS (VM) 1대
- Managed Node : Windows 10 Dev (VM) 1대
로 단순하게 구성했고 아래의 기본 설정 후 하나하나 구성을 진행해보았다.
root@ersia:~# cat /etc/ansible/hosts
[windows]
srv1 ansible_host=192.168.0.5
Windows Host 통신 확인
일반적인 ansible ping 대신 Windows는 win_ping 모듈을 사용하는데 임의의 테스트 계정을 생성하고 아래와 같이 호출하면 에러가 발생한다.
root@ersia:~/ansible# ansible windows -m win_ping -u user -k
SSH password:
srv1 | UNREACHABLE! => {
"changed": false,
"msg": "Failed to create temporary directory. In some cases, you may have been able to authenticate and did not have permissions on the target directory. Consider changing the remote tmp path in ansible.cfg to a path rooted in \"/tmp\", for more error information use -vvv. Failed command was: ( umask 77 && mkdir -p \"` echo ~/.ansible/tmp `\"&& mkdir \"` echo ~/.ansible/tmp/ansible-tmp-1705771919.0218897-7350-71004021993265 `\" && echo ansible-tmp-1705771919.0218897-7350-71004021993265=\"` echo ~/.ansible/tmp/ansible-tmp-1705771919.0218897-7350-71004021993265 `\" ), exited with result 1",
"unreachabl
기본적으로 ansible은 Linux를 가정하고 있기 때문에 기본 설정을 사용하면 Linux Shell에서 동작하는 명령어가 수행된다.
Windows를 사용하면 어떤 Shell을 쓸지 지정해줘야 한다.
# -m : 어떤 모듈을 사용할지 명시
# -u : 사용자를 지정함
# -k : 명령어 수행 시 암호를 별도로 입력받는다
# -e : --extra-vars와 같은 설정이며 ansible_shell_type을 사용하기 위한 설정
ansible windows -m win_ping -u user -k -e ansible_shell_type=cmd
root@ersia:~/ansible# ansible windows -u user -k -e ansible_shell_type=cmd -m win_ping
SSH password:
srv1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
위와 같은 별도 설정을 추가하기 귀찮을 경우 Host에 대한 Variable로 추가해주면 조금 편리하다.
root@ersia:~/ansible# cat /etc/ansible/hosts
[windows]
srv1 ansible_host=192.168.0.5
[windows:vars]
remote_tmp = C:\Users\user\.ansible\tmp # ansible의 Windows 기본 디렉토리이며, 변경가능
become_method = runas # Ansible 2.3부터 지원가능
ansible_connection=ssh # 만약 winrm 구성을 했다면 winrm도 사용가능
ansible_user=user
ansible_shell_type = cmd # 또는 powershell로도 사용 가능
# 기존 명령어보다 간소화 해서 사용 가능
root@ersia:~/ansible# ansible windows -k -m win_ping
SSH password:
srv1 | SUCCESS => {
"changed": false,
"ping": "pong"
}
다만 Windows에서 권한이 없는 계정으로 접속해서 administrator로 권한 상승하는 것은 지원되지 않는다고 하며,
become을 사용해 사용자 계정 전환을 하고 싶을 경우 처음부터 administrator 권한을 가진 계정을 통해서 접속해야 한다.
만약 ansible_shell_type을 powershell로 사용한다면, 접속하는 Windows 서버의 openssh 기본 Shell도 powershell로 맞춰춰야 한다. 맞춰주지 않을 경우 아래와 같은 에러가 발생한다.
아래는 powershell을 openssh의 기본 Shell로 설정하는 스크립트이다.
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
Windows Host Gathering Facts
Windows에서의 Gathering Facts는 Powershell을 통해서 수행하는데 이 Powershell 로직 중 SMB를 통해서 연결을 생성하는 부분이 있어서 Workstation(LanmanWorkstation)서비스를 활성화 해줘야 한다.
만약 해당 서비스가 활성화 되어 있지 않으면, 아래와 같은 Warning 메시지를 확인할 수 있다.
(글자가 깨지는 것은 Windows와 Linux간의 인코딩 문제로 인코딩만 맞춰주면 정상적으로 보인다.)
Windows Host의 프로그램 설치
Windows는 GUI가 기본이지만 Ansible에서 제공하는 다양한 Command 설치 모듈이 제공된다.
Powershell 같은 스크립트로 Nuget이나 dotnet을 사용하거나 여러가지 방법이 있지만 테스트 해본 결과 주로 아래의 방법이 사용하기에 편리했다.
- Ansible Galaxy를 통한 모듈 설치 : https://galaxy.ansible.com/docs
- Chocolatey 모듈을 통한 설치 : https://docs.ansible.com/ansible/latest/collections/chocolatey/chocolatey/win_chocolatey_module.html#ansible-collections-chocolatey-chocolatey-win-chocolatey-module
- Windows 기능 활성화 모듈을 통한 설치 : https://docs.ansible.com/ansible/latest/collections/ansible/windows/win_feature_module.html#ansible-collections-ansible-windows-win-feature-module
- Windows 설치 패키지 모듈을 통한 설치 : https://docs.ansible.com/ansible/latest/collections/ansible/windows/win_package_module.html#ansible-collections-ansible-windows-win-package-module
Linux에서도 비슷했지만 Windows에서도 마찬가지로 Ansible을 사용할 때 스크립트를 통해서 설치하거나 작업을 할 경우 에러가 발생하는 것에 대해 예외처리 하기가 많이 귀찮아진다.
Ansible은 스크립트를 실행할 경우 스크립트에서 에러가 나거나 출력이 잘못되더라도 스크립트를 실행하지 못한게 아니라면 정상 동작으로 처리하기 때문에 가능한 한 Ansible Module을 사용하는 것이 바람직하다.
Ansible Galaxy를 통한 모듈 설치
Ansible Galaxy는 Ansible 커뮤니티에서 개발자들이 작성한 재사용 가능한 Ansible Role 및 Collection을 공유하고 관리하는 온라인 플랫폼이다. Docker Hub 같은 역할이라고 보면 되는데 Windows에 대해 재사용 가능한 ansible 모듈(role)과 구조화 된 패키지(Collection)가 생각보다 많이 있다.
(다만 Redhat에서 제공하는 공식 role들은 subscription(유료)을 하지 않으면 사용할 수 없다.)
재사용이 가능한 것들이 많이 있기 때문에 찾아보면 대부분 내가 필요한 기능들은 이미 있는 것을 알 수 있다.
root@ersia:~/ansible# ansible-galaxy collection list
# /usr/lib/python3/dist-packages/ansible_collections
Collection Version
----------------------------- -------
amazon.aws 6.5.0
...
ansible.windows 1.14.0
...
chocolatey.chocolatey 1.5.1
...
vyos.vyos 4.1.0
wti.remote 1.0.5
root@ersia:~/ansible# ansible-galaxy info chocolatey
Role: chocolatey
description: Configures Chocolatey on Windows Systems
commit: 70eab91a774c4ac5efb49964956a9791551a8aaa
commit_message: Version 1.3.6
fix meta
created: 2023-05-08T20:27:25.662076Z
download_count: 511844
github_branch: master
github_repo: ansible.chocolatey
github_user: arillso
id: 2989
imported: 2020-10-22T07:29:31.123103-04:00
modified: 2023-10-29T18:44:46.170631Z
path: ('/root/.ansible/roles', '/usr/share/ansible/roles', '/etc/ansible/roles')
upstream_id: 25136
username: arillso
Ansible에는 자주 사용되는 Collection들이 설치되어있지만 Ansible Core에는 이런 Collection들이 설치되어있지 않기 때문에 아래의 명령어로 설치를 수행해야 한다.
ansible-galaxy collection install chocolatey.chocolatey
root@ersia:~/ansible# ansible-galaxy collection install chocolatey.chocolatey
Starting galaxy collection install process
Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`.
Chocolatey의 경우 Ansible Galaxy를 사용하면 쉽게 사용 가능하다.
Chocolatey 모듈을 통한 설치
Chocolatey 모듈을 설치한 후 아래와 같은 yml을 수행하면 관리 대상 Windows에 chocolatey에서 지원하는 필요한 패키지를 설치할 수 있다.
root@ersia:~/ansible# cat chocolatey.yml
- hosts: windows
tasks:
- name: Install_chocolatey
win_chocolatey:
name: "chocolatey"
register: download_choco
until: download_choco is succeeded
retries: 4
delay: 5
- name: display download_choco
debug:
var: download_choco
- name: Install multiple packages sequentially
win_chocolatey:
name: '{{ item }}'
state: present
loop:
- git
- python
- openjdk
register: install_result
- name: display install_result
debug:
var: install_result
root@ersia:~/ansible# ansible-playbook chocolatey.yml -k
SSH password:
PLAY [windows] *********************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [srv1]
TASK [Install_chocolatey] **********************************************************************************************
ok: [srv1]
TASK [display download_choco] ******************************************************************************************
ok: [srv1] => {
"download_choco": {
"attempts": 1,
"changed": false,
"choco_cli_version": "2.0.0",
"failed": false,
"rc": 0
}
}
TASK [Install multiple packages sequentially] **************************************************************************
ok: [srv1] => (item=git)
ok: [srv1] => (item=python)
ok: [srv1] => (item=openjdk)
TASK [display install_result] ******************************************************************************************
ok: [srv1] => {
"install_result": {
"changed": false,
"msg": "All items completed",
"results": [
{
"ansible_loop_var": "item",
"changed": false,
"choco_cli_version": "2.0.0",
"failed": false,
"invocation": {
"module_args": {
"allow_empty_checksums": false,
...
...
"item": "openjdk",
"rc": 0
}
],
"skipped": false
}
}
PLAY RECAP *************************************************************************************************************
srv1 : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Windows 기능 활성화 모듈을 통한 설치
Windows 기능 활성화는 아래 그림과 같은 Windows에서 기본 제공하는 기능을 켜고 끄는데 사용할 수 있다.
다만 해당 기능은 Windows Server에서 제공하는 Server Manager를 사용하기 때문에 일반적인 Windows에서는 사용할 수 없다.
root@ersia:~/ansible# cat feature.yml
- hosts: windows
tasks:
- name: Install IIS (Web-Server only)
ansible.windows.win_feature:
name: Web-Server
state: present
register: result_enable
- name: display result_enable
debug:
var: result_enable
Windows 설치 패키지 모듈을 통한 설치
일반적으로 Windows에 프로그램을 설치할 때 자주 사용되는 아래의 확장자를 설치하는데 사용하는 모듈이다.
Supports .exe, .msi, .msp, .appx, .appxbundle, .msix, and .msixbundle
해당 ansible.windows.win_package 모듈은 네트워크 공유(인터넷 포함)를 통해 제공되는 exe, msi 파일을 다운로드 받아서 바로 설치해줄 수 있는 기능도 제공한다.
다만 네트워크에서 바로 다운받아서 설치하는 기능을 쓸 때는 상당히 골치 아픈 경우 부분이 있는데, 바로 product_id라는 인자를 필수로 넣어줘야 한다는 것이다.
The product id of the installed packaged.
This is used for checking whether the product is already installed and getting the uninstall information if state=absent.
For msi packages, this is the ProductCode (GUID) of the package. This can be found under the same registry paths as the registry provider.
For msp packages, this is the PatchCode (GUID) of the package which can found under the Details -> Revision number of the file’s properties.
For msix packages, this is the Name or PackageFullName of the package found under the Get-AppxPackage cmdlet.
For registry (exe) packages, this is the registry key name under the registry paths specified in provider.
This value is ignored if path is set to a local accesible file path and the package is not an exe.
This SHOULD be set when the package is an exe, or the path is a url or a network share and credential delegation is not being used. The creates_* options can be used instead but is not recommended.
root@ersia:~/ansible# cat package.yml
- hosts: windows
tasks:
- name: Install 7zip from a network share with specific credentials
ansible.windows.win_package:
path: https://www.7-zip.org/a/7z2301-x64.exe
product_id: 7-Zip
arguments: /S
state: present
register: result_install
- name: display result_install
debug:
var: result_install
root@ersia:~/ansible# ansible-playbook package.yml -k
SSH password:
PLAY [windows] *********************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************
ok: [srv1]
TASK [Install 7zip from a network share with specific credentials] *****************************************************
ok: [srv1]
TASK [display result_install] ******************************************************************************************
ok: [srv1] => {
"result_install": {
"changed": true,
"elapsed": 3.1377924999999998,
"failed": false,
"msg": "OK",
"rc": 0,
"reboot_required": false,
"status_code": 200
}
}
PLAY RECAP *************************************************************************************************************
srv1 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
해당 product_id를 구하는 방법은 설명에 적혀있는대로 Windows Registry를 찾아서 알아내거나,
msi 파일 한정이지만 아래의 블로그 글에서 product_id를 추출하는 powershell 스크립트가 있다.
param(
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.IO.FileInfo]$Path,
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[ValidateSet("ProductCode", "ProductVersion", "ProductName", "Manufacturer", "ProductLanguage", "FullVersion")]
[string]$Property
)
Process {
try {
# Read property from MSI database
$WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
$MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0))
$Query = "SELECT Value FROM Property WHERE Property = '$($Property)'"
$View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))
$View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)
$Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)
$Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)
# Commit database and close view
$MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null)
$View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)
$MSIDatabase = $null
$View = $null
# Return the value
return $Value
}
catch {
Write-Warning -Message $_.Exception.Message ; break
}
}
End {
# Run garbage collection and release ComObject
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($WindowsInstaller) | Out-Null
[System.GC]::Collect()
}
출처: https://hakurei.tistory.com/299
# 필요한 경우 powershell 스크립트 수행을 위해 정책 변경
Set-ExecutionPolicy -ExecutionPolicy Unrestricted
.\find_product_id.ps1 -Path "C:\Users\user\Downloads\OpenSSH-Win64-v9.5.0.0.msi" -Property ProductCode
{75CC4F13-D9D7-45EE-A061-C367C19DC8D0}