Ansibleでファイルの行を書き換える3つの方法


もっと色々あると思いますが、基本的には`lineinfile`モジュール、`replace`モジュールシェルあたりを使って、どうしようもないとシェルというのが一般的でしょうか。

| 方法 | メリット | デメリット |
| :-- | :-- | :-- |
| `lineinfile` モジュール | ある程度複雑な行の指定が可能 | 該当行が複数あると難あり |
| `replace` モジュール | 複数箇所の変更が可能 | 複雑な行指定ができない |
| シェル | `sed`や`awk`で何でもできる | 面倒、冪等性の確保はユーザ次第 |


## lineinfile

こんな感じで使います。

```yaml
- name: "設定の修正(1)"
  lineinfile: >-
    dest='/path/to/file/'
    state=present
    backrefs=yes
    regexp='^#?\s*ServerTokens'
    line='ServerTokens Prod'
```

- `dest`: 書き換える対象のファイル
- `state=present`: 「この行があるべき」ことを示す
- `backrefs=yes`: 正規表現内のバックスラッシュが使えるように
- `regexp`: 書き換えたい行にマッチする正規表現
- `line`: 書き換えたい内容

他にもオプションがいろいろあるので、詳細は[公式ドキュメント](http://docs.ansible.com/lineinfile_module.html)をどうぞ。

ひとつのファイル内で複数箇所を書き換える場合は、`with_items`と組み合わせます。

```yaml
- name: "設定の修正(2)"
  lineinfile: >-
    dest='/path/to/file'
    state=present
    backrefs=yes
    regexp='{{ item.regexp }}'
    line='{{ item.line }}'
  with_items:
  - regexp: '^#?\s*ServerTokens'
    line: 'ServerTokens Prod'
  - regexp: '^#?\s*ServerSignature'
    line: 'ServerSignature Off'
```

## replace

[公式ドキュメント](http://docs.ansible.com/replace_module.html)を見ると分かりますが、`lineinfile`と比べるとオプションが多くありません。変更は行単位でなくてもOK。複数箇所にマッチする場合も、すべてを書き換えます。Pythonのマルチラインモードで動作するので、`^`/`$`は各行の先頭/最後にマッチします。そのため、複数行に渡る正規表現はマッチしません。

```yaml
- name: "設定の修正(3)"
  replace: >-
    dest='/path/to/file'
    regexp='AllowOverride None'
    replace='AllowOverride All'
```

- `dest`: 書き換える対象のファイル
- `regexp`: 書き換えたい部分にマッチする正規表現
- `replace`: 書き換えたい内容

## シェル

基本的には、`lininfile`と`replace`で事足りるのですが、Apacheの`httpd.conf`のように巨大なものだとそうもいかなくなってきます。例えば、次の例のように`^\s*AllowOverride`だけで特定しようとすると、どちらなのか判断ができません。

```
...
<Directory />
    Options FollowSymLinks
    AllowOverride None
</Directory>

<Directory "/var/www/html">
    Options FollowSymLinks
    AllowOverride None
</Directory>
...
```

このうちの後者だけ変更したければ、例えば次のように書けます。ちょっと分かりにくいのですが、前半は変数に代入しているだけです。

```yaml
- name: "設定の修正(4)"
  shell: >-
    c='/path/to/file' &&
    k='<Directory "\/var\/www\/html">' &&
    s='AllowOverride None' &&
    r='AllowOverride All' &&
    mv $c $c.backup &&
    awk "/$k/{f=1} f==1&&/$s/{sub(/.+/,\"$r\"); f=0} 1" $c.backup > $c
```

`awk`コマンド部分を抜き出すとこうなります。

```
awk "
  /$k/{f=1}
  f==1&&/$s/{sub(/.+/,\"$r\"); f=0}
  1
"
```

- `$k`に該当する行が見つかったら
- `$s`に該当する行を見つけ
- `$r`に置き換える

の意です。※この部分、[richmikan](http://qiita.com/richmikan@github)さんに教えてもらいました。感謝。

### コマンドの選択肢

ここでは`awk`で処理していますが、使えるコマンドとしては次の3つが有力候補。

- `awk`: sedよりは、扱いやすい
- `sed`: 久々に使うと、モダン(?)な正規表現との違いがちょとつらい
- `perl`: モダンな正規表現になれた人にやさしい

なお、次の3つはだいたい同じ意味になります。

```bash
$ awk '/search/gsub(/search/replace)' target.txt > target.txt
$ sed -i -e 's/search/replace/g' target.txt
$ perl -p -i -e 's/search/replace/g' target.txt
```

複数行に対して、正規表現のみでマッチさせるには、perlが比較的簡単です。`-p0777`オプションと、マッチ演算子`s`がポイントです。

```bash
$ perl -p0777 -i -e 's/search/replace/gs' target.txt
```


## サンプル

実際に動く(はず)のサンプルをひとつ置いておきます。CentOSで、Apacheを動かすための最小構成(?)です。

```yaml
---
- hosts: "webservers"
  sudo: yes
  vars:
    document_root: "/var/www/html"
    httpd_conf: "/etc/httpd/conf/httpd.conf"
  tasks:
    - name: "yumのアップデート"
      command: "yum -y update"

    - name: "パッケージのインストール"
      yum: >-
        name={{ item }}
        state=present
      with_items:
        - "httpd"
        - "libmcrypt"
        - "libselinux-python"

    - name: "httpd.confの修正 (1)"
      lineinfile: >-
        dest='{{ httpd_conf }}'
        state=present
        backrefs=yes
        regexp='{{ item.regexp }}'
        line='{{ item.line }}'
      with_items:
        - regexp: '^#?\s*ServerTokens'
          line: 'ServerTokens Prod'
        - regexp: '^#?\s*ServerSignature'
          line: 'ServerSignature Off'
    
    - name: "httpd.confの修正 (2)"
      shell: >-
        c='{{ httpd_conf }}' &&
        k='<Directory "{{ document_root|replace("/","\/") }}">' &&
        t='AllowOverride None' &&
        s='/None$/' &&
        r='All' &&
        awk "/$k/{f=1} f==1&&/$t/{sub($s,\"$r\"); f=0} 1" $c > $c

    - name: "サービスを起動: httpd"
      service: >-
        name= httpd
        state=started
        enabled=yes

```