Ansibleのenvironmentを利用して「sudo su -」相当のPATHを設定する

## ■ Ansibleとは
+ **Ansibleの特徴**
    - エージェントレスでサーバ構築を自動化してくれるツールです。
    - sshdが動作しているマシンにSSHで接続し環境を構築してくれます。
    - 設定ファイルはYAMLで書きます。プログラムの知識（Ruby、Python、etc.）は不要です。
    - 数台～数十台規模のサーバ構築だったらchefよりも簡単です。

    　  
+ **環境の準備**
以前の記事を参照してください。
[Ansible 最初の一歩](http://qiita.com/olympic2020/items/9c21d53066c9c6e5357f)


# ■ はじめに
Ansible 1.9から、他のユーザの権限（例えばroot権限）で実行するための'become'が追加されました。

rootでの直接アクセスが禁止されているサーバを管理する場合、一旦、一般ユーザとしてログインし、その後、`sudo su -`でrootになって操作されるかと思います。
Ansibleで`become: ture`してコマンドを実行する場合と、`sudo su -`した後にコマンドラインからコマンドを直接実行する場合で、環境変数のPATHが異なるため、コマンドの実行結果が異なる場合があり注意が必要です。

今まで、この辺をあまり意識せずに使っていたため、コマンドを直接実行して確認済みのコマンドが、PlayBookでは失敗する事態が発生し、少し苦労したので、`become`と環境変数のPATHについて、改めて調べてみました。

### つまづいたポイント

+ 「`become: true` ≠ `sudo su -`」だったこと。
+ `sudo su -`の状態で各種コマンドを実行する方法が不明なこと。

# Ansible本家の見解
Ansible公式HP、GitHubのissuesを調査してみました。

### Ansible公式HP（[リンク](http://docs.ansible.com/ansible/become.html#only-one-method-may-be-enabled-per-host)）
> **Only one method may be enabled per host**
> 
> Methods cannot be chained. You cannot use `sudo /bin/su -` to become a user, you need to have privileges to run the command as that user in sudo or be able to su directly to it.

意訳：
> `sudo`と`su`は、同時に使えないので、`sudo su -`したい人は、直接コマンドを実行してください。


### Ansible GitHub issues
本家でのディスカッションも結論がでていない模様（ステータスがOpenのままです）。
　　[GitHub issues#12686](https://github.com/ansible/ansible/issues/12686)

# ■ 動作検証
まずは、PATHがどのようになるのか調査してみました。
調査環境：Amazon Linux AMI 2016.03

remote_user: ec2-user

**1.　become: false（ec2-user）**
　　/usr/local/bin:/bin:/usr/bin:/opt/aws/bin
**2.　become: true（root）**
　　/sbin:/bin:/usr/sbin:/usr/bin
**3.　lookup('pipe', 'echo $PATH')**
　　/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:
　　/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin
**4.　lookup('env', 'PATH')**
　　/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:
　　/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin
**5.　ec2-user**
　　/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:
　　/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin
**6.　sudo su -**
　　/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:
　　/opt/aws/bin:/root/bin
**7.　sudo su**
　　/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin

見事にバラバラですね ^^;

# ■ 「sudo su -」相当のPATHを設定する
Googleで検索すると、commandやshellで.bashrcや.bash_profileを読み込む方法が紹介されていますが、これをやってしまうと冪等性の保証が難しくなってしまいます。そこで、environmentでPATHを設定する方法について紹介します。

### **設定前の状態**
まず、何もしていないとどのようになるのか実行してみます。

```playbook.yml
- hosts: all
  remote_user: ec2-user
  tasks:
    - command: echo $PATH
      register: become_path
      become: true
      changed_when: False

    - name: become
      debug: var=become_path.stdout

    - command: echo $PATH
      register: not_become_path
      changed_when: False

    - name: not become
      debug: var=not_become_path.stdout
```

実行してみます。


```bash
$ ansible-playbook env_path.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [target03]

TASK [command] *****************************************************************
ok: [target03]

TASK [become] ******************************************************************
ok: [target03] => {
    "become_path.stdout": "/sbin:/bin:/usr/sbin:/usr/bin"
}

TASK [command] *****************************************************************
ok: [target03]

TASK [not become] **************************************************************
ok: [target03] => {
    "not_become_path.stdout": "/usr/local/bin:/bin:/usr/bin:/opt/aws/bin"
}

PLAY RECAP *********************************************************************
target03                   : ok=5    changed=0    unreachable=0    failed=0
```

一般ユーザのPATH：`/usr/local/bin:/bin:/usr/bin:/opt/aws/bin`
rootのPATH：`/sbin:/bin:/usr/sbin:/usr/bin`

になっています。いわゆるログインしていない状態のようです。

lookupを使った場合は以下のようになります。

```playbook.yml
---
- hosts: all
  remote_user: ec2-user
  vars:
    pipe_path: "{{ lookup('pipe', 'echo $PATH') }}"
    env_path: "{{ lookup('env', 'PATH') }}"
  tasks:
    - name: pipe path
      debug: msg={{ pipe_path }}

    - name: env path
      debug: msg={{ env_path }}
```

lookupを使った場合の実行結果は以下のようになります。

```bash
$ ansible-playbook playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [target01]

TASK [pipe path]] **************************************************************
ok: [target01] => {
    "msg": "/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin"
}

TASK [env path]] ***************************************************************
ok: [target01] => {
    "msg": "/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/aws/bin:/home/ec2-user/.local/bin:/home/ec2-user/bin"
}

PLAY RECAP *********************************************************************
target01                   : ok=3    changed=0    unreachable=0    failed=0
```

こちらは、ec2-userでログインした状態になっていました。

### **environmentでPATHを設定してみる**
environmentでPATHを設定してみます。

あらかじめ、`sudo su -`して、`echo $PATH`を調べておきます。

```bash
$ sudo su -
# echo $PATH
/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin:/root/bin
```

PlayBookは以下のようになります。

```playbook.yml
- hosts: all
  remote_user: ec2-user
  environment:
    PATH: "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin:/root/bin"
  tasks:
    - command: echo $PATH
      register: become_path
      become: true
      changed_when: False

    - name: become
      debug: var=become_path.stdout

    - command: echo $PATH
      register: not_become_path
      changed_when: False

    - name: not become
      debug: var=not_become_path.stdout
```

実行してみましょう。

```bash
$ ansible-playbook playbook.yml

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [target01]

TASK [command] *****************************************************************
ok: [target01]

TASK [become] ******************************************************************
ok: [target01] => {
    "become_path.stdout": "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin:/root/bin"
}

TASK [command] *****************************************************************
ok: [target01]

TASK [not become] **************************************************************
ok: [target01] => {
    "not_become_path.stdout": "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin:/root/bin"
}

PLAY RECAP *********************************************************************
target01                   : ok=5    changed=0    unreachable=0    failed=0

```

`environment:`をPlayBookに対して設定したため、`become: true`の場合も、そうではない場合も、いずれも設定したPATHになっています。

ちなみに、`environment:`は、タスクのほうに書くことも可能です。タスクごとに設定したい場合は、タスクに対して指定していくことになります。PlayBookは以下のようになります。

```playbook.yml
- hosts: all
  remote_user: ec2-user
  tasks:
    - command: echo $PATH
      register: become_path
      become: true
      changed_when: False
      environment:
        PATH: "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin:/root/bin"

    - name: become
      debug: var=become_path.stdout

    - command: echo $PATH
      register: not_become_path
      changed_when: False
      environment:
        PATH: "/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin:/root/bin"

    - name: not become
      debug: var=not_become_path.stdout
```
実行結果は、PlayBook全体に対して`environment:`を設定した場合と同じになります。

### **environmentのスコープ**
PlayBookやTaskに対して設定した`environment:`のスコープは、設定した範囲になります。
例えば、PlayBookに対して設定した場合、別のPlayBookへは影響がでないようになっています。
自分でコマンドを羅列して/bin/shで実行する場合は、設定したPATHを元に戻すコマンドも書かなければならないのですが、AnsibleのPlayBookは、その辺を自動で行ってくれるので便利です。

# ■ おわりに
今回は、environmentを使ってPATHを設定する方法を紹介しました。

PATHの設定方法は、説明した通りになりますが、PATH依存のPlayBookは思わぬ落とし穴にはまってしまう可能性があります。PlayBookで、shellやcommandモジュールを使う際には、フルパスで書く習慣をつけておくと、安全でよいかと思います。

+ 落とし穴にはまる可能性あり（PATH依存）
    
    ```bash
    - command: cmd
    ```
    
+ 安全な書き方
    
    ```bash
    - command: /usr/local/bin/cmd
    ```

Ansibleのような構成管理ツールを使うにあたり、単に動作しますという状態ではなく、保守性や堅牢性を考慮した運用ができるように心がけていけるとよいですね。