🐇

Ansible:Linux サーバーセキュリティ設定の自動化

2021/03/20に公開

この記事でやること

以前書いた Linux サーバー SSH 設定でやった SSH とファイアウォールの設定を Ansible によって自動化し、5分くらいでできるようにします。

まぁ、手でやるほうが難易度的には圧倒的に簡単ですが、

  1. 遊び用のサーバーは躊躇なく破壊したいし、再構築で貴重な休日を潰したくない。
  2. 手でやるとどこかでミスる可能性があるので、極力サーバーは直接触りたくない。
  3. 複数台のサーバーでの設定を頼まれたときに起こる頭痛・めまい・吐き気等の緩和。

といった事情で完全自動化したいわけです。「ハッカーは3時間でできる作業を3日かけて自動化する」とは言ったものです。

本記事で使用するコードは以下のリポジトリにまとめてあります。今回はついでに git, make, docker, docker-compose をサーバーにインストールして、コンテナデプロイ用サーバー構築の演習とします。

https://github.com/wsuzume/docker-ansible

実験環境

  • クライアント:Dockerで仮想化しているのでなんでもいいと思います。
  • ホスト:Ubuntu 20.04 LTS

Ansible とは

Ansible は SSH 通信を介してサーバーのセットアップを playbook という単位で自動化するためのソフトウェアです。Ansible には以下のような特徴があります。

  • プッシュベース
    • 操作を行いたいタイミングでクライアント(開発用PC)からホスト(サーバー)に必要なスクリプトやコードを送信し、ホスト上で操作を実行します。
    • 対義語のプルベースの設定管理システムは、ホスト上にエージェントをインストールし、リポジトリの変更やタイマーなどの何らかのイベントをきっかけにしてリポジトリから必要なスクリプトやコードをプルし、ホスト上で操作を実行します。Ansible はプルベースの設定管理システムに比べ、ホスト上に余計なソフトウェアをインストールする必要がないため、サーバーの構成はシンプルとなり保守性が高まります。
  • 宣言的
    • ホストで何をするか(手続き的)ではなく、ホストがどういう状態であるかを記述します。つまり開発者は具体的な操作やその手順をあまり意識しなくてもホストを望んだ状態に設定することができます。
  • 冪等性
    • ホストが既に playbook によって指定された状態にある場合、Ansible は何もしません。つまり Ansible を何度実行してもホストは同じ状態になります。間違って複数回実行してもホストの環境を破壊することはありません。

最近はクラウド上に構築したシステムに CI ツールをインストールする構成がオーソドックスなので Ansible を使う機会は少ないと思いますが、私のように安い VPS を借りて細々と趣味のプログラミングをしている人にとっては便利なツールです。

また、Ansible は複数のシステムに対して一連の操作を行うオーケストレーション(たとえばメンテナンス時にネットワークの設定を変更すると同時に一部のシステムの起動や停止を実行する)に使用することも可能で、人間がいちいち手で対応できないような大規模プロジェクトでもワークします。

Ansible のインストール

Ansible はクライアントにのみインストールすれば OK で、ホスト側にはなにも特別なことをする必要はありません。インストール方法についてはこちらを参照してください。

本記事ではクライアントの Docker 上に Ansible をインストールした Ubuntu イメージを作成し、そのコンテナにログインすることで Ansible を使用します。リポジトリから抜粋した Dockerfile は以下のようになります。

Dockerfile
FROM ubuntu:20.04

# installing tzdata stops if this environment variable is not defined
ENV DEBIAN_FRONTEND=noninteractive

# sshpass is needed for ansible first login to remote server
RUN apt-get update \
    && apt-get install --no-install-recommends -y \
        make vim curl git python3 python3-pip sshpass \
        software-properties-common \
    && add-apt-repository --yes --update ppa:ansible/ansible \
    && apt-get install -y ansible \
    && apt-get clean

ARG UID=1000
RUN useradd -m -u ${UID} josh
USER josh

WORKDIR /playbook

最初に注意しておくこと

Ansible は SSH を使用してリモートのホストにアクセスするので、SSH の設定を変更すると Ansible 自身が締め出されますが、それが正しい挙動です。つまり以下の 2 つの設定ファイルを用意することになります。

  1. SSH セットアップ用 SSH 設定
    • パスワードによりホストにログインし、SSH セットアップ完了後にはこの設定でのアクセスは不可能になります。
  2. SSH セットアップ完了後用 SSH 設定
    • 公開鍵暗号によりホストにログインし、SSH セットアップ完了後にはこの設定でのみアクセス可能になります。

「何度実行しても同じ実行結果になる」が売りの Ansible ですが、SSH の設定に限っては原理的に一度しか実行できません。複数回実行できるならドデカいセキュリティホールが開いていることになるので反省してください

最終的なディレクトリ構成

最終的なディレクトリ構成は以下のようになります。作業ディレクトリと章番号は

  • root: 『1. root: SSH セットアップ用 playbook』
  • user: 『2. user: SSH セットアップ完了後用 playbook』

のように対応しているので、それぞれの章では各ディレクトリに移動して作業してください。

docker-ansible
├── Dockerfile
└── playbook
    ├── root
    │   ├── ansible.cfg
    │   ├── inventory
    │   ├── playbook.yml
    │   └── tasks
    │       ├── 0010_install_requirements.yml
    │       ├── 0020_create_groups.yml
    │       ├── 0021_create_admin_user.yml
    │       ├── 0022_create_common_user.yml
    │       ├── 0030_ssh_security_settings.yml
    │       ├── 0040_ufw_security_settings.yml
    │       ├── 9999_reboot.yml
    │       └── M001_ssh_config.yml
    └── user
        ├── playbook.yml
        └── inventory

0. 実験環境の準備

今回は ConoHa VPS で実験します。別に さくらのVPS でも AWS EC2 でも Vagrant 上で動かしている Ubuntu でもなんでもいいです。ポチポチとボタンを押して Ubuntu 20.04 を選択しましょう(何度か実験しているので画像では再構築ボタンになっています)。ここで入力した root パスワードは Ansible が最初にサーバーにアクセスするために必要になります。

下にチラチラ見えている SSH Key の登録ボタンを押したら幸せになれる気がしますが、それでは企画が終了してしまうので見なかったことにします[1]

これからの作業で必要になるので、鍵は予め生成しておいてください。今回はセキュリティポリシーとして、管理者とデプロイ用ユーザーで鍵をわけることにして、それぞれ~/.ssh/id_admin~/.ssh/id_commonとします。

$ ssh-keygen -t ed25519

作成するユーザー名は adminsirius です。

$ ssh admin
$ ssh sirius

でアクセスできる状態になってほしいので、~/.ssh/configを(なければ作成して)以下のように編集してください。HostNameはご自身がアクセスするサーバーのものを指定し、Portは変更する場合は指定してください。

~/.ssh/config
Host admin
    HostName    192.168.1.11
    Port        2222
    IdentityFile    ~/.ssh/id_admin
    User        admin

Host sirius
    HostName    192.168.1.11
    Port        2222
    IdentityFile    ~/.ssh/id_common
    User        sirius

1. root: SSH セットアップ用 playbook

やること

サーバーの初期設定で、それなりにやることがあります。

  1. 必要なソフトウェアのインストール
    • git, make, docker, docker-compose
  2. ユーザーグループの作成
    • admin: 全権限を付与した管理者用グループ
    • common: makeコマンド と docker コマンドを sudo で実行する権限を与えられた、サービスデプロイ用のグループ
    • docker: dockerコマンドを sudo なしで実行可能なグループ。管理者がいちいち sudo するのは面倒くさいので。
  3. 管理者ユーザーの作成
  4. 一般ユーザーの作成
  5. SSH の設定
  6. ファイアウォールの設定
  7. サーバーの再起動

サービスデプロイ用のユーザーは常に攻撃の危険に晒されるため、本来なら使えるコマンド等をより制限すべきでしょうが、今回は特に危険な docker コマンドの権限を明確にしています。もしも何らかの方法で

$ docker container run -it --rm -v /:/workdir [イメージ] bash

を実行されて、しかも Docker のイメージがうっかり root ユーザのままになっていたら、そのコンテナのシェルは root 権限で任意のディレクトリにアクセス可能となり、root 権限のシェルを奪われたも同然です。Docker はコンテナが陥落してもコンテナの外に影響はありませんが、攻撃者が docker コマンドを実行可能な状態でユーザーアカウントが陥落した場合にはそのサーバーは終わりです。

最初の playbook

Ansible では YAML 形式の playbook と呼ばれる単位でホストの設定を管理します。あとはこいつをゴリゴリと書いていくだけです。最初のディレクトリ構成は以下のようにしましょう。ansible.cfginventoryplaybook.ymlはそれぞれ空のファイルとして作成しておいてください。

docker-ansible
├── Dockerfile
└── playbook
    └── root
        ├── ansible.cfg
        ├── inventory
        └── playbook.yml

最初に docker-ansible のディレクトリにいるとして、このとき playbook は

$ cd playbook/root
$ ansible-playbook -i inventory playbook.yml

で実行できます。ここで inventory は INI 形式で書かれた Ansible の設定ファイル名で、playbook.yml は実行する playbook のファイル名です。もしもクライアント側でも Docker を使っている人は、

# イメージのビルド
## -f はビルドに用いる Dockerfile の名前で省略可能です。
## -t はイメージのタグ名で自由に設定できますが、基本は `username/imagename:version` です。
(shell) $ docker image build -f Dockerfile -t wsuzume/docker-ansible:latest .

# イメージからコンテナを作成してそのシェルにログイン
(shell) $ docker container run -it --rm -v ${PWD}/playbook:/playbook wsuzume/docker-ansible:latest bash
(docker)$ cd root
(docker)$ ansible-playbook -i inventory playbook.yml

で実行できます[2]。ただし(shell)$docker コマンドを実行するシェル、(docker)$は Docker コンテナのシェルです。

試しに以下のコマンドを実行してみてください。playbook のシンタックスに問題がないかをチェックしてくれます。

$ ansible-playbook -i inventory playbook.yml --syntax-check

正しいディレクトリで実行できていれば、playbook は存在するものの空である旨の以下のエラーが出ます。まだ何も記述していないのでこれでいいです。

[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost
does not match 'all'
ERROR! Empty playbook, nothing to do

ディレクトリが誤っていれば playbook が存在しない旨の以下のエラーが出ます。

ERROR! the playbook: playbook.yml could not be found

もしこのエラーが出たら ls コマンドを実行してファイルの存在を確認してください。以下のようにならなければおかしいです。

$ ls
inventory  playbook.yml

ここまでうまく行っていたら、ansible.cfginventoryplaybook.yml をそれぞれ以下のように編集しましょう。サーバーにアクセスしてログを出力するだけの簡易的なものです。192.168.1.11はどう見てもご家庭内のサーバーを指しているので、みなさんはきちんと自分のサーバーのアドレスに書き直してください。

ansible.cfg
[defaults]
host_key_checking=False
inventory
[server]
# SSH で接続したいサーバーの IP アドレスまたはホスト名
192.168.1.11
# 複数のサーバーを同じ設定にしたい場合にはここに列挙します
# 192.168.1.12
# 192.168.1.13
# 192.168.1.14
# ...

[server:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_port=22
ansible_user=root
# サーバーの root パスワード
ansible_ssh_pass=XXXXXX
playbook.yml
---
- hosts: server
  tasks:
    - debug: msg="Hello, world!"

シンタックスチェック[3]を行います。

$ ansible-playbook -i inventory playbook.yml --syntax-check

playbook: playbook.yml

大丈夫そうです。以下のコマンドを実行します。

$ ansible-playbook -i inventory playbook.yml

PLAY [server] *********************************************************************************************

TASK [Gathering Facts] ************************************************************************************
ok: [192.168.1.11]

TASK [debug] **********************************************************************************************
ok: [192.168.1.11] => {
    "msg": "Hello, world!"
}

PLAY RECAP ************************************************************************************************
192.168.1.11             : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

無事に接続できました。これでサーバーは言うことを聞くようになったので、頑張って設定を書いていきます。

2021/03/24追記

以前までは $ ansible-playbook を実行する前に SSH Host Key Checking があるため以下の手順を行うよう書いていました。しかしこの手順は ansible_ssh_common_args='-o StrictHostKeyChecking=no' を指定することで回避できる、という情報提供[4]があったため、対応する設定について調べたところ、ansible.cfghost_key_checking=False を指定することで同等の効果が得られることがわかりました。ansible.cfg に関しては Ansible の動作の制御をご覧ください。

すぐにでも実行したいところですが、一度

$ ssh 192.168.1.11

を実行して yes を入力してください。Ansible は SSH 接続先を勝手に登録する権限を持っ> ていないのでこれを実行しないとエラーを起こします。一度フィンガープリントを登録してしまえ> ばその後は実際にアクセスせずに Ctrl+C などで中断しても大丈夫です。Docker でやってい> る人は、Docker コンテナ上でこれを実行してください(コンテナから exit すると --rm > オプションの影響で毎回新しいコンテナが作成され設定がリセットされるので、コンテナに入り直> す度にこの手順が必要です。それが面倒なら --rm オプションを外した上で、ご自身でコンテナを管理してください)。

$ ssh 192.168.1.11
The authenticity of host '192.168.1.11 (192.168.1.11)' can't be established.
ECDSA key fingerprint is 
SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.1.11' (ECDSA) to the list of known hosts.
root@192.168.1.11's password: [Ctrl+C]

playbook の分割

Ansible のタスクは include_tasks によって他の YAML 形式のファイルをインポートすることができます。また、このとき vars に変数を指定することが可能で、インクルードした YAML の{{ }}で書かれたテンプレートに展開されます。これはたとえば複数のユーザーを似たような設定で作成するときに YAML をモジュールとして使い回せるので便利です。

name タグはタスクに付与可能な名前で、指定しなくてもよいですが、ログが見やすくなるので適宜挿入していきます。

inventory
playbook.yml
tasks
    hello_world.yml
playbook.yml
---
- name: "Configure VPS"
  hosts: server
  tasks:
    - include_tasks: tasks/hello_world.yml
      vars:
        message: "Hello, world!"
tasks/hello_world.yml
---
- name: "hello world"
  debug: msg={{ message }}

ここから先はぶっちゃけひたすら泥臭い作業になりますし、特に解説もしないので、読み飛ばしてもよいです。

必要なソフトウェアのインストール

Ansible はサーバー上でシェルスクリプトを実行することも可能なのですが、手続き的な操作を行ってしまうと、宣言的であるという Ansible のメリットが失われてしまいます。そのため、Ansible では各操作に対応したモジュールを用います。

Ubuntu では apt モジュールによってソフトウェアのインストールが可能です。

tasks/0010_install_requirements.yml
---
- name: "install requirements"
  apt:
    update_cache: yes
    name:
      - git
      - make
      - docker
      - docker-compose

ユーザーグループの作成

group モジュールを使います。

tasks/0020_create_groups.yml
---
- name: "create admin group (is written in sudoers file from the first)"
  group:
    name: admin
    gid: 5001

- name: "create common group"
  group:
    name: common
    gid: 5002

- name: "create docker group"
  group:
    name: docker
    gid: 5003

- name: "allow 'sudo make' to common"
  lineinfile:
    dest: /etc/sudoers
    line: "%common ALL=(root) /usr/bin/make"

- name: "allow 'sudo docker' to common"
  lineinfile:
    dest: /etc/sudoers
    line: "%common ALL=(root) /usr/bin/docker"

管理者ユーザーの追加

user モジュールを使います。

tasks/0021_create_admin_user.yml
---
- name: "create admin user: {{ username }}"
  user:
    name: "{{ username }}"
    uid: "{{ uid }}"
    group: admin
    groups: docker
    create_home: yes
    home: "/home/{{ username }}"
    password: "{{ password }}"
    update_password: on_create
    shell: /bin/bash

### SSH config
- include_tasks: M001_ssh_config.yml
  vars:
    home: "/home/{{ username }}"

以下は SSH の公開鍵をユーザーのホームディレクトリに書き込むための設定です。これが意外に面倒くさいです。

tasks/M001_ssh_config.yml
---
- name: "{{ username }}: remove ssh config directory"
  file:
    path: "{{ home }}/.ssh"
    state: absent

- name: "{{ username }}: create ssh config directory"
  become_user: "{{ username }}"
  file:
    path: ~/.ssh
    state: directory

- name: "{{ username }}: copy ssh pubkey to server"
  become_user: "{{ username }}"
  copy:
    src: "~/.ssh/{{ ssh_pubkey }}"
    dest: "~/.ssh/{{ ssh_pubkey }}"

- name: "{{ username }}: copy ssh pubkey to ansible variable"
  become_user: "{{ username }}"
  slurp:
    path: "~/.ssh/{{ ssh_pubkey }}"
  register: var_ssh_pubkey

- name: "{{ username }}: add ssh pubkey to authorized keys"
  become_user: "{{ username }}"
  lineinfile:
    create: yes
    dest: ~/.ssh/authorized_keys
    line: "{{ var_ssh_pubkey.content | b64decode }}"
    mode: 0600

- name: "{{ username }}: change ssh config directory mode"
  file:
    path: "{{ home }}/.ssh"
    state: directory
    mode: 0700

呼び出し側(playbook.yml)のタスクを抜粋します。

playbook.ymlから抜粋
    - include_tasks: tasks/0021_create_admin_user.yml
      vars:
        username: admin
        uid: 1001
        password: "{{ admin_password | password_hash('sha512') }}"
        ssh_pubkey: "{{ ssh_admin_pubkey }}"

admin_password, ssh_admin_pubkeyinventory[server:vars]に追加します。

inventory
[server]
192.168.1.11

[server:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_port=22
ansible_user=root
ansible_ssh_pass=XXXXXX

# クライアント側にある公開鍵の場所です。これが `~/.ssh` からホストにコピーされます。
ssh_admin_pubkey=id_admin.pub

admin_password=XXXXXX

デプロイ用ユーザーの追加

0022_create_common_user.yml
---
- name: "create common user: {{ username }}"
  user:
    name: "{{ username }}"
    uid: "{{ uid }}"
    group: common
    create_home: yes
    home: "/home/{{ username }}"
    password: "{{ password }}"
    update_password: on_create
    shell: /bin/bash

### SSH config
- include_tasks: M001_ssh_config.yml
  vars:
    home: "/home/{{ username }}"

呼び出し側です。

playbook.ymlから抜粋
    - include_tasks: tasks/0022_create_common_user.yml
      vars:
        username: sirius
        uid: 2001
        password: "{{ sirius_password | password_hash('sha512') }}"
        ssh_pubkey: "{{ ssh_common_pubkey }}"

インベントリに以下を追加します。

inventoryから抜粋
# クライアント側にある公開鍵の名前です。これが `~/.ssh` からホストにコピーされます。
ssh_common_pubkey=id_common.pub

sirius_password=XXXXXX

他にもユーザーを追加したい場合はいくらでもタスクをコピペして、パスワードを追加すればよいです。Ansible はループも記述できるので、その気になれば機械的に無数のユーザーを追加できます。

SSH の設定

うまいことどうにかする方法が見当たらなかったので、lineinfile モジュールを駆使して力技でやります。ご自身で SSH 設定を変えたい場合はこちらを頑張って変更してください

tasks/0030_ssh_security_settings.yml
---
- name: "disable password authentication"
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PasswordAuthentication"
    insertafter: "^#PasswordAuthentication"
    line: "PasswordAuthentication no"

- name: "disable empty password"
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PermitEmptyPasswords"
    insertafter: "^#PermitEmptyPasswords"
    line: "PermitEmptyPasswords no"

- name: "disable challenge response"
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^ChallengeResponseAuthentication"
    insertafter: "^#ChallengeResponseAuthentication"
    line: "ChallengeResponseAuthentication no"

- name: "disable root login"
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PermitRootLogin"
    insertafter: "^#PermitRootLogin"
    line: "PermitRootLogin no"

- name: "enable pubkey authentication"
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^PubkeyAuthentication"
    insertafter: "^#PubkeyAuthentication"
    line: "PubkeyAuthentication yes"

- name: "change ssh port to {{ new_ssh_port }}"
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "^Port"
    insertafter: "^#Port"
    line: "Port {{ new_ssh_port }}"

new_ssh_port という変数がありますが、これはインベントリに記述されたものから上書きする必要がないので、呼び出し側の playbook.yml に指定する必要はありません(ユーザー作成のときはパスワードをハッシュ化する必要があったので上書きしていました)。

inventoryから抜粋
new_ssh_port=XXXX

ファイアウォールの設定

Ubuntu の ufw ならば ufw モジュールで設定可能です。こちらも用途に合わせてご自身で変更してください

0040_ufw_security_settings
---
- name: "deny incoming"
  ufw:
    default: deny
    direction: incoming

- name: "allow SSH ({{ new_ssh_port }})"
  ufw:
    rule: allow
    log: yes
    port: "{{ new_ssh_port }}"

- name: "allow HTTP (80/TCP)"
  ufw:
    rule: allow
    proto: tcp
    port: "80"

- name: "allow HTTPS (443/TCP)"
  ufw:
    rule: allow
    proto: tcp
    port: "443"

- name: "enable firewall"
  ufw:
    state: enabled

サーバーの再起動

最後に SSH とファイアウォールのデーモンを再起動したあとで、念の為サーバーを再起動します。

9999_reboot.yml
---
- name: "reload ufw"
  ufw:
    state: reloaded

- name: "reboot sshd"
  service:
    name: sshd
    state: restarted

- name: "reboot machine"
  shell: reboot
  async: 1
  poll: 0

inventory と playbook.yml の全体

最終的なディレクトリ構造
docker-ansible
├── Dockerfile
└── playbook
    └── root
        ├── ansible.cfg
        ├── inventory
        ├── playbook.yml
        └── tasks
            ├── 0010_install_requirements.yml
            ├── 0020_create_groups.yml
            ├── 0021_create_admin_user.yml
            ├── 0022_create_common_user.yml
            ├── 0030_ssh_security_settings.yml
            ├── 0040_ufw_security_settings.yml
            ├── 9999_reboot.yml
            └── M001_ssh_config.yml
playbook.yml
- name: Configure VPS
  hosts: server
  become: True
  tasks:
    ### Install requirements
    - include_tasks: tasks/0010_install_requirements.yml

    ### create user groups
    - include_tasks: tasks/0020_create_groups.yml

    ### create users
    - include_tasks: tasks/0021_create_admin_user.yml
      vars:
        username: admin
        uid: 1001
        password: "{{ admin_password | password_hash('sha512') }}"
        ssh_pubkey: "{{ ssh_admin_pubkey }}"

    - include_tasks: tasks/0022_create_common_user.yml
      vars:
        username: sirius
        uid: 2001
        password: "{{ sirius_password | password_hash('sha512') }}"
        ssh_pubkey: "{{ ssh_common_pubkey }}"

    ### SSH security settings
    - include_tasks: tasks/0030_ssh_security_settings.yml

    ### firewall settings
    - include_tasks: tasks/0040_ufw_security_settings.yml

    ### reboot firewall and sshd
    - include_tasks: tasks/9999_reboot.yml
inventory
[server]
192.168.1.11

[server:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_port=22
ansible_user=root
ansible_ssh_pass=XXXXXX

new_ssh_port=XXXX

# 管理者ユーザー
ssh_admin_pubkey=id_admin.pub

admin_password=XXXXXX

# 一般ユーザー
ssh_common_pubkey=id_common.pub

sirius_password=XXXXXX

あとは実行するだけです。Docker でやっている人は、起動するときにローカルの ~/.ssh ディレクトリをコンテナ内にコピーするため、以下のように細工します。

$ docker container run -it --rm \
	-v $playbook:/playbook \
	-v $~/.ssh:/tmp/.ssh \
	wsuzume/docker-ansible:latest \
	bash -c "cp -r /tmp/.ssh ~/.ssh && bash"
$ ansible-playbook -i inventory playbook.yml --syntax-check
$ ansible-playbook -i inventory playbook.yml

この時点でサーバーは root ユーザーへのログインやパスワードによるログインが不可能になり、

$ ssh admin
$ ssh sirius

によって、それぞれ管理者ユーザー、デプロイ用ユーザーにアクセスできるようになっていると思います。できなければ、私か、あなたか、万象一切を支配する大宇宙の理が間違っています。できるまで運命に抗い続けてください。

2. user: SSH セットアップ完了後用 playbook

今度は Ansible で公開鍵を用いてサーバーにアクセスせねばなりませんが、これはインベントリと実行時のコマンドを少し書き換えるだけで大丈夫です。とりあえずHello world が実行できれば OK です。

playbook.yml
---
- hosts: server
  tasks:
    - debug: msg="Hello, world!"
inventory
[server]
192.168.1.11

[server:vars]
ansible_port=XXXX
ansible_sudo_pass=XXXXXX

ansibple_portは変更後のポートです。ansible_sudo_passはユーザーに指定したパスワードで、Ansible で実行するタスクが sudo で管理者権限に昇格する際に使用されます。

Docker でやっている人は、先程と同じ方法でコンテナを起動します。

$ docker container run -it --rm \
	-v $playbook:/playbook \
	-v $~/.ssh:/tmp/.ssh \
	wsuzume/docker-ansible:latest \
	bash -c "cp -r /tmp/.ssh ~/.ssh && bash"

あとは以下のコマンドで Hello world が実行できます。

$ ansible-playbook -i inventory playbook.yml -u sirius --private_key="~/.ssh/id_common"

-uはタスクを実行するサーバーのユーザー名で、--private_keyには使用する公開鍵を指定します。

GitHub に push したリポジトリについて

GitHub に push してあるリポジトリは、ここまで説明した項目に加えて少しだけ修正が加わっています。ひとつはいくつかの操作を Make で自動化してあることです。Dockerコンテナの中から以下の Makefile を実行することで、ansible-playbook ~~~ のような長ったらしいコマンドを入力しなくてもよいようになっています。また、このコマンドが使いやすいように ansible.cfgMakefile と同じ場所に移動されています。

.PHONY: root
root:
	ansible-playbook -i root/inventory/server root/playbook.yml

.PHONY: user
user:
	ansible-playbook -i user/inventory/server user/playbook.yml \
		-u castor --private-key="~/.ssh/id_common"

もうひとつは間違ってもインベントリに書き込んだ設定が流出しないように、インベントリをリポジトリの外に置いてあることです。リポジトリにはインベントリのテンプレートのみが置いてあり、Docker コンテナ起動時にリポジトリ外部の本当のインベントリをテンプレートがあった場所にマウントしています。デフォルトでは

$ make copy_inventory

コマンドの実行によって ~/config/[appname] の場所にインベントリのテンプレートがコピーされるので、こちらを編集してください。

一連の手順でサーバー自動セットアップ

それでは Conoha VPS でサーバーに Ubuntu 20.04 LTS をクリーンインストールした状態でよーいドン。

(shell) $ git clone git@github.com:wsuzume/docker-ansible
(shell) $ cd docker-ansible
(shell) $ make copy_inventory
(shell) $ make image

# Docker がイメージをビルドまたはプルしている間にインベントリを適切に編集する

(shell) $ make shell
(docker)$ make root
(docker)$ make user

記録 5 分。いい感じ。

脚注
  1. Ansible を使えば複数台のサーバーを同時にセットアップすることが可能なので、その場合は1台1台手で SSH Key を登録するよりも Ansible のほうが速いです。 ↩︎

  2. イメージ名に wsuzume/docker-ansible:latest を使用する限りは、$ docker container run のときに私のリポジトリからイメージがプルされるのでビルドは省略しても問題ありません。 ↩︎

  3. YAML形式のファイルは厳密にはファイルの開始 --- を記述しますが、これは省略しても問題ありません。 ↩︎

  4. すぎむら @sugitkさん、ありがとうございます。 ↩︎

Discussion