pipenvによるpythonアプリケーション実行環境をDockerとVirtualBox(Vagrant)で作る


前置き

pythonでバッチやWebアプリケーション(Django/Flask等)の環境を作るときのベースイメージとなるものをDockerとVagrantで作ったのでその記録です。(DockerもVagrant(VirtualBox)もベースはCentOS(7.6)です)

本当は、nginxやuWSGIなどとの連携や簡単なアプリケーションまで作って載せよう思いましたが、長くなったので、気が向いたら書くことにします。


Dockerコンテナ版


前提

sshで接続できるpipenvで作るpython実行環境のDockerコンテナのサンプルです。

これをこのまま利用するというよりはこれでビルドしたイメージをベースにDjangoやuWSGIをインストールした派生イメージを作ったり、pythonのバッチアプリケーションを実行するコンテナを作ったりする想定で作ったものです。

試したDockerのバージョンは以下の通りです。

Client: Docker Engine - Community

Version: 18.09.2
API version: 1.39
Go version: go1.10.8
Git commit: 6247962
Built: Sun Feb 10 04:12:39 2019
OS/Arch: darwin/amd64
Experimental: false

Server: Docker Engine - Community
Engine:
Version: 18.09.2
API version: 1.39 (minimum version 1.12)
Go version: go1.10.6
Git commit: 6247962
Built: Sun Feb 10 04:13:06 2019
OS/Arch: linux/amd64
Experimental: false


Dockerfileの中身

FROM centos:7

ARG APP_USR_PWD
ARG BASE_GROUP=apps
ARG APP_USER=app
ARG APP_HOME=/home/app
# only python3.x.y
ARG PYTHON_VERSION=3.7.4

RUN test -n "${APP_USR_PWD}" && \
groupadd ${BASE_GROUP} && \
useradd -d ${APP_HOME} -G ${BASE_GROUP} -s /bin/bash ${APP_USER} && \
yum install -y epel-release && \
yum install -y https://centos7.iuscommunity.org/ius-release.rpm && \
yum install -y sudo python-pip openssh openssh-server passwd logrotate sudo wget vim jq \
gcc git libffi-devel zlib-devel bzip2 bzip2-devel readline readline-devel sqlite sqlite-devel openssl openssl-devel && \
yum -y update && yum -y clean all && rm -rf /var/cache/yum/ && \
echo "${APP_USER}:${APP_USR_PWD}" | chpasswd && \
echo "${APP_USER} ALL=(ALL) ALL" > /etc/sudoers.d/${APP_USER} && \
pip install pip --upgrade && pip install pipenv

# install pyenv
RUN git clone https://github.com/pyenv/pyenv.git ${APP_HOME}/.pyenv && \
chown -R ${APP_USER}:${BASE_GROUP} ${APP_HOME}/ && \
echo '' >> ${APP_HOME}/.bash_profile && \
echo 'export PYENV_ROOT="${HOME}/.pyenv"' >> ${APP_HOME}/.bash_profile && \
echo 'export PATH="${PYENV_ROOT}/bin:${PATH}"' >> ${APP_HOME}/.bash_profile && \
echo 'if command -v pyenv 1>/dev/null 2>&1; then' >> ${APP_HOME}/.bash_profile && \
echo ' eval "$(pyenv init -)"' >> ${APP_HOME}/.bash_profile && \
echo 'fi' >> ${APP_HOME}/.bash_profile && \
echo "export PIPENV_DEFAULT_PYTHON_VERSION=${PYTHON_VERSION}" >> ${APP_HOME}/.bash_profile && \
echo 'export PIPENV_VENV_IN_PROJECT=true' >> ${APP_HOME}/.bash_profile

# install python.
RUN su - ${APP_USER} -c "pyenv install ${PYTHON_VERSION} && pyenv global ${PYTHON_VERSION} && pyenv rehash && pip install --upgrade pip && pip install pipenv"

# setup ssh.
RUN ssh-keygen -q -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa && \
ssh-keygen -q -f /etc/ssh/ssh_host_ecdsa_key -N '' -t ecdsa && \
ssh-keygen -q -f /etc/ssh/ssh_host_ed25519_key -N '' -t ed25519 && \
mkdir -p ${APP_HOME}/.ssh && chmod 700 ${APP_HOME}/.ssh
# Add public key to container.
ADD id_rsa.pub ${APP_HOME}/.ssh/authorized_keys
RUN chown -R ${APP_USER}:${BASE_GROUP} ${APP_HOME}/.ssh/

EXPOSE 22

ENTRYPOINT ["/usr/sbin/sshd", "-D"]


事前作業


  • Dockerfileと同じディレクトリにSSHログインする秘密鍵に対応する公開鍵をid_rsa.pubというファイル名で配置すること!!

  • 社内のプロキシ環境下の場合は、DockerfileのFROMの直後当たりに環境変数の設定を追加する。

例)

FROM centos:7

ENV http_proxy=http://proxy.xx.yy:9999 \
https_proxy=http://proxy.xx.yy:9999

# 以下省略・・・


ビルド方法

docker build -t {イメージ名}[:{タグ}] --build-arg APP_USR_PWD={SSHログインユーザがsudoするときに必要なパスワード}

上記以外に以下の値が--build-argで上書き可能である。


  • BASE_GROUP:アプリケーションの実行(=SSH接続)ユーザの所属グループ(デフォルトapps

  • APP_USER:アプリケーションの実行(=SSH接続)ユーザ名(デフォルトapp

  • APP_HOME:アプリケーションの実行(=SSH接続)ユーザのホームディレクトリ(デフォルト/home/app

  • PYTHON_VERSION:pipenvでインストールするアプリケーションの実行(=SSH接続)ユーザのグローバルのpythonバージョン(デフォルト3.7.4)

PYTHON_VERSIONはすべてのバージョンを確認しているわけではないので、指定するものによってはインストールするパッケージが不足している可能性もあります。


起動方法

docker run -d -p ホスト側のポート番号:22 --name {任意のコンテナ名} {イメージ名}[:{タグ}]


dockerホストからのアクセス


  • root

    docker exec -it {任意のコンテナ名} bash

  • app

    docker exec -it --user app {任意のコンテナ名}


Dockerホスト外からSSH接続方法

DockerホストOS側のファイアーウォール設定などで起動方法に記載した「ホスト側のポート番号」でアクセス可能であること。

(この点についてはDockerホスト環境に依存するので割愛)

接続情報は以下の通りです。適宜、利用するSSHクライアントツールに以下の情報を指定してください。


  • ホスト:接続元から見たDockerホストのホスト名 or IPアドレス

  • ポート番号:起動方法に記載した「ホスト側のポート番号」

  • ユーザ:app(--build-arg APP_USER=xxxとした場合はxxx)

  • 秘密鍵:Dockerfileと同じディレクトリに配置した公開鍵の秘密鍵


Vagrant+VirtualBox版


前提

VagrantのproviderはVirtualBoxを前提としていますが、VagrantfileのVirtualBox依存部分を他のプロバイダーに書き換えれば、問題なく動くと思います。

Vagrant版を作ったのはDockerが動かない(正確には動かすのにいろいろな作業が必要になりそう)環境の人がいたので、急遽つくったのものなので、

DockerfileのRUNでいろいろインストールしているところを外だしのprovistion.shに移したものです。

個人的には、実稼働環境の構築も考慮して、Ansible-Playbookで書きたいところなので、そのうち気が向いたら書き直します。

試したVagrantとVirutualBOXのバージョンは以下の通りです。

$ VBoxManage --version

6.0.8r130520
$ vagrant version
Installed Version: 2.2.5
Latest Version: 2.2.5

You're running an up-to-date version of Vagrant!


事前作業


  • Vagarnt sshでログインできるのでSSHの設定は特別には行っていません。

  • 社内のプロキシ環境下の場合は、以下の作業を実施しておくこと。



    • vagrant-proxyconfをインストールする。

      vagrant plugin install vagrant-proxyconf

    • 環境変数にhttp_proxy/https_proxy/no_proxyを登録する。)




Vagrantfileとprovision.sh

Vagrantfileのprovision設定をinlineで書くとみづらい&書きづらいのでprovision.shに外だししました。

forwarded_portのホスト側のポート番号、private_networkのIPアドレス、sync_folderのパス部分は各自の環境に合わせて変更してください。


Vagranfile

# -*- mode: ruby -*-

# vi: set ft=ruby :

Vagrant.configure("2") do |config|

# 利用するプラグイン定義
config.vagrant.plugins = ["vagrant-vbguest", "vagrant-winnfsd"]

# boxイメージ指定
config.vm.box = "centos/7"
config.vm.box_version = "1902.01"
config.vm.box_check_update = false

# GuestAdditionsを更新しない
config.vbguest.auto_update = false

# プロキシ設定
if Vagrant.has_plugin?("vagrant-proxyconf")
if ENV['http_proxy']
config.proxy.http = ENV['http_proxy']
end
if ENV['https_proxy']
config.proxy.https = ENV['https_proxy']
end
if ENV['no_proxy']
config.proxy.no_proxy = ENV['no_proxy']
end
end

# ポートフォワード設定(外部PCからのSSHアクセス用)
config.vm.network "forwarded_port", id: "ssh", guest: 22, host: 2222

# NFSに必要なprivate(host only)ネットワーク
# ipの代わりに`type: "dhcp"`でも可。
config.vm.network :private_network, id: "default-network", ip: "192.168.132.101"

# ホスト名
config.vm.hostname = "app.python.localohost"

# ローカルでコーディングしたものをNFSマウントでゲスト側に認識させるためのディレクトリ
# ⇒アプリを置く場所の想定です。
config.vm.synced_folder ".", "/home/vagrant/apps", type: "nfs", nfs_export: true, nfs_version: 3

# ゲストOSのリソースは適宜変更してください。
config.vm.provider :virtualbox do |vb|
vb.name = "python-app" # VirtualBox上のゲストOS名
vb.gui = false
vb.memory = 2048
vb.cpus = 2
vb.check_guest_additions = false
vb.functional_vboxsf = false
end
# python3に必要なパッケージやpipenv(pyenv)のインストールなど。
config.vm.provision :shell do |shell|
shell.name = 'provision'
shell.env = {
:PYTHON_VERSION => "3.7.4",
}
shell.path = "provision.sh"
end
end



provision.sh

#!/bin/bash

set -eo pipefail

echo "Start provision!!"

# パラメータ定義
# pythonのバージョン定義(環境変数で上書き可能)
PYTHON_VERSION=${PYTHON_VERSION:-"3.7.4"}
# めんどくさいのでvagrantをそのまま使う。
APP_GROUP="vagrant"
APP_USER="vagrant"
APP_HOME="/home/${APP_USER}"

# rootのssh接続を不可/パスなし接続不可(公開鍵認証のみ)
sed -i -e 's/#PermitRootLogin yes/PermitRootLogin no/' \
-e 's/#PermitEmptyPasswords no/PermitEmptyPasswords no/' /etc/ssh/sshd_config

# パッケージのインストール等
# epelとiusの追加
yum install -y epel-release https://centos7.iuscommunity.org/ius-release.rpm
# update
yum update -y
# 必要なパッケージの追加(pythonに特化すれば全部ではなく、一部はサーバー上に必要そうなコマンド群も含む)
yum install -y wget jq git openssl \
gcc python-pip python-devel \
zlib-devel libffi-devel bzip2-devel \
openssl-devel ncurses-devel sqlite-devel \
readline-devel tk-devel gdbm-devel \
libuuid-devel xz-devel systemd-devel
# お掃除
yum -y clean all && rm -rf /var/cache/yum/

# pyenvをインストールする
git clone https://github.com/pyenv/pyenv.git ${APP_HOME}/.pyenv
# アプリユーザで利用できるようにインストール
cat << EOS >> ${APP_HOME}/.bash_profile
# Add pyenv setting
export PYENV_ROOT="\
$HOME/.pyenv"
export PATH="\
$PYENV_ROOT/bin:\$PATH"
if command -v pyenv 1>/dev/null 2>&1; then
eval "\
$(pyenv init -)"
fi
export PIPENV_DEFAULT_PYTHON_VERSION=
${PYTHON_VERSION}
export PIPENV_VENV_IN_PROJECT=true
EOS
chown -R ${APP_USER}:${APP_GROUP} ${APP_HOME}/.pyenv
# アプリケーションユーザ用のpythonはpyenvでglobalにセット
su - ${APP_USER} -c "pyenv install ${PYTHON_VERSION} && pyenv global ${PYTHON_VERSION} && pyenv rehash && pip install --upgrade pip && pip install pipenv"

echo "Fisnish provision!!"



起動方法

Vagrantfile/provision.shを同じディレクトリにおいて、そのディレクトリをカレントにした状態で

vagrant up


ホストからのアクセス

vagrant ssh


VirtualBoxホスト外からSSH接続方法

ホストOS側のファイアーウォール設定などで起動方法に記載した「ホスト側のポート番号」でアクセス可能であること。

接続情報は以下の通りです。適宜、利用するSSHクライアントツールに以下の情報を指定してください。


  • ホスト:接続元から見たVirtualBoxホストのホスト名 or IPアドレス

  • ポート番号:2222

  • ユーザ:vagrant

  • 秘密鍵:vagrant ssh-configで実行した``に秘密鍵のパスが記載されています。

例)

Host default

HostName 127.0.0.1
User vagrant
Port 2222
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
PasswordAuthentication no
IdentityFile {カレントディレクトリ}/.vagrant/machines/default/virtualbox/private_key
IdentitiesOnly yes
LogLevel FATAL

なお、これはUnix/Linux/Macなどの${HOME}/.ssh/configに記載する内容となります。ただ、上の設定はlocalhostを前提にしているのでHostNameに指定する値は接続元から見たVirtualBOXホストのホスト名 or IPアドレスにしてください。

また、先頭のdefaultはVagrantの識別名なのでここも任意変更可能で、例えばHost pythonのように指定した場合、ssh pythonのように打つことでログインすることができます。(Vagrantの仕様とは直接関係ないので細かいことは割愛)


SSH接続後の利用例(共通)

pipenvのことが書きたいわけではないので、ほんの触りだけ。。。

# プロジェクトディレクトリ作る。

mkdir test
cd test

# pipenv(pyenv)による仮想環境を作る
pipenv --python 3.7.4
# =>これでPipfileが作られる
# PyPIパッケージモジュールをインストールする(上位人気のPyPIパッケージ)
pipenv install simplejson setuptools requests python-dateutil PyYAML
# => Pipfileに追記され、Piplockファイルにインストールされたバージョン情報とか諸々書き込まれる
# あとは作るものに依存するので割愛します。


追記

↑のVagrantfileのconfig.vm.synced_folder ".", "/home/vagrant/apps", type: "nfs"の部分についてですが、VPN接続時にマウントエラーが発生するという事象にぶち当たりました。

回避方法は別記事にしましたので、ご参照ください。