AzurepipelinesとmoleculeでWindowsRoleのテスト自動化

# はじめに
Azure DevOps上でのAnsible Role開発をより効率的に行うべく、AzurePipelineとMoleculeを使ってテストの自動化を行います。

# Agentの選定
Azure Pipelineから渡された処理を実行するエージェントはMS hosted AgentとSelf hosted Agentの2種類存在します。本環境ではMS hosted Agentを使用しました。

###MS Hosted Agent
Pipelineが起動される度にMicrosoftが予め用意したVMイメージ上で渡された処理を実行する

### Self hosted Agent
ユーザー自身が準備/カスタマイズしたVM上でAzure Pipelineから渡された処理を実行する

# リソースグループの作成
moleculeが作成するインスタンスのリソースグループを先行して作成しておく必要があります。ここではmolecule-rgという名前で作成しています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189641/57e5b2f5-d265-3681-b70f-f91881693e4f.png)

# azure credentials情報の準備

[Ansible公式ドキュメントのAzure操作](
https://docs.ansible.com/ansible/2.9_ja/scenario_guides/guide_azure.html)に記載の通りAnsibleからAzureのリソースを操作するにあたり、下記のようなcredentials情報が必要になります。

```
[default]
subscription_id=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
client_id=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
secret=xxxxxxxxxxxxxxxxx
tenant=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```

秘匿情報であり、そのまま平文でリポジトリに登録はできないのでAzure pipelineのLibraryにSecret fileとして登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189641/fe49520e-27ca-f19c-8907-b2da0889753c.png)


# Roleリポジトリの準備

## Roleの準備

molecule-azureのインストールされているマシンにてmolecule init role <role名> -d azureを実行するとazure用のmoleculeのディレクトリが作成されます。

```
$ molecule init role pipeline_moleculetest -d azure
$ ls pipeline_moleculetest
README.md  files     meta      tasks      tests
defaults   handlers  molecule  templates  vars
```

ここではtasks/main.ymlはwin_pingをするだけのシンプルなtaskを作成します

```tasks/main.yml
---
- name: win_ping
  win_ping:
```


またmolecule azure driver標準で作られた状態ではLinuxインスタンスが作成されるのでWindows用にカスタマイズする必要があります。以下はカスタマイズのサンプルです。

```molecule/default/create.yml
---
# yamllint disable rule:line-length
- name: Create
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  vars:
    resource_group_name: molecule-rg
    location: japaneast
    user: molecule
    password: "Password1!"
    remote_connection: winrm
    remote_port: 5986
    virtual_network_name: molecule-rg-vnet
    subnet_name: molecule_subnet
    keypair_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"
  tasks:
    - name: Create resource group
      azure_rm_resourcegroup:
        name: "{{ resource_group_name }}"
        location: "{{ location }}"

    - name: Create security group that allows incoming traffic on SSH/HTTP/HTTPS/RDP/WINRM
      azure_rm_securitygroup:
        resource_group: "{{ resource_group_name }}"
        name: "sg{{ virtual_network_name }}"
        rules:
          - name: SSH
            protocol: Tcp
            destination_port_range: 22
            access: Allow
            priority: 101
            direction: Inbound
          - name: RDP
            protocol: Tcp
            destination_port_range: 3389
            access: Allow
            priority: 102
            direction: Inbound
          - name: HTTP
            protocol: Tcp
            destination_port_range: 80
            access: Allow
            priority: 103
            direction: Inbound
          - name: HTTPS
            protocol: Tcp
            destination_port_range: 443
            access: Allow
            priority: 104
            direction: Inbound
          - name: WINRM
            protocol: Tcp
            destination_port_range: 5986
            access: Allow
            priority: 105
            direction: Inbound

    - name: Create virtual network
      azure_rm_virtualnetwork:
        resource_group: "{{ resource_group_name }}"
        name: "{{ virtual_network_name }}"

    - name: Create molecule instance(s)
      azure_rm_virtualmachine:
        resource_group: "{{ resource_group_name }}"
        name: "{{ item.name }}"
        vm_size: "{{ item.vm_size }}"
        admin_username: "{{ user }}"
        admin_password: "{{ password }}"
        os_type: Windows
        image:
          publisher: "MicrosoftWindowsServer"
          offer: "WindowsServer"
          sku: "2019-Datacenter"
          version: "latest"
        open_ports:
          - "{{ remote_port }}"
        public_ip_allocation_method: Dynamic
        data_disks: "{{ data_disks|default(omit) }}"
      register: server
      with_items: "{{ molecule_yml.platforms }}"
      async: 7200
      poll: 0

    - name: Wait for instance(s) creation to complete
      async_status:
        jid: "{{ item.ansible_job_id }}"
      register: azure_jobs
      until: azure_jobs.finished
      retries: 300
      with_items: "{{ server.results }}"

    - name: Create Windows VM Extension
      azure_rm_virtualmachineextension:
        resource_group: "{{ resource_group_name }}"
        name: "{{ item.name }}"
        virtual_machine_name: "{{ item.name }}"
        publisher: Microsoft.Compute
        virtual_machine_extension_type: CustomScriptExtension
        type_handler_version: 1.9
        settings:
          CommandToExecute: "powershell -ExecutionPolicy Unrestricted -File ConfigureRemotingForAnsible.ps1"
          FileUris: ["https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"]
      ignore_errors: true
      with_items: "{{ molecule_yml.platforms }}"

    # Mandatory configuration for Molecule to function.
    - name: Populate instance config dict
      set_fact:
        instance_conf_dict: {
          'instance': "{{ item.ansible_facts.azure_vm.name }}",
          'address': "{{ item.ansible_facts.azure_vm.properties.networkProfile.networkInterfaces[0].properties.ipConfigurations[0].properties.publicIPAddress.properties.ipAddress }}",  # noqa 204
          'user': "{{ user }}",
          'port': "{{ remote_port }}",
          'connection': "{{ remote_connection }}",
          'identity_file': "{{ keypair_path }}",
        }
      with_items: "{{ azure_jobs.results }}"
      register: instance_config_dict
      when: server is changed

    - name: Convert instance config dict to a list
      set_fact:
        instance_conf: "{{ instance_config_dict.results | map(attribute='ansible_facts.instance_conf_dict') | list }}"
      when: server is changed

    - name: Dump instance config
      copy:
        content: "{{ instance_conf | to_json | from_json | to_yaml }}"
        dest: "{{ molecule_instance_config }}"
        mode: 0644
      when: server is changed

```

```molecule/default/molecule.yml
---
dependency:
  name: galaxy
lint: |
  set -e
  yamllint -c molecule/default/yamllint.cfg .
  ansible-lint -x 701 .
driver:
  name: azure
platforms:
  - name: instance
    vm_size: Standard_D2_v3
provisioner:
  name: ansible
verifier:
  name: ansible
```

```molecule/default/converge.yml
---
- name: Converge
  hosts: all
  vars:
    ansible_user: "molecule"
    ansible_password: "Password1!"
    ansible_port: 5986
    ansible_connection: winrm
    ansible_winrm_server_cert_validation: ignore
  roles:
    - role: pipeline_moleculetest
```


パイプラインで実行されるazure-pipelineを準備します。
事前に準備したcredentialsも、ここで.azure/配下にコピーします。
moleculeの実行自体はtoxで実施してます。

```azure-pipelines.yml
---
trigger:
  branches:
    include:
      - "*"

parameters:
  - name: python_versions
    type: object
    default:
      - 3.6

jobs:
  - job: molecule_test
    pool:
      vmImage: 'ubuntu-20.04'

    steps:
      - ${{ each python_version in parameters.python_versions }}:
          - task: UsePythonVersion@0
            inputs:
              versionSpec: ${{ python_version }}
            displayName: Use Python ${{ python_version }}

      - script: |
          python -m pip install --upgrade pip
          pip install tox
        displayName: Install tox

      - script: cp -pr . $(Agent.TempDirectory)/$(Build.Repository.Name)
        displayName: Prepare a working directory for molecule test

      - task: DownloadSecureFile@1
        displayName: 'Download azure credential file'
        name: azurecredential
        inputs:
          secureFile: credentials

      - script: |
          mkdir ~/.azure
          cp $(azurecredential.secureFilePath) ~/.azure/credentials
        displayName: 'set azure credential'

      - script: tox
        workingDirectory: $(Agent.TempDirectory)/$(Build.Repository.Name)
        displayName: Run tests
```

toxでpythonパッケージでmoleculeやlint系の他、azureを操作するために必要なlibrary、windows操作のためのpywinrmをインストールします。azure関連のlibraryは、最新にするとazure_rm_系のansibleモジュールがまともに動かないのでバージョン指定してます。

最終行あたりのcommandsでmolecule testを実行しています。

```tox.ini
[tox]
envlist = py{36}-ansible{29}
skipsdist = True

[testenv]
passenv = TERM
deps =
    ansible-lint == 4.3.7
    yamllint == 1.25.0
    molecule == 3.2.2
    pywinrm==0.3.0
    azure-cli-core==2.0.35
    azure-cli-nspkg==3.0.2
    azure-common==1.1.11
    azure-graphrbac==0.40.0
    azure-keyvault==1.0.0a1
    azure-mgmt-authorization==0.51.1
    azure-mgmt-automation==0.1.1
    azure-mgmt-batch==5.0.1
    azure-mgmt-cdn==3.0.0
    azure-mgmt-compute==4.4.0
    azure-mgmt-containerinstance==1.4.0
    azure-mgmt-containerregistry==2.0.0
    azure-mgmt-containerservice==4.4.0
    azure-mgmt-cosmosdb==0.5.2
    azure-mgmt-devtestlabs==3.0.0
    azure-mgmt-dns==2.1.0
    azure-mgmt-hdinsight==0.1.0
    azure-mgmt-iothub==0.7.0
    azure-mgmt-keyvault==1.1.0
    azure-mgmt-loganalytics==0.2.0
    azure-mgmt-marketplaceordering==0.1.0
    azure-mgmt-monitor==0.5.2
    azure-mgmt-network==2.3.0
    azure-mgmt-nspkg==2.0.0
    azure-mgmt-rdbms==1.4.1
    azure-mgmt-redis==5.0.0
    azure-mgmt-resource==2.1.0
    azure-mgmt-servicebus==0.5.3
    azure-mgmt-sql==0.10.0
    azure-mgmt-storage==3.1.0
    azure-mgmt-trafficmanager==0.50.0
    azure-mgmt-web==0.41.0
    azure-nspkg==2.0.0
    azure-storage==0.35.1
    molecule-azure==0.5.0
    ansible29: ansible>=2.9,<2.10
    ansiblelatest: ansible
commands =
    python --version
    molecule test
```

#いざ実行

作成したリポジトリをAzure Reposに登録。
Azure pipelineに、作成したazure-pipelines.ymlを登録し、パイプラインを実行
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189641/804009d8-0ec9-6370-5180-812ba7e1f745.png)

molecule実行中にテスト用instanceが構築されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/189641/3db61942-4278-1e09-f967-f62fd24b9e6f.png)



#参考にした文献

[Azure Pipelines で Ansible を使う](https://qiita.com/KoKeCross/items/63d395976c3a295a2d09)

#参考にしたリポジトリ
https://github.com/sperreault/molecule-azure-windows-ex