みなさんこんにちは、ユニファの赤沼です。 この記事は Unifa Advent Calendar 2023 の10日目の記事です。
最近はポジション的にプロダクションのコードを書くことはほとんどなくなってしまいましたが、ChatGPTなどLLM界隈の動きもあって手元でコードを書いて試してみたいという機会が増えてきました。
Rubyのコードが動けばよいだけならローカルのターミナルで直接試すのでも良いんですが、端末固有の環境に影響を受けたり、ライブラリのバージョンの依存関係の問題などもあり、まっさらな環境を手軽に用意したいということで、最近(今更感もありますが) Dev Containers を使い始めたので Ruby や Python の実行環境の構築について書いてみたいと思います。
Dev Containers について詳しくはこちら。
Extention のインストール
Dev Containers とはなんぞやというそもそものところは上記の公式ページなどに譲るとして、環境を作っていきたいと思います。
まずは Dev Containers の Extention を VSCode にインストールします。
インストールが完了すると左下の隅に青く Extention による表示が追加され、ここから Dev Containers の機能を実行できたりします。
必要なファイル
Dev Containers による環境を構築するのに必要なファイルは下記の通りです。
devcontainers.json
と Dockerfile
だけでも良いのですが、例えば Rails のアプリ開発などでアプリとは別にDB用のコンテナなども構成することを考え、 docker-compose.yml
も使う形にしています。これらをプロジェクトルートの .devcontainer
ディレクトリに配置します。それぞれのファイルの内容は後述します。
devcontainer.json
まず devcontainer.json
の内容は下記のようにしています。
{ "name": "${localWorkspaceFolderBasename}", "dockerComposeFile": "docker-compose.yml", "service": "app", "workspaceFolder": "/workspace", "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": "true", "username": "vscode", "userUid": "1000", "userGid": "1000", "upgradePackages": "true" }, "ghcr.io/devcontainers/features/aws-cli:1": { "version": "latest" } }, "customizations": { "vscode": { "extensions": [ "rebornix.Ruby", "ms-python.python", "ms-azuretools.vscode-docker", "github.copilot", "github.copilot-chat", "github.copilot-labs" ] } }, "remoteEnv": { "AWS_CONFIG_FILE": "$HOME/.aws/config_stoken", "AWS_SHARED_CREDENTIALS_FILE": "$HOME/.aws/credentials_stoken" }, "forwardPorts": [], "remoteUser": "vscode" }
devcontainer.json
の記述の仕方の詳細は下記のリファレンスを参照ください。
今回は "name": "${localWorkspaceFolderBasename}"
とすることで .devcontainer
ディレクトリの親ディレクトリの名前が環境の名前として使われるので、使用中に判別しやすくなります。
また、 features
には様々な機能の設定を追加できます。どのような機能が追加可能かは下記から確認できます。
今回指定している common-utils
は各種便利なツールを導入してくれるもので、コンテナ内でのユーザとして root ユーザ以外を設定したり、 wget
や jq
などのパッケージがインストールされます。
インストールされるパッケージなどの詳細は下記から確認できます。
AWS CLI も使うことが多いので併せて features
でインストールされるように指定しています。
customizations.vscode.extentions
ではインストールする VSCode の Extention を指定しています。すでに Extention がインストールされている環境では下記のように ~/.vscode-server/extensions
でそのリストがみれますので、バージョン表記を除いた部分を指定します。
$ ls -l ~/.vscode-server/extensions total 44 drwxr-xr-x 7 vscode vscode 4096 Dec 6 13:09 amazonwebservices.aws-toolkit-vscode-2.1.0 -rw-r--r-- 1 vscode vscode 5394 Dec 6 13:09 extensions.json drwxr-xr-x 5 vscode vscode 4096 Dec 6 13:09 github.copilot-1.140.0 drwxr-xr-x 5 vscode vscode 4096 Dec 6 13:09 github.copilot-chat-0.10.2 drwxr-xr-x 4 vscode vscode 4096 Dec 6 13:09 github.copilot-labs-0.17.1121 drwxr-xr-x 4 vscode vscode 4096 Dec 6 13:09 ms-azuretools.vscode-docker-1.28.0 drwxr-xr-x 13 vscode vscode 4096 Dec 6 13:09 ms-python.python-2023.20.0 drwxr-xr-x 4 vscode vscode 4096 Dec 6 13:09 ms-python.vscode-pylance-2023.11.10 drwxr-xr-x 7 vscode vscode 4096 Dec 6 13:09 rebornix.ruby-0.28.1 drwxr-xr-x 6 vscode vscode 4096 Dec 6 13:09 wingrunr21.vscode-ruby-0.28.0
Extention の画面右下の Identifier の表示からも確認できます。
Dockerfile
続いて Dockerfile を用意します。
今回は Ruby と Python が使える環境を用意したかったので、 Dev Containers のリポジトリに用意されている下記の内容をベースとしています。
ベースとしているイメージは Docker オフィシャルの Ruby 用イメージです。
これに Python の実行環境を追加するために、 Docker オフィシャルの Python 用イメージの Dockerfile の内容をほぼそのまま追記しています。
結果として Dockerfile の内容は下記のようにしています。
# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.2, 3.1, 3.0, 3-bookworm, 3.2-bookworm, 3.1-bookworm, 3-bullseye, 3.2-bullseye, 3.1-bullseye, 3.0-bullseye, 3-buster, 3.2-buster 3.1-buster, 3.0-buster ARG VARIANT=3-bookworm FROM ruby:${VARIANT} RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 && apt-get purge -y imagemagick imagemagick-6-common # ensure local python is preferred over distribution python ENV PATH /usr/local/bin:$PATH # http://bugs.python.org/issue19846 # > At the moment, setting "LANG=C" on a Linux system *fundamentally breaks Python 3*, and that's not OK. ENV LANG C.UTF-8 # runtime dependencies RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ libbluetooth-dev \ tk-dev \ uuid-dev \ ; \ rm -rf /var/lib/apt/lists/* ENV GPG_KEY 7169605F62C751356D054A26A821E680E5FA6305 ENV PYTHON_VERSION 3.12.0 RUN set -eux; \ \ wget -O python.tar.xz "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz"; \ wget -O python.tar.xz.asc "https://www.python.org/ftp/python/${PYTHON_VERSION%%[a-z]*}/Python-$PYTHON_VERSION.tar.xz.asc"; \ GNUPGHOME="$(mktemp -d)"; export GNUPGHOME; \ gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$GPG_KEY"; \ gpg --batch --verify python.tar.xz.asc python.tar.xz; \ gpgconf --kill all; \ rm -rf "$GNUPGHOME" python.tar.xz.asc; \ mkdir -p /usr/src/python; \ tar --extract --directory /usr/src/python --strip-components=1 --file python.tar.xz; \ rm python.tar.xz; \ \ cd /usr/src/python; \ gnuArch="$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)"; \ ./configure \ --build="$gnuArch" \ --enable-loadable-sqlite-extensions \ --enable-optimizations \ --enable-option-checking=fatal \ --enable-shared \ --with-lto \ --with-system-expat \ --without-ensurepip \ ; \ nproc="$(nproc)"; \ EXTRA_CFLAGS="$(dpkg-buildflags --get CFLAGS)"; \ LDFLAGS="$(dpkg-buildflags --get LDFLAGS)"; \ make -j "$nproc" \ "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \ "LDFLAGS=${LDFLAGS:-}" \ "PROFILE_TASK=${PROFILE_TASK:-}" \ ; \ # https://github.com/docker-library/python/issues/784 # prevent accidental usage of a system installed libpython of the same version rm python; \ make -j "$nproc" \ "EXTRA_CFLAGS=${EXTRA_CFLAGS:-}" \ "LDFLAGS=${LDFLAGS:--Wl},-rpath='\$\$ORIGIN/../lib'" \ "PROFILE_TASK=${PROFILE_TASK:-}" \ python \ ; \ make install; \ \ # enable GDB to load debugging data: https://github.com/docker-library/python/pull/701 bin="$(readlink -ve /usr/local/bin/python3)"; \ dir="$(dirname "$bin")"; \ mkdir -p "/usr/share/gdb/auto-load/$dir"; \ cp -vL Tools/gdb/libpython.py "/usr/share/gdb/auto-load/$bin-gdb.py"; \ \ cd /; \ rm -rf /usr/src/python; \ \ find /usr/local -depth \ \( \ \( -type d -a \( -name test -o -name tests -o -name idle_test \) \) \ -o \( -type f -a \( -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \) \) \ \) -exec rm -rf '{}' + \ ; \ \ ldconfig; \ \ python3 --version # make some useful symlinks that are expected to exist ("/usr/local/bin/python" and friends) RUN set -eux; \ for src in idle3 pydoc3 python3 python3-config; do \ dst="$(echo "$src" | tr -d 3)"; \ [ -s "/usr/local/bin/$src" ]; \ [ ! -e "/usr/local/bin/$dst" ]; \ ln -svT "$src" "/usr/local/bin/$dst"; \ done # if this is called "PIP_VERSION", pip explodes with "ValueError: invalid truth value '<VERSION>'" ENV PYTHON_PIP_VERSION 23.2.1 # https://github.com/pypa/get-pip ENV PYTHON_GET_PIP_URL https://github.com/pypa/get-pip/raw/4cfa4081d27285bda1220a62a5ebf5b4bd749cdb/public/get-pip.py ENV PYTHON_GET_PIP_SHA256 9cc01665956d22b3bf057ae8287b035827bfd895da235bcea200ab3b811790b6 RUN set -eux; \ \ wget -O get-pip.py "$PYTHON_GET_PIP_URL"; \ echo "$PYTHON_GET_PIP_SHA256 *get-pip.py" | sha256sum -c -; \ \ export PYTHONDONTWRITEBYTECODE=1; \ \ python get-pip.py \ --disable-pip-version-check \ --no-cache-dir \ --no-compile \ "pip==$PYTHON_PIP_VERSION" \ ; \ rm -f get-pip.py; \ \ pip --version CMD ["python3"]
docker-compose.yml
最後に docker-compose.yml
を用意します。
今回はシンプルに先程の Dockerfile をベースとしたコンテナを立ち上げているだけで、 volume のマウントだけしています。
git や AWS CLI を使う際にローカルのホームディレクトリにある認証情報をそのまま使いたかったため、コンテナ内のユーザのホームディレクトリにマウントしています。
version: '3.8' services: app: build: . tty: true stdin_open: true volumes: - ../:/workspace:cached - ${HOME}/.ssh:/home/vscode/.ssh - ${HOME}/.aws:/home/vscode/.aws
Gemfile
私の場合は Ruby のコードを書くことが多いので、 Gemfile もついでに追加し、ひとまず Rails が使えるようにしておきます。
ちなみに Rails のバージョン指定は執筆時点の Rails Tutorial で使用されていたバージョンに合わせています。
# frozen_string_literal: true source "https://rubygems.org" gem "rails", "7.0.4.3"
コンテナ起動
ここまでで必要なファイルが揃ったのでコンテナを立ち上げてみます。
画面左下の青い Extention 表示をクリックすると下記のようなメニューが表示されるので、 Reopen in Container
を選択します。
もしくはファイルが揃っている状態で VSCode を起動すると下記のような表示が出るので、ここから Reopen in Container
を選択しても同様になります。
初回のビルドは少し時間がかかるかと思います。 特に今回のケースでは Python のビルドで結構時間がかかります。 2回目以降の起動はあまり時間はかからずすぐに起動できるかと思います。
起動が終わると左下の Extention の表示のところに devcontainer.json で指定した環境名が表示されます。
VSCode のターミナルから Ruby と Python も使えるようになっています。
bundle install
を実行すれば rails も使えるようになります。
vscode ➜ /workspace $ bundle install Fetching gem metadata from https://rubygems.org/.......... 〜〜〜中略〜〜〜 Bundle complete! 1 Gemfile dependency, 45 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed. vscode ➜ /workspace $ vscode ➜ /workspace $ bundle exec rails -v Rails 7.0.4.3
ボリュームマウントした .ssh の認証情報や、 features で指定した AWS CLI なども使えるようになっています。
vscode ➜ /workspace (main) $ ssh -T git@github.com Hi h-akanuma! You've successfully authenticated, but GitHub does not provide shell access. vscode ➜ /workspace (main) $ vscode ➜ /workspace (main) $ aws --version aws-cli/2.14.5 Python/3.11.6 Linux/6.4.16-linuxkit exe/aarch64.debian.12 prompt/off
テンプレートリポジトリ化
ここまでに用意した .devcontainer
ディレクトリの内容を GitHub のリポジトリにプッシュし、そのディレクトリをテンプレートリポジトリとして設定しておくことで、このリポジトリをテンプレートとして新しいリポジトリを作ることができるようになります。
該当のリポジトリの設定画面から Template repository
にチェックを入れるだけで設定できます。
すると Use this template
ボタンから Create a new repository
が選択できるようになります。
リポジトリの作成画面からリポジトリ名を入力すれば新しいリポジトリが作成されます。
Rails アプリを立ち上げるまでをやってみる
上記でテンプレートを元に作成したリポジトリから試しに Rails アプリを立ち上げるところまでをやってみます。
まずはローカルに clone します。
$ git clone git@github.com:h-akanuma/devcontainer_rails_sample.git
そして VSCode で上記のディレクトリを開き、 Reopen in Container
でコンテナを立ち上げます。
立ち上がったら bundle install
を実行して rails を使えるようにします。
vscode ➜ /workspace $ bundle install
rails new
で Rails のアプリを作成します。
vscode ➜ /workspace (main) $ bundle exec rails _7.0.4.3_ new sample_app
作成した Rails アプリのディレクトリで rails server
を実行します。
vscode ➜ /workspace/sample_app (main) $ bundle exec rails server => Booting Puma => Rails 7.0.8 application starting in development => Run `bin/rails server --help` for more startup options Puma starting in single mode... * Puma version: 5.6.7 (ruby 3.2.2-p53) ("Birdie's Version") * Min threads: 5 * Max threads: 5 * Environment: development * PID: 3374 * Listening on http://127.0.0.1:3000 Use Ctrl-C to stop
すると画面右下に下記のような表示が出て、 Open in Browser
を選択すると、 Rails アプリが使用しているポート 3000 を参照する形でブラウザでアクセスできます。
PORTS タブには現在転送されているポートのリストが表示されるので、そのリストから地球(?)マークをクリックすることでも同様になります。
ブラウザには下記のように Rails アプリによる画面が表示されます。
作成された Rails アプリも含めて git にコミットしたいところですが、 Rails アプリのディレクトリ内にも .git
ディレクトリがあるため、サブモジュールのような形になりそのままでは Rails アプリのディレクトリを元の git リポジトリのコミットに含めることができません。なので Rails アプリのディレクトリ内の .git
ディレクトリを削除します。
vscode ➜ /workspace $ rm -rf sample_app/.git
この状態で git add
しようとすると下記のようなエラーになります。
vscode ➜ /workspace $ git add sample_app/ fatal: detected dubious ownership in repository at '/workspace' To add an exception for this directory, call: git config --global --add safe.directory /workspace
エラーメッセージに表示されている git コマンドを実行して、workspace を safe.directory に設定します。
vscode ➜ /workspace $ git config --global --add safe.directory /workspace
これによって git add
と git commit
も行えるようになります。
vscode ➜ /workspace (main) $ git add sample_app/ vscode ➜ /workspace (main) $ git commit -m 'First commit'
あとは git push
すればOKなので、一連の開発サイクルとしては回せるようになったかと思います。
vscode ➜ /workspace (main) $ git push origin main
まとめ
まだ色々と手探りでやってみているところですが、何か新しいことを試したいときにサクッと新しい環境を用意できるのはとても便利です。少し前に GitHub Codespace を使っていましたが、無料では月間60時間までの制限もあるので、 Dev Containers であればそういったことも気にせず、ローカルボリュームのマウントなどもできるので良い感じです。
実際のプロダクトの環境はだいぶ複雑なので簡単には適用できないかもしれませんが、いずれは活用していけると良いかと思っています。
ユニファでは開発環境の改善も含めて共に保育をHackしてくれる仲間を募集中です!少しでも興味をお持ちいただけた方は、ぜひ一度カジュアルにお話しましょう!