Docker/Kubernetes構成のためのPython開発プロジェクトの雛形

# はじめに

PythonスクリプトをPipenv環境で開発し、実行するためのコンテナをEKS上で起動するというケースを想定しています。

次のソフトウェアを用います。

- vim（ほかのエディタでも構いません）
- venv
- Pipenv
- Python3.8
- Docker
- Amazon ECR
- Amazon EKS

次の事項は扱いません。

- 各構成要素のPros/Cons的な話
- Amazon ECR／EKSのセットアップ（eksctlなどでクラスタを作れるようになっている必要があります）

# 構成ファイル

```text
.
├── Pipenv（Pythonの環境管理）
├── Dockerfile（Pythonスクリプトを動作させるコンテナを定義）
├── deployment.yaml（Kubernetes上でコンテナを動作させるデプロイメント定義）
├── src
│   ├── main.py（開発対象のPythonスクリプト）
```

# Python開発

サンプルスクリプト（./src/main.py）を作成して動作確認します。

## サンプルスクリプト

今回は次のサンプルスクリプトを使用します。
仕様：1秒毎に標準出力に現在時刻を表示する。

```python:./src/main.py
#
# 一秒毎に時刻を表示するプログラム
#

import time
import datetime

while 1 == 1:
    print("{}".format(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')))
    time.sleep(1)
```

今回は解説しませんが、Amazon Container Insightsを使うとKubernetesのPodのLogs（標準出力を含む）をCloud Watchで取得できるため、ローカル／Docker／Kubernetesのログ回りを単一のコードで実現できて便利です。サイドカー＋Fluentなど専用のコードを入れる場合に比べ柔軟性は低いですが。

## Pipenv環境を作る

次のPipfileを用意します。
dev-packagesの中身はvim環境のためのものなので、無くても実行できます。
（ご参考：[『最低限の手間で、開発にも使えるVim環境を構築する。』](https://qiita.com/YuukiMiyoshi/items/28e26baa3fe534b32009)）

```conf:./Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[scripts]
dev = "python ./src/main.py"

[dev-packages]
flake8 = "*"
jedi = "*"
python-vim = "*"
pylint = "*"

[packages]

[requires]
python_version = "3.8"
```

Venv環境のセットアップ

```sh
# venv環境のセットアップ
# Python3が入っていない場合は事前に入れておいてください。
python3 -m venv .venv
```

Venv環境を有効にする

```sh
# b系シェルの場合
source .venv/bin/activate

# fishシェルの場合
source .venv/bin/activate.fish
```

Pipenv／必要パッケージをインストール

```sh
# Pipenvをインストール
pip install pipenv

# Pipfileを元に必要パッケージをインストール
pipenv install
```

## Pythonスクリプトを実行する

venv環境を有効にした状態で、次のようにPipenvを用いてPythonスクリプトを実行します。
現在日時が一秒毎に表示されれば成功です。

```sh
pipenv run dev

> 2021/03/25 02:24:20
> 2021/03/25 02:24:21
> 2021/03/25 02:24:22
> 2021/03/25 02:24:23
```

# コンテナの作成と実行

上記で作成したPythonプログラムを実行するコンテナを作成します
教科書通りのMulti-stage Build構成です。


```docker:./Dockerfile
#
# ビルダーコンテナ
#

FROM python:3.8-buster as builder

WORKDIR /app

COPY requirements.lock /app
RUN pip3 install -r requirements.lock

#
# メインコンテナ
#

FROM python:3.8-slim-buster as main
COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages

COPY src /app

WORKDIR /app

ENTRYPOINT ["python", "/app/main.py"]
```

マルチステートビルドは本記事のスクリプト程度ではメリットを感じにくいですが、ミドルウェアを（特にコンパイルして）利用するプロジェクトなどでは実稼働コンテナに不要ファイルを生成する必要が無く有用です。

## コンテナをビルド

```sh
# パッケージ情報を作成
# pipenvのパッケージ情報を出力します
pipenv lock -r > requirements.lock

# ビルドしてイメージを作成
# （一括してビルドする場合）
docker build -t ＜コンテナ名＞:latest .

# 前段と後段を別々にビルドしたい場合は次の２つ
docker build --target builder -t ＜コンテナ名＞:builder .
docker build --target main -t ＜コンテナ名＞:latest .
```

## コンテナを実行

```sh
docker run -it --rm ＜コンテナ名＞:latest

> 2021/03/25 02:24:20
> 2021/03/25 02:24:21
> 2021/03/25 02:24:22
> 2021/03/25 02:24:23
```

Pipenvで実行した場合と同様、日時が１秒毎に表示されれば成功です。

# Amazon EKSへデプロイする

次の事項はあらかじめ済ませておいてください。
・ECRのセットアップ
・ECRリポジトリの作成
・EKSのセットアップ
・eksctlのセットアップ（クラスタ作成で利用する場合）

## ECRへコンテナイメージをプッシュする

ECRに作成したリポジトリにコンテナイメージをプッシュします。
リポジトリのURLは確認しておいてください。

手順は他のクラウドやDocker HUBと同様ですが、
ECRでは認証情報を引き込む必要があります。

```sh
# イメージプッシュ用のタグを作成
docker tag ＜コンテナ名＞:latest ＜リポジトリURL＞:latest

# ECRの認証情報をローカルに引き込む
aws ecr get-login-password --region ＜リージョン名:ap-northeast-1＞ | docker login --username AWS --password-stdin ＜リポジトリURLルート部分＞

# プッシュを実行
docker push ＜リポジトリURL＞:latest
```

## EKSへデプロイ

本記事ではeksctlを用いてテスト用のクラスタを起動します。
EKSは更新が速いので、オプション構成は都度見直した方が良いかもしれません。
手元では起動に約20分かかります。

```sh
eksctl create cluster \
        --vpc-cidr 10.0.0.0/16 \
        --vpc-nat-mode HighlyAvailable \
        --name ＜クラスタ名＞ \
        --version 1.18 \
        --region ＜リージョン名:ap-northeast-1＞ \
        --nodegroup-name ＜ノードグループ名＞ \
        --node-type t3.large \
        --nodes 3 \
        --nodes-min 1 \
        --nodes-max 4
```

つぎの内容でデプロイメント用のyamlファイルを作成します
ネームスペースは別途作成しても良いですが、今回は同居させました。

```yaml:deployment.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: ＜ネームスペース名＞
  labels:
    name: ＜ネームスペース名＞
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ＜ワークロード名＞
  namespace: ＜ネームスペース名＞
  labels:
    app: ＜ワークロード名＞
spec:
  replicas: 1
  progressDeadlineSeconds: 300
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: ＜ワークロード名＞
  template:
    metadata:
      labels:
        app: ＜ワークロード名＞
      namespace: ＜ネームスペース名＞
    spec:
      containers:
      - name: ＜コンテナ名＞
        image: ＜リポジトリURL＞
        imagePullPolicy: IfNotPresent
        resources:
          requests:
            cpu: "250m"
            memory: "256Mi"
          limits:
            cpu: "250m"
            memory: "256Mi"
      restartPolicy: Always
      securityContext: {}
      terminationGracePeriodSeconds: 30
```

次のコマンドでデプロイを実施します

```sh
kubectl apply -f deployment.yaml
```

## デプロイ結果の確認

様々な方法があると思いますがシンプルにpod名を調べてlogsで確認してみます。
ワークロードの起動完了までに数分かかります。起動完了前でもkubectl logsコマンドを実行できますが何も取得できません。

```sh
# pod名を確認
# （本クラスタには記事のワークロードのみデプロイしていると想定しています）
kubectl get pods -n ＜ネームスペース名＞

# podの出力を表示
# Pythonが一秒毎に表示している日付文字列が表示されれば成功です
kubectl logs ＜pod名＞ -n ＜ネームスペース名＞
> 2021/03/25 02:24:20
> 2021/03/25 02:24:21
> 2021/03/25 02:24:22
> 2021/03/25 02:24:23
```