Jaybanuan's Blog

どうせまた調べるハメになることをメモしていくブログ

AWX 21.6.0をdocker composeで動かす

はじめに

docker composeを利用して、AWXのお試し環境を構築したときのメモ。 AWXのバージョンによって構築方法が結構異なるので、注意が必要。

なお、今回構築するのは「開発用のAWX」になる。 開発用と言っても、本番環境に対する開発環境という意味ではなく、AWX自体を開発するためのもの、という意味合い。

普通に動作させる場合はKubernetes(やMiniKube等)が前提のようだが、ちょっと試すにはオーバースペックなので、ここでは敢えて「開発用のAWX」を利用している。

環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 22.04.1 LTS"

$ uname -srvm
Linux 5.15.0-47-generic #51-Ubuntu SMP Thu Aug 11 07:51:15 UTC 2022 x86_64

$ docker version
Client: Docker Engine - Community
 Version:           20.10.18
(略)
Server: Docker Engine - Community
 Engine:
  Version:          20.10.18
(略)

AWXのバージョン

AWXのバージョンは、執筆時点の最新版の21.6.0とする。

github.com

AWXの構築

以下を参照して、AWXの環境を構築する。

github.com

(1) GitHubからソースコードをcloneする

$ git clone -b 21.6.0 https://github.com/ansible/awx.git

(2) ビルドに必要なツール/ライブラリをインストールする

DockerとDocker Composeが動作する環境であれば、あとはPythonのライブラリを整備するぐらい。 まずは、実行したコマンドを示す。

$ pip3 install -U requests urllib3
$ pip3 install docker-compose ansible "setuptools_scm[toml]"

まず最初に、Pythonのライブラリであるrequestsurllib3をアップデートする。 これはAWXのビルド手順には記載はないが、次の手順のAWXのビルドで出たエラーを地道に潰した結果、今回の環境ではアップデートが必要であることが分かったため実施している。

次に、新規にPythonのライブラリをインストールする。 docker-composeansibleはAWXのビルド手順に記載があったが、setuptools_scm[toml]についてはビルド時のエラーを潰した結果、必要であることが分かった。

ライブラリの過不足については、かなり環境に依存しそうである。

(3) AWXをビルドする

以下のコマンドを実行して、AWXのコンテナイメージをビルドする。

$ cd awx
$ make SHELL=/bin/bash PYTHON=python3 docker-compose-build

ここで、makeの引数にSHELL=/bin/bashを指定してシェルをbashに切り替えているが、理由はMakefileの中の一部記述がbashを想定しているようであるため。 ちなみにmakeのデフォルトのシェルは/bin/shである。

www.gnu.org

AWXはRedHatが開発しているが、RHEL系のディストリビューションでは/bin/sh/bin/bashへのシンボリックリンクなので、Makefileが緩く作られてしまったのだろうと推測する。 Ubuntuでは、/bin/shPOSIX互換の/bin/dashへのシンボリックリンクなので、bashの拡張構文は使えない。

また、makeの引数にPYTHON=python3を引き渡しているのは、Makefileの中でPythonインタプリタが以下のようにpython3.9と指定されているのだが、今回の環境に合わせてpython3に切り替える必要があるため。

PYTHON ?= python3.9

(4) AWXのコンテナを立ち上げる

以下のコマンドを実行して、AWXのコンテナをdocker composeで立ち上げる。

$ make SHELL=/bin/bash PYTHON=python3 docker-compose

初回起動時には、DBへの初期データ投入などの初期化処理がまだ実行されていないためエラーが大量に発生するが、初期化処理が進むにつれて(ほぼ)エラーは発生しなくなる。 自分の環境が非力なせいか、安定するまでに10分以上かかった。

(5) AWXのWeb UIを構築する

実は、前述の手順で起動されるのは、AWXのAPIサーバであり、Web UIは別になっている。 READMEの手順に従うと、APIサーバの中にWeb UIサーバも同居させることができるため、Web UIサーバ用のコンテナを立ち上げずにすむ。

Web UIサーバの構築は、実行中のAPIサーバのコンテナの中に入って行う。

$ docker exec tools_awx_1 make clean-ui ui-devel

上記のコマンドが完了した後、以下のコマンドを実行して、Web UIにログインするための管理ユーザを作成する。 入力用のプロンプトが表示されるので、必要事項を入力する。

$ docker exec -ti tools_awx_1 awx-manage createsuperuser
Username (leave blank to use 'awx'): 
Email address: 
Password: 
Password (again): 
Superuser created successfully.

動作確認

(1) AWXのWeb UIにアクセスする

ウェブブラウザで https://localhost:8043/ にアクセスする。

先の手順で作成した管理ユーザでログインして以下のようなトップ画面が表示できれば、ひとまずはAWXの構築は完了。

APIサーバでのデータの送信方式の整理

はじめに

APIサーバを設計する際に、データの送信方法、特にファイルの送信方式で悩むことがある。 ここでは、HTTPのPOSTでデータを送信する時のパターンを整理してみた。

送りたいデータ: なし

言うまでもないが、この場合は単純にPOSTのリクエストを送信すればよい。 curlの実行例は以下の通りで、明示的にオプション-Xまたは--requestでPOSTであることを指定して実行する。

$ curl \
      --request POST \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*

送りたいデータ: JSON

普通にAPIを設計するならば、JSONの送受信となる。 curlコマンドの実行例を次に示す。

$ curl \
      --header "Content-Type: application/json" \
      --data '{"name":"Jaybanuan", "age":10, "favorite": "チョコ棒"}' \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 58

{"name":"Jaybanuan", "age":10, "favorite": "チョコ棒"}

送りたいデータ: YAML

JSONの場合とおおよそ同じやり方で送信できると思うが、メディアタイプapplication/yamlがまだ正式に登録されていない。 なのでContent-Typeは、ドラフトであることを承知でapplication/yamlを使うか、適当にアプリ固有のものを定義して使うことになる。

redj.hatenablog.com

YAMLJSONと互換があるので、現時点ではYAMLを直接転送するよりは、YAMLJSONに変換して、それをContent-Type: application/jsonで送信するのが無難かと思う。

送りたいデータ: Key-Value

送りたいデータが単純なKey-Valueの場合は、HTTPの仕様に頼って送るのも一つの手である。 メリットは、クライアント側としてはcurlコマンド等で送りやすいことと、サーバ側としてはWebアプリのフレームワークがある程度処理を肩代わりしてくれること。 デメリットは、利用者が通信プロトコル(HTTP)をある程度理解する必要があること。

(1) Content-Type: application/x-www-form-urlencodedを利用

この場合は、Key-Valueの個々のデータを&で連結したものが、HTTPボディとして送信される。 curlコマンドでは、以下のオプションを利用することでKey-Valueのデータを送付できる。

  • -d または --data
  • --data-urlencode
  • --data-raw

これらのオプションの利用時は、自動的に以下が設定される。

  • HTTPメソッドはPOSTに設定
  • HTTPヘッダのContent-Typeapplication/x-www-form-urlencodedに設定

curlコマンドの実行例を次に示す。

$ curl \
      --data name=jaybanuan \
      --data-raw age=10 \
      --data-urlencode "favorite=チョコ棒" \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 67
Content-Type: application/x-www-form-urlencoded

name=jaybanuan&age=10&favorite=%E3%83%81%E3%83%A7%E3%82%B3%E6%A3%92

(2) Content-Type: multipart/form-dataを利用

この場合は、Key-Valueの個々のデータは、マルチパートとして送信される。 curlコマンドでは、以下のオプションを利用することでKey-Valueのデータを送付できる。

  • -F または --form

これらのオプションの利用時は、自動的に以下が設定される。

  • HTTPメソッドはPOSTに設定
  • HTTPヘッダのContent-Typemultipart/form-dataに設定

curlコマンドの実行例を次に示す。

$ curl \
      --form name=jaybanuan \
      --form age=10 \
      --form "favorite=チョコ棒" \
      http://localhost:8080/

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 351
Content-Type: multipart/form-data; boundary=------------------------e297819c8d0c28ba

--------------------------e297819c8d0c28ba
Content-Disposition: form-data; name="name"

jaybanuan
--------------------------e297819c8d0c28ba
Content-Disposition: form-data; name="age"

10
--------------------------e297819c8d0c28ba
Content-Disposition: form-data; name="favorite"

チョコ棒
--------------------------e297819c8d0c28ba--

application/x-www-form-urlencodedの場合と比べるとHTTPボディが重くなっている。 しかし、Key-Valueの送信に加えて、後述のようにファイルも送信する場合は、こちらを利用する必要がある。

送りたいデータ: ファイル

ここで、以下の内容の2つのファイルをサーバに送りたいとする。 1つ目はgreeting.txtというファイル名のテキストファイルで、内容は次のとおり。

Hello, World!

2つ目はgreeting.jsonというファイル名のJSONで、内容は次のとおり。

{
    "greeting": "Hello, World!"
}

(1) Content-Type: multipart/form-dataを利用

HTTPのマルチパートにより、ファイルを転送する。 HTTPでファイル送信を行う際の一般的な方法であり、広く使われている。 curlコマンドの実行例を次に示す。

$ curl \
      --form greeting-en=@greeting.txt \
      --form "greeting-json=@greeting.json;type=application/json" \
      http://localhost:8080/

各パートのContent-Typeを指定したい場合は、ファイル名の後ろにtypeで指定する。 また、各パートにその他のヘッダを付与したい場合はheadersで指定する。

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Length: 405
Content-Type: multipart/form-data; boundary=------------------------7aa5ee307312820c

--------------------------7aa5ee307312820c
Content-Disposition: form-data; name="greeting-en"; filename="greeting.txt"
Content-Type: text/plain

Hello, World!
--------------------------7aa5ee307312820c
Content-Disposition: form-data; name="greeting-json"; filename="greeting.json"
Content-Type: application/json

{
    "greeting": "Hello, World!"
}
--------------------------7aa5ee307312820c--

この方式のメリットは、大抵のHTTPクライアントツールやWebアプリのフレームワークはマルチパートをサポートしているため、それを活用することで自力での実装部分を減らせること。 一方でデメリットは、HTTPの都合が色濃く出るので、JSONベースのAPIでのファイルアップロードに利用すると、統一感が出ないこと。

(2) ファイルをJSONに詰め込んで送信

ファイル送信用のJSONベースのAPIを設計して、ファイル名やファイルの内容をJSONに詰め込んで送信する。 以下は簡素なJSONベースのファイル送信APIの例。 この例では、ファイルの内容はJSONの文字列として扱えるようにBASE64エンコードしている。

[
    {
        "filename": "greeting.txt",
        "content": "SGVsbG8sIFdvcmxkIQ=="
    },
    {
        "filename": "greeting.json",
        "content": "ewogICAgImdyZWV0aW5nIjogIkhlbGxvLCBXb3JsZCEiCn0="
    }
]

この時のHTTPリクエストは次のようになる。

POST / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 204

[    {        "filename": "greeting.txt",        "content": "SGVsbG8sIFdvcmxkIQ=="    },    {        "filename": "greeting.json",        "content": "ewogICAgImdyZWV0aW5nIjogIkhlbGxvLCBXb3JsZCEiCn0="    }]

上記のcurlの実行例では、オプション--dataを利用しているので、JSONから改行がなくなっている。 JSONの場合は改行に依存しないので問題ないが、余計な変換をしたくない場合はオプション--data-rawを利用すればよい。

マルチパートの時の裏返しになるが、この方式のメリットは、JSONベースのAPIでの実装になり統一感が出ること。 一方でデメリットは、サーバ側ではAPIの設計から実装まで自力で行う必要があることと、クライアント側ではJSONの組み立てが面倒でcurlコマンドで手軽にファイル送信ができないこと。

参考

weblabo.oscasierra.net

YAMLのMedia Type (application/yaml)

APIを設計していて、YAMLのMedia Type (application/yaml)って見たことないけどあったっけ?と思って調べてみた。 2022年8月時点では、YAMLはまだIETFのMedia Typeの一覧に載っていない。

どうもイタリア政府のDigital Transformation Departmentという機関がYAMLのMedia Typeを登録しようとしているようで、現時点ではまだドラフト。 ぜひ登録してほしい。

Nginx Unitのコンテナのdocker-entrypoint.sh

Nginx Unitをベースとしたコンテナイメージを作る際に、/docker-entrypoint.dというドロップインディレクトリにNginx Unitの設定ファイルを配置する。

unit.nginx.org

ただ、詳細な説明がないので、挙動の確認はドキュメント読むよりもコード読んだほうが早い。 以下にNginx Unitのdocker-entrypoint.shのリンクを貼っておく。

github.com

ContentType: application/x-www-form-urlencodedの使い道

curlでPOSTのbodyを送るときに、オプションの付与の仕方が何種類かあって、その中でURLエンコードってなんだっけとなって悩んだ。 はるか昔に調べて納得した記憶はあるが、詳細は忘れてしまった。 ズバリの回答が以下にある。

teratail.com

当該部分を引用しておく。

さてではコンテントタイプがapplication/x-www-form-urlencodedのときにURLエンコードが必要となる理由。これはkey=value形式のデータを&区切りで並べる以上、value部に'&'が入っているとvalueの一部なのか区切り文字なのか見分けがつかないからエスケープする必要があるという、HTTPよりは高レベルのアプリケーション上の要求でとなります。

つまり、HTMLのformをうまく取り扱うためのContentTypeということが分かる。 逆に言うと、form以外では使い道はないと思う。

純粋にPOSTでデータを送りたいときは、

  • 単純にbodyにデータを突っ込んでデータに合わせたContentType (JSONならapplication/json)を設定するか、
  • マルチパートを使う

ことになるはず。