🐋

docker を使ってバージョンを気にせずに、インフラ管理ツールを実行してみる

2023/02/17に公開

はじめに

最近のソフトウェアはバージョンアップが速く、せっかく導入したソフトウェアも1年後には仕様が変わってしまうことも多いかと思います。インフラ管理系のツールもバージョンアップが速く仮想環境を駆使し、工夫して管理しているのではないでしょうか。
私も以前、ansible で venv を使った仮想環境での記事を投稿しました。

https://qiita.com/t_ume/items/d662837e6de7c0390252

言語毎、ソフトウェア毎に仮想環境を用意されているパターンもありますが、よくある悩みは以下かと思います。

  • 開発端末に色々なバージョンのソフトウェアが入ってしまう(俗に言う端末が汚れていく)
  • バージョンアップが必要だと煩雑になる(下手するとコードが動かなくなる、戻せない)
  • 開発者間で同じバージョンのソフトウェアが使いたい(パッチバージョンまで合わせたい)

そこで今回はコンテナの「実行環境が分離できる」ことを利用して、バージョンを気にせずに各種インフラ管理ツールを動かしていこうと思います。

今回試したソフトウェア以下の通りです。

  • Ansible
  • Terraform
  • Serverspec

実行環境

  • docker:20.10.23

Ansible

最初に構成管理ツールの Ansible です。
docker hub にあるイメージはどれも古いようです。
https://hub.docker.com/u/ansible

今回は Python のコンテナイメージを使って Dockerfile を作成し docker build して使ってみます。

作業ディレクトリ作成とDockefile作成
# $HOME : ホームディレクトリ(今回は /root)
# $_    : 直前に実行されたコマンドの最後の引数
$ mkdir $HOME/ansible && cd $_
$ vim Dockerfile

今回は以下のバージョンでコンテナイメージを作成します。

  • python : 3.9.16
  • ansible-core : 2.12.10
Dockerfile
FROM python:3.9.16-slim

RUN apt-get update \
    && apt-get install -y openssh-client \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir ansible-core==2.12.10

準備ができたので、docker build と、作成したイメージを使って ansible を実行します。

# -t : タグを付与、分かりやすい名前を。
# .  : カレントディレクトリの Dockerfile を読み込む。
$ docker build -t my-ansible:v2.12 .
Step 1/2 : FROM python:3.9.16-slim
・・・
Successfully tagged my-ansible:v2.12

# 作成したイメージを使って ansible を実行
# docker run --rm <image_name:tag> <exec command>
# --rm : コンテナ実行終了時に自動で削除 
$ docker run --rm my-ansible:v2.12 ansible --version
ansible [core 2.12.10]
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/lib/python3.9/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.9.16 (main, Feb  9 2023, 05:40:23) [GCC 10.2.1 20210110]
  jinja version = 3.1.2
  libyaml = True

無事に Ansible が実行できるイメージの作成・実行ができました。
アドホック、プレイブックで実際に Ansible を試していきます。
まずはアドホックで実行します。
今回は docker を動かしているホストマシン (192.168.10.51) に対して ping を実行します。

# -v : ホストマシンのボリュームをコンテナにマウント
#    : 今回はホストマシンの $HOME/.ssh をコンテナの /root/.ssh にマウント(認証用)
# 実行コマンド:ansible -i "192.168.10.51," 192.168.10.51 -m ping
$ docker run --rm \
-v $HOME/.ssh:/root/.ssh \
my-ansible:v2.12 \
ansible -i "192.168.10.51," 192.168.10.51 -m ping
< ここまでコマンド >

192.168.10.51 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Playbook も実行してみましょう。
以下ファイルを用意します。

inventory
myhost ansible_host=192.168.10.51
playbook.yaml
---
- hosts: myhost
  become: yes
  gather_facts: no
  collections:
  - ansible.builtin
  tasks:
  - name: ping host
    ping:

準備ができたので Playbook を実行します。

# -v $PWD:/mnt/ansible : 作業ディレクトリをコンテナにマウント。
#                      : Playbook / Invetory ファイルをコンテナに受け渡します。
# -w /mnt/ansible : コンテナ内のデフォルト作業ディレクトリを指定。
#                 : 今回は上述でマウントしたディレクトリ。
# 実行コマンド:ansible-playbook -i inventory playbook.yaml -v
#             : -v 結果出力詳細用にデバッグを指定
$ docker run --rm \
-v $HOME/.ssh:/root/.ssh \
-v $PWD:/mnt/ansible \
-w /mnt/ansible \
my-ansible:v2.12 \
ansible-playbook -i inventory playbook.yaml -v
< ここまでコマンド >

No config file found; using defaults

PLAY [myhost] ******************************************************************

TASK [ping host] ***************************************************************
ok: [myhost] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": false, "ping": "pong"}

PLAY RECAP *********************************************************************
myhost                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

無事に ping モジュールが実行できたかと思います。
今度は Ansible のバージョンを変更して再実行してみます。
まずは Dockerfile を準備します。

Dockerfile
FROM python:3.9.16-slim

RUN apt-get update \
    && apt-get install -y openssh-client \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir ansible-core==2.14.2

変更した箇所は最下行の Ansible のバージョンのみです。
レイヤーを分けたことで、apt の実行が省略され、前回よりビルドの時間が減ったかと思います。
さて、実際に docker build してイメージを作成し、再度 ansible を実行します。

# タグを変更してビルドを実行
$ docker build -t my-ansible:v2.14 .
Step 1/3 : FROM python:3.9.16-slim
・・・
Successfully tagged my-ansible:v2.14

# 作ったイメージファイルの確認
# 元の Python のイメージも並べて比較
$ docker images
REPOSITORY  TAG           IMAGE ID       CREATED          SIZE
my-ansible  v2.14         df7ebb004a9a   4 seconds ago    172MB
my-ansible  v2.12         708ff58cb9b5   19 minutes ago   172MB
python      3.9.16-slim   c1b47271448b   2 days ago       124MB

# 再度アドホックで実行
$ docker run --rm \
-v $HOME/.ssh:/root/.ssh \
my-ansible:v2.14 \
ansible -i "192.168.10.51," 192.168.10.51 -m ping
< ここまでコマンド >
192.168.10.51 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

無事にどちらのバージョンでも実行できました。

Terraform

次に Terraform です。構成管理ツールとして、幅広いクラウドサービスに対して利用されているかと思います。
Terraform は Hashicorp から Docker Hub に最新版のイメージを用意して頂いているのでそちらを利用します。

今回は Azure にリソースを作成します。
Azure をターゲットにすることから Terraform 実行には Azure の認証情報が必要となります。
Azure の認証情報はホームディレクトリの隠しディレクトリにあることので、そちらをコンテナにマウントし Terraform を実行していきます。
まずは Azure の認証情報を取得します。

# 認証情報格納用のディレクトリ作成
$ mkdir $HOME/.azure

# Azure にログイン
$ docker run --rm -v $HOME/.azure:/root/.azure mcr.microsoft.com/azure-cli az login
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code XXXXX to authenticate.

# 表示された URL にアクセスして、出力されたコード(XXXXの部分)を入力。合わせて Azure にログインしましょう。
# 認証が完了するとプロンプトが戻ります。

コンテナ終了後に $HOME/.azure ディレクトリ内に認証情報一式が格納されます。
次に Terraform の準備を行います。
ディレクトリを用意してコードを作成します。

$ mkdir $HOME/terraform && cd $_
$ vim main.tf

今回は ResourceGroup を作成するコードを記述します。

main.tf
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "test" {
  name     = "rg-test"
  location = "japaneast"
}

さて、実際に terraform コマンドを実行していきましょう。
Terraform のイメージを使った実行では、サブコマンドからの指定になります。
まずは初期化の init から。

# $HOME/.azure:/root/.azure : 前述で初期化した Azure の認証情報
# $PWD:/mnt/terraform       : カレントディレクトリを /mnt 配下にマウント
# -w /mnt/terraform         : マウントしたディレクトリを初期作業ディレクトリとして設定
# ※実行コマンドの指定はサブコマンドの init のみ。
$ docker run --rm \
-v $HOME/.azure:/root/.azure \
-v $PWD:/mnt/terraform \
-w /mnt/terraform \
hashicorp/terraform:1.3.7 \
init
< ここまでコマンド >
Unable to find image 'hashicorp/terraform:1.3.7' locally
1.3.7: Pulling from hashicorp/terraform
...
Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/azurerm...
- Installing hashicorp/azurerm v3.42.0...
- Installed hashicorp/azurerm v3.42.0 (signed by HashiCorp)
・・・
Terraform has been successfully initialized!
・・・

# 実行後にカレントディレクトリを確認すると init 時に作成されたファイルがあります。
$ ls -a
.  ..  .terraform  .terraform.lock.hcl  main.tf

# init できたので次に plan で実行計画を確認します。
$ docker run --rm \
-v $HOME/.azure:/root/.azure \
-v $PWD:/mnt/terraform \
-w /mnt/terraform \
hashicorp/terraform:1.3.7 \
plan
< ここまでコマンド >
?
x Error: building AzureRM Client: please ensure you have installed Azure CLI version 2.0.79 or newer. Error parsing json result from the Azure CLI: launching Azure CLI: exec: "az": executable file not found in $PATH.
x
x   with provider["registry.terraform.io/hashicorp/azurerm"],
x   on main.tf line 9, in provider "azurerm":
x    9: provider "azurerm" {
x
?

失敗したようですね・・・
どうやら、Azure リソースの操作には az コマンド(Azure CLI)が必要なようです。
計画を変更して Dockerfile を作成し、Terraform のイメージに Azure CLI をインストールします。
以下、今回作成した Dockerfile のポイントを記載しています。

  • インストールには以下の GitHub Issue を参考にさせて頂きました。
  • Terraform のイメージはベースが AlpineLinux で作成されているので、apk を使って必要なパッケージをインストールしています。
  • Azure CLI インストールの前に必要なパッケージをインストールし、最後に不要となったパッケージをアンインストールしてます。
  • コンテナイメージのレイヤーは積み上がる方式のため、「&&」 でコマンドを繋げています。
    • 繋げないと最後のコマンドの削除が機能せずに大きなイメージとなってしまいます。

https://github.com/Azure/azure-cli/issues/19591

Dockerfile
FROM hashicorp/terraform:1.3.7

RUN apk --no-cache add py3-pip \
    && apk --no-cache add gcc musl-dev python3-dev libffi-dev openssl-dev cargo make \
    && pip install --no-cache-dir --upgrade pip \
    && pip install --no-cache-dir azure-cli \
    && apk del --purge gcc musl-dev python3-dev libffi-dev openssl-dev cargo make

それではイメージをビルドします。参考までにイメージのサイズも載せました。
現時点(2023/02/16)では上記サイトでも議論されているようですが、Azure CLI をいれると 1 GB 超えるイメージに。。。

$ docker build -t my-tera:v1.3.7 .
...
Successfully tagged my-tera:v1.3.7

# hashicorp/terraform : 本家 Terraform のコンテナイメージ
# my-tera:v1.3.7      : 上記のコード (&& でコマンドを繋げた場合)のイメージ
# my-tera:v1.3.7-huge : && を使わずに各行を RUN で指定した場合のイメージ
$ docker images
REPOSITORY             TAG           IMAGE ID       CREATED         SIZE
hashicorp/terraform    1.3.7         9d26cd76b76f   4 weeks ago     83.6MB
my-tera                v1.3.7        9326ee5dadaa   9 minutes ago   1.11GB
my-tera                v1.3.7-huge   2061fd8eca80   2 minutes ago   2.18GB

作成したコンテナイメージを利用し、改めて terraform を実行します。

# マウント等のオプションが長いので変数でひとまとめに。
# $PWD を使っているので作業ディレクトリを移動する際には注意してください。
$ TERA_OPT="--rm -v $HOME/.azure:/root/.azure -v $PWD:/mnt/terraform -w /mnt/terraform"

# plan 実行
$ docker run $TERA_OPT my-tera:v1.3.7 plan
...
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.test will be created
  + resource "azurerm_resource_group" "test" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "rg-test"
    }

Plan: 1 to add, 0 to change, 0 to destroy.
...

# apply 実行
# -auto-approve : 最終確認を省略
$ docker run $TERA_OPT my-tera:v1.3.7 apply -auto-approve

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.test will be created
  + resource "azurerm_resource_group" "test" {
      + id       = (known after apply)
      + location = "japaneast"
      + name     = "rg-test"
    }

Plan: 1 to add, 0 to change, 0 to destroy.
azurerm_resource_group.test: Creating...
azurerm_resource_group.test: Creation complete after 1s [id=/subscriptions/xxxx/resourceGroups/rg-test]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

# 作成できたリソースグループを確認します。
$ docker run --rm \
-v $HOME/.azure:/root/.azure mcr.microsoft.com/azure-cli \
az group show -g rg-test -o table
< ここまでコマンド >
Location    Name
----------  -------
japaneast   rg-test

無事に Terraform を使ったリソース作成ができました。
一工夫が必要だったのですが、何とか対処できました。
他クラウドを利用する際にも、場合によっては対処が必要かもしれませんね。

Python

最後に Python をコンテナで実行させます。
Ansible のベースイメージでも使ったのですが、バッチ処理や API リクエストが含まれる手順の自動化にも使ったりもします。

今回はこちらの API を利用させていただき、API をリクエストしてレスポンスを出力します。
http://zipcloud.ibsnet.co.jp/doc/api

$ mkdir $HOME/python && cd $_

# 必要なパッケージのリスト作成
$ echo requests > requirements.txt

# スクリプト作成
$ vi script.py

Python のスクリプトは以下の通りです。
引数に郵便番号を渡して住所を出力させます。

script.py
import sys
import requests

zipcode = sys.argv[1]

r = requests.get('https://zipcloud.ibsnet.co.jp/api/search?zipcode=' + zipcode)
print(r.text)

本来は Dockerfile を作成しコンテナイメージを作って実行するところですが、今回は簡易的にシェル実行で済ましてみます。
それでは実行してみましょう。

# Python のバージョンを 3.9.16 で実行
# pip install -r requirements.txt : requirements.txt に書かれている Python パッケージをインストール
$ docker run --rm \
-v $PWD:/mnt/python \
-w /mnt/python \
python:3.9.16-slim \
sh -c "pip install -r requirements.txt && python script.py 1010044"
< ここまでコマンド >
Collecting requests
  Downloading requests-2.28.2-py3-none-any.whl (62 kB)
...
Installing collected packages: charset-normalizer, urllib3, idna, certifi, requests
Successfully installed certifi-2022.12.7 charset-normalizer-3.0.1 idna-3.4 requests-2.28.2 urllib3-1.26.14
...
{
        "message": null,
        "results": [
                {
                        "address1": "東京都",
                        "address2": "千代田区",
                        "address3": "鍛冶町",
                        "kana1": "トウキョウト",
                        "kana2": "チヨダク",
                        "kana3": "カジチョウ",
                        "prefcode": "13",
                        "zipcode": "1010044"
                }
        ],
        "status": 200
}

無事に実行が完了し、郵便番号「1010044」の住所がわかりました。
それではPython のバージョンを変えて実行してみましょう

# Python のバージョンを 3.10.9 にして実行
$ docker run --rm \
-v $PWD:/mnt/python \
-w /mnt/python \
python:3.10.9-slim \
sh -c "pip install -r requirements.txt; python script.py 1010044"
< ここまでコマンド >
Collecting requests
  Downloading requests-2.28.2-py3-none-any.whl (62 kB)
...
Installing collected packages: charset-normalizer, urllib3, idna, certifi, requests
Successfully installed certifi-2022.12.7 charset-normalizer-3.0.1 idna-3.4 requests-2.28.2 urllib3-1.26.14
...
{
        "message": null,
        "results": [
                {
                        "address1": "東京都",
                        "address2": "千代田区",
                        "address3": "鍛冶町",
                        "kana1": "トウキョウト",
                        "kana2": "チヨダク",
                        "kana3": "カジチョウ",
                        "prefcode": "13",
                        "zipcode": "1010044"
                }
        ],
        "status": 200
}

Python 3.10 でも無事に実行できました。
pip の警告文で差異があったものの、ライブラリのバージョンも同一のものでした。

おわりに

コンテナでのソフトウェア実行はいかがでしたでしょうか。
コンテナでの実行では実行時間がかかりそうかなと最初思ったのですが、初回のコンテナイメージダウンロードには時間がかかるものの、繰り返し実行するソフトウェアについては思った以上にサクサク動きました。

最初に説明したように、Dockerfile でのコード管理ができるため再現性、共有用途にも使えるかと思います。また、最近の CI/CD ソフトウェアでは実行環境をコンテナで指定する場合も多いため、実行環境をそのまま流用して使う、なんてのもあるではないでしょうか。

最後に、イメージの作りすぎはディスクを圧迫するので注意しましょう・・・

おまけ(Dockerfileの作り方)

環境準備するのに毎回ビルドして実行は大変かと思うので、私の作り方を共有できればと。
今回の Terraform では試し試しで行っていたので、以下の手順で行っていました。

  1. Terraform のイメージを起動し、コンテナ内でシェルを実行する。
    起動する際は --rm -it と 各種シェル(/bin/sh/bin/bash/bin/ash(Alpine))を指定して実行します。
  1. 起動後の環境で実際に環境を構築してみる。
    apt や apk などの OS のパッケージ管理や各言語のパッケージを使って環境を整えます。実際に行ったコマンドは控えて置き、Dockerfile 作成時に利用しましょう。
    exit で環境から抜けると、--rm オプションのおかげでコンテナごと綺麗に削除されます。

  2. 控えたメモで Dockefileを作成する。
    軽量にする方法や再利用性など、考えることはたくさんありますが、まずは動くものを作成するのが良いかと思います。ただしディスクと相談で・・・

何かの参考になれば幸いです。

Discussion