Ansible Moleculeで変数および環境変数を渡す方法を考察する

Moleculeで変数、および環境変数を渡す処理について考察してみます。

サンプルコードは[こちら](https://github.com/ansible-playbooks-centos7/wordpress_install)になります。

## Ansibleの変数とは

Ansibleの変数は主にPlaybookの処理の変更にバリエーションをもたらすために用いられます。例えばホストAでは値Xを適応したいがホストBでは値Yを適応したい場合、などです。

## Moleculeで変数を渡す場合

Moleculeが変数を利用するポイントはいくつかあります。

- molecule.yml
- group_vars/all
- varsファイル

## molecule.ymlで変数を設定し利用するケース

molecule.ymlは「molecule/scenario-name/molecule.yml」に配置されるMoleculeの動作をシナリオレベルで決める事の出来る設定ファイルです。

``` yaml
---
dependency:
  name: galaxy
driver:
  name: ec2
platforms:
  - name: instance1
    image: ami-xxxxxxxxxxxxxx
    instance_type: instance.type
    vpc_subnet_id: subnet-xxxxxxxxxxxxx
    keypair_name: keypair_instance1
    security_group_name: sg_instance1
  - name: instance2
    image: ami-xxxxxxxxxxxxxx
    instance_type: instance.type
    vpc_subnet_id: subnet-xxxxxxxxxxxxx
    keypair_name: keypair_instance1
    security_group_name: sg_instance1
provisioner:
  name: ansible
  connection_options:
    var1: vaule1
    var2: vaule2
    var3: vaule3
  log: true
  playbooks:
    converge: ../../install.yml
verifier:
  name: ansible
scenario:
  test_sequence:
    - dependency
    - cleanup
    - destroy
    - syntax
    - create
    - converge
    - cleanup
    - destroy
```

### platformsセクション

platformsセクションで設定した変数はどのシナリオファイルからでも呼び出す事が出来ます。例えばcreate.ymlでAWS EC2へインスタンスを起動したい場合

このplatformsセクションで指定してcreate.yml側で呼び出せます。

<details><summary>クリックで記述を表示する</summary><div>

``` yaml:molecule/scenario-name/create.yml
---
- name: Create
  hosts: localhost
  connection: local
  gather_facts: false
  no_log: "{{ molecule_no_log }}"
  vars:
    ssh_user: centos
    ssh_port: 22

    security_group_description: Security group for testing Molecule
    security_group_rules:
      - proto: tcp
        from_port: "{{ ssh_port }}"
        to_port: "{{ ssh_port }}"
        cidr_ip: '0.0.0.0/0'
      - proto: icmp
        from_port: 8
        to_port: -1
        cidr_ip: '0.0.0.0/0'
    security_group_rules_egress:
      - proto: -1
        from_port: 0
        to_port: 0
        cidr_ip: '0.0.0.0/0'
    keypair_path: "{{ lookup('env', 'MOLECULE_EPHEMERAL_DIRECTORY') }}/ssh_key"

  tasks:
    - name: Create security group
      ec2_group:
        name: "{{ item.security_group_name }}"
        description: "{{ item.security_group_name }}"
        rules: "{{ security_group_rules }}"
        rules_egress: "{{ security_group_rules_egress }}"
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        index_var: index
      register: security_group

    - name: Test for presence of local keypair
      stat:
        path: "{{ keypair_path }}"
      register: keypair_local

    - name: Delete remote keypair
      ec2_key:
        name: "{{ item.keypair_name }}"
        state: absent
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        index_var: index
      when: not keypair_local.stat.exists

    - name: Create keypair
      ec2_key:
        name: "{{ item.keypair_name }}"
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        index_var: index
      register: keypair

    - name: Persist the keypair
      copy:
        dest: "{{ keypair_path }}"
        content: "{{ keypair.results[0].key.private_key }}"
        mode: 0600
      when: keypair.changed

    - name: Get the ec2 ami(s) by owner and name, if image not set
      ec2_ami_facts:
        owners: "{{ item.image_owner }}"
        filters:
          name: "{{ item.image_name }}"
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        index_var: index
      when: item.image is not defined
      register: ami_facts

    - name: Create molecule instance(s)
      ec2:
        key_name: "{{ item.keypair_name }}"
        image: "{{ item.image
          if item.image is defined
          else (ami_facts.results[index].images | sort(attribute='creation_date', reverse=True))[0].image_id }}"
        instance_type: "{{ item.instance_type }}"
        vpc_subnet_id: "{{ item.vpc_subnet_id }}"
        group: "{{ item.security_group_name }}"
        instance_tags: "{{ item.instance_tags | combine({'instance': item.name})
          if item.instance_tags is defined
          else {'instance': item.name} }}"
        wait: true
        assign_public_ip: true
        exact_count: 1
        count_tag:
          instance: "{{ item.name }}"
      register: server
      loop: "{{ molecule_yml.platforms }}"
      loop_control:
        index_var: index
      async: 7200
      poll: 0

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

    # Mandatory configuration for Molecule to function.

    - name: Populate instance config dict
      set_fact:
        instance_conf_dict: {
          'instance': "{{ item.instances[0].tags.instance }}",
          'address': "{{ item.instances[0].public_ip }}",
          'user': "{{ ssh_user }}",
          'port': "{{ ssh_port }}",
          'identity_file': "{{ keypair_path }}",
          'instance_ids': "{{ item.instance_ids }}",
          'group_id': "{{ security_group.results[0].group_id }}", }
      with_items: "{{ ec2_jobs.results }}"
      register: instance_config_dict
      when: server.changed | bool

    - 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.changed | bool

    - name: Dump instance config
      copy:
        content: "{{ instance_conf | to_json | from_json | molecule_to_yaml | molecule_header }}"
        dest: "{{ molecule_instance_config }}"
      when: server.changed | bool

    - name: Wait for SSH
      wait_for:
        port: "{{ ssh_port }}"
        host: "{{ item.address }}"
        search_regex: SSH
        delay: 10
        timeout: 320
      with_items: "{{ lookup('file', molecule_instance_config) | molecule_from_yaml }}"

    - name: Wait for boot process to finish
      pause:
        minutes: 2
```

</div></details>

### provisonerセクション

provisonerセクションで指定された変数はconvergeの処理を上書きします。例えば元々group_vars/allに初期値となる変数の値が設定されていてそれを上書きしたい場合はこのprovisionerセクションから上書きします。

**Moleculeの仕様上ansible-playbookの-eオプションのように変数をmoleculeコマンドから渡す機能は現在はありません**。

そのため以下のように書くような事はできません

``` bash
molecule test -e "var1=vaule1" -e "var2=vaule2"
```

### varsファイル

通常のPlaybookを実行するようにvarsファイルへ変数を設定して各シナリオファイルで変数として読み込む事が可能です。

## Moleculeでの「defaults/main.yml」、「group_vars/all」の扱い

`defaults/main.yml`や`group_vars/all`はRoleやPlaybookで利用される全体の変数を設定するファイルです。ただしMoleculeは外部Roleへは`defaults/main.yml`や`group_vars/all`で指定した変数が渡らないため前述のprovisonerセクションに変数を記述します。

下記のコードはRoleで外部Roleを利用する際に外部Roleの変数を変更するときの一例です。

``` yaml:default/main.yml
---
# 外部Roleの変数を「default/main.yml」で指定してもMoleculeから外部Roleには渡らない
external_roles_var_1: some_val
external_roles_var_2: some_val
external_roles_var_3: some_val

# 自身のRoleの変数は渡る
this_role_var_1: some_val
this_role_var_2: some_val
this_role_var_3: some_val
```

``` yaml:molecule/default/main.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance1
    image: some_docker_image
provisioner:
  name: ansible
  connection_options:
    # 外部Roleの変数はprovisionerセクションへ再度書く事になる 
    external_roles_var_1: some_val
    external_roles_var_2: some_val
    external_roles_var_3: some_val
```

``` yaml:molecule/default/converge.yml
---
- name: Converge
  hosts: all
  become: true
  roles:
    - external_role.some_role_name
    - this_role
```

## Moleculeで環境変数を渡す場合

Moleculeへ処理を渡すときに可変になる変数がある場合があります。例えばMoleculeが処理を実行するAMIのID、Dockerイメージ、クラウドの設定等です。

これはmolecule.ymlでディフォルト値を指定し例えば「env "var=vaule" molecule test」コマンドから渡す事が可能です。

例えばAWSで開発環境で利用するAMIやVPC Subnetを環境ごとに分けたい場合

```
---
dependency:
  name: galaxy
driver:
  name: ec2
platforms:
  - name: instance
    image: ${AMI_ID}
    instance_type: t2.micro
    vpc_subnet_id: ${VPC_SUBNET_ID}
.
.
.
.
.
```

このように記述すると環境変数からAMI IdやVPC Subnet Idを指定できるようになります。


## 参考サイト

* [変数の使用 — Ansible Documentation](https://docs.ansible.com/ansible/2.9_ja/user_guide/playbooks_variables.html)
* [コードサンプル - ansible-playbooks-centos7/wordpress_install | GitHub](https://github.com/ansible-playbooks-centos7/wordpress_install)