Ansibleコントロールノード を Dockerコンテナ でビルドし、リモートサーバに公開鍵認証でSSH接続してインストール済みのパッケージの一覧を取得するまで

サーバの設定を機械化したいと考えました。

IaC (Infrastructure as Code) という分野は知っている。 Chef や Puppet 、 Ansible といったツールも存在は知っており、3年ほど前に非常に簡単な内容ではあるものの、 Ansible を軽く触ったこともありました。

今回はその再来となります。

ただし、「 Ansibleコントロールノード を実マシンではなく Dockerコンテナ として構築する」という部分が今回の新たなチャレンジです。

環境

今回の試験環境です。

  • ホストコンピュータ: Windows 10 Pro
    • IP: YYY.YYY.YYY.YYY
  • Docker: Docker for Windows (version 19.03.13)
  • リモートサーバ: CentOS 7.6
    • IP: XXX.XXX.XXX.XXX
    • SH接続用ユーザ: SSH_REMOTEUSER
    • 管理者ユーザ: ADMIN_USER

設定

適当なディレクトリを掘って各種設定ファイルを用意します。

ディレクトリ階層

PROJECT_ROOT/
  ├ workspace/               // データ永続化領域
  │    ├ entrypoint.sh      // Dockerコンテナ起動時に実行するシェルスクリプト
  │    ├ get_packages.yml   // Ansible の playbook (リモートサーバのパッケージとバージョンの一覧を取得)
  │    ├ hosts              // リモートサーバの一覧 (今回は1つのみ)
  │    └ package_list.j2    // パッケージとバージョンの一覧のリストのテンプレート
  │
  ├ docker-compose.yml      // Docker Compose 設定ファイル
  └ Dockerfile              // Dockerfile

一揃いをGithubにも置いてみました。

Docker

Docker に関する設定。

Dockerfile

FROM centos
RUN yum -y install epel-release
RUN yum install -y sudo
RUN yum -y install openssh-clients
RUN yum -y install ansible
RUN mkdir /workspace

Ansible 本体の他、 SSHクライアント として openssh-clients 、あとエントリポイントのシェルスクリプト実行のために sudo も入れます。

docker-compose.yml

version: '3.1'
services:
  ansible:
    build: .
    volumes:
      - ./workspace:/workspace
    tty: true
    entrypoint: bash -c "bash /workspace/entrypoint.sh && /bin/bash"

データ永続化のための volumes の指定と entrypoint の指定をしています。

tty: true にしているのですが entrypoint: bash -c "bash /workspace/entrypoint.sh" だと Dockerコンテナがexitしてしまう現象に遭遇したので && /bin/bash でもう一度 bash を起動しています。

entrypoint.sh

#!/bin/bash

FILENAME="PRIVATE_KEY"
PUBFILENAME="PRIVATE_KEY.pub"
ORIGINPATH="/workspace/"
COPYPATH="/root/.ssh/"

# ssh key generating
if [ ! -e $ORIGINPATH$FILENAME ]; then
    sudo ssh-keygen -t rsa -b 4096 -f $ORIGINPATH$FILENAME -N ""
    echo "success: generating ssh key"
else
    echo "no operation: ssh key"
fi

# dir
if [ ! -d $COPYPATH ]; then
    # mkdir
    sudo mkdir $COPYPATH
    # chmod
    sudo chmod 600 $COPYPATH
    echo "success: mkdir"
else
    echo "no operation: mkdir"
fi

if [ ! -e $COPYPATH$FILENAME ]; then
    # file copy
    sudo cp $ORIGINPATH$FILENAME $COPYPATH$FILENAME

    # chown
    sudo chown root:root $COPYPATH$FILENAME

    # chmod
    sudo chmod 600 $COPYPATH$FILENAME

    echo "success: copy ssh private key"
else
    echo "no operation: copy ssh private key"
fi

if [ ! -e $COPYPATH$PUBFILENAME ]; then
    # file copy
    sudo cp $ORIGINPATH$PUBFILENAME $COPYPATH$PUBFILENAME

    # chown
    sudo chown root:root $COPYPATH$PUBFILENAME

    # chmod
    sudo chmod 600 $COPYPATH$PUBFILENAME

    echo "success: copy ssh private key"
else
    echo "no operation: copy ssh public key"
fi

エントリポイントは以下のことを実行しています。

  1. データ永続化領域 にSSH接続の公開鍵認証のための秘密鍵が存在しなければ生成
  2. ディレクトリが存在していなければディレクトリを作成します
  3. 2.で作成したディレクトリへ公開鍵認証のための秘密鍵と公開鍵をコピーし、所有者・権限を設定

Ansible

Ansible に関する設定。

今回は Ansible の動作試験として「リモートサーバのパッケージとバージョンの一覧を取得する」ため、以下の記事の内容をお借りしました。

get_packages.yml

- name: Get packages from hosts
  become: yes
  become_user: ADMIN_USER
  become_method: su

  hosts:
    - test_servers

  tasks:
    - name: Get packages
      package_facts:
        manager: auto
      become: true

    - name: Output packages
      template:
        src: ./package_list.j2
        dest: "/workspace/{{ inventory_hostname }}_packages"
        mode: '0644'
      delegate_to: localhost

ほぼ上述記事の内容そのままですが、3ヶ所ほど変更を。

  • SSHユーザは管理者権限を持たないため昇格させます。そのために become, become_user, become_method を追加
  • 今回はホスト名ではなくIPアドレスでアクセスするため、 inventory_hostname_short (接続対象の最初の . までの文字列を取得するマジック変数)だと最初のオクテットしか拾えず何のことやらさっぱりなので、今回はフルネームの inventory_hostname に変更
  • templatedest で存在しないディレクトリを指定すると Destination directory /PATH/TO/FILEBASE/ does not exist エラーで怒られてしまうので、ホスト名のディレクトリの中に package というファイルを作成するのではなく、 ホスト名_package というファイルを作成することにしました

hosts

[test_servers]
XXX.XXX.XXX.XXX

サーバのIPを指定。

package_list.j2

# {{ inventory_hostname }}
{% if ansible_facts.os_family == 'RedHat' %}
{%   for package_name in ansible_facts.packages.keys()|sort %}
{%     for package in ansible_facts.packages[package_name] %}
{{       package['name'] }}-{{package['version']}}-{{package['release']}}.{{package['arch']}}
{%     endfor %}
{%   endfor %}
{% elif ansible_facts.os_family == 'Debian' %}
{%   for package_name in ansible_facts.packages.keys()|sort %}
{%     for package in ansible_facts.packages[package_name] %}
{{       [package['name'], package['version']] | join('_') }}
{%     endfor %}
{%   endfor %}
{% endif %}

こちらもAnsible を利用したシステム構成情報の自動取得 – SIDfm 脆弱性の管理と対策に纏わる話の内容ほぼそのまま。

ただし、 get_packages.yml と同様 inventory_hostname_short だと識別不可なので inventory_hostname に変更しました。

手順

Docker

設定を一通り揃えたところでまずは Dokerコンテナ をビルドします。

PowerShell を管理者実行で起動し、設定ファイルを置いたプロジェクトのディレクトリまで移動します。

> docker-compose up -d
Creating network "ANSIBLE_TEST_default" with the default driver
Building ansible
Step 1/6 : FROM centos
 ---> AAAAAAAAAAAA
Step 2/6 : RUN yum -y install epel-release
 ---> Running in BBBBBBBBBBBB

## 略

Complete!
Removing intermediate container BBBBBBBBBBBB
 ---> CCCCCCCCCCCC
Step 3/6 : RUN yum install -y sudo
 ---> Running in DDDDDDDDDDDD

## 略

Complete!
Removing intermediate container DDDDDDDDDDDD
 ---> EEEEEEEEEEEE
Step 4/6 : RUN yum -y install openssh-clients
 ---> Running in FFFFFFFFFFFF

## 略

Complete!
Removing intermediate container FFFFFFFFFFFF
 ---> GGGGGGGGGGGG
Step 5/6 : RUN yum -y install ansible
 ---> Running in HHHHHHHHHHHH

## 略

Complete!
Removing intermediate container HHHHHHHHHHHH
 ---> IIIIIIIIIIII
Step 6/6 : RUN mkdir /workspace
 ---> Running in JJJJJJJJJJJJ
Removing intermediate container JJJJJJJJJJJJ
 ---> KKKKKKKKKKKK

Successfully built KKKKKKKKKKKK
Successfully tagged ANSIBLE_TEST_default:latest
WARNING: Image for service ansible was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating ANSIBLE_TEST_default_1 ... done

OKです。

> docker-compose exec ansible /bin/bash
#

bash でログインもできました。

# ls -al /root/.ssh/
total 16
drw------- 2 root root 4096 Mon dd hh:ii .
dr-xr-x--- 1 root root 4096 Mon dd hh:ii ..
-rw------- 1 root root 3381 Mon dd hh:ii PRIVATE_KEY
-rw------- 1 root root  743 Mon dd hh:ii PRIVATE_KEY.pub

コピー・権限設定もOKですね。

リモートサーバ側の設定

さてここで閑話休題。公開鍵認証のためにリモートサーバに上述の公開鍵、つまり PRIVATE_KEY.pub を送ります。

方法は何でも良いのですが、ディレクトリを作成しなければならなかったので手っ取り早く手作業で作りました。

$ su -
# vi /etc/ssh/sshd_config


#PermitRootLogin yes
PermitRootLogin no

#PermitEmptyPasswords no
PermitEmptyPasswords no

# systemctl reload sshd
# exit

SSHの設定変更。

$ mkdir .ssh
$ vi .ssh/authorized_keys

## PRIVATE_KEY.pub の内容を貼り付け

$ chmod 700 .ssh/
$ chmod 600 .ssh/authorized_keys

これで準備完了です。

SSH接続の試験

さっそく公開鍵認証で接続してみます。

# ssh -i ~/.ssh/PRIVATE_KEY SSH_REMOTEUSER@XXX.XXX.XXX.XXX
The authenticity of host 'XXX.XXX.XXX.XXX (XXX.XXX.XXX.XXX)' can't be established.
ECDSA key fingerprint is SHA256:ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'XXX.XXX.XXX.XXX' (ECDSA) to the list of known hosts.
Last login: Wek Mon dd hh:ii:ss yyyy from YYY.YYY.YYY.YYY

$ exit
logout
Connection to XXX.XXX.XXX.XXX closed.
# ssh -i ~/.ssh/PRIVATE_KEY SSH_REMOTEUSER@XXX.XXX.XXX.XXX
Last login: Wek Mon dd hh:ii:ss yyyy from YYY.YYY.YYY.YYY
$ exit
logout
Connection to XXX.XXX.XXX.XXX closed.

2回試してみました。OKですね。

Ansible の試験

SSH接続ができることを確認したところで、 Ansible の試験をしたいと思います。

# ansible-playbook -i /workspace/hosts /workspace/get_packages.yml -u SSH_REMOTEUSER --private-key="/root/.ssh/PRIVATE_KEY" -K
BECOME password:

PLAY [Get packages from hosts] *****************************************************************************************

TASK [Gathering Facts] *************************************************************************************************
ok: [XXX.XXX.XXX.XXX]

TASK [Get packages] ****************************************************************************************************
ok: [XXX.XXX.XXX.XXX]

TASK [Output packages] *************************************************************************************************
changed: [XXX.XXX.XXX.XXX]

PLAY RECAP *************************************************************************************************************
XXX.XXX.XXX.XXX                 : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

OKです。

ちなみに今回は色々オプションを付けていますが……

  • -i: サーバの一覧のファイルを指定
  • -u: SSH接続する際のユーザ名を指定
  • --private-key: 秘密鍵のパスを指定
  • -K: become で昇格する際にパスワード認証をする

としています。

結果 (/workspace/XXX.XXX.XXX.XXX_packages)

# XXX.XXX.XXX.XXX
acl-2.2.51-14.el7.x86_64

## 略

zlib-devel-1.2.7-18.el7.x86_64

無事インストールされているパッケージの一覧が取得できました。

途中トライアンドエラーを何度も繰り返しましたが、ひとまずここまで到達できたので書き留めておきます。

参考

Docker

Dockerfile

Dockerfile の所在や扱い方。任意のディレクトリで良いのか、というところからスタートしました……(そこから?

データの永続化

docker-compose down すると Dockerコンテナ 内の変更は消えることは分かっていたのですが、永続化とまともに向き合うことに。

Docker Compose

docker-compose up -d-d オプションはバックグラウンド実行。 Ansible の Dockerコンテナ はここの情報をベースに。

Docker Compose 周りはたまたま手元にあったこの書籍も参考に。

Docker のネットワーク

docker network ls の使い方など。

よくまとまっていて非常に参考になりました。

Docker for Windows のネットワーク

Docker compose のエントリポイント

エントリポイント全般。

エントリポイントを実行するとコンテナがexitしてしまって bash で入れない現象の回避。

SSH

SSHクライアント

Docker の centos イメージには初期インストールされていなかったので openssh-clients をインストール。

SSH接続、公開鍵認証

鍵の強度

ssh-keygen するときのオプションで。

ssh-keygen

エントリポイントでSSHの秘密鍵をシェルスクリプトから作成しましたが、その際に対話式ではなくコマンドワンライナーで完結させたかったのでオプションを漁ることに。

SSHの秘密鍵をデータ永続化しつつパーミッションの警告を回避する方法

エントリポイントを使用して公開鍵認証用の鍵のパーミッション警告を回避する手法の参考に。

Ansible

テスト用の playbook

SSH接続時のユーザ名指定

-u オプションで。

昇格

become 周りについて。

マジック変数

Destination directory /PATH/TO/FILEBASE/ does not exist

結局解決策は分からなかったので回避しました。

その他

sudo

sudo が必要な場面があったので。これも Docker の centos イメージにはなかったので入れることに。

シェルスクリプト

普段あまり書かないので文法をおさらい。

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。