PipenvのセットアップしてFlaskでCRUDのアプリ作ってDockerでコンテナ化するまでの手順


## 記事を書いた背景

今月で現職のSIerを退職することになり、心機一転インフラまわりの技術者になろうと思ってます。
近年のパブリッククラウドを駆使したインフラ、CI/CD環境構築にあたって、様々な言語でアプリをビルドしたり、疎通確認用のアプリを作ったりすることは、私のようなレガシーエンジニアであっても避けては通れなくなってきていると感じるこの頃です。

いろいろな言語のビルドとちょっとしたWebアプリぐらいを作れるように頑張ろうと思って、今回はPython、PipenvでFlaskのアプリ（ほんとに触り程度）を作ってDockerコンテナ化するまでの手順を記載しました。


## Pipenvのインストール

近年のPythonの環境構築はPipenvというツールを使って構築するっぽいので使ってみました。
JavaScriptのnpmみたいで使いやすいです。

```
pip install pipenv
```

## アプリプロジェクトの作成


```
mkdir flask-app
cd flask-app
```

今回はPython3.8を使うことにします。

```
pipenv --python 3.8
pipenv install flask
pipenv install --dev autopep8
```

これから作成するファイルを空で作成しておきます。

```
mkdir src
touch src/main.py
touch Dockerfile
touch .dockerignore
```

`Pipfile`の`scripts`セクションに追記します。

```Pipfile
[scripts]
start = "python src/main.py"
```


## Flaskアプリケーションのコーディング

Python素人が書いたコードになってしまいますが、以下のような感じで作成しました。

```src/main.py
import sqlite3
import flask

app = flask.Flask(__name__)


def get_db():
    db = getattr(flask.g, '_database', None)
    if db is None:
        db = flask.g._database = sqlite3.connect('mydb.sqlite')
    return db


@app.teardown_appcontext
def close_conn(exception):
    db = getattr(flask.g, '_database', None)
    if db is not None:
        db.close()


@app.route('/todos/<id>', methods=['GET'])
@app.route('/todos', methods=['GET', 'POST'])
def todos(id=None):
    db = get_db()

    # リクエストをもらったらテーブル作成
    curs = db.cursor()
    curs.execute(
        'create table if not exists todos('
        'id integer primary key autoincrement, title string)'
    )

    id = flask.request.values.get('id', id)

    if flask.request.method == 'GET' and id is not None:
        curs.execute('select * from todos where id = {}'.format(id))
        response = curs.fetchone()
        if response is not None:
            id, title = response
            return flask.jsonify(todo={"id": id, "title": title}), 200

        return flask.jsonify(message='resource not found'), 400

    if flask.request.method == 'GET':
        curs.execute('select * from todos')
        response = curs.fetchall()
        todos = []
        for todo in response:
            id, title = todo
            todos.append({"id": id, "title": title})

        return flask.jsonify(todos=todos), 200

    if flask.request.method == 'POST':
        title = flask.request.json['title']
        curs.execute('insert into todos(title) values("{}")'.format(title))
        db.commit()
        return flask.jsonify(message='created new toto')


def main():
    app.debug = True
    app.run(host='0.0.0.0', port=5000)


if __name__ == "__main__":
    main()
```

ここまででローカル環境で動作するようになるはずです。

```
pipenv run start

 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 208-926-045
```

curlコマンド叩いて確認します。

```
curl -XPOST -H 'Content-Type: application/json' localhost:5000/todos -d '{"title": "new task1"}'
curl -XPOST -H 'Content-Type: application/json' localhost:5000/todos -d '{"title": "new task2"}'
curl localhost:5000/todos
curl localhost:5000/todos/1
```

## Dockerfileの作成とビルド

Pipenvと組み合わせた場合のプラクティスがわかってないのですが、シンプルイズベストで作ってみました。

```Dockerfile
FROM python:3.8

ENV LC_ALL=C.UTF-8 \
    LANG=C.UTF-8

WORKDIR /app
COPY . .

RUN pip install pipenv
RUN pipenv install

CMD ["pipenv", "run", "start"]

EXPOSE 5000
```

`.dockerignore`ファイルも作成しておきます。

```.dockerignore
*.sqlite
.git
```

ビルドします。

```
docker image build -t flaskapp:latest . 
```

コンテナを起動します（ローカル側のポートを15000番として起動しています）。

```
docker container run -it -p 15000:5000 --rm flaskapp:latest

 * Serving Flask app "main" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 954-331-726
```

curlコマンド叩いて確認できればOKです。

```
curl -XPOST -H 'Content-Type: application/json' localhost:15000/todos -d '{"title": "new task1"}'
curl -XPOST -H 'Content-Type: application/json' localhost:15000/todos -d '{"title": "new task2"}'
curl localhost:15000/todos
curl localhost:15000/todos/1
```

## おわりに

nodejsでのnpm、yarnに慣れきった生活を送っていたこともあり、pythonの環境構築を避けていたのですが、Pipenvが便利だなと感じました。Pipenv知らなかったので今まで損していた気分になりました。

とりあえず、インフラエンジニア目線(主観）ではここまでで出来たので満足です。
アプリエンジニアの手を煩わせずに環境をご提供できるように今後努力いたします。


しかし、5年ぐらい前までは覚えることそんなになかったのに、昨今のIT技術を取り巻く環境変化はすごいスピードです。置いていかれてるのですが、すこしでも追いつけるように必死こいてます。