venvで環境構築する

普段何気なく利用している、venvを利用した環境構築について簡単にまとめてみました。

venvを利用するメリット

そもそも最初期は「なんで直接PCにインストールしたらいけないの?面倒くさくない?」と思っていたので、まずはそのメリットを説明します。

  • 他の環境(PC)でも同じ環境を作成しやすい
    • いわゆる仮想環境系の全般に共通した話
  • 他の環境を壊さずに済む
    • 例えば、Django4系のプロジェクトがあるとする
    • 新規にDjango5系を利用するプロジェクトを作成する必要が出てきた
    • Djangoのバージョンを上げる = Django4系のプロジェクトが破壊されてしまう
    • 仮想環境を利用することで、両方を満たす環境を作成できる

Macでの手順の一例を紹介

私がよく使うコマンドは以下

python3 -m venv venv
source venv/bin/activate

もう少し、それぞれ何をしているのか見てみます。

python3 -m venv venv について

  • python3

    • python3部分は人によってはpythonや特定バージョンを利用したい時にはpython3.12などになる
    • 自身の$PATHが通っているPythonを指定することになる
      • = python3とターミナルで叩いた時に利用されるバージョンを利用する、ということ
  • -mPythonのモジュールをスクリプトとして実行するためのコマンド

  • 後続のvenvモジュールをスクリプトとして実行するという意味になる

  • 最後のvenvは作成する環境の名前

  • 観測範囲だと多いものはvenvなので、自分の場合、普段はvenvとしていることが多い

source venv/bin/activateについて

python3 -m venv venvで作成した仮想環境を有効にする。 これで仮想環境を利用する準備は整ったので、あとは必要に応じてコードを書いたり、pip install djangoなどをしていけば大丈夫です。

一度作った環境を無効化・削除するとき

例えば、最初に実行するpython3でPython3.12の$PATHが通っていて、本来利用したいのはPython3.11など別バージョンだった場合、環境を作り直す必要があります。 そのときは、以下のコマンドを実行します。

deactivate
rm -r venv

別バージョンのPythonを利用する場合

他のバージョンを利用したい時にはpython3.11 -m venv venvのように特定のバージョンを指定します。 最初の例だと、python3としていましたが私の環境ではPython3.12の$PATHが通っており、ちょっとしたコードは最新バージョンで書くことが多いので簡略化した例を紹介していた具合でした。

参考

https://docs.python.org/ja/3/library/venv.html

pip install httpieが失敗した時の備忘録

DRFのTutorialを実行していく中で起こった内容の記録と暫定対処法

結論

  • これを読む人は、これが記事執筆時点の正攻法ではないことを認識ください
  • MULTIDICT_NO_EXTENSIONS=1 pip install multidict
  • pip install httpie
  • を実行することで、インストール可能になった

エラー内容

私はpip install httpieを実行した際に次のようになりました。

(env) tatsuya@MacBook-Pro-3 drf-tutorial-main % pip install httpie
Collecting httpie
  Downloading httpie-3.2.2-py3-none-any.whl.metadata (7.6 kB)
Requirement already satisfied: pip in ./env/lib/python3.12/site-packages (from httpie) (23.3.2)
Collecting charset-normalizer>=2.0.0 (from httpie)
  Downloading charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl.metadata (33 kB)
Collecting defusedxml>=0.6.0 (from httpie)
  Downloading defusedxml-0.7.1-py2.py3-none-any.whl (25 kB)
Collecting requests>=2.22.0 (from requests[socks]>=2.22.0->httpie)
  Downloading requests-2.31.0-py3-none-any.whl.metadata (4.6 kB)
Requirement already satisfied: Pygments>=2.5.2 in ./env/lib/python3.12/site-packages (from httpie) (2.17.2)
Collecting requests-toolbelt>=0.9.1 (from httpie)
  Downloading requests_toolbelt-1.0.0-py2.py3-none-any.whl (54 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 54.5/54.5 kB 7.5 MB/s eta 0:00:00
Collecting multidict>=4.7.0 (from httpie)
  Downloading multidict-6.0.4.tar.gz (51 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 51.3/51.3 kB 6.0 MB/s eta 0:00:00
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Installing backend dependencies ... done
  Preparing metadata (pyproject.toml) ... done
Collecting setuptools (from httpie)
  Using cached setuptools-69.0.3-py3-none-any.whl.metadata (6.3 kB)
Collecting rich>=9.10.0 (from httpie)
  Downloading rich-13.7.0-py3-none-any.whl.metadata (18 kB)
Collecting idna<4,>=2.5 (from requests>=2.22.0->requests[socks]>=2.22.0->httpie)
  Downloading idna-3.6-py3-none-any.whl.metadata (9.9 kB)
Collecting urllib3<3,>=1.21.1 (from requests>=2.22.0->requests[socks]>=2.22.0->httpie)
  Downloading urllib3-2.1.0-py3-none-any.whl.metadata (6.4 kB)
Collecting certifi>=2017.4.17 (from requests>=2.22.0->requests[socks]>=2.22.0->httpie)
  Downloading certifi-2023.11.17-py3-none-any.whl.metadata (2.2 kB)
Collecting PySocks!=1.5.7,>=1.5.6 (from requests[socks]>=2.22.0->httpie)
  Downloading PySocks-1.7.1-py3-none-any.whl (16 kB)
Collecting markdown-it-py>=2.2.0 (from rich>=9.10.0->httpie)
  Downloading markdown_it_py-3.0.0-py3-none-any.whl.metadata (6.9 kB)
Collecting mdurl~=0.1 (from markdown-it-py>=2.2.0->rich>=9.10.0->httpie)
  Downloading mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Downloading httpie-3.2.2-py3-none-any.whl (127 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 127.4/127.4 kB 8.4 MB/s eta 0:00:00
Downloading charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl (119 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 119.4/119.4 kB 14.6 MB/s eta 0:00:00
Downloading requests-2.31.0-py3-none-any.whl (62 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 7.3 MB/s eta 0:00:00
Downloading rich-13.7.0-py3-none-any.whl (240 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 240.6/240.6 kB 22.9 MB/s eta 0:00:00
Using cached setuptools-69.0.3-py3-none-any.whl (819 kB)
Downloading certifi-2023.11.17-py3-none-any.whl (162 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 162.5/162.5 kB 7.5 MB/s eta 0:00:00
Downloading idna-3.6-py3-none-any.whl (61 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.6/61.6 kB 8.1 MB/s eta 0:00:00
Downloading markdown_it_py-3.0.0-py3-none-any.whl (87 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 87.5/87.5 kB 9.0 MB/s eta 0:00:00
Downloading urllib3-2.1.0-py3-none-any.whl (104 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 104.6/104.6 kB 12.1 MB/s eta 0:00:00
Building wheels for collected packages: multidict
  Building wheel for multidict (pyproject.toml) ... error
  error: subprocess-exited-with-error
  
  × Building wheel for multidict (pyproject.toml) did not run successfully.
  │ exit code: 1
  ╰─> [78 lines of output]
      *********************
      * Accelerated build *
      *********************
      running bdist_wheel
      running build
      running build_py
      creating build
      creating build/lib.macosx-10.9-universal2-cpython-312
      creating build/lib.macosx-10.9-universal2-cpython-312/multidict
      copying multidict/_multidict_py.py -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      copying multidict/_abc.py -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      copying multidict/__init__.py -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      copying multidict/_multidict_base.py -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      copying multidict/_compat.py -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      running egg_info
      writing multidict.egg-info/PKG-INFO
      writing dependency_links to multidict.egg-info/dependency_links.txt
      writing top-level names to multidict.egg-info/top_level.txt
      reading manifest file 'multidict.egg-info/SOURCES.txt'
      reading manifest template 'MANIFEST.in'
      warning: no previously-included files matching '*.pyc' found anywhere in distribution
      warning: no previously-included files found matching 'multidict/_multidict.html'
      warning: no previously-included files found matching 'multidict/*.so'
      warning: no previously-included files found matching 'multidict/*.pyd'
      warning: no previously-included files found matching 'multidict/*.pyd'
      no previously-included directories found matching 'docs/_build'
      adding license file 'LICENSE'
      writing manifest file 'multidict.egg-info/SOURCES.txt'
      /private/var/folders/73/0yft0xs11fndmt7p68_6xypc0000gn/T/pip-build-env-0pud64ky/overlay/lib/python3.12/site-packages/setuptools/command/build_py.py:207: _Warning: Package 'multidict._multilib' is absent from the `packages` configuration.
      !!
      
              ********************************************************************************
              ############################
              # Package would be ignored #
              ############################
              Python recognizes 'multidict._multilib' as an importable package[^1],
              but it is absent from setuptools' `packages` configuration.
      
              This leads to an ambiguous overall configuration. If you want to distribute this
              package, please make sure that 'multidict._multilib' is explicitly added
              to the `packages` configuration field.
      
              Alternatively, you can also rely on setuptools' discovery methods
              (for example by using `find_namespace_packages(...)`/`find_namespace:`
              instead of `find_packages(...)`/`find:`).
      
              You can read more about "package discovery" on setuptools documentation page:
      
              - https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
      
              If you don't want 'multidict._multilib' to be distributed and are
              already explicitly excluding 'multidict._multilib' via
              `find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`,
              you can try to use `exclude_package_data`, or `include-package-data=False` in
              combination with a more fine grained `package-data` configuration.
      
              You can read more about "package data files" on setuptools documentation page:
      
              - https://setuptools.pypa.io/en/latest/userguide/datafiles.html
      
      
              [^1]: For Python, any directory (with suitable naming) can be imported,
                    even if it does not contain any `.py` files.
                    On the other hand, currently there is no concept of package data
                    directory, all directories are treated like packages.
              ********************************************************************************
      
      !!
        check.warn(importable)
      copying multidict/__init__.pyi -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      copying multidict/py.typed -> build/lib.macosx-10.9-universal2-cpython-312/multidict
      running build_ext
      building 'multidict._multidict' extension
      creating build/temp.macosx-10.9-universal2-cpython-312
      creating build/temp.macosx-10.9-universal2-cpython-312/multidict
      clang -fno-strict-overflow -Wsign-compare -Wunreachable-code -fno-common -dynamic -DNDEBUG -g -O3 -Wall -arch arm64 -arch x86_64 -g -I/Users/tatsuya/drf-tutorial-main/env/include -I/Library/Frameworks/Python.framework/Versions/3.12/include/python3.12 -c multidict/_multidict.c -o build/temp.macosx-10.9-universal2-cpython-312/multidict/_multidict.o -O2 -std=c99 -Wall -Wsign-compare -Wconversion -fno-strict-aliasing -pedantic
      xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
      error: command '/usr/bin/clang' failed with exit code 1
      [end of output]
  
  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for multidict
Failed to build multidict
ERROR: Could not build wheels for multidict, which is required to install pyproject.toml-based projects

httpieが依存しているmultidictがインストールできず、エラーになっていました。

調査したこと

multidictGitHubをみてみたところ、Python3.12は2024年1月8日時点では、正式な対応前の状態でした。

また、Library Installationには次のような記述がありました。

PyPI contains binary wheels for Linux, Windows and MacOS. If you want to install multidict on another operating system (or Alpine Linux inside a Docker) the tarball will be used to compile the library from source. It requires a C compiler and Python headers to be installed.

To skip the compilation, please use the MULTIDICT_NO_EXTENSIONS environment variable, e.g.: $ MULTIDICT_NO_EXTENSIONS=1 pip install multidict

そこで、コンパイルをスキップしてみる方法を取ることにしました。

(env) tatsuya@MacBook-Pro-3 drf-tutorial-main % MULTIDICT_NO_EXTENSIONS=1 pip install multidict
Collecting multidict
  Using cached multidict-6.0.4.tar.gz (51 kB)
  Installing build dependencies ... done
  Getting requirements to build wheel ... done
  Installing backend dependencies ... done
  Preparing metadata (pyproject.toml) ... done
Building wheels for collected packages: multidict
  Building wheel for multidict (pyproject.toml) ... done
  Created wheel for multidict: filename=multidict-6.0.4-py3-none-any.whl size=9708 sha256=f98015dd832bbcb0c10f9383b4c33ea04f539baef44449867d14e059b62dce29
  Stored in directory: /Users/tatsuya/Library/Caches/pip/wheels/f6/d8/ff/3c14a64b8f2ab1aa94ba2888f5a988be6ab446ec5c8d1a82da
Successfully built multidict
Installing collected packages: multidict
Successfully installed multidict-6.0.4

まず、上記の対応でmultidictinstallは成功して、その後のhttpieinstallも成功しました。

(env) tatsuya@MacBook-Pro-3 drf-tutorial-main % pip install httpie
Collecting httpie
  Using cached httpie-3.2.2-py3-none-any.whl.metadata (7.6 kB)
Requirement already satisfied: pip in ./env/lib/python3.12/site-packages (from httpie) (23.3.2)
Collecting charset-normalizer>=2.0.0 (from httpie)
  Using cached charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl.metadata (33 kB)
Collecting defusedxml>=0.6.0 (from httpie)
  Using cached defusedxml-0.7.1-py2.py3-none-any.whl (25 kB)
Collecting requests>=2.22.0 (from requests[socks]>=2.22.0->httpie)
  Using cached requests-2.31.0-py3-none-any.whl.metadata (4.6 kB)
Requirement already satisfied: Pygments>=2.5.2 in ./env/lib/python3.12/site-packages (from httpie) (2.17.2)
Collecting requests-toolbelt>=0.9.1 (from httpie)
  Using cached requests_toolbelt-1.0.0-py2.py3-none-any.whl (54 kB)
Requirement already satisfied: multidict>=4.7.0 in ./env/lib/python3.12/site-packages (from httpie) (6.0.4)
Collecting setuptools (from httpie)
  Using cached setuptools-69.0.3-py3-none-any.whl.metadata (6.3 kB)
Collecting rich>=9.10.0 (from httpie)
  Using cached rich-13.7.0-py3-none-any.whl.metadata (18 kB)
Collecting idna<4,>=2.5 (from requests>=2.22.0->requests[socks]>=2.22.0->httpie)
  Using cached idna-3.6-py3-none-any.whl.metadata (9.9 kB)
Collecting urllib3<3,>=1.21.1 (from requests>=2.22.0->requests[socks]>=2.22.0->httpie)
  Using cached urllib3-2.1.0-py3-none-any.whl.metadata (6.4 kB)
Collecting certifi>=2017.4.17 (from requests>=2.22.0->requests[socks]>=2.22.0->httpie)
  Using cached certifi-2023.11.17-py3-none-any.whl.metadata (2.2 kB)
Collecting PySocks!=1.5.7,>=1.5.6 (from requests[socks]>=2.22.0->httpie)
  Using cached PySocks-1.7.1-py3-none-any.whl (16 kB)
Collecting markdown-it-py>=2.2.0 (from rich>=9.10.0->httpie)
  Using cached markdown_it_py-3.0.0-py3-none-any.whl.metadata (6.9 kB)
Collecting mdurl~=0.1 (from markdown-it-py>=2.2.0->rich>=9.10.0->httpie)
  Using cached mdurl-0.1.2-py3-none-any.whl (10.0 kB)
Using cached httpie-3.2.2-py3-none-any.whl (127 kB)
Using cached charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl (119 kB)
Using cached requests-2.31.0-py3-none-any.whl (62 kB)
Using cached rich-13.7.0-py3-none-any.whl (240 kB)
Using cached setuptools-69.0.3-py3-none-any.whl (819 kB)
Using cached certifi-2023.11.17-py3-none-any.whl (162 kB)
Using cached idna-3.6-py3-none-any.whl (61 kB)
Using cached markdown_it_py-3.0.0-py3-none-any.whl (87 kB)
Using cached urllib3-2.1.0-py3-none-any.whl (104 kB)
Installing collected packages: urllib3, setuptools, PySocks, mdurl, idna, defusedxml, charset-normalizer, certifi, requests, markdown-it-py, rich, requests-toolbelt, httpie
Successfully installed PySocks-1.7.1 certifi-2023.11.17 charset-normalizer-3.3.2 defusedxml-0.7.1 httpie-3.2.2 idna-3.6 markdown-it-py-3.0.0 mdurl-0.1.2 requests-2.31.0 requests-toolbelt-1.0.0 rich-13.7.0 setuptools-69.0.3 urllib3-2.1.0

ただし、今回対応したビルドしない方法は非常に遅くなるようでGitHubのREADMEには以下の説明がありました。

Please note, the pure Python (uncompiled) version is about 20-50 times slower depending on the usage scenario!!!

https://github.com/aio-libs/multidict?tab=readme-ov-file#library-installation

今回はDRFのTurotialを実行しているだけだった、ということもあるので特に大きな問題も考えられないためこの方法を取りました。 そもそも、Python3.12が正式対応されていないので、本来は3.11以下を利用する、又は別の方法を取るというのも良いかと思います(DRFのTutorialではcurlにも触れられていたので、curlを利用しましたがどうやったら利用できるか気になったので調べてみた具合でした)。

DRFを触ってみた

はじめに

Django REST framework(略称 DRF)を触ってみました。

https://www.django-rest-framework.org

DRFDjangoREST APIを作成する時に利用する有名なライブラリです。 今までちゃんと触ったことがなかったので、改めて学習してみました。 まずはQuickstartレベルですが、学んだことを以下に記します。

Quickstartをやってみた

Quickstartのリンクはこちら。

https://www.django-rest-framework.org/tutorial/quickstart/

普段はrequirements.txtを用意しますが、今回はクイックスタートに従って特にバージョンを指定しませんでした。 その結果、 djangorestframework==3.14.0Python3.12 Django==5.0.1 で動作することは確認できました。

PyPI上ではDjango4.1, Python3.10までは対応しているみたいなので、更に上のバージョンでもクイックスタートレベルは問題なく動作するみたいです。

https://pypi.org/project/djangorestframework/

何かやってて思ったことはある?

通常のDjangoと違った概念として  Serializers  ViewSet という概念が初めて出てきたので、それは最初「どういうものかな?」と思いました。

自分の中では、SerializersDRF版のFormViewSetDRF版のViewだという理解に落ち着いています。

現時点の理解度としては(RESTな)Web APIを作成、構築する上で必要な機能を提供するため存在しているのかな、という所です(学習して理解が深まったらまた別途記事を書きます)。

Serializers

Serializersfrom rest_framework import serializersのように記述して呼び出します。

from django.contrib.auth.models import Group, User
from rest_framework import serializers


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ['url', 'username', 'email', 'groups']


class GroupSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Group
        fields = ['url', 'name']

https://www.django-rest-framework.org/tutorial/quickstart/#serializers より引用。

これを見ると、Djangoの通常のFormとほぼ同じようなコードになっていることがわかります。

ViewSet

ViewSetfrom rest_framework import permissions, viewsetsのように記述して呼び出します。

from django.contrib.auth.models import Group, User
from rest_framework import permissions, viewsets

from tutorial.quickstart.serializers import GroupSerializer, UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = User.objects.all().order_by('-date_joined')
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]


class GroupViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows groups to be viewed or edited.
    """
    queryset = Group.objects.all()
    serializer_class = GroupSerializer
    permission_classes = [permissions.IsAuthenticated]

https://www.django-rest-framework.org/tutorial/quickstart/#views より引用。

これも書き方的にはDjangoのViewと同じようになっていることがわかりました。

URLs

from django.urls import include, path
from rest_framework import routers

from tutorial.quickstart import views

router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)

# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

urlpatterns += router.urls

https://www.django-rest-framework.org/tutorial/quickstart/#urls より引用。

ここでは普段のDjangoだと記述しない、routersというものがありました。

Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.

リファレンスには上記のように記述されており、URLconfを自動的に生成できると理解しました。

また、他に見慣れない記述であるpath('api-auth/', include('rest_framework.urls', namespace='rest_framework'))というものがありましたが、リファレンスの以下の記述より、ログイン/ログアウトのviewを作成してくれる機能だというのも分かりました。Django自体も基本的に記述量が少なく色々なことをしてくれますが、初見だと「何をやっているんだろう?」と思うポイントの1つでした。

Finally, we're including default login and logout views for use with the browsable API. That's optional, but useful if your API requires authentication and you want to use the browsable API.

おわりに

今回はまずQuickstartを触ってみました。 思えば今までWeb APIを触る機会も少なく、初めて触れる概念で最初はちょっとだけ怯みましたが、DRFがWeb APIを作成するのに必要な機能を提供しているんだなーという理解まで落とし込めることが出来てよかったです。

Docker(docker-compose)のhealthcheckを試してみた

背景

自作のDocker, docker-composeを利用したアプリケーションで、docker-compose内でdepends_onを利用 + アプリケーション側でチェックする仕組みを作っていましたが、これだけでは不足してたことと、healthcheckというものがあると聞いたので試してみました。

結論

  • healthcheckが通ったからといってもアプリケーション側で接続できるようになっているかどうかは断定できない(=一時的にhealthcheckが通っただけという可能性もある)
  • なので、アプリケーション側でもチェックする仕組みはあった方が良さそう
  • もしかしたら、wait-for-itといった他のツールの方が良いのかもしれない

ちょっと曖昧なところがありますが、こういう結論でした。

なぜdepends_onでは駄目なのか?

depends_onはサービス間の起動順番と終了順番の依存関係を表すものです。

https://docs.docker.jp/compose/compose-file/index.html#depends-on

記述することで

  • DBコンテナよりも後に起動
  • DBコンテナよりも後に削除

といったことをすることが出来ます。 ただし、これはあくまでも順番を制御するのみ、なので実際にコンテナがアプリケーションから利用可能かどうかとは異なるようでした。

そのため、docker-composeでは

  • ツールを使う
  • 自分でラッパースクリプトを書いてアプリケーション用のヘルスチェックを処理する

といったことをやるように書かれています。

https://docs.docker.jp/compose/startup-order.html

healthcheckとは

https://docs.docker.jp/compose/compose-file/index.html#healthcheck によると、サービスがHealthyかどうかを指定したコマンドでチェックする機能のようです。

docker-composeのversion2.1から追加されたようでした。 https://docs.docker.com/compose/compose-file/compose-file-v2/#depends_on

以下はDjangoMySQLのコンテナをそれぞれ利用するdocker-composeです。 この書き方だと、webがdbの後に起動、後に削除されるようになっています。

before

version: '2.1'  
  
services:  
  db:  
    container_name: docker_healthcheck_db  
    image: mysql:8.0.32  
    volumes:  
      - db_data:/var/lib/mysql  
    environment:  
      - MYSQL_DATABASE=docker_healthcheck  
      - MYSQL_USER=docker_healthcheck  
      - MYSQL_PASSWORD=docker_healthcheck  
      - MYSQL_ROOT_PASSWORD=docker_healthcheck  
  web:  
    build:  
      context: ..  
      dockerfile: docker/Dockerfile  
    container_name: docker_healthcheck_web  
    command: python manage.py runserver 0.0.0.0:8000  
    volumes:  
      - ..:/docker_healthcheck  
    ports:  
      - "8000:8000"  
    environment:  
      - MYSQL_DATABASE=docker_healthcheck  
      - MYSQL_USER=docker_healthcheck  
      - MYSQL_PASSWORD=docker_healthcheck  
      - MYSQL_HOST=db  
      - MYSQL_PORT=3306  
      - DJANGO_SETTINGS_MODULE=config.settings  
    depends_on:  
      - db
volumes:  
    db_data:

上記を以下のように書き換えて、db側にhealthcheckとチェック内容, webdepends_oncondition: service_healthy を追記します。

after

version: '2.1'  
  
services:  
  db:  
    container_name: docker_healthcheck_db  
    image: mysql:8.0.32  
    volumes:  
      - db_data:/var/lib/mysql  
    environment:  
      - MYSQL_DATABASE=docker_healthcheck  
      - MYSQL_USER=docker_healthcheck  
      - MYSQL_PASSWORD=docker_healthcheck  
      - MYSQL_ROOT_PASSWORD=docker_healthcheck  
    healthcheck:  
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]  
      interval: 10s  
      timeout: 5s  
      retries: 5  
  web:  
    build:  
      context: ..  
      dockerfile: docker/Dockerfile  
    container_name: docker_healthcheck_web  
    command: python manage.py runserver 0.0.0.0:8000  
    volumes:  
      - ..:/docker_healthcheck  
    ports:  
      - "8000:8000"  
    environment:  
      - MYSQL_DATABASE=docker_healthcheck  
      - MYSQL_USER=docker_healthcheck  
      - MYSQL_PASSWORD=docker_healthcheck  
      - MYSQL_HOST=db  
      - MYSQL_PORT=3306  
      - DJANGO_SETTINGS_MODULE=config.settings  
    depends_on:  
      db:  
        condition: service_healthy  
volumes:  
    db_data:

beforeだと、このようにStartedになったタイミングで次の処理に移ります(私だと、Djangoが立ち上がるようになって、最初のMySQLへの接続に失敗する状態でした)。

[+] Running 4/4
 ✔ Network docker_default            Created                                                                                                  0.0s 
 ✔ Volume "docker_db_data"           Created                                                                                                  0.0s 
 ✔ Container docker_healthcheck_db   Started                                                                                                  0.0s 
 ✔ Container docker_healthcheck_web  Started                                                                                                  0.0s 

afterだと、Startedになった後に

[+] Running 4/4
 ✔ Network docker_default            Created                                                                                                  0.0s 
 ✔ Volume "docker_db_data"           Created                                                                                                  0.0s 
 ✔ Container docker_healthcheck_db   Started                                                                                                  0.0s 
 ✔ Container docker_healthcheck_web  Created                                                                                                  0.0s 

Healthyであることをチェックして、次の処理へ進むようになりました。

[+] Running 4/4
 ✔ Network docker_default            Created                                                                                                  0.0s 
 ✔ Volume "docker_db_data"           Created                                                                                                  0.0s 
 ✔ Container docker_healthcheck_db   Healthy                                                                                                  0.0s 
 ✔ Container docker_healthcheck_web  Started                                                                                                  0.0s 

のようになります。

ただし、私の環境だとHealthyであってもDjango側で接続に失敗してOperationalErrorが発生しました。恐らくinterval: 10s20sなど広めに取れば大丈夫ではありそうでした。 ありそう、というのは実際に試してOKではあったものの、それが常に大丈夫だと言える保証もないかなーと考えたからです。

上記より、あくまでもHealthyかどうかチェックできるだけで、実際にDjango側から接続可能なのかどうかは別問題かなと感じました。

以降は実際に出たエラーやもう少し追加で調べた記録です。

実際のエラー

ちょっと長いですが、こういう感じでした。

[+] Running 4/4
 ✔ Network docker_default            Created                                                                                                  0.0s 
 ✔ Volume "docker_db_data"           Created                                                                                                  0.0s 
 ✔ Container docker_healthcheck_db   Created                                                                                                  0.0s 
 ✔ Container docker_healthcheck_web  Created                                                                                                  0.0s 
Attaching to docker_healthcheck_db, docker_healthcheck_web
docker_healthcheck_db   | 2024-01-02 23:19:55+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.32-1.el8 started.
docker_healthcheck_db   | 2024-01-02 23:19:56+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'
docker_healthcheck_db   | 2024-01-02 23:19:57+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.32-1.el8 started.
docker_healthcheck_db   | 2024-01-02 23:19:58+00:00 [Note] [Entrypoint]: Initializing database files
docker_healthcheck_db   | 2024-01-02T23:19:58.491975Z 0 [Warning] [MY-011068] [Server] The syntax '--skip-host-cache' is deprecated and will be removed in a future release. Please use SET GLOBAL host_cache_size=0 instead.
docker_healthcheck_db   | 2024-01-02T23:19:58.494119Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.32) initializing of server in progress as process 354
docker_healthcheck_db   | 2024-01-02T23:19:58.587680Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
docker_healthcheck_db   | 2024-01-02T23:19:58.961840Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
docker_healthcheck_db   | 2024-01-02T23:20:00.556381Z 6 [Warning] [MY-010453] [Server] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.
docker_healthcheck_db   | 2024-01-02 23:20:03+00:00 [Note] [Entrypoint]: Database files initialized
docker_healthcheck_db   | 2024-01-02 23:20:03+00:00 [Note] [Entrypoint]: Starting temporary server
docker_healthcheck_db   | 2024-01-02T23:20:03.934177Z 0 [Warning] [MY-011068] [Server] The syntax '--skip-host-cache' is deprecated and will be removed in a future release. Please use SET GLOBAL host_cache_size=0 instead.
docker_healthcheck_db   | 2024-01-02T23:20:03.938944Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.32) starting as process 414
docker_healthcheck_db   | 2024-01-02T23:20:04.083260Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
docker_healthcheck_db   | 2024-01-02T23:20:04.362314Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
docker_healthcheck_db   | 2024-01-02T23:20:04.950096Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
docker_healthcheck_db   | 2024-01-02T23:20:04.950288Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
docker_healthcheck_db   | 2024-01-02T23:20:04.954519Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
docker_healthcheck_db   | 2024-01-02T23:20:04.998689Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.32'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server - GPL.
docker_healthcheck_db   | 2024-01-02T23:20:04.998722Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: /var/run/mysqld/mysqlx.sock
docker_healthcheck_db   | 2024-01-02 23:20:05+00:00 [Note] [Entrypoint]: Temporary server started.
docker_healthcheck_db   | '/var/lib/mysql/mysql.sock' -> '/var/run/mysqld/mysqld.sock'
docker_healthcheck_db   | Warning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.
docker_healthcheck_db   | Warning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.
docker_healthcheck_db   | Warning: Unable to load '/usr/share/zoneinfo/leapseconds' as time zone. Skipping it.
docker_healthcheck_db   | Warning: Unable to load '/usr/share/zoneinfo/tzdata.zi' as time zone. Skipping it.
docker_healthcheck_db   | Warning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.
docker_healthcheck_db   | Warning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.
docker_healthcheck_db   | 2024-01-02 23:20:08+00:00 [Note] [Entrypoint]: Creating database docker_healthcheck
docker_healthcheck_db   | 2024-01-02 23:20:08+00:00 [Note] [Entrypoint]: Creating user docker_healthcheck
docker_healthcheck_db   | 2024-01-02 23:20:08+00:00 [Note] [Entrypoint]: Giving user docker_healthcheck access to schema docker_healthcheck
docker_healthcheck_db   | 
docker_healthcheck_db   | 2024-01-02 23:20:08+00:00 [Note] [Entrypoint]: Stopping temporary server
docker_healthcheck_db   | 2024-01-02T23:20:08.995225Z 14 [System] [MY-013172] [Server] Received SHUTDOWN from user root. Shutting down mysqld (Version: 8.0.32).
docker_healthcheck_web  | Performing system checks...
docker_healthcheck_web  | 
docker_healthcheck_web  | System check identified no issues (0 silenced).
docker_healthcheck_web  | Exception in thread django-main-thread:
docker_healthcheck_web  | Traceback (most recent call last):
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 282, in ensure_connection
docker_healthcheck_web  |     self.connect()
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
docker_healthcheck_web  |     return func(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 263, in connect
docker_healthcheck_web  |     self.connection = self.get_new_connection(conn_params)
docker_healthcheck_web  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
docker_healthcheck_web  |     return func(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/mysql/base.py", line 247, in get_new_connection
docker_healthcheck_web  |     connection = Database.connect(**conn_params)
docker_healthcheck_web  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/MySQLdb/__init__.py", line 123, in Connect
docker_healthcheck_web  |     return Connection(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/MySQLdb/connections.py", line 185, in __init__
docker_healthcheck_web  |     super().__init__(*args, **kwargs2)
docker_healthcheck_web  | MySQLdb.OperationalError: (2002, "Can't connect to MySQL server on 'db' (115)")
docker_healthcheck_web  | 
docker_healthcheck_web  | The above exception was the direct cause of the following exception:
docker_healthcheck_web  | 
docker_healthcheck_web  | Traceback (most recent call last):
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
docker_healthcheck_web  |     self.run()
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/threading.py", line 975, in run
docker_healthcheck_web  |     self._target(*self._args, **self._kwargs)
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/autoreload.py", line 64, in wrapper
docker_healthcheck_web  |     fn(*args, **kwargs)
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/core/management/commands/runserver.py", line 137, in inner_run
docker_healthcheck_web  |     self.check_migrations()
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/core/management/base.py", line 564, in check_migrations
docker_healthcheck_web  |     executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
docker_healthcheck_web  |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/migrations/executor.py", line 18, in __init__
docker_healthcheck_web  |     self.loader = MigrationLoader(self.connection)
docker_healthcheck_web  |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/migrations/loader.py", line 58, in __init__
docker_healthcheck_web  |     self.build_graph()
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/migrations/loader.py", line 235, in build_graph
docker_healthcheck_web  |     self.applied_migrations = recorder.applied_migrations()
docker_healthcheck_web  |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/migrations/recorder.py", line 81, in applied_migrations
docker_healthcheck_web  |     if self.has_table():
docker_healthcheck_web  |        ^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/migrations/recorder.py", line 57, in has_table
docker_healthcheck_web  |     with self.connection.cursor() as cursor:
docker_healthcheck_web  |          ^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
docker_healthcheck_web  |     return func(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 323, in cursor
docker_healthcheck_web  |     return self._cursor()
docker_healthcheck_web  |            ^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 299, in _cursor
docker_healthcheck_web  |     self.ensure_connection()
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
docker_healthcheck_web  |     return func(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 281, in ensure_connection
docker_healthcheck_web  |     with self.wrap_database_errors:
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/utils.py", line 91, in __exit__
docker_healthcheck_web  |     raise dj_exc_value.with_traceback(traceback) from exc_value
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 282, in ensure_connection
docker_healthcheck_web  |     self.connect()
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
docker_healthcheck_web  |     return func(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/base/base.py", line 263, in connect
docker_healthcheck_web  |     self.connection = self.get_new_connection(conn_params)
docker_healthcheck_web  |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
docker_healthcheck_web  |     return func(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/mysql/base.py", line 247, in get_new_connection
docker_healthcheck_web  |     connection = Database.connect(**conn_params)
docker_healthcheck_web  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/MySQLdb/__init__.py", line 123, in Connect
docker_healthcheck_web  |     return Connection(*args, **kwargs)
docker_healthcheck_web  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
docker_healthcheck_web  |   File "/usr/local/lib/python3.11/site-packages/MySQLdb/connections.py", line 185, in __init__
docker_healthcheck_web  |     super().__init__(*args, **kwargs2)
docker_healthcheck_web  | django.db.utils.OperationalError: (2002, "Can't connect to MySQL server on 'db' (115)")
docker_healthcheck_db   | 2024-01-02T23:20:10.505232Z 0 [System] [MY-010910] [Server] /usr/sbin/mysqld: Shutdown complete (mysqld 8.0.32)  MySQL Community Server - GPL.
docker_healthcheck_db   | 2024-01-02 23:20:11+00:00 [Note] [Entrypoint]: Temporary server stopped
docker_healthcheck_db   | 
docker_healthcheck_db   | 2024-01-02 23:20:11+00:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.
docker_healthcheck_db   | 
docker_healthcheck_db   | 2024-01-02T23:20:11.563285Z 0 [Warning] [MY-011068] [Server] The syntax '--skip-host-cache' is deprecated and will be removed in a future release. Please use SET GLOBAL host_cache_size=0 instead.
docker_healthcheck_db   | 2024-01-02T23:20:11.568460Z 0 [System] [MY-010116] [Server] /usr/sbin/mysqld (mysqld 8.0.32) starting as process 1
docker_healthcheck_db   | 2024-01-02T23:20:11.673006Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
docker_healthcheck_db   | 2024-01-02T23:20:12.004580Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
docker_healthcheck_db   | 2024-01-02T23:20:12.432642Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
docker_healthcheck_db   | 2024-01-02T23:20:12.432818Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
docker_healthcheck_db   | 2024-01-02T23:20:12.437618Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
docker_healthcheck_db   | 2024-01-02T23:20:12.484355Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.32'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
docker_healthcheck_db   | 2024-01-02T23:20:12.487790Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock

この、最後から2行目のdocker_healthcheck_db | 2024-01-02T23:20:12.484355Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.32' socket: '/var/run/mysqld/mysqld.sockの状態になって初めて接続できることになりそうです。そのため、ここに至るまでにHealthyと判定されたら次に進む = アプリケーション側で失敗する、ということにはなりそうです。

追加でちょっと実験してみた

2つのターミナルを用意して、 - コンテナを立ち上げる - 片方で起動した後、もう片方でコンテナに入りmysqladminでpingを通す ことをしてみました。

tatsuya@MacBook-Pro-3 docker % docker exec -it docker_healthcheck_db bash

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqladmin: connect to server at 'localhost' failed

error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'

Check that mysqld is running and that the socket: '/var/run/mysqld/mysqld.sock' exists!

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqladmin: connect to server at 'localhost' failed

error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'

Check that mysqld is running and that the socket: '/var/run/mysqld/mysqld.sock' exists!

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqladmin: connect to server at 'localhost' failed

error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'

Check that mysqld is running and that the socket: '/var/run/mysqld/mysqld.sock' exists!

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqladmin: connect to server at 'localhost' failed

error: 'Access denied for user 'docker_healthcheck'@'localhost' (using password: YES)'

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqld is alive

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqladmin: connect to server at 'localhost' failed

error: 'Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)'

Check that mysqld is running and that the socket: '/var/run/mysqld/mysqld.sock' exists!

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqld is alive

bash-4.4# mysqladmin -u docker_healthcheck -pdocker_healthcheck ping

mysqladmin: [Warning] Using a password on the command line interface can be insecure.

mysqld is alive

すると、全てのケースではなく、たまたまですが上記のようなケース(一度mysqld is aliveになっても後で失敗する)もあったので、どういう仕組みかは別ですが、最後の状態にならずともmysqld is aliveになることはありそう。

mysqladminのpingについて

https://dev.mysql.com/doc/refman/8.0/ja/mysqladmin.html

  • ping

サーバーが使用可能かどうかをチェックします。 サーバーが稼働中の場合は mysqladmin のリターンステータスは 0 になり、稼働していない場合は 1 になります。 Access denied のようなエラーの場合でも 0 となります。これは、サーバーは稼働しているが接続を拒否したことを意味しており、サーバーが稼働していない状態とは異なるからです。

とのことで、これ以上の記述がないですがやはり稼働できた = 接続できた とはならならそう。

また、 https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0012

によると、以下の記述がありました。

ただし、残念ながらmysqladmin pingコマンドも(psコマンドよりは便利に使えたとしても⁠)⁠、「⁠mysqldプロセスが起動しており、MySQLプロトコルでしゃべりかけた場合にMySQLプロトコルで応答を返した」以上のことは判定できません。

となると、やっぱりアプリケーションレイヤーからの接続可否のチェックが出来ている訳ではない、ということだと思っています。そのため、Healthyかどうかを判断材料にするのはよろしくないかな、と考えるようになりました。

次はdocker-composeのマニュアルに紹介されているようなものを試してみたいと思います。

参考記事

Docker の healthcheck を初めて使った話

docker-composeでMySQLの起動を待つ

DockerのHEALTHCHECKの動きを理解する

2023年のふりかえり

2023年のふりかえり

もう気がつけば2023年の年末ですね。 他の人のふりかえり記事を読んでいると自分もこの1年をふりかえりたくなったので、記事を書きました。

私の今年1年を自分なりに3行にまとめてみるとこんな感じになりました。

  • 転職した
  • DjangoCongressJP 2023に登壇した
  • 奈良から愛知に引っ越した

というわけで、それぞれについて書いていきます。 DjangoCongressJP 2023については別の記事に書いているので、実質は転職と引っ越しについて書いていきます。

転職した

Q.なんで転職したの?

A.大きく2つの理由で転職しました。1つは家庭の都合と、もう1つはキャリアを考えてという背景があります。

1つ目の理由の家庭の都合について。

元々はリモートワークが始まり1LDKの部屋が手狭になったことと、将来的に家を買う予定で大阪から奈良に引っ越していました(奈良は関西住みやすい街ランキングで上位に入っている & 当時の夫婦の通勤には便利が良かった)。

そして家探しを実際に進めており、家探しの過程でライフプランも一緒に考える機会があり、その結果妻からは「地元に戻りたい」と相談されました。 家を買う予定をしていたくらいなので関西に根を生やすつもりでしたが、妻の希望なら出来るだけ叶えたいと思ったこともあり、妻の地元である愛知県で働けるように動くことにしました。

2つ目のキャリアについて。

まず、愛知で働くという目的を実現するだけなら前職でも可能ではありました。 実際に相談して愛知への転勤の選択肢も選択肢の1つとして提示してもらいましたが、結果的にはその選択肢を選ばずに転職を選びました。

大きな理由としては前職で携われる可能性が高い技術と、自分がやりたい技術の方向性とのアンマッチさを感じていたためです。

アンマッチになる理由の1つには(自分の都合である)愛知への引っ越しもありますが、 関西に(そのまま前職に)居たとしても自分が触れたい技術には触れられそうにないな、というのは常々感じていたこともあり転職へ舵を取ることに決めました。

結果的に転職することを選びましたが、未経験からエンジニアとして勤務できたのは前職(とお客様先)のお陰なので感謝しています。

エンジニアの成長支援の施策も多く自分も色々な機会を頂戴して学ばせてもらいました。 周りの方にも恵まれて、エンジニアとして最初に勤務する事ができてよかったです。

転職活動はどうだったの?

前の話含め、転職活動だけで1つの記事が出来る位には色々と語れる所もありますが、まず結論から述べると株式会社ビープラウドに転職しました(できました)。

ありがたいことに以前は奈良県から、現在は愛知県からリモートワークをしています。 ということで愛知に引っ越すという事もビープラウドのおかげで叶える事ができました。

一方、転職活動そのものはかなり苦労しました。

基本は書類で落ちる・一次面接で落ちるということが多く、当時はなんとかなるはずと思いつつも焦りはかなりありました(お客様先との契約などもあったので)。 ある意味、未経験転職時代よりも苦労したかもしれません。

転職活動そのものはカジュアル面談含め、2022年の8月くらいから開始。 実際に各社の選考に進み始めたのが2023年2月頃、終わったのが2023年5月でした。 *1

最終的には素敵な会社に縁があって良かったです。

元々会社のことは知っており、自分が入社できるとは思っていなかったというのが正直な所でした。 その理由として、勉強会へ参加する際に利用するWebサイト connpassの運営や、PyCon・DjangoCongressなどのイベント登壇や書籍を執筆されている方も多く、そうした「強い」エンジニアの方々が多数在籍している環境に入れるとは思っていなかった、という具合です。

そして入社後、案件に入ってみて思ったのは、自分が思っていた以上にスキルが足りていないということでした。 自分がよくやる気をもらっていた書籍「情熱プログラマー」では"1番の下手くそでいよう"という章があり、実際に1番の下手くそになってみると、それはもう大変なことばかりでした(笑)。

実装に時間がかかる、レビューに出しても修正する必要があることも多く、現在進行形で如何に自分のスキルが不足しているかを実感しています。

ちょっと「大丈夫かな...」という気持ちも生まれたことはありますが、その気持ちを素直に相談させてもらった時には温かい言葉をかけてもらえて、そうした意味でも入社して(できて)良かったと感じていました。

それにレビューで修正する必要がある = 今まで知らなかったこと、考えられていなかったことを学べる機会でもあり、技術力が高い先輩方と一緒に働かせてもらう機会を頂戴出来ているという事かな、とも思っています。

とはいえここについては、逆の立場で「ここは知ってて・抑えて欲しいな」と思う初歩的な所も多々あると思うので、レビューしてもらえるから大丈夫だと開き直るのではなく。日々教わった事を記録して、理解するようにし、可能な範囲で自己学習も行いスキルアップに努めています。

伸び代しかないので頑張ります。

また、技術面以外でも会社の文化・制度面で良いところが多く助かっています。 例えばフレックスを利用して朝早くから仕事を開始して早めに終わる事もできるので、歯医者への通院や市役所の手続きも休みを取らずに出来たので助かりました。

後述するDjangoCongress JP2023への登壇時は練習の機会を設けてくれ、フィードバックも多数もらい資料を昇華できたので、 繰り返しですが入社できて良かったと感じています。

他にも色々と良いと感じているところはありますが、どんな会社か気になった方は採用情報のページを貼っておくので良ければご覧ください。

www.beproud.jp

また何か書きたい事が出てきたら別の記事に書くことにします。

一旦、良い会社に転職できて転職活動は成功したよ、という結論で締めることにします。

DjangoCongress JP 2023に登壇した

詳しくはこちらの記事を参照ください。

nibutan.hatenablog.com

奈良から愛知に引っ越した

そして、妻の実家方面に引っ越しました。 名古屋まで電車で15分程度のところです。

実際に愛知に住んでみて、今の所は周辺環境面で引っ越して良かったなと思っています。

  • 道が広く歩きやすい
  • 安いスーパーがある
  • 近くにお店も多い

いずれも、単純な愛知と奈良の比較ではなく、単に奈良の山の住宅街の中に住んでいた、という所と対比しての感想かもと思いましたが・・・(笑)

大学を卒業してから全く乗っていなかったロードバイクも、近くにある自転車屋さんに修理を依頼しており、久しぶりに乗れることになり楽しみです。 ボルダリングジムも近くにあるようなので、そちらにも行ってみようと思っています。

一方で引っ越してみて、室内乾燥機が置けるはずと思っていたら置けなかっただとか、食洗機も置くには微妙に幅が狭くすぐ置けなかっただとか色々とありますが、今の所は満足しています。 次引っ越すとしたら、家を買うときかなと思っていますが、どうなることやら。

おわりに

こうやってふりかえって見ると、本当に良い1年でした。

いつもながら、ですが来年はより良い年にしていきたいです。 そのためには、Software Engineerとして飛躍の年にするべく、今まで学んでいなかったことを学び、実践していく事が大事だと思っています。 ぼんやりと「こういう事をやろう」というのはありますが、年末年始の間、ある程度具体的な形に目標を定めておくことにします。

2024年も良い年になりますように。

*1:転職時期は、本当は12月ごろからスタートする予定でしたが、前職の業務都合で評価面談などを挟んでいたため残業も多く、難しかったため延期した背景があります。

DjangoCongress JP 2023 に登壇しました

はじめに

今回、10/7にサイボウズ東京オフィスにて開かれたDjangoCongress JP 2023 にて登壇してきました。
内容はこちら。 Djangoを仕事で使っていくために学んだこと

どういう内容か

Djangoを仕事で使っていくために学んだこと 」と言うタイトルで、 私が今までDjangoを実務で触ったことが無かったので、使うために学んだことを発表しました。
Django自体が現職のビープラウドに入社するまで触る経験は余り無いことや、 他のフレームワークも実務で触れた経験が無かったのでほぼまっさらの状態からの発表でした。

公式リファレンスに記載のある事項をもとに喋りっぱなしの45分(40分程度で終了)でした。

どう準備したか

ある時期まではネタ探しのため、ひたすらDjangoの公式リファレンスを読みました。 そこから、自分が疑問に思ったことや、過去に教えてもらったことを思い出して、この辺りを話してみようかな、と組み立てていきました。

45分、出来るわけがないと思っていましたが「まぁ5分のLTを8本やれば45分か」とネタを集めようと考える事ができたのが良かったのかもしれません。

個人の非公開のScrapboxで大元のネタの構成を準備し、個別の内容は個人のメモとして公開している内容を基にしています。

例:

また、Googleスライドで作成しました。プレゼンターモードがあるのは便利ですね。

何か工夫した点はあるか?

先輩からのアドバイスですが、先輩と後輩の対話形式を出来るだけ各章の冒頭に置くようにしました。 これは弊社ビープラウドの先輩方が執筆した自走プログラマーで会話形式のスタイルを取っていることもあります。

そのほか、頂いたアドバイスとしては次のようなものがありました。

  • 章の切り替わりが聴講者としては唐突に感じられるので、間にスライドを1つ挟んでも良いかも
  • コードの背景は黒く無いほうが良い(会場によっては見えにくいため)
  • スライドの文字数が多い印象がある

あとは改めてスライド周りを改善すべく

この辺りのページを参考に、フォント・フォントサイズを見直しました。

リアル開催のためこうした考慮をする必要があると言うのはアドバイスいただけなかったら気が付かなかったのではと思いました。

練習はどうしたか?

合計3, 4回ほど行いました。 時間を計測しつつ、ですがなかなか時間も気力も使うので大変でした。
また、内1回は自社の先輩に見てもらいました。ありがたかったです。 概ね38分, 39分程度だったのですが本番ではちょっと延びました。
とはいえ質問の時間が無くなると言うこともなく、ちょうど良かったのでは無いかと思います。

練り直すとしたら?

改めて聞いてほしい聴講者の層とそれに相応しい内容かどうか、は見直したいと思います。
例えば、QuerySetの評価タイミング・キャッシュについては初心者の知らなかった方にも参考になると思いますが、 objectsとは、と言う話は極論知らなくても落とし穴にはならない気がするので、盛り込むべきだったのかどうかなとか。

きっかけ

自社の先輩に後押しされたことがきっかけです。
カンファレンスへの初登壇で45分と言うこともあり躊躇はしましたが、後押しされなかったら申し込まなかっただろうなとも思います。

自分もかつて、初めて参加した勉強会で「LTしないんですか?」と声をかけてもらってからLTをするようになり、 今回ああ言ってもらえたから申し込んだ・・・みたいなことがあるので、自分も今度はきっかけを与える側になりたいですね。
ヴェルダースオリジナルのおじいさんのように。

発表してみて

発表後に、温かい言葉をかけてもらいました。
自走プログラマーの著者の1人でもあり、DjangoCongressJPのChairであるkyさんからは「nibu, 発表良かったよ」と言って頂けました。 Pythonの偉大な先人の方から、とても嬉しい言葉を頂けて感無量でした。

他にも、素敵な発表でしたと声をかけて頂くことがありとても嬉しかったです。

やっぱり自分の発表は本格的なものでは無いので、どうなのかなと思っていたのですが、こうした温かいリアクションをいただけたことで、 やって良かったなと思いました。

本編について

以下のセッションを聴講しました。

  • Djangoテンプレートエンジンを使いこなそう! Shinya Okano
  • Django初心者が中級者になるために知るべきこと Hiroki Kiyohara
  • Djangoアプリに作り込んで学ぶ脆弱性SQLインジェクションXSS篇) nikkie
  • データ分析者にとってのDjango: StreamlitやDashとの比較 廻船孝行 (Takayuki KAISEN)
  • GraphQLライブラリStrawberryのDjangoプロジェクトへの適用事例: 実践から学ぶヒントと戦略 Miyashita Yosuke

何の発表も素晴らしい・素敵なものばかりで、知らないことも多かったですが、知らないことを知ることが出来るのがカンファレンスの良い所だと思っているんので、 これでよしとしておこうと思います。

また、他の方の発表もセッションの都合上聞けていないものが多いので、公開されたものはまた拝読をする予定です。

  • Django ナレッジ共有 ~DB管理は別の部署 & 開発tips~ 松野 一貴

こちらの松野さんからは、私がなんとpdbについて発表を被せてしまっていたそうですが、そのことを紹介しても良いか確認いただけて、非常に丁寧な方でした。 本当に皆さん、温かい・・・。

セッションに関する余談

マイクがあることを全く考えていませんでしたが、私はデモを行うスタイルにしなかったこともあり、ハンドマイクで対応させてもらいました。 あとは発表する内容をあらかじめ文字に起こしていたのは良かったかなと思います。 じゃないといらないことばかり喋ってしまう。

没ネタ

ちなみに、没ネタが他にもまだまだありました。 ここは時間の制約と、改めて伝えたい層や自分が話せる内容、全体の構成を考えて泣く泣く削った所です。

  • Django標準のUnittestについて
  • QuerySetのキャッシュされる属性について
  • Formについて
  • DjangoはどうやってHTML上でエスケープしているのか
  • カスタムマネージャーについて
  • システムチェックフレームワークを自分で実装する方法について
  • dir()について
  • テーブル名がapp名_model名になる仕組み
  • manage.pyでどう言う動きをしているか
  • Djangoの思想はどう言うものか
  • django-debug-toolbarの機能について詳細
  • SQLのEXPLAINについて

また、スライドの太字や文字などもっとメリハリを効かせたりしたかったですが、ここは本筋ではないし・・・と作業をある程度で留めるようにしました。

そのほか全体の感想

  • サイボウズ東京オフィス綺麗だった
  • 会場も貸してもらえてありがたかった・・・
  • 去年のPyConでお会いした方に再開できた
  • 「あの本の著者の方ですよね」とお知り合いになる事ができた

皆さん参加している方が温かい方ばかりで、Djangoコミュニティも良いものだなと感じました。
積極的に皆で盛り上げていきたいですね。
このブログ記事を読んだ方がいらっしゃったら、ぜひ来年CfPを提出して一緒に盛り上げていきましょう!!

石川旅行記

石川旅行

7月17日、18日に妻と一緒に石川県へ旅行してきました。

行ったところ

最寄駅を6:23の電車で出発し、9:38に金沢駅着。 金沢駅の第一印象は「結構小さい」でした。

そこからまずは周遊バスに乗って、近江町市場へ。 バスは100円か200円かで乗れました。

近江町市場では回転寿司を食べたいとのことで、最初はもりもり寿し近江町店を目指したのですが、 待ち組数が40組を超えていたため断念。 10:30からオープンするかいてん寿し 大倉 さんへ。

https://tabelog.com/ishikawa/A1701/A170101/17000822/

ネタが新鮮でどれも美味しく、スーパーで食べられるお寿司と段違いでした。 イカ、マグロ、甘海老、ガス海老、のどぐろ、白海老・・・ ちょっと普段お寿司を気軽に食べるのは我慢して、こういう機会にしっかり食べようと思いました。

その次は歩いて金沢城公園経由で兼六園へ。 しかしながら非常に暑く、自分にしては珍しく休憩を挟みながらじゃないと無理な気温でした。 風景も良いところだったのですが、楽しんだのも束の間。早々に金沢21世紀美術館へ向かいました。

金沢21世紀美術館はどんなところか最初知らずに行きましたが、プールがあるところでした。 https://www.kanazawa21.jp/

展覧会では「Alex Da Corte Fresh Hell アレックス・ダ・コルテ 新鮮な地獄」と「コレクション展1 それは知っている:形が精神になるとき」が開かれており、それぞれ見てみました。

が、まだ芸術は自分には分からないなというのが率直な感想でした・・・w ポイントポイントでみると面白いものもあったので、それをみれただけでも良しかな?という感じです。

その後、ひがし茶屋街方面へ歩いて行きました。

道中にあった末広堂というお店の砂糖を使用していないきんつばが美味しく、またリピートしたかったです。 調べたらAmazonふるさと納税でも入手できそうでした。

そして途中でたまたま立ち寄った「ハム&ゴー 橋場町スタンド」というお店が非常に良く、加賀棒茶ソフトを食べましたが非常に美味しかったです。また食べたい。 https://tabelog.com/ishikawa/A1701/A170101/17010046/

そしてひがし茶屋街へ。ここでは街並みを散策、お店へふらっと入るのみでしたが良いところでした。 小京都と呼ばれるらしいのですが、それも納得できる感じですが個人的には京都よりも風情を感じることができて良かったです。

その後は金沢駅へ戻り、加賀温泉駅まで戻り、送迎バスで山代温泉へ向かいました。

宿泊したのは「白山菖蒲亭」さん。 https://www.shoubutei.co.jp/

ほぼ貸切状態で、他に泊まっている方々は1組?2組?くらいでした。 そのため温泉がほぼ貸切状態で、途中からは私1人の貸切状態に。 男湯が露天にもなっている時間帯で、外の風景を眺めながらゆっくりと湯船に浸かる時間は至高でした。 水風呂もちょっと温かいくらいの温度で非常に満喫できました。

夕食には鮑が出てきて、新鮮な(動いていました)ものを食べることができました。 他の料理も非常に美味しかったです。

翌朝にも朝風呂を浴びて、食事を出していただき、旅館を出ました。 皆様非常に丁寧で心地よい宿でした。

そのあとは山代温泉の総湯・古総湯へ。 特に古総湯は道後温泉を思い出す趣のある温泉でした。 温度がとても熱く、ちょっと入って終わりました。

温泉卵ソフトなるものを妻と一緒に食べました。 みたらしだんごのような感じで美味しかったです。

その後はバスに乗って加賀温泉駅へ戻り、CANBUSというバスに乗り海まわり線にて尼御前岬へ寄ったのち、片山津温泉へ。

http://www.kaga-canbus.jp/

尼御前岬では海を眺めたあと、サービスエリアへ入れたので、そこで金沢のチャンピオンカレーを食べました。 そのあとは海水浴場へ行き、少しだけ砂浜を歩きました。

片山津温泉も非常に熱かったので入ったのは一瞬だけでしたが、良い景色でした。

その後はバスに乗り、金沢駅へ戻り、サンダーバードで帰宅しました。 17時20分発でした。

こうやって温泉旅館を訪れてみて感じたのは、かつて栄えた温泉街も今は人がまばらになっているので、もっと人が増えてほしいなという気持ちですね・・・ とはいえそれも仕方ないことなのかなとも・・・

妻と旅行に行けて楽しかったので、またどこかへ行こうと思います。