AnsibleでGit

0
89

AnsibleはIaC(Infrastructure as Code)ツールの一つで、あらかじめPlaybookと呼ばれるファイルに定義した内容に従ってサーバ環境の構築(ソフトのインストールや設定など)をバッチ的に実行してくれる機能です。

1つのプロダクトに関するサーバ環境構築は、通常は複数台(複数回)に渡って行われます。最低限、開発時の動作検証環境と本番運用環境は別サーバ上に構築しますし、負荷分散を行う場合は本番運用環境として複数台のサーバに同じ環境を構築する場合もあります。万が一、運用していたサーバでリカバリ困難な障害が発生した場合、急遽別のサーバ上に環境を再構築する必要が生じるかもしれません。

このようにサーバ環境構築ツールは複数台の対象サーバの存在を想定するため、通常は当該ソフトをインストールした環境構築用マシンから対象サーバに対して通信を介して必要な操作ができるようになっています。この時、同分野の他のソフトでは通信を行うための相手側ソフト(エージェント)を対象サーバ側にもインストールする必要があるようですが、Ansibleはその必要がなく、環境構築用マシンから対象サーバにSSHでログイン可能になっていれば良いだけです。
この辺のお手軽さがAnsibleの長所の一つです。

ということで、今回はAnsibleの使い方に関して書きたいと思いますが、サーバ環境を一から構築する操作に関して書こうと思うと記事が巨大になるので、目的を限定し、Gitで管理されたプロダクトのインストールおよびアップデートをAnsibleで行えるようにする手順、つまりはAnsibleをデプロイツールとして使用する方法について書きたいと思います。

Ansibleのインストール

まずはAnsibleを実行できる環境構築が必要です。

同環境は手元のPC等でも構築可能ですが、できれば様々なプロダクトで汎用的に利用できる環境を構築したいので、今回はAnsibleもサーバ上にインストールしたいと思います。
ただし、このサーバはAnsibleでの環境構築対象となるサーバとは別物で、あくまでAnsibleの運用に特化したサーバを意味します。
よって、混同しないよう、Ansibleを運用するサーバを「Ansibleサーバ」、Ansibleによる環境構築の対象となるサーバを「被管理サーバ」と表現することにします。

Ansibleサーバの構築ですが、サーバOSとしては現在弊社一推しのUbuntuで考えたいと思います。OS自体のインストールや必要な設定は本記事のテーマから外れますので、ここでは割愛します。

よって、いきなりAnsibleのインストールですが、以下のように実行します。

apt update
apt install software-properties-common
apt-add-repository --yes --update ppa:ansible/ansible
apt install ansible

細かくは良く分からなくても、与えられた情報を元に呪文のように実行すれば期待した結果が得られる点がパッケージ管理機能(apt)の良いところ!
これでAnsibleがインストールできました。

被管理サーバへのアクセス設定

先に書きましたように、Ansibleではエージェントを必要としませんが、被管理サーバにSSHでログインできるようにしておく必要があります。
パスワードによる認証も行えるようですが、やはりここは公開鍵認証の方がスマートです。
ということで、公開鍵の作成と被管理サーバへの設定を行います。

まずは、Ansibleサーバ側でAnsibleを実行するユーザを決めましょう(私の場合はベタに「ansible」というユーザを作成しましたが)。

次に同ユーザのホームディレクトリ配下に「.ssh」というディレクトリを作成し、同ディレクトリに移動した後に以下のように実行します。

ssh-keygen -t rsa

上記操作により、.ssh配下に「id_rsa」と「id_rsa.pub」の2つのファイルが作成されます。前者が秘密鍵、後者が公開鍵です。
この公開鍵の方を被管理サーバに転送しておきます。

被管理サーバではプロダクト管理用ユーザのホームディレクトリに「.ssh」を作成します(既に作成済みの場合はそのまま利用)。
同ディレクトリに移動して「authorized_keys」というファイルを作成しますが、同ファイルは複数の公開鍵を管理できるようになっているため、既に他の用途による公開鍵が設定済み(authorized_keys作成済み)かもしれません。
既存データを消してしまわないよう、以下のようにして今回作成した公開鍵をauthorized_keysに追加します。

cat id_rsa.pub >> authorized_keys

なお、新規にauthorized_keysを作成した場合、パーミッションが適正なものになっていない可能性があるので、以下のように設定します。

chmod 600 authorized_keys

上記設定を行なった上で、Ansibleサーバから被管理サーバに以下の操作でログインできればOKです。

ssh -i ~/.ssh/id_rsa プロダクト管理ユーザ@被管理サーバアドレス(IP等)

インベントリ作成

上記まででAnsibleサーバから被管理サーバにSSHでアクセスできるようになりましたが、Ansibleの実行に際して同被管理サーバを指定する必要があります。この情報を記述したものが「インベントリ」です。

インベントリは以下のような内容になります。

[group_a]
xxx.xxx.xxx.001
xxx.xxx.xxx.002

[group_a:vars]
ansible_user=hoge
ansible_ssh_private_key_file="~/.ssh/id_rsa"

インベントリでは、まずは被管理サーバを指定する必要がありますが、負荷分散環境では複数台のサーバを対象とする場合があります。よって、同じ操作の対象となる被管理サーバをまとめてグループ化することができます。

上記では「group_a」というグループを作成し、そこに対象となる被管理サーバのアドレスを列記しています。被管理サーバのアドレスの指定方法は上記のようにIPアドレスでも良いですし、ドメイン名でも良いです。

なお、インベントリ内に複数のグループを定義可能であり、開発環境と本番環境など実行のタイミングや実行する内容が異なる被管理サーバを別のグループとして定義しておくと良いです。

また、インベントリでは被管理サーバとの通信に必要な情報を変数として持つことができ、特に前述のグループ単位で変数を定義することができます。

上記では2つの変数「ansible_user」「ansible_ssh_private_key_file」を定義していますが、前者は被管理サーバにSSH接続する際のユーザ名、後者は同操作における認証用の秘密鍵を指定しています。

被管理サーバへのアクセス設定」の最後で触れたように、SSHで被管理サーバにアクセスするためには被管理サーバアドレス(IPまたはドメイン)、ユーザー名および認証用秘密鍵の3つの情報が必要になりますが、これらをインベントリで定義している訳です。

これで被管理サーバへの接続に必要な情報をAnsibleが入手可能になりました。

Playbook作成

いよいよAnsibleに具体的な操作内容を指示するための「Playbook」の内容を記述します。
ただ、今回はGitによるデプロイに特化した内容になるため、記述内容は簡単です。

---
- name: デプロイ
  hosts: group_a
  tasks:
    - name: Git実行
      git:
          repo: https://ユーザー:パスワード@リポジトリアドレス:ポート/リポジトリ
          dest: /home/hoge/product
          version: master
      register: git_result

    - name: 結果出力
      debug:
          msg: "{{ git_result }}"

まず先頭行のハイフン3つですが、これは当該ファイルがYAML形式で書かれたファイルであることを意味します。

2行目や5行目はインデントを除いた先頭の1文字がハイフンになっていますが、これはYAML形式におけるリスト的な構造を示す記号です。あるインデントにおける先頭がハイフンの行から次に同インデントのハイフンを持つ行の手前までを1つのまとまった情報とみなし、複数の情報のまとまりをリスト的に表記できる訳です。

特にPlaybookの場合は「play」と「task」という2つの情報を表現することになります。
「play」は対象となる被管理サーバを指定して、同被管理サーバに対してどのような「task」を実行するかということを記述したものです。
「task」は具体的に実施する処理内容を記述したものです。
上記例ではインベントリで定義された「group_a」に対してデプロイを行うplayが1つ定義されており、同playとして期待される処理としてはGitの実行とその結果の出力という2つのtaskが定義されていることになります。

「name」は各リストの要素に対して同要素の処理内容・目的などを記述できるもので、Playbook内での各要素の識別に役立ちますし、実際のAnsibleの実行においても同情報が表示されるため、どの段階まで処理が進んでいるかが分かるので便利です。

「hosts」は当該playが対象とする被管理サーバを指定するものです。

「tasks」では当該playで期待される処理内容(task)を記述して行きます。
Ansibleでは実行する処理を「モジュール」と呼びますが、上記例では「git」「register」「debug」という3つのモジュールを使用しています。

「git」モジュールは文字通りgitコマンドを実行する機能ですが、正確には「git clone」を実行する機能として定義されているようです。ただ、同モジュールのパラメータとして「update」というものがあり、これが「yes」であれば「git pull」も併せて実行されるというもので、デフォルトは「yes」です。また、「git clone」は1つの被管理サーバに対して一度だけ実行すれば良い操作ですが、Ansibleは冪等性を保証しようとするので、多重に実行しても無視されるだけです。
つまりは2回目以降の実行においては実質的には「git pull」を行う機能と考えて良く、更新内容の反映に継続的に使用できます。

gitモジュールのパラメータですが、「repo」では文字通り対象となるリポジトリを指定します。リポジトリへのアクセスはSSH,HTTPSいずれの方法も使用できます。上記例ではHTTPSを使用していますが、通常はリポジトリへのURLは「https://リポジトリアドレス:ポート/リポジトリ」(例:https//xxx.xxx.xxx.xxx:8443/user/repo.git)のような形式になっていますが、さらに認証用のユーザーとパスワード情報を合わせて指定しています。
「dest」ではgitで管理されているファイルを展開するディレクトリを指定します。
「version」では対象となるブランチを指定します。

上記指定で「git clone + git pull」の実行に必要な設定は完了ですが、これだけだとgitの実行結果に関してはほとんど情報が得られません。
よって、とりあえず「register」モジュールで実行結果の情報を取得しておいて、「debug」モジュールでその内容を出力しています。
ただ、これでも得られる情報は下記程度ですが。

ok: [被管理サーバアドレス] => {
    "msg": {
        "after": "9125cf6d08466c985f6fff6427e63315253db2d7",
        "before": "2a0aacaf2e232a416f43bba3f004298a3ea14698",
        "changed": true,
        "failed": false,
        "remote_url_changed": false
    }
}

Playbook実行

上記でAnsibleによるGit実行の準備は整いました。

では実際にPlaybookを実行してみます。
インベントリは「inventory」、Playbookは「playbook.yml」という名称のファイルとした場合、Playbookの実行は以下のように行います。

ansible-playbook -i inventory playbook.yml

実行の最後に以下のような行が出力されます。

PLAY RECAP *********************************************************************
被管理サーバアドレス  : ok=6    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

とりあえず「unreachable」(対象被管理サーバにアクセスできなかったタスク数)「failed」(対象被管理サーバでエラーが発生したタスク数)が0であればPlaybookの実行としては成功です。

ただし、あくまでPlaybookの各タスクが問題なく実行できたと言うだけで、期待されている処理が正しく行われたことを保証するものではありません。例えばPlaybookの記述自体に漏れや間違いがあって、タスクの実行としては問題なくても、期待した結果が得られていないと言うことは往々にして有ります。
Playbookの作成や更新後は、実際に被管理サーバ上で期待した結果が得られていることを確認することも必要です。

総括

とりあえずGitを用いたデプロイという限定的テーマでAnsibleを使ってみました。

単に「git clone」「git pull」を行うだけであれば専用のモジュールが用意されているので簡単に定義できます。特にcloneとpullを区別する必要がない点は楽ですね。

このように目的にドンピシャなモジュールがない場合でも、被管理サーバで実行可能なコマンドであれば「command」や「shell」と言うモジュールを使用して実行することができます。さらにそのようなコマンド実行において入力を求められるようなケースもあると思うのですが、想定される入力も含めて定義できる「expect」というモジュールもあったりします。
被管理サーバにSSHでログインして実行できる処理であれば、大半はAnsibleから実行可能と考えて良いのではないかと思います(確証はありませんが)。ただ冪等性が確保できているかどうかは注意する必要があります。

なお、Ansibleの本来の目的はサーバ環境構築全般に関わるIaC化であり、そのような目的を達成するためのPlaybookを書こうと思うと記述量も格段に多くなります。そうなると1つのファイルに全てを記述するのは煩雑になってきますのでファイル分割や共有もしたくなってきます。
当然ながらAnsibleにはPlaybookの内容を分割したり共有したりする仕組みも用意されていますが、その辺についてはまた別の機会に。