はじめに

本記事では、Docker Composeを用いてAlma Linux9.4で動くAnsibleの検証環境を構築する手順を記載します。

Ansibleを触りたいときは、KVMやVagrantを用いた仮想環境を使っていたのですが、自分の環境だと起動に時間がかかってしまうため、より軽量に動かすためにDockerで作ってみました。

環境

  • Ubuntu 22.04.3
  • Docker 27.0.3
  • Docker Compose version v2.28.1

構成

以下にDocker Composeの構成を記載していきます。

諸々のファイルは以下のリポジトリにもアップロードしています。

https://github.com/mozochan/ansible_dockerlab

簡単な使い方はREADMEにも記載しているので、併せて確認いただけると幸いです。

本記事では、どのような点を工夫したのか、迷ったのかということを中心に記載していきます。

ディレクトリ構成

ansible_dockerlab
├── Makefile
├── compose.yml
├── control-node
│   ├── .ansible.cfg
│   ├── Dockerfile
│   └── hosts
└── target-node
    └── Dockerfile

Docker Composeをupさせると、以下のコンテナが起動します。

コンテナ名/ホスト名 用途
control-node ansible-coreをインストールしたAnsibleコントロールノード
target-node1 Ansibleターゲットノード1
target-node2 Ansibleターゲットノード2

Ansibleコントロールノード関連ファイル

control-node/Dockerfile
FROM almalinux:9.4

# Install necessary packages
RUN dnf update -y && \
    dnf install -y epel-release && \
    dnf install -y ansible-core openssh-clients vim sudo && \
    dnf clean all

# Create ansibleuser and grant sudo privileges
RUN useradd -m -s /bin/bash ansibleuser && \
    echo "ansibleuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Setup ssh_key for target-node
RUN mkdir -p /home/ansibleuser/.ssh && \
    chmod 700 /home/ansibleuser/.ssh && \
    ssh-keygen -t rsa -f /home/ansibleuser/.ssh/id_rsa -q -C "" -N "" && \
    chown -R ansibleuser:ansibleuser /home/ansibleuser/.ssh

# Copy ansible.cfg and hosts to container
COPY --chown=ansibleuser:ansibleuser .ansible.cfg /home/ansibleuser/
COPY --chown=ansibleuser:ansibleuser hosts /home/ansibleuser/

# Set default user to ansibleuser
USER ansibleuser

# Set default directory
WORKDIR /home/ansibleuser

# Keep Docker Container running
CMD ["tail", "-F", "/dev/null"]
  • DockerイメージとしてAlma Linux9.4を使用し、ansible-coreをインストールしています。
  • AnsibleコントロールノードとSSH公開鍵認証を行うため、openssh-clientsをインストールし、鍵の作成をコントロールノードで行っています。
  • ansible_become_method sudoを試したかったので、コントロールノード - ターゲットノード間のSSH接続はansibleuserというユーザを作成し、接続する方式としています。
  • 後述する.ansible.cfgとhosts(インベントリ)を/home/ansibleuserに配置しています。
control-node/.ansble.cfg
[defaults]
inventory=/home/ansibleuser/hosts
host_key_checking=False
become=True
become_method=sudo
become_ask_pass=False
control-node/hosts
[target]
target-node1
target-node2

[target:vars]
ansible_user=ansibleuser
ansible_ssh_private_key_file=/home/ansibleuser/.ssh/id_rsa
  • コントロールノード - ターゲットノード間で通信を行うための設定を記載したansible.cfgとインベントリを配置しています。

Ansibleターゲットノード関連ファイル

target-node/Dockerfile
FROM almalinux:9.4

# Install SSH Server and sudo
RUN dnf update -y && \
    dnf install -y openssh-server sudo && \
    dnf clean all

# Create ansibleuser and grant sudo privileges
RUN useradd -m -s /bin/bash ansibleuser && \
    echo "ansibleuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

# Create .ssh directory and SSH Host Key
RUN mkdir -p /home/ansibleuser/.ssh && \
    chmod 700 /home/ansibleuser/.ssh && \
    chown ansibleuser:ansibleuser /home/ansibleuser/.ssh && \
    ssh-keygen -A

# Permit PubkeyAuthentication
RUN sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config

# Copy Public Key from control-node image
COPY --from=ansible_dockerlab-control-node --chown=ansibleuser:ansibleuser /home/ansibleuser/.ssh/id_rsa.pub /home/ansibleuser/.ssh/authorized_keys

# Start SSH Server
CMD ["/usr/sbin/sshd", "-D"]
  • コントロールノードとSSH接続するためにopenssh-serverをインストールしています。
  • ansible_become_method sudoを試すために、ansibleuserを作成し、ノンパスのsudo権限を付与しています。
  • Dockerイメージを作成する過程でsshを起動した際に「sshd: no hostkeys available – exiting.」というエラーが発生したため、ssh-keygen -Aでホストキーを作成しています。
  • コントロールノードとのSSH公開鍵認証のため、Dockerfileの COPY –fromを利用して、コントロールノードのDockerイメージから公開鍵をターゲットノードのauthorized_keysとしてコピーしています。

Makefileとcompose.yml

Makefile
.PHONY: build_control
build_control:
        @docker compose build control-node

.PHONY: build_target
build_target: build_control
        @docker compose build target-node1

.PHONY: up
up: build_target
        @docker compose up -d

.PHONY: ps
ps:
        @docker compose ps -a

.PHONY: stop
stop:
        @docker compose stop

.PHONY: down
down:
        @docker compose down

.PHONY: con
con:
        @docker compose exec control-node bash

.PHONY: tar1
tar1:
        @docker compose exec target-node1 bash

.PHONY: tar2
tar2:
        @docker compose exec target-node2 bash

.PHONY: clean
clean:
        @docker compose down --rmi all --volumes --remove-orphans
compose.yml
x-target: &target-params
  build: ./target-node
  depends_on:
    - control-node
  command: /sbin/init
  privileged: true
  tty: true

services:
  control-node:
    build: ./control-node
    container_name: control-node
    hostname: control-node
    init: true
    tty: true

  target-node1:
    <<: *target-params
    container_name: target-node1
    hostname: target-node1

  target-node2:
    <<: *target-params
    container_name: target-node2
    hostname: target-node2

  • ターゲットノードのDockerfileでCOPY –fromを使ってコントロールノードの公開鍵をコピーするために、必ずコントロールノード→ターゲットノードの順番でDockerイメージの作成が必要と考えました。
  • Docker Composeにはdepends_onという項目でサービスの依存関係を記載することができますが、buildの依存関係まで記載できないようだったので、Makefileでコントロールノード→ターゲットノードの順番でbuild + upまで実行できるコマンド(make up)を定義しました。
  • ターゲットノードでsystemctlコマンドを実行したい場合があると考え、「command: /sbin/init」「privileged: true」を設定し、無理やりコンテナ内で/sbin/initをPID1で起動しています。
  • compose.ymlで2つのターゲットノードで共通となる項目はアンカー(&target-params)を設定し、「«: *target-params」の部分で項目を継承する設定としています。

Ansibleのテスト

以下のコマンドでコンテナを起動できます。

$ make up

以下のコマンドでコントロールノードにログイン、テストを行い、“ping”: “pong"と表示されたらAnsibleでの接続はOKです。

$ make con
$ ansible all -m ping
target-node2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
target-node1 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

試しにhttpdのインストール、起動を行うplaybookも実行します。

$ vim test.yml
---
- name: Install and start httpd
  hosts: all
  become: true
  tasks:
    - name: Install httpd
      ansible.builtin.dnf:
        name: httpd
        state: present
    - name: Start httpd
      ansible.builtin.systemd_service:
        name: httpd.service
        enabled: true
        state: started

$ ansible-playbook test.yml

PLAY [Install and start httpd] ************************************************************************************************************

TASK [Gathering Facts] ********************************************************************************************************************
ok: [target-node1]
ok: [target-node2]

TASK [Install httpd] **********************************************************************************************************************
changed: [target-node2]
changed: [target-node1]

TASK [Start httpd] ************************************************************************************************************************
changed: [target-node2]
changed: [target-node1]

PLAY RECAP ********************************************************************************************************************************
target-node1               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
target-node2               : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

ターゲットノード1にログインしhttpdが起動しており、かつ自動起動がオンになっていればOKです。

$ make tar1
# systemctl status httpd
● httpd.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; preset: disabled)
     Active: active (running) since Mon 2024-11-11 15:30:34 UTC; 1min 32s ago
       Docs: man:httpd.service(8)
   Main PID: 451 (httpd)
     Status: "Total requests: 0; Idle/Busy workers 100/0;Requests/sec: 0; Bytes served/sec:   0 B/sec"
     CGroup: /docker/b8885a897f36edda0629985116b607fd9e93c6e0681ea43c1b1ac6e6d6474a33/system.slice/httpd.service
             ├─451 /usr/sbin/httpd -DFOREGROUND
             ├─452 /usr/sbin/httpd -DFOREGROUND
             ├─453 /usr/sbin/httpd -DFOREGROUND
             ├─454 /usr/sbin/httpd -DFOREGROUND
             └─455 /usr/sbin/httpd -DFOREGROUND

Nov 11 15:30:34 target-node1 systemd[1]: Starting The Apache HTTP Server...
Nov 11 15:30:34 target-node1 httpd[451]: AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.>
Nov 11 15:30:34 target-node1 httpd[451]: Server configured, listening on: port 80
Nov 11 15:30:34 target-node1 systemd[1]: Started The Apache HTTP Server.

ホストOSに戻り、以下のコマンドでDockerコンテナ、ネットワーク、イメージを丸ごと削除できます。

$ make clean

反省点や今後の展望

今回、Dockerコンテナでsystemctlを利用したいがために特権モードを与えていますが、コンテナが攻撃者に悪用された際にホストのリソースへの高度なアクセス権限を持たせてしまうため、セキュリティリスクがあります。

また、ディスクやネットワーク周りのAnsible検証環境としては、仮想マシンに分があるので、腰を据えてAnsibleの検証をしたいときは仮想マシン、サクッと確認して環境もすぐに壊していい場合はDockerを利用するというような住み分けで利用していこうかと考えています。

後は、今回は特に意識しなかったのですが、Dockerイメージのサイズが重くなってしまったので、暇があればもっと軽量なコンテナを作ってみようかと考えています。

$ docker compose images
CONTAINER           REPOSITORY                       TAG                 IMAGE ID            SIZE
control-node        ansible_dockerlab-control-node   latest              d736727bff71        330MB
target-node1        ansible_dockerlab-target-node1   latest              e0c36db1a778        259MB
target-node2        ansible_dockerlab-target-node2   latest              fb2cc3fa04f9        259MB

終わりに

Ansibleを勉強したいと思ったのに、途中からDockerの環境構築が楽しくなってしまい、目的を見失っておりました…