docker-composeでMySQL認証エラーが出た時の対応

背景

以前の記事でdocker-composeを利用してDjango+MySQLの環境を作成しました。

今回、別のプロジェクトでDjango+MySQLのアプリケーションを作成することにしたので、久々環境を構築したところ、以下のエラーにハマったので備忘録として記載します。

エラー内容

エラーはこんな感じでした。

1
django-web-1  | django.db.utils.OperationalError: (1044, "Access denied for user 'django'@'%' to database 'django'")

認証に失敗しているようです…

docker-compose.ymlの確認

まずMySQLコンテナの認証を設定しているdocker-compose.ymlを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
version: '3'

services:
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: 'django'
MYSQL_PASSWORD: 'django'
MYSQL_DATABASE: 'django'
ports:
- 3306:3306
volumes:
- mysql:/var/lib/mysql
web:
build: .
command: python3 manage.py runserver 0.0.0.0:8000
volumes:
- .:/code
- site-packages:/usr/local/lib/python3.10/site-packages
- usr-local-bin:/usr/local/bin
ports:
- "8000:8000"
links:
- db

volumes:
site-packages:
driver: local
usr-local-bin:
driver: local
mysql:
driver: local

問題なさそうです。

Djangoのデータベース接続設定の確認

次に接続に行っているDjango側の設定を確認します。プロジェクト配下のsettings.pyを確認します。

1
2
3
4
5
6
7
8
9
10
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django',
'USER': 'django',
'PASSWORD': 'django',
'HOST': 'db',
'PORT': 3306,
}
}

こちらも問題なさそうです。

原因

だいぶ長い間ハマって気づいたのは、mysqlのvolumeが意図しない動作を引き起こしているのではないかということでした。

一度volumeを削除してみます。

1
$ docker volume rm 該当のvolume

(volumeの確認はdocker volume lsで行いました)

削除後にdocker-compose upを行うと、認証のエラーはなくなり、無事アプリケーションが起動しました。

公式ドキュメントを確認

MySQLコンテナのドキュメントEnvironment Variablesを確認するとちゃんと書いてありました。

Do note that none of the variables below will have any effect if you start the container with a data directory that already contains a database: any pre-existing database will always be left untouched on container startup.

Google翻訳にすると

すでにデータベースが含まれているデータディレクトリでコンテナを起動した場合、以下の変数はいずれも効果がないことに注意してください。既存のデータベースは、コンテナの起動時に常に変更されません。

ということなので、認証情報を変更する場合は毎回volumeを削除する必要があるということでした。

まとめ

MySQLコンテナはvolumeを使っている場合、認証情報は環境変数で変更することはできないことがわかりました。(最初の1回だけ環境変数で指定)

認証情報だけではなく、データベース名なども変更する場合は、既存のvolumeを削除してから起動すると、環境変数で指定した値が反映されます。

Amazon EventBridgeを使ってAWS Batchを起動する

背景

少し前からAWS Batchを使ってDjangoのカスタムコマンドを実行する仕組みを実装していました。今回はAWS BatchをAmazon EventBridgeを使って実行してみようと思います。

一つ気になるところがありまして、AWS Batchで利用するコンテナイメージやジョブ定義ファイルはGitHub Actionsを用いてデプロイしています。(以前の記事参照

ジョブ定義ファイルを更新した場合、Amazon EventBridgeも更新する必要があるのかどうかを検証してみたいと思います。

Amazon EventBridgeの設定

まず、EventBridgeの設定を行います。

マネジメントコンソールからAmazon EventBridgeを選択し、左メニューにあるルールをクリックします。

イベントバスはdefaultを選択したままにしておきます。

ルールの一覧の右上にルールを作成というボタンがあるのでそれを押下します。

ルールの作成

ルールの作成画面に遷移します。

名前と説明は適当に入力します。名前は必須なので、必ず入力します。

パターンの定義では大きく2種類あって、イベントパターンとスケジュールがあります。

今回はCronのように特定の時間でAWS Batchを起動したいので、スケジュールを選択します。

スケジュールの設定

スケジュールをクリックすると、さらに下に設定項目が表示されます。
固定速度ごとCron式が選択できます。Cronのように設定したいので、Cron式を選択します。

試しに設定してみます。5分ごとに実行するようにしてみます。

1
0/5 * * * * *

一見あっていそうですが、この記述方法では設定できません…

なにがダメなのかというと、曜日の設定は、日が*の場合は、*ではなく、?となっています。曜日を指定する場合は、日を?にしないといけなかったりします。この辺りの設定についてはドキュメントを読んでみてください。

また、Cronの設定が正しいと、実行する日時が下に表示されます。
Cron式の設定

設定が正しいかどうかは実行日時が表示されるかどうかで判断することができます。時刻の表記はGMTではなくローカルタイムゾーンにしておいたほうがわかりやすいと思います。基本的には9時間を引いてCron式を設定することになります。

イベントバスの設定

イベントバスはAWSのデフォルトのイベントバスのままにします。

ターゲットの設定

次にターゲットの設定を行います。今回のターゲットはAWS Batchなので、ターゲットのところでバッチジョブのキューを選択します。

すると、ジョブキュージョブ定義,ジョブ名が必須の入力項目として現れます。

ジョブキュージョブ名はジョブキューのARNとジョブ名を入力すれば良いのですが、注意が必要なのはジョブ定義です。

ジョブ定義はジョブ定義の名前またはARNと書かれています。ジョブ定義のARNを参照すると、

1
arn:aws:batch:ap-northeast-1:xxxxxxxxxxxx:job-definition/jobname:1

のように最後にrevisionがついています。revisionまで指定してしまうと、GitHub Actionsでのデプロイでジョブ定義ファイルが更新されて新しいrevisionが作成された場合、EventBridgeから起動できなくなってしまいます。

では、どうすればいいのかというと、方法は2つあります。

  • ジョブ定義の名前で指定する
  • ARNで指定する場合は:revisionを書かない(jobnameまでにする)

この2つの方法で設定すると、GitHub Actionsでジョブ定義ファイルを更新しても、EventBridgeが最新のジョブ定義ファイルで実行してくれます。

ロールの設定

ロールは新しいロールを追加します。

再試行ポリシーとデッドレターキューの設定

この辺り、細かく設定できるので、うまく実行できなかった場合は通知を受け取ることもできると思いますが、一旦スキップします。

最後に作成ボタンを押下してルールを作成します。

バッチ実行確認

指定した時間にジョブが起動し、正しく実行されることを確認しました。

また、GitHub Actionsでデプロイし、ジョブ定義ファイルを更新した後も正しく実行されることを確認しました。

まとめ

Amazon EventBridgeを使ってAWS Batchを起動する方法を確認しました。

最初はGitHub Actionsでジョブ定義ファイルをデプロイした後に、Amazon EventBridgeの設定を修正する処理を追加しないといけないかなと思っていたのですが、最新のrevisionを指定できるようになっていてよかったです。

Poetryを使ってみる

背景

以前の記事では、Dockerコンテナ上でPipenvを用いてパッケージ管理をしようとしていました。

利用していて、難点だったのが、仮想環境を用いずPipfile.lockを利用してパッケージをインストールできないという点でした。

--systemというオプションを指定しても、Pipfile.lockを利用する場合は以下のようになってしまっていました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web python -m pipenv sync --system      
Creating a virtualenv for this project...
Pipfile: /code/Pipfile
Using /usr/local/bin/python3 (3.10.0) to create virtualenv...
⠦ Creating virtual environment...created virtual environment CPython3.10.0.final.0-64 in 391ms
creator CPython3Posix(dest=/root/.local/share/virtualenvs/code-_Py8Si6I, clear=False, no_vcs_ignore=False, global=False)
seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/root/.local/share/virtualenv)
added seed packages: pip==21.3.1, setuptools==58.3.0, wheel==0.37.0
activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator

✔ Successfully created virtual environment!
Virtualenv location: /root/.local/share/virtualenvs/code-_Py8Si6I
Installing dependencies from Pipfile.lock (d9bd7e)...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 15/15 — 00:00:02
All dependencies are now up-to-date!

しかし、やはりlockファイルを利用してバージョンを固定したいと思いました。後輩に相談してみると、poetryというパッケージ管理ツールがあるので試してみてはどうか?と教えてもらったので試してみます。

既存のパッケージの削除

パッケージはDocker volume内に残っていますので、一旦既存パッケージを削除します。

1
2
$ docker-compose run --rm web python -m pip freeze > pip_list
$ docker-compose run --rm web python -m pip uninstall -r pip_list -y

-yをつけることで、uninstallしていいかどうかの確認を省略できます。

アンインストールできたかを確認します

1
2
3
4
5
6
$ docker-compose run --rm web python -m pip list
Package Version
---------- -------
pip 21.3.1
setuptools 57.5.0
wheel 0.37.0

アンインストールできていることが確認できました。

poetryのインストール

ドキュメントを確認すると、いろんな仮想環境でpoetryを用いる場合は、インストーラーを使った方がよいと書いてあります。

しかし、わたしはDockerを利用するため、特定のPythonのバージョンに依存してよいかと思いますので、非推奨になっているpipを用いてインストールさせてもらいます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
$ docker-compose run --rm web python -m pip install poetry
Collecting poetry
Downloading poetry-1.1.11-py2.py3-none-any.whl (175 kB)
|████████████████████████████████| 175 kB 3.3 MB/s
Collecting cachy<0.4.0,>=0.3.0
Downloading cachy-0.3.0-py2.py3-none-any.whl (20 kB)
Collecting clikit<0.7.0,>=0.6.2
Downloading clikit-0.6.2-py2.py3-none-any.whl (91 kB)
|████████████████████████████████| 91 kB 4.6 MB/s
Collecting pkginfo<2.0,>=1.4
Downloading pkginfo-1.7.1-py2.py3-none-any.whl (25 kB)
Collecting crashtest<0.4.0,>=0.3.0
Downloading crashtest-0.3.1-py3-none-any.whl (7.0 kB)
Collecting html5lib<2.0,>=1.0
Downloading html5lib-1.1-py2.py3-none-any.whl (112 kB)
|████████████████████████████████| 112 kB 6.9 MB/s
Collecting requests-toolbelt<0.10.0,>=0.9.1
Downloading requests_toolbelt-0.9.1-py2.py3-none-any.whl (54 kB)
|████████████████████████████████| 54 kB 3.4 MB/s
Collecting poetry-core<1.1.0,>=1.0.7
Downloading poetry_core-1.0.7-py2.py3-none-any.whl (424 kB)
|████████████████████████████████| 424 kB 6.8 MB/s
Collecting shellingham<2.0,>=1.1
Downloading shellingham-1.4.0-py2.py3-none-any.whl (9.4 kB)
Collecting virtualenv<21.0.0,>=20.0.26
Downloading virtualenv-20.10.0-py2.py3-none-any.whl (5.6 MB)
|████████████████████████████████| 5.6 MB 7.8 MB/s
Collecting keyring<22.0.0,>=21.2.0
Downloading keyring-21.8.0-py3-none-any.whl (32 kB)
Collecting cachecontrol[filecache]<0.13.0,>=0.12.4
Downloading CacheControl-0.12.10-py2.py3-none-any.whl (20 kB)
Collecting pexpect<5.0.0,>=4.7.0
Downloading pexpect-4.8.0-py2.py3-none-any.whl (59 kB)
|████████████████████████████████| 59 kB 3.6 MB/s
Collecting tomlkit<1.0.0,>=0.7.0
Downloading tomlkit-0.7.2-py2.py3-none-any.whl (32 kB)
Collecting cleo<0.9.0,>=0.8.1
Downloading cleo-0.8.1-py2.py3-none-any.whl (21 kB)
Collecting requests<3.0,>=2.18
Downloading requests-2.26.0-py2.py3-none-any.whl (62 kB)
|████████████████████████████████| 62 kB 941 kB/s
Collecting packaging<21.0,>=20.4
Downloading packaging-20.9-py2.py3-none-any.whl (40 kB)
|████████████████████████████████| 40 kB 3.8 MB/s
Collecting msgpack>=0.5.2
Downloading msgpack-1.0.2.tar.gz (123 kB)
|████████████████████████████████| 123 kB 7.5 MB/s
Preparing metadata (setup.py) ... done
Collecting lockfile>=0.9
Downloading lockfile-0.12.2-py2.py3-none-any.whl (13 kB)
Collecting pylev<2.0,>=1.3
Downloading pylev-1.4.0-py2.py3-none-any.whl (6.1 kB)
Collecting pastel<0.3.0,>=0.2.0
Downloading pastel-0.2.1-py2.py3-none-any.whl (6.0 kB)
Collecting six>=1.9
Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Collecting webencodings
Downloading webencodings-0.5.1-py2.py3-none-any.whl (11 kB)
Collecting jeepney>=0.4.2
Downloading jeepney-0.7.1-py3-none-any.whl (54 kB)
|████████████████████████████████| 54 kB 792 kB/s
Collecting SecretStorage>=3.2
Downloading SecretStorage-3.3.1-py3-none-any.whl (15 kB)
Collecting pyparsing>=2.0.2
Downloading pyparsing-3.0.5-py3-none-any.whl (97 kB)
|████████████████████████████████| 97 kB 5.9 MB/s
Collecting ptyprocess>=0.5
Downloading ptyprocess-0.7.0-py2.py3-none-any.whl (13 kB)
Collecting certifi>=2017.4.17
Using cached certifi-2021.10.8-py2.py3-none-any.whl (149 kB)
Collecting charset-normalizer~=2.0.0
Downloading charset_normalizer-2.0.7-py3-none-any.whl (38 kB)
Collecting idna<4,>=2.5
Downloading idna-3.3-py3-none-any.whl (61 kB)
|████████████████████████████████| 61 kB 5.1 MB/s
Collecting urllib3<1.27,>=1.21.1
Downloading urllib3-1.26.7-py2.py3-none-any.whl (138 kB)
|████████████████████████████████| 138 kB 8.5 MB/s
Collecting distlib<1,>=0.3.1
Using cached distlib-0.3.3-py2.py3-none-any.whl (496 kB)
Collecting filelock<4,>=3.2
Downloading filelock-3.3.2-py3-none-any.whl (9.7 kB)
Collecting platformdirs<3,>=2
Using cached platformdirs-2.4.0-py3-none-any.whl (14 kB)
Collecting backports.entry-points-selectable>=1.0.4
Downloading backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl (6.2 kB)
Collecting cryptography>=2.0
Downloading cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl (3.5 MB)
|████████████████████████████████| 3.5 MB 8.5 MB/s
Collecting cffi>=1.12
Downloading cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (446 kB)
|████████████████████████████████| 446 kB 10.2 MB/s
Collecting pycparser
Downloading pycparser-2.21-py2.py3-none-any.whl (118 kB)
|████████████████████████████████| 118 kB 8.8 MB/s
Building wheels for collected packages: msgpack
Building wheel for msgpack (setup.py) ... done
Created wheel for msgpack: filename=msgpack-1.0.2-cp310-cp310-linux_x86_64.whl size=15833 sha256=fee9473ca391ef73f69dde97df9a6829fdd66addbd0cfa04b1cc7cd40600d620
Stored in directory: /root/.cache/pip/wheels/c6/4d/b6/98ae445826f0944cb9a6a963bceb7259c3b7467fd4cb8df818
Successfully built msgpack
Installing collected packages: pycparser, urllib3, idna, charset-normalizer, cffi, certifi, requests, pylev, pastel, msgpack, jeepney, cryptography, crashtest, webencodings, six, SecretStorage, pyparsing, ptyprocess, platformdirs, lockfile, filelock, distlib, clikit, cachecontrol, backports.entry-points-selectable, virtualenv, tomlkit, shellingham, requests-toolbelt, poetry-core, pkginfo, pexpect, packaging, keyring, html5lib, cleo, cachy, poetry
Successfully installed SecretStorage-3.3.1 backports.entry-points-selectable-1.1.1 cachecontrol-0.12.10 cachy-0.3.0 certifi-2021.10.8 cffi-1.15.0 charset-normalizer-2.0.7 cleo-0.8.1 clikit-0.6.2 crashtest-0.3.1 cryptography-35.0.0 distlib-0.3.3 filelock-3.3.2 html5lib-1.1 idna-3.3 jeepney-0.7.1 keyring-21.8.0 lockfile-0.12.2 msgpack-1.0.2 packaging-20.9 pastel-0.2.1 pexpect-4.8.0 pkginfo-1.7.1 platformdirs-2.4.0 poetry-1.1.11 poetry-core-1.0.7 ptyprocess-0.7.0 pycparser-2.21 pylev-1.4.0 pyparsing-3.0.5 requests-2.26.0 requests-toolbelt-0.9.1 shellingham-1.4.0 six-1.16.0 tomlkit-0.7.2 urllib3-1.26.7 virtualenv-20.10.0 webencodings-0.5.1
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv

たくさんのパッケージがインストールされました。確認してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ docker-compose run --rm web python -m pip list
Package Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.1
CacheControl 0.12.10
cachy 0.3.0
certifi 2021.10.8
cffi 1.15.0
charset-normalizer 2.0.7
cleo 0.8.1
clikit 0.6.2
crashtest 0.3.1
cryptography 35.0.0
distlib 0.3.3
filelock 3.3.2
html5lib 1.1
idna 3.3
jeepney 0.7.1
keyring 21.8.0
lockfile 0.12.2
msgpack 1.0.2
packaging 20.9
pastel 0.2.1
pexpect 4.8.0
pip 21.3.1
pkginfo 1.7.1
platformdirs 2.4.0
poetry 1.1.11
poetry-core 1.0.7
ptyprocess 0.7.0
pycparser 2.21
pylev 1.4.0
pyparsing 3.0.5
requests 2.26.0
requests-toolbelt 0.9.1
SecretStorage 3.3.1
setuptools 57.5.0
shellingham 1.4.0
six 1.16.0
tomlkit 0.7.2
urllib3 1.26.7
virtualenv 20.10.0
webencodings 0.5.1
wheel 0.37.0

ショートカットも確認します

1
2
$ docker-compose run --rm web which poetry
/usr/local/bin/poetry

無事インストールできました。

プロジェクトの設定

公式のドキュメントを読むと、まずプロジェクトの設定を行なっています。

プロジェクトのディレクトリはすでに存在しているので、ドキュメントにあるこちらの方法で雛形を作成します。

現在プロジェクトのルートにいるので、そのままコマンドを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
$ docker-compose run --rm web poetry init

This command will guide you through creating your pyproject.toml config.

Package name [code]:
Version [0.1.0]:
Description []:
Author [None, n to skip]:
expected string or bytes-like object
Author [None, n to skip]: n
License []:
Compatible Python versions [^3.10]:

Would you like to define your main dependencies interactively? (yes/no) [yes]
You can specify a package in the following forms:
- A single name (requests)
- A name and a constraint (requests@^2.23.0)
- A git url (git+https://github.com/python-poetry/poetry.git)
- A git url with a revision (git+https://github.com/python-poetry/poetry.git#develop)
- A file path (../my-package/my-package.whl)
- A directory (../my-package/)
- A url (https://example.com/packages/my-package-0.1.0.tar.gz)

Search for package to add (or leave blank to continue):

Would you like to define your development dependencies interactively? (yes/no) [yes]
Search for package to add (or leave blank to continue):

Generated file

[tool.poetry]
name = "code"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.10"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"


Do you confirm generation? (yes/no) [yes]

特になにも入力せずデフォルト値で作成しました。あとから記載する形でよいかと思います。

パッケージの追加

パッケージの追加はpoetry addコマンドで行います。もともと入っていたboto3をインストールしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker-compose run --rm web poetry add boto3
Creating virtualenv code-MATOk_fk-py3.10 in /root/.cache/pypoetry/virtualenvs
Using version ^1.20.1 for boto3

Updating dependencies
Resolving dependencies... (2.7s)

Writing lock file

Package operations: 11 installs, 0 updates, 0 removals

• Installing six (1.16.0)
• Installing jmespath (0.10.0)
• Installing python-dateutil (2.8.2)
• Installing urllib3 (1.26.7)
• Installing botocore (1.23.1)
• Installing certifi (2021.10.8)
• Installing charset-normalizer (2.0.7)
• Installing idna (3.3)
• Installing s3transfer (0.5.0)
• Installing boto3 (1.20.1)
• Installing requests (2.26.0)

virtualenvを作成してしまっているのでインストールされません…
virtualenvを利用しないようにします。

poetryの設定

poetryの設定はpoetry configコマンドで行います。

設定項目については、こちらに記載があります。

virtualenvを利用しないようにするには以下のようにします

1
$ docker-compose run --rm web poetry config virtualenvs.create false --local

--localのオプションがないと反映されませんでした)

実行後の設定を確認します

1
2
3
4
5
6
7
$ docker-compose run --rm web poetry config --list
cache-dir = "/root/.cache/pypoetry"
experimental.new-installer = true
installer.parallel = true
virtualenvs.create = false
virtualenvs.in-project = null
virtualenvs.path = "{cache-dir}/virtualenvs" # /root/.cache/pypoetry/virtualenvs

再度パッケージを追加してみる

ではこの設定でパッケージの追加を行います。同じようにboto3を追加してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web poetry add boto3
Skipping virtualenv creation, as specified in config file.
Using version ^1.20.3 for boto3

Updating dependencies
Resolving dependencies... (0.8s)

Package operations: 5 installs, 0 updates, 0 removals

• Installing jmespath (0.10.0)
• Installing python-dateutil (2.8.2)
• Installing botocore (1.23.3)
• Installing s3transfer (0.5.0)
• Installing boto3 (1.20.3)
$

インストール作業は完了しました。念のため、pip listコマンドで確認します

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ docker-compose run --rm web python -m pip list        
Package Version
--------------------------------- ---------
backports.entry-points-selectable 1.1.1
boto3 1.20.3
botocore 1.23.3
CacheControl 0.12.10
cachy 0.3.0
certifi 2021.10.8
cffi 1.15.0
charset-normalizer 2.0.7
cleo 0.8.1
clikit 0.6.2
crashtest 0.3.1
cryptography 35.0.0
distlib 0.3.3
filelock 3.3.2
html5lib 1.1
idna 3.3
jeepney 0.7.1
jmespath 0.10.0
keyring 21.8.0
lockfile 0.12.2
msgpack 1.0.2
packaging 20.9
pastel 0.2.1
pexpect 4.8.0
pip 21.3.1
pkginfo 1.7.1
platformdirs 2.4.0
poetry 1.1.11
poetry-core 1.0.7
ptyprocess 0.7.0
pycparser 2.21
pylev 1.4.0
pyparsing 3.0.5
python-dateutil 2.8.2
requests 2.26.0
requests-toolbelt 0.9.1
s3transfer 0.5.0
SecretStorage 3.3.1
setuptools 57.5.0
shellingham 1.4.0
six 1.16.0
tomlkit 0.7.2
urllib3 1.26.7
virtualenv 20.10.0
webencodings 0.5.1
wheel 0.37.0

正しくインストールされていることを確認しました。

Djangoもインストールしておきます

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web poetry add django
Skipping virtualenv creation, as specified in config file.
Using version ^3.2.9 for Django

Updating dependencies
Resolving dependencies... (1.7s)

Writing lock file

Package operations: 4 installs, 0 updates, 0 removals

• Installing asgiref (3.4.1)
• Installing pytz (2021.3)
• Installing sqlparse (0.4.2)
• Installing django (3.2.9)

devパッケージの追加

linterなどdevelopment環境でしか使用しないパッケージを追加します。

development dependencyに設定するには--devを指定します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker-compose run --rm web poetry add flake8 --dev
Skipping virtualenv creation, as specified in config file.
Using version ^4.0.1 for flake8

Updating dependencies
Resolving dependencies... (3.2s)

Writing lock file

Package operations: 4 installs, 0 updates, 0 removals

• Installing mccabe (0.6.1)
• Installing pycodestyle (2.8.0)
• Installing pyflakes (2.4.0)
• Installing flake8 (4.0.1)

インストールできました。pyproject.tomlを確認すると、tool.poetry.dev-dependenciesに記載されいていることがわかります。

1
2
[tool.poetry.dev-dependencies]
flake8 = "^4.0.1"

その他、isort, blackもインストールしておきます。

依存関係の設定

インストールしたパッケージはpoetry.lockに記載されています。これをバージョン管理することによって、いつでも環境を再現できるようになります。

再現するには

1
$ docker-compose run --rm web poetry install

とします。dev-dependenciesをインストールしたくない場合は--no-devをオプションに指定します。

まとめ

今回pythonのパッケージマネージャとしてpoetryを試してみました。Pipenvのときのような、lockファイルの作成でハマったりしなかったですし、とてもシンプルで使いやすいという印象でした。

今後しばらくはpoetryを使っていきたいと思います。

永続的なクレデンシャルではなくIAMロールを利用してGithubActionsで認証する

背景

前回、GitHub ActionsでAWS Batchのデプロイを行いました。

AWSの認証では事前にGitHubでSECRETを設定し、GitHub Actionsの中で、ACCESS_KEY_IDSECRET_ACCESS_KEYを設定したSECRETとして利用することで実現していました。

しかし、今は永続的なCredentialsを使うことなく、AWSの認証ができるようです。

https://twitter.com/toricls/status/1438120050167189510

今回は、永続的なCredentialsを用いずにAWSの認証を行ってみようと思います。

IAM Roleの設定

クラスメソッドさんの記事がまとまっていたので、参考にさせていただきました。

GitHub ActionsでAWSの永続的なクレデンシャルを渡すことなくIAM Roleが利用できるようになったようです

ただ、IAM Roleの設定がCloudFormationで書かれていて、CloudFormationに明るくない自分としてはちょっとどうしていいのかわからない状態です。

なので、このCloudFormationの設定をなんとかして、ポチポチで実現しようと思います。

IDプロバイダの追加

まずIDプロバイダの追加を行います。

マネジメントコンソールでIAM -> IDプロバイダを選択します。

  • プロバイダのタイプはOpenID Connectを選択
  • プロバイダのURLにhttps://https://token.actions.githubusercontent.com
  • 対象者にsigstore

と入力し、プロバイダの追加ボタンを押下します。

これでプロバイダの作成は完了です。

IAM Roleの作成

次にロールの作成を行います。

IAM -> IDプロバイダから先程作成したIDプロバイダを選択すると、右上にロールの割り当てというボタンがあるので、押下します。

すると、新しいロールを作成か、既存のロールを使用するを選択できるので、新しいロールを作成を選択します。するとロールの作成画面に遷移します。

ロールの作成画面にて、信頼されたエンティティの種類を選択となっていて、すでにウェブIDが選択されており、IDプロバイダーには先程選択したIDプロバイダーが入力されています。

IDプロバイダ選択

Audienceには、プロバイダーを作成したときに入力したsigstoreが選択できるので、選択して次のステップに移動します。

次はアクセス権限ポリシーをアタッチします。今はテストで行なっていて、この権限の設定で問題が発生すると切り分けが大変なので、一旦強い権限(AdministratorAccess)をアタッチします。

ロールの作成

タグはとばして、ロール名を入力し、ロールの作成ボタンを押下します。

IAM Roleの確認

作成したロールを確認します。

ロールを選択後、信頼関係というタブを押下します。そして、信頼関係の編集というボタンを押下すると、JSON形式で確認できます。

以下のようになっているとOKです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sigstore"
}
}
}
]
}

これでIAM Roleの設定は完了しました。

GitHub Actionsの設定

IAM Roleの準備が整ったので、GitHub Actionsの設定を行います。

基本的には、クラスメソッドさんの記事そのまま実装すれば大丈夫そうです。

以下に実装例を記述します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: Deploy to AWS Batch

on:
push:
branches:
- dev

env:
AWS_REGION: ap-northeast-1
AWS_ROLE_ARN: arn:aws:iam::xxxxxxxxxxxx:role/GithubActionsDeployRole
AWS_WEB_IDENTITY_TOKEN_FILE: /tmp/awscreds
ECR_REPOSITORY: test-repository
BATCH_JOB_DEFINITION: aws/batch/job-definition.json
BATCH_JOB_DEFINITION_TEMPLATE: aws/batch/job-definition-template.json

jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Configure AWS
run: |
echo AWS_WEB_IDENTITY_TOKEN_FILE=$AWS_WEB_IDENTITY_TOKEN_FILE >> $GITHUB_ENV
echo AWS_ROLE_ARN=$AWS_ROLE_ARN >> $GITHUB_ENV
echo AWS_DEFAULT_REGION=$AWS_REGION >> $GITHUB_ENV
curl --silent -H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" | jq -r '.value' > $AWS_WEB_IDENTITY_TOKEN_FILE

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

Login to Amazon ECR以降は永続的なCredentialsを使っている時と同じです。

こちらでActionsを起動して、コンテナイメージのpush、ジョブ定義ファイルのデプロイが完了しました。

ハマったポイント

ハマったポイントとしましては、最初、GitHub Actions内で参照している環境変数ACTIONS_ID_TOKEN_REQUEST_TOKENACTIONS_ID_TOKEN_REQUEST_URLの値が取れておらずなんでだろうと考えていましたが、結果としては

1
2
3
permissions:
id-token: write
contents: read

この設定が漏れていました…

id-tokenに対して、書き込む権限がないと、上記の環境変数は空になってしまうようです。

まとめ

永続的なCredentialsを使うことなくGitHub ActionsにてAWSの認証を行うことができました。

正式な発表はないらしいですが、GitHub Actionsを利用するためだけに専用ユーザーを作成し、永続的なCredentialsを発行してGitHubに設定するという、セキュリティ的に良くないことをしなくてよくなることを考えると、この方法が今後はデファクトになっていくんだろうなと思います。

次回は、先程の環境変数が取得できずに困っていたときに利用した、GitHub ActionsでのSSHデバッグについて記載したいと思います。

GitHub Actionsを使ってAWS Batchをデプロイする

背景

前回、AWS Batchのジョブ定義ファイルを取得するため、AWS CLIでスイッチロール先のAWSアカウントのCredentialsの設定を行いました。

今回はそのジョブ定義ファイルを使って、GitHub ActionsでAWS Batchのデプロイを行いたいと思います。

Batchのデプロイ

Batchのデプロイの範囲ですが、今回は以下の部分を対象とします。

  • ECRにログイン
  • Batch実行コンテナイメージのビルド
  • Batch実行コンテナイメージをECRに登録
  • ジョブ定義ファイル内に記載したコンテナイメージのURLを上記で登録したコンテナイメージのURLで置き換え
  • ジョブ定義ファイルのデプロイ

GitHub Actionsの実装

ではGitHub Actionsを実装していきます。環境変数が至る所で参照されていますが、jobsの上あたりで全て定義してあります。また、GitHub上でsecretsを登録しております。

ECRにログイン

ECRへのログインはAmazon ECSのテンプレートと同じ形で実行できます。

1
2
3
4
5
6
7
8
9
10
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

ここではまだ永続的なCredentialsを使っていますが、近いうちにIAM Roleを利用した一時的なCredentialsに置き換えようと思います。

コンテナイメージのビルド、ECRへ登録

コンテナイメージのビルドとECRへの登録もAmazon ECSのテンプレートと同じ形で実行できます。

1
2
3
4
5
6
7
8
9
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

最後に、コンテナイメージのURLを出力しています。

ジョブ定義ファイルの更新

次にジョブ定義ファイルの更新を行います。

ジョブ定義ファイル内のコンテナイメージのURLを新しくECRに登録したコンテナイメージのURLに差し替える必要があります。

いろんな方法があると思いますが、わたしは前回取得したジョブ定義ファイルをテンプレートファイルとしてGit管理しておき、そのテンプレートファイルのコンテナイメージURLの部分だけ置換するような方法にしました。

1
2
3
4
5
6
- name: Fill in the new image ID in the AWS Batch job definition
id: task-def
env:
IMAGE: ${{ steps.build-image.outputs.image }}
run: |
sed -e "s#__image__#$IMAGE#" $BATCH_JOB_DEFINITION_TEMPLATE > $BATCH_JOB_DEFINITION

ジョブ定義ファイル内のコンテナイメージの箇所は__image__としておき、それをECRに登録したコンテナイメージのURLに置き換え、ファイルとして保存します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"jobDefinitionName": "test-batch",
"type": "container",
"parameters": {},
"retryStrategy": {
"attempts": 1,
"evaluateOnExit": []
},
"containerProperties": {
"image": "__image__",
"command": [
"echo",
"'hello world'"
],

コマンドの箇所はplaceholderが利用できるようですが、コマンドの箇所だけのようです。
https://docs.aws.amazon.com/ja_jp/batch/latest/userguide/job_definition_parameters.html#parameters

これで新しいコンテナイメージのジョブ定義ファイルが作成できました。

ジョブ定義ファイルのデプロイ

最後にジョブ定義ファイルのデプロイを行います。

1
2
3
4
- name: Deploy AWS Batch job definition
id: deploy-job-def
run: |
aws batch register-job-definition --cli-input-json file://$BATCH_JOB_DEFINITION

aws batch register-job-definitionコマンドを用いて、ジョブ定義ファイルを登録します。そうすることで新しいジョブ定義ファイルが有効になります。

異常で、AWS Batchのデプロイは完了です。

まとめ

今回はAWS BatchをGitHub Actionsを用いてデプロイしました。AWS BatchのトリガーとしてAmazon EventBridgeを利用しているのであれば、EventBridge側の変更も必要になるかもしれません。こちらは近いうちに調査してみます。

また、今回はaccess_key_id, secret_access_keyという永続的なCredentialsを利用しましたが、セキュリティ的に永続的なCredentialsを利用するのは好ましくありません。次回は一時的なCredentialsを利用してAWS Batchをデプロイするよう修正したいと思います。

xmlのparseで利用するためにlxmlをインストールする

背景

以前はxmlのparseにxml.etree.ElementTreeを利用しました。実際に利用したところ、ファイルサイズが大きめのXMLを読み込んでループを回したところで、out of memoryになり、停止してしまいました。

なにか別のライブラリはないかなと思って探したところlxmlというパッケージが見つかったのでそれを利用してみようと思います。

インストール

pipでインストールします。Pipfilelxml = "*"を追加して、インストールします。

1
2
3
4
5
6
7
8
9
$ docker-compose run --rm web python -m pipenv install --system --skip-lock
Creating test_site_web_run ... done
Installing dependencies from Pipfile...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 — 00:00:00
An error occurred while installing lxml! Will try again.
Installing initially failed dependencies...
(中略)
[pipenv.exceptions.InstallError]: error: command 'gcc' failed: No such file or directory
...

エラーが起こってしまいました。'gcc' failed: No such file or directoryというエラーになっているので確認してみると、gccがインストールされていません。

gccのインストール

Dockerfileに以下の1行を追記します。

1
RUN apk add gcc

再ビルドします。

1
2
3
4
5
6
7
$ docker-compose build                                                     
Building web
[+] Building 23.5s (12/12) FINISHED
(中略)
#12 20.56 [pipenv.exceptions.InstallError]: /tmp/xmlXPathInitzi6405c3.c:1:10: fatal error: libxml/xpath.h: No such file or directory
#12 20.56 [pipenv.exceptions.InstallError]: 1 | #include "libxml/xpath.h"
...

xpath.hがないということです。調べてみると、libxslt-devというパッケージが必要とのことなので、以下のように修正してインストールします。

1
RUN apk add gcc libxslt-dev

ビルドします。

1
2
3
4
5
6
7
8
9
$ docker-compose build
Building web
(中略)
#12 11.11 [pipenv.exceptions.InstallError]: /usr/local/include/python3.9/Python.h:11:10: fatal error: limits.h: No such file or directory
#12 11.11 [pipenv.exceptions.InstallError]: 11 | #include <limits.h>
(中略)
#12 11.11 [pipenv.exceptions.InstallError]: /usr/include/libxml2/libxml/tree.h:15:10: fatal error: stdio.h: No such file or directory
#12 11.11 [pipenv.exceptions.InstallError]: 15 | #include <stdio.h>
...

また失敗しました。ちょっとわからないので、調べてみます。

musl-devというパッケージが必要とのことなので、そちらもインストールします。

1
RUN apk add gcc libxslt-dev musl-dev

もう一度ビルドします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ docker-compose build
Building web
[+] Building 185.8s (13/13) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 260B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.9.7-alpine 2.5s
=> [1/8] FROM docker.io/library/python:3.9.7-alpine@sha256:eb1b2038f12c8916be54329319a625de2dfec4266b718efdb798cc149b342a2f 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 18.50kB 0.1s
=> CACHED [2/8] RUN apk update 0.0s
=> [3/8] RUN apk add gcc libxslt-dev musl-dev 4.6s
=> [4/8] RUN mkdir /code 0.4s
=> [5/8] WORKDIR /code 0.0s
=> [6/8] ADD . /code/ 0.1s
=> [7/8] RUN pip install pipenv 8.1s
=> [8/8] RUN python -m pipenv install --system --skip-lock 168.1s
=> exporting to image 1.8s
=> => exporting layers 1.8s
=> => writing image sha256:499cdeca8be5a7281b86e0e691498568fef3dec05495c64552e99aea0322d53e 0.0s
=> => naming to docker.io/library/test_site_web 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

無事ビルドできました!

まとめ

lxmlを利用するには、gcc, libxslt-dev, musl-devが必要でした。

次回はlxmlを使ってparseする処理を実装していきたいと思います。

参考図書

停止しているコンテナのイメージを削除する

背景

dockerイメージを整理しようと思い、docker imagesコマンドを実行したら相当な数のイメージが表示されました。

REPOSITORYとTAGの両方が<none>になっているイメージが多々あったので削除しようと思います。

イメージの削除

ではイメージの削除を行います。docker rmiコマンドを使います。

1
2
$ docker rmi d0df5002f11a
Error response from daemon: conflict: unable to delete d0df5002f11a (must be forced) - image is being used by stopped container ed33132b39b4

エラーが返ってきてしまいました。メッセージを見ると、消そうとしたイメージは止まっているコンテナ(コンテナID ed33132b39b4)で利用されているということです。

コンテナの状況確認

コンテナの状況を確認します。止まっているコンテナも表示したいので、docker ps -aを実行します。

1
2
3
4
5
6
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bbe9ff0a5ec5 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago relaxed_herschel
0af5cf244837 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago peaceful_carson
8216a78943ca d0df5002f11a "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago nervous_mclean
ed33132b39b4 d0df5002f11a "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago hungry_kepler

こちらの記事で利用していたコンテナが止まった状態で残ってしまっているようです。実行時に--rmオプションをつけ忘れたのでしょうか。

ではこれらのコンテナを削除します。

コンテナの削除

コンテナの削除にはdocker rmコマンドを利用します。コンテナIDを指定して実行します。

1
2
$ docker rm ed33132b39b4
ed33132b39b4

確認します。

1
2
3
4
5
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bbe9ff0a5ec5 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago relaxed_herschel
0af5cf244837 my-ruby-app "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago peaceful_carson
8216a78943ca d0df5002f11a "bundle exec ruby ./…" 4 days ago Exited (0) 4 days ago nervous_mclean

削除できているので、もう一度イメージの削除を行います。

1
2
$ docker rmi d0df5002f11a
Error response from daemon: conflict: unable to delete d0df5002f11a (must be forced) - image is being used by stopped container 8216a78943ca

別のコンテナも利用しているようです…このコンテナも削除します。

1
2
$ docker rm 8216a78943ca
8216a78943ca

イメージを削除します。

1
2
$ docker rmi d0df5002f11a
Deleted: sha256:d0df5002f11a7a3ae48c2567ae218af364facdb8916f93589fc39576815d045d

無事削除できました。

まとめ

イメージを削除できない原因は止まっているコンテナが利用しているイメージだったからでした。

docker run実行時のオプションに--rmを指定しておけば、コンテナ終了時に自動的にコンテナも削除されるので、まずはこのオプションを指定しておくのが良さそうです。

オプションを指定し忘れた場合はdocker rmコマンドで終了させます。そうすればイメージも削除できるはずです。

次回はREPOSITORYやTAGが<none>になってしまうのはなぜなのかを調べてみたいと思います。

参考図書

スイッチロールユーザーのcredentialsの設定

背景

AWS BatchをGithubActionsでデプロイするために、ジョブ定義ファイルを取得しようとしました。マネジメントコンソールでは取得できないようでした…

aws cliを用いて取得するしかないと思ったのですが、AWS Batchの設定を行ったのは、スイッチロール先のAWSアカウントです。

スイッチロールしたIAMでジョブ定義ファイルを取得するための設定メモを記載します。

configファイルの編集

スイッチロール先のAWSアカウントのprofileの設定を~/.aws/configファイルに行います。

1
2
3
4
5
[profile switch-role-to]
region = ap-northeast-1
output = json
source_profile = switch-role-from
role_arn = arn:aws:iam::スイッチロール先アカウントID:role/スイッチロール

他の設定と異なるところはsource_profilerole_arnの設定です。

source_profileには、スイッチロール元のprofile名を設定します。role_arnにはスイッチロール先のロールを指定します。

アクセスを試す

それではアクセスできるかどうか試してみましょう。

1
2
3
$ aws batch describe-job-definitions --profile switch-role-to

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::スイッチロール元アカウントID:user/スイッチロール元IAMユーザー名 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::スイッチロール先アカウントID:role/スイッチロール

ん〜エラーになってしまいました。ググってみるとMFAの設定をしているとこのメッセージが表示されるようです。

MFA対応

MFAに対応するために、~/.aws/configに以下の内容を追記しましょう。

1
mfa_serial = arn:aws:iam::スイッチロール元アカウントID:mfa/スイッチロール元IAMユーザー名

再度試してみます。

1
2
$ aws batch describe-job-definitions --profile switch-role-to
Enter MFA code for arn:aws:iam::スイッチロール元アカウントID:mfa/スイッチロール元IAMユーザー名 :

MFAのコードを聞かれているので、入力します。するとジョブ定義ファイルが正常に表示されました。この設定の仕方だと、すべてのジョブ定義ファイルが全てのリビジョンにおいて出力されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ aws batch describe-job-definitions --profile switch-role-to
Enter MFA code for arn:aws:iam::スイッチロール元アカウントID:mfa/スイッチロール元IAMユーザー名 :
{
"jobDefinitions": [
{
"jobDefinitionName": "test",
...
(中略)
...
"propagateTags": false,
"platformCapabilities": [
"FARGATE"
]
}
]
}

無事取得することができました!

まとめ

スイッチロール先のAWSアカウントにAWS CLIでアクセスする方法をまとめました。

次回は取得したジョブ定義ファイルを使ってデプロイを試したいと思います。

簡単なRubyスクリプトをDockerで実行する

背景

データの修正のために、ちょっとしたバッチを書くことになりました。環境はEC2でAmazon Linux2でした。

rbenvをインストールしようかなと思って準備をし、いざrubyをインストールしようとしたところで、gccが入っておらずコンパイルができないことに気がつきました…

いつもならyum install -y gccなど行うところですが、パッケージの追加もあまりしてほしくないということだったので、Dockerコンテナで実行することにしました。

Dockerfileの作成

まずはDockerfileを作成します。

EC2上に適当なディレクトリを作成して、その中にDockerfileを作成します。

1
2
3
$ mkdir app
$ cd app
$ touch Dockerfile

ベースイメージの取得

それではDockerfileのFROMに記載するベースイメージを決めようと思います。

Docker image Rubyでググると、Rubyのオフィシャルイメージが見つかります。

https://hub.docker.com/_/ruby

以前Rubyコミッタの方が最新のバージョンを使うのが速度的には一番良いとおっしゃっていたので、最新の3.0.2を利用します。

Dockerfileつづき

先程のRubyのオフィシャルイメージのページの下の方を見ていくと、How to use this imageというセクションがあり、実際の利用方法が記載されています。

例に書いてあるのを倣って作成し一度ビルドしてみます。Dockerfileは以下のようになりました。

1
2
3
4
5
6
7
8
9
10
11
$ cat Dockerfile
FROM ruby:3.0.2

RUN bundle config --global frozen 1

WORKDIR /usr/src/app

COPY Gemfile Gemfile.lock ./
RUN bundle install

CMD ["bundle", "exec", "ruby", "./test.rb"]

Gemfileは今回必要となるデータベース(MySQL RDS)への接続と、その接続のための認証情報を取得するためのgemのみ記載しました。

1
2
3
4
5
$ cat Gemfile
source 'https://rubygems.org'

gem 'mysql2'
gem 'dotenv'

イメージのビルド

ではこれでビルドします

1
2
3
4
5
6
7
8
9
10
11
$ docker build -t my-ruby-app .
[+] Building 5.0s (8/11)
=> [internal] load build definition from Dockerfile 0.0s
...
(中略)
...
=> ERROR [4/7] COPY Gemfile Gemfile.lock ./ 0.0s
------
> [4/7] COPY Gemfile Gemfile.lock ./:
------
failed to compute cache key: "/Gemfile.lock" not found: not found

Gemfile.lockはなかったですね。Dockerfileの7行目からGemfile.lockを削除して再実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker build -t my-ruby-app .
[+] Building 5.4s (9/11)
=> [internal] load build definition from Dockerfile 0.0s
...
(中略)
...
=> CACHED [4/7] COPY Gemfile ./ 0.0s
=> ERROR [5/7] RUN bundle install 1.0s
------
> [5/7] RUN bundle install:
#9 1.002 The deployment setting requires a Gemfile.lock. Please make sure you have
#9 1.002 checked your Gemfile.lock into version control before deploying.
------
executor failed running [/bin/sh -c bundle install]: exit code: 16

エラーになってしまいました。調べてみると、Dockerfileの3行目にある

1
RUN bundle config --global frozen 1

で、GemfileやGemfile.lockの変更を禁止しているからのようです。

https://bundler.io/v2.2/man/bundle-config.1.html

その行を削除しても良いのですが、一般的にimageのビルドはそのビルドしたimageをデプロイして動作させるので、デプロイの一環でもあります。上記のbundlerのマニュアルにもある通り、--deploymentフラグを有効にすると、BUNDLE_FROZENtrueになるようですし、Gemfile.lockはソースコード管理されているはずなので、Dockerfileはこのままでいきます。

Gemfile.lockの作成

ではGemfile.lockはどのように作成すれば良いでしょうか。

先程のDockerHubのページに作成方法が書いてありました。

1
2
3
4
Generate a Gemfile.lock
The above example Dockerfile expects a Gemfile.lock in your app directory. This docker run will help you generate one. Run it in the root of your app, next to the Gemfile:

$ docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app ruby:2.5 bundle install

イメージのバージョンだけ現在の3.0.2に合わせて実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker run --rm -v "$PWD":/usr/src/app -w /usr/src/app ruby:3.0.2 bundle install
Unable to find image 'ruby:3.0.2' locally
3.0.2: Pulling from library/ruby
...
(中略)
...
Status: Downloaded newer image for ruby:3.0.2
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using bundler 2.2.22
Fetching mysql2 0.5.3
Fetching dotenv 2.7.6
Installing dotenv 2.7.6
Installing mysql2 0.5.3 with native extensions
Bundle complete! 2 Gemfile dependencies, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

無事Gemfile.lockが作成できました。

再ビルド

ではもう一度ビルドしてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker build -t my-ruby-app .
[+] Building 11.6s (12/12) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 236B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/ruby:3.0.2 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 641B 0.0s
=> [1/7] FROM docker.io/library/ruby:3.0.2 0.0s
=> CACHED [2/7] RUN bundle config --global frozen 1 0.0s
=> CACHED [3/7] WORKDIR /usr/src/app 0.0s
=> [4/7] COPY Gemfile Gemfile.lock ./ 0.0s
=> [5/7] RUN bundle install 11.2s
=> [6/7] COPY .env ./ 0.0s
=> [7/7] COPY . . 0.0s
=> exporting to image 0.2s
=> => exporting layers 0.1s
=> => writing image sha256:a67fb078890bb0301ed294fed21fe1d349a1ab731604328470ac759 0.0s
=> => naming to docker.io/library/my-ruby-app 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

イメージを確認します

1
2
$ docker images | grep -i ruby-app
my-ruby-app latest a67fb078890b 40 seconds ago 906MB

ちょっとサイズが大きい気がしますが、ビルドできました。

スクリプトの実行

では実行します。なにも指定しなければ、Dockerfileで最後に記述したコマンドが実行されます。

1
$ docker run my-ruby-app

実行ログなどを出力したい場合は、Gemfile.lockを作成した時のようにログを置くディレクトリを-vでマウントします。

test.rb

1
2
3
4
5
require 'dotenv'
require 'mysql2'

log = File.open('test.log', 'w')
log.write('log message')
1
2
3
$ docker run -v "$PWD":/usr/src/app my-ruby-app
$ cat test.log
log message

無事書き込みできていました。

まとめ

環境を変更することなく実行できるコンテナの長所を利用するといろんなことができるようになると思います。

これからはなにかを行うにしても1から実行環境を整えることもなくなりそうですね。便利な世の中になりました。

Python3でxmlをparseする

背景

構造化されたデータを読み込むことになり、そのデータが特定のURLにアクセスするとxml形式で取得できることがわかりました。

Python3をあまり知らないこともあり、同じような処理をこれからも行っていく予定なので、まとめようと思います。

HTTPクライアント

特定のURLにアクセスするためにHTTPクライアントを生成する必要があります。

私の知っている言語だとHTTPクライアントはいろんなライブラリがあったりして、それぞれ一長一短があります。Python3ではどうかなと思い、入門Python3を調べてみると、標準ライブラリurllib.requestが紹介された後に、requestsが紹介されていて、ほとんどの目的ではrequestsを使った方がウェブ開発が簡単になるようだと記載されています。わたしも倣ってrequestsを使ってみようと思います。

requestsのインストール

まずはじめにインストールする必要があります。

現在インストールされているパッケージを確認しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ docker-compose run --rm web python -m pip list
Creating network "django_default" with the default driver
Creating django_web_run ... done
Package Version
--------------------------------- ---------
asgiref 3.4.1
backports.entry-points-selectable 1.1.0
boto3 1.18.38
botocore 1.21.38
certifi 2021.5.30
distlib 0.3.2
Django 3.2.7
filelock 3.0.12
jmespath 0.10.0
pip 21.2.4
pipenv 2021.5.29
platformdirs 2.3.0
python-dateutil 2.8.2
pytz 2021.1
s3transfer 0.5.0
setuptools 57.5.0
six 1.16.0
sqlparse 0.4.1
urllib3 1.26.6
virtualenv 20.7.2
virtualenv-clone 0.5.7
wheel 0.37.0

インストールされていないようなのでインストールします。Pipfileに追記します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
Django = "==3.2.7"
boto3 = "*"
requests = "*"

[dev-packages]

[requires]
python_version = "3.9"

インストールします。

1
2
3
4
$ docker-compose run --rm web python -m pipenv install --system --skip-lock
Creating django_web_run ... done
Installing dependencies from Pipfile...
🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 1/1 —

インストールされたかどうか確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ docker-compose run --rm web python -m pip list
Creating django_web_run ... done
Package Version
--------------------------------- ---------
asgiref 3.4.1
backports.entry-points-selectable 1.1.0
boto3 1.18.38
botocore 1.21.38
certifi 2021.5.30
charset-normalizer 2.0.6
distlib 0.3.2
Django 3.2.7
filelock 3.0.12
idna 3.2
jmespath 0.10.0
pip 21.2.4
pipenv 2021.5.29
platformdirs 2.3.0
python-dateutil 2.8.2
pytz 2021.1
requests 2.26.0
s3transfer 0.5.0
setuptools 57.5.0
six 1.16.0
sqlparse 0.4.1
urllib3 1.26.6
virtualenv 20.7.2
virtualenv-clone 0.5.7
wheel 0.37.0

無事インストールされました。

requestsの基本的な使い方

次に基本的な利用方法を見ていきましょう。試すだけなのでDjango Shellを利用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker-compose run --rm web python manage.py shell
Creating django_web_run ... done
Python 3.9.7 (default, Aug 31 2021, 19:01:35)
[GCC 10.3.1 20210424] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import requests
>>> res = requests.get('https://google.co.jp')
>>> res.status_code
200
>>> res.headers['content-type']
'text/html; charset=Shift_JIS'
>>> res.encoding
'Shift_JIS'
>>> res.text
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="description">...(省略)

HTTPクライアントを作成し、コンテンツを取得できました。requestsはかなり直感的でわかりやすいと思います。では実際にXMLコンテンツを取得し、parseしたいと思います。

XML parserの選定

python xml parserでググるとxml.etree.ElementTreeというライブラリが出てきます。

ドキュメント: https://docs.python.org/ja/3/library/xml.etree.elementtree.html

これは標準ライブラリのようなのでインストールは不要でimportすれば利用できます。

サンプルのXMLを使って、試してみましょう。

1
2
3
4
5
6
7
8
>>> import requests
>>> import xml.etree.ElementTree as ET
>>> res = requests.get('https://www.w3schools.com/xml/simple.xml')
>>> res.status_code
200
>>> root = ET.fromstring(res.text)
>>> root.tag
'breakfast_menu'

XMLのサンプルは

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<breakfast_menu>
<food>
<name>Belgian Waffles</name>
<price>$5.95</price>
<description>Two of our famous Belgian Waffles with plenty of real maple syrup</description>
<calories>650</calories>
</food>
<food>
<name>Strawberry Belgian Waffles</name>
<price>$7.95</price>
<description>Light Belgian waffles covered with strawberries and whipped cream</description>
<calories>900</calories>
</food>
<food>
<name>Berry-Berry Belgian Waffles</name>
<price>$8.95</price>
<description>Light Belgian waffles covered with an assortment of fresh berries and whipped cream</description>
<calories>900</calories>
</food>
<food>
<name>French Toast</name>
<price>$4.50</price>
<description>Thick slices made from our homemade sourdough bread</description>
<calories>600</calories>
</food>
<food>
<name>Homestyle Breakfast</name>
<price>$6.95</price>
<description>Two eggs, bacon or sausage, toast, and our ever-popular hash browns</description>
<calories>950</calories>
</food>
</breakfast_menu>

なので、正しくrootが取得できています。

また、先程のrootはイテレート可能な子ノードを持ちます。

1
2
3
4
5
6
7
8
>>> for child in root:
... print(child.tag, child.attrib)
...
food {}
food {}
food {}
food {}
food {}

上から構造化データを辿っていくことが可能です。

データ構造を調査する

上記の例で言うと、foodの下の階層の要素がわからない場合はどうしたらよいでしょうか?

そう言った場合は先程のrootと同じように以下のようにすると下の階層の要素のtagがわかります。

1
2
3
4
5
6
7
8
>>> foods = root.findall('food')
>>> for e in foods[0]:
... print(e.tag)
...
name
price
description
calories

そうすると、どう言ったtagを持った要素が存在するのかわかります。tagがわかればtag名でアクセス可能です。

1
2
>>> foods[0].find('name').text
'Belgian Waffles'

まとめ

Python3でHTTPクライアントとXMLパーサを触ってみました。

HTTPクライアントのrequestsはとても利用しやすく、直感的にわかる感じのパッケージでした。今後もrequestsを利用していこうと思います。

XMLパーサについては直感的に理解するのが難しく、Elementの要素に対してアクセスするのも一苦労でした。利用しながら慣れていこうと思います。

参考図書