2022年10月9日日曜日

fly.io に Python Flask アプリをデプロイ

はじめに

PaaS である fly.io で Python Flaskアプリ (DB は Postgres を利用) をデプロイしてみたのでまとめています。

なお、「fly.io に Python Flask アプリをデプロイするためのローカルの作業及び開発環境を作成」にて作成した Docker コンテナ内で作業します。

fly.io サイトでアカウント登録

https://fly.io/ でアカウントを登録しておきます。

作業用の Docker コンテナを起動

fly.io に Python Flask アプリをデプロイするためのローカルの作業及び開発環境を作成」にて作成した Docker コンテナを起動

$ docker start --interactive fly-work

woker ユーザーに su して作業ディレクトリに移動

# su - worker
$ cd ~/work/

この状態で以下の 2つのファイルが存在します。

$ tree -a ./
./
├── requirements.txt
└── testapp.py

0 directories, 2 files

flyctl をインストール

fly.io のコマンドラインツール flyctl をインストール

$ curl -L https://fly.io/install.sh | sh

環境変数を .bashrc に記載

$ echo 'export FLYCTL_INSTALL="/home/worker/.fly"' >> ~/.bashrc
$ echo 'export PATH="$FLYCTL_INSTALL/bin:$PATH"' >> ~/.bashrc

環境変数を反映

$ source ~/.bashrc

fly.io にログイン

$ flyctl auth login
  • GUI 環境が無いので以下のメッセージが出ます。

    failed opening browser. Copy the url (https://fly.io/app/auth/cli/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) into a browser and continue
    Opening https://fly.io/app/auth/cli/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
    

ブラウザでメッセージ中の URL https://fly.io/app/auth/cli/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx を開き、未ログインの場合はログインし、「Continue as [登録したメールアドレス]」をクリック

作業環境で以下のメッセージとなり、flyctl でのログイン完了

successfully logged in as xxxxxxxx@xxxx.xxx

Python Flask アプリをローンチ

flyctl コマンドでローンチを実施します。しかし、ローカルに Docker イメージを作成する環境が無いためか、この段階ではデプロイまでは実施されません。

Fly アプリをローンチ

$ flyctl launch
  • アプリ名を聞いてくるので入力

    ? App Name (leave blank to use an auto-generated name): myflyapp
    
  • リージョンを聞いてくるので Tokyo を選択してみました

    ? Select region:  [Use arrows to move, type to filter]
    ... 省略 ...
    > nrt (Tokyo, Japan)
    
  • Postgres DB をセットアップするか聞いてきますが、別で作成しているので、ここではセットアップしません

    ? Would you like to set up a Postgresql database now? No
    
  • 最後のこのメッセージが表示され、作成された Procfile を実際にあわせて編集する必要があるとのことです

    We have generated a simple Procfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application.
    

二つのアプリが設定されました

$ flyctl apps list
NAME                OWNER           STATUS          PLATFORM        LATEST DEPLOY
fly-builder-XXXX    personal        suspended       machines
myflyapp            personal        pending
  • myflyapp

    • flyctl launch で設定したアプリ
  • fly-builder-XXXX

    • デプロイ時に Docker Image を作成するためのリモートビルダーアプリ
    • ローカルで Docker イメージを作成して Fly サイトへ push するのがデフォルトの動作のようですが、Docker デーモンがローカルに無い場合はローカルで Docker イメージを作成できないので Fly サイトにリモートビルダーアプリが作成され、これが Docker イメージを作成するとのことです。

また、Procfile と fly.toml が作成されました。

$ tree -a
.
├── Procfile
├── fly.toml
├── requirements.txt
└── testapp.py

0 directories, 4 files

fly.toml は fly アプリの設定ファイルとのことです。

Procfile の中身を確認

$ cat Procfile
# Modify this Procfile to fit your needs
web: gunicorn server:app

Procfile を以下のように書き換え

$ cat Procfile
# Modify this Procfile to fit your needs
web: gunicorn testapp:app --log-file=-
  • server:app の部分を実際の Python スクリプトに合わせて testapp:app に書き換え

  • testapp.py スクリプトの中の Flask オブジェクトが app = Flask(__name__) となっているので testapp:app と指定します

  • ログを取得できるように --log-file=- を追加

WSGI サーバーとして gunicorn を使用するので requirements.txt に追加

$ cat requirements.txt
gunicorn
Flask
Flask-SQLAlchemy
psycopg2-binary

Python Flask アプリが使用する Postgres DB 用の fly アプリを作成

Postgres DB 用の fly アプリを作成

$ flyctl postgres create
  • アプリ名を聞いてくるので入力

    ? Choose an app name (leave blank to generate one): mydbapp
    
  • リージョンを聞いてくるので Tokyo を選択してみました

    ? Select regions:  [Use arrows to move, type to filter]
    ... 省略 ...
    > Tokyo, Japan (nrt)
    
  • 設定を聞いてくるので以下を選択してみました。(これ以外の選択だと有料プランが必要になるかも)

    ? Select configuration:  [Use arrows to move, type to filter]
    > Development - Single node, 1x shared CPU, 256MB RAM, 1GB disk
    
  • DB への接続情報が表示されました。後で参照できるよう、どこかセキュアな場所に保存しておきます。(二度と確認することができないとのことです)

    Postgres cluster mydbapp created
      Username:    postgres
      Password:    XXXXXXXXXXXXXXXXXXXXXXX
      Hostname:    mydbapp.internal
      Proxy Port:  5432
      Postgres Port: 5433
    

Postgres DB アプリ mydbapp が作成され起動していることを確認

$ flyctl apps list
NAME                OWNER           STATUS          PLATFORM        LATEST DEPLOY
fly-builder-XXXX    personal        suspended       machines
mydbapp             personal        running         nomad           2m49s ago
myflyapp            personal        pending

flyctl を使って Postgres DB に接続

$ flyctl postgres connect --app mydbapp
  • 通常の psql ツールで接続されているようです。

  • ユーザー名は指定していませんが、postgres ユーザーで接続されました。パスワードは不要でした。

  • データベースを確認

    postgres=# \l
                                        List of databases
      Name    |   Owner    | Encoding |  Collate   |   Ctype    |     Access privileges
    -----------+------------+----------+------------+------------+---------------------------
    postgres  | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 |
    template0 | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 | =c/flypgadmin            +
              |            |          |            |            | flypgadmin=CTc/flypgadmin
    template1 | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 | =c/flypgadmin            +
              |            |          |            |            | flypgadmin=CTc/flypgadmin
    (3 rows)
    
    • 通常の以下のデータベースのみ作成されていました。
      • postgres
      • template0
      • template1
  • 接続先のデータベース名も postgres データベースでした。

    postgres=# select current_database();
    current_database
    ------------------
    postgres
    (1 row)
    
  • 終了

    postgres=# \q
    

ローカルの psql ツールやローカルの Python スクリプトから Postgres DB に接続するには flyctl proxy を使うとのことです。

  • flyctl proxy 起動

    $ flyctl proxy 15432:5432 --app mydbapp
    Proxying local port 15432 to remote [mydbapp.internal]:5432
    
    • ローカルでリッスンする proxy ポートは 15432
    • リモートの Postgres DB ポートは 5432
  • 例えば、ローカルの psql ツールで接続する場合

    $ psql postgres://postgres:XXXXXXXXXXXXXXXXXXXXXXX@localhost:15432/
    
    • パスワード XXXXXXXXXXXXXXXXXXXXXXX は上記で控えたものを使用
  • psql 終了

    postgres=# \q
    
  • flyctl proxy も Ctrl-C で終了

Python Flask アプリと Postgres DB アプリを紐づけてテーブル作成とデータ挿入

Python Flask アプリ myflyapp が Postgres DB アプリ mydbapp に接続できるよう、両者を紐づけ

$ flyctl postgres attach \
  --app myflyapp \
  mydbapp

Postgres cluster mydbapp is now attached to myflyapp
The following secret was added to myflyapp:
  DATABASE_URL=postgres://myflyapp:XXXXXXXX@top2.nearest.of.mydbapp.internal:5432/myflyapp
  • myflyapp と mydbapp が紐づけられました
  • myflyapp に上記の環境変数 DATABASE_URL が設定されました
  • mydbapp に以下が作成されています
    • myflyapp データベース
    • myflyapp ユーザー (パスワード XXXXXXXX)

ローカルの psql ツールで mydbapp に接続するための flyctl proxy 起動

$ flyctl proxy 15432:5432 --app mydbapp

Python Flask アプリ testapp.py では Flask-SQLAlchemy を使用しているため、テーブル作成やデータ挿入等は Python を使って testapp.py で作成した SQLAlchemy オブジェクトを使用します。

Python 起動

$ DATABASE_URL=postgres://myflyapp:XXXXXXXX@localhost:15432/myflyapp \
python3
  • 後に testapp.py をモジュールとしてインポートしますが、環境変数として DATABASE_URL を参照するので設定して Python を起動してます。

  • testapp.py をモジュールとしてインポート

    >>> import testapp
    
  • テーブル作成

    >>> testapp.db.create_all()
    
  • 作成された test_record テーブルにデータ挿入

    >>> record = testapp.TestRecord()
    >>> record.test_col = 'Hello World'
    
    >>> testapp.db.session.add(record)
    >>> testapp.db.session.commit()
    
  • Python 終了

    >>> exit()
    

testdb データベースが作成されていることを確認

$ psql \
postgres://myflyapp:XXXXXXXX@localhost:15432/myflyapp \
--command '\l'

   Name    |   Owner    | Encoding |  Collate   |   Ctype    |     Access privileges
-----------+------------+----------+------------+------------+---------------------------
 myflyapp  | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 |
 postgres  | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 |
 template0 | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 | =c/flypgadmin            +
           |            |          |            |            | flypgadmin=CTc/flypgadmin
 template1 | flypgadmin | UTF8     | en_US.utf8 | en_US.utf8 | =c/flypgadmin            +
           |            |          |            |            | flypgadmin=CTc/flypgadmin
(4 rows)

testapp.py の TestRecord オブジェクトに対応する test_record テーブルが testdb データベースに作成されていることを確認

$ psql \
postgres://myflyapp:XXXXXXXX@localhost:15432/myflyapp \
--command '\d'

            List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | test_record | table | myflyapp
(1 row)

test_record テーブルにデータが挿入されていることを確認

$ psql \
postgres://myflyapp:XXXXXXXX@localhost:15432/myflyapp \
--command 'select * from test_record'

  test_col
-------------
 Hello World
(1 row)
  • flyctl proxy を Ctrl-C で終了

Python Flask アプリをデプロイ

Python Flask アプリ myflyapp をデプロイ

$ flyctl deploy

割り当てられた IP を確認

$ fly ips list
VERSION IP                      TYPE    REGION  CREATED AT
v4      XXX.XXX.XXX.XXX         public  global  5m19s ago
v6      XXXX:XXXX:X::XXXX       public  global  5m16s ago

myflyapp アプリのステータスを確認

$ flyctl status
App
  Name     = myflyapp
  Owner    = personal
  Version  = 0
  Status   = running
  Hostname = myflyapp.fly.dev
  Platform = nomad

Deployment Status
  ID          = XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  Version     = v0
  Status      = successful
  Description = Deployment completed successfully
  Instances   = 1 desired, 1 placed, 1 healthy, 0 unhealthy

Instances
ID              PROCESS VERSION REGION  DESIRED STATUS  HEALTH CHECKS           RESTARTS        CREATED
XXXXXXXX        app     0       nrt     run     running 1 total, 1 passing      0               1m31s ago
  • 特に、Hostname = myflyapp.fly.dev より、外部のブラウザから Python Flask アプリ myflyapp にアクセスするための URL が http://myflyapp.fly.dev/ であることが確認できる

  • また、flyctl open を実行すると作業環境は GUI が無いのでブラウザの起動に失敗しますが、表示されるメッセージからもアクセスする URL を確認することができる

    $ flyctl open
    opening http://myflyapp.fly.dev ...
    Error failed opening http://myflyapp.fly.dev: exec: "xdg-open": executable file not found in $PATH
    

ブラウザで http://myflyapp.fly.dev/ でアクセス

  • myflyapp アプリが mydbapp から取得した Hello World が表示されれば正常に動作しています。

参考

2022年10月8日土曜日

fly.io に Python Flask アプリをデプロイするためのローカルの作業及び開発環境を作成

はじめに

PaaS である fly.io で Python Flaskアプリ (DB は Postgres を利用) をデプロイするためのローカルの作業及び開発のための環境を Docker コンテナで構築してみたので以下にまとめています。

なお本記事はローカル環境の構築までについて記載しており、その先の fly.io の操作や fly.io へのアプリのデプロイについては別途記事にする予定です。

Docker コンテナ準備

ベースとなる ubuntu:22.04 docker イメージをローカルに取り込み

$ docker image pull ubuntu:22.04

コンテナを作成して起動

$ docker run \
  --publish 5001:5000 \
  --name fly-work \
  --interactive \
  --tty \
  ubuntu:22.04
  • コンテナ内で実行する開発環境の Python Flask アプリは 5000番ポートをリッスンするので --publish 5001:5000 によりホスト側の 5001番ポートに紐づけます。
  • 後述する通り、ホストのブラウザーからコンテナ内の Python Flask アプリにアクセスする場合は URL として http://localhost:5001/ を指定することになります。

なお次回以降の PC 起動時等、停止しているコンテナを開始する場合は上述の docker run ではなく、以下の通り docker start となります。

$ docker start --interactive fly-work

コンテナ内のシェルが起動するので、まずは作業用の worker ユーザーを作成しパスワードも worker に設定

# useradd \
  --create-home \
  --groups sudo \
  --shell /bin/bash \
  worker

# echo 'worker:worker' | chpasswd

worker ユーザーで個別にインストールが必要なツールをインストール

# apt update && apt -y install sudo net-tools tree

worker ユーザーに su

# su - worker

以降はこのコンテナ内の worker ユーザーでの作業となります。

Ubuntu の基本的な設定

パッケージを最新化

$ sudo apt update && sudo apt -y upgrade

よく使うコマンドを使えるようにするための基本的なパッケージをインストール

$ sudo apt install -y \
  ubuntu-minimal \
  ubuntu-standard \
  ubuntu-server
  • geographic area を聞いてくるので Asia - Tokyo を指定
  • Encoding を聞いてくるので UTF-8 を指定
  • character set を聞いてくるので Guess optimal character set を指定

以降のパッケージインストールで Failed to retrieve available kernel versions. 等のメッセージが出ないようにするためのおまじない

$ sudo apt purge -y needrestart

不要なパッケージを削除

$ sudo apt autoremove -y

日本語環境の設定

$ sudo apt install -y \
  language-pack-ja-base \
  language-pack-ja \
  locales

$ sudo locale-gen ja_JP.UTF-8
$ echo "export LANG=ja_JP.UTF-8" >> ~/.bashrc
$ source ~/.bashrc

ロケールの確認

$ locale
LANG=ja_JP.UTF-8
LANGUAGE=
LC_CTYPE="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_PAPER="ja_JP.UTF-8"
LC_NAME="ja_JP.UTF-8"
LC_ADDRESS="ja_JP.UTF-8"
LC_TELEPHONE="ja_JP.UTF-8"
LC_MEASUREMENT="ja_JP.UTF-8"
LC_IDENTIFICATION="ja_JP.UTF-8"
LC_ALL=

Postgres DB をインストール

Python Flask アプリをローカルの開発環境で実行する場合にアプリがアクセスする Postgres をインストールします。

Postgres DB をインストール

$ sudo apt install -y postgresql

Postgres DB を起動

$ sudo service postgresql start

$ sudo service postgresql status
14/main (port 5432): online

postgres ユーザーのパスワードを変更

$ sudo su - postgres --command "
    psql --command \"
      alter
        user postgres
        with encrypted password 'pgpass';
    \"
  "

なお、直接 psql コマンドで接続する場合は以下のいずれでもいけました。

  • $ sudo su - postgres --command "psql --dbname postgres"
  • $ psql postgres://postgres:pgpass@localhost/postgres

Python Flask のサンプルアプリを起動

Ubuntu パッケージをインストール

$ sudo apt install -y python3-pip

アプリ用のディレクトリを作成して移動

$ mkdir ~/work
$ cd ~/work/

requirements.txt を配置

$ cat <<EOF > requirements.txt
Flask
Flask-SQLAlchemy
psycopg2-binary
EOF

Python パッケージをインストール

$ sudo pip install -r requirements.txt

スクリプトを配置

$ cat <<EOF > testapp.py
#!/usr/bin/env python3

import os
import re
from flask import (
  Flask,
  Response
)
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = re.sub(
  '^postgres://',
  'postgresql://',
  os.environ['DATABASE_URL']
)

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

class TestRecord(db.Model):
  test_col = db.Column(db.Text, primary_key=True)

@app.route('/')
def root():
  query = db.session.query(TestRecord)
  record = query.one()

  return Response(record.test_col)
EOF

アプリがアクセスする Postgres のデータベースを作成

$ sudo su - postgres --command "
    psql --command \"
      create database testdb;
    \"
  "

テーブルを作成しデータをインサート

$ DATABASE_URL=postgres://postgres:pgpass@localhost/testdb python3

>>> from testapp import db, TestRecord

>>> db.create_all()

>>> test_record = TestRecord()
>>> test_record.test_col = 'hello world'

>>> db.session.add(test_record)
>>> db.session.commit()

>>> exit()

アプリを起動

$ DATABASE_URL=postgres://postgres:pgpass@localhost/testdb FLASK_APP=testapp flask run --host=0.0.0.0

ブラウザで http://localhost:5001/ にアクセス

参考

2022年1月28日金曜日

Heroku で Python Flask で Postgres へ接続 (ローカル開発環境は Docker コンテナ)

Heroku の Python Flask アプリの環境で Postgres への接続を試してみました。

一つの Flask アプリでコードを書き換えずにローカル環境で実行した場合はローカルの Postgres、ネット上の Heroku 環境で実行した場合は Heroku の Postgres に接続できることを確認しています。

ローカルの開発環境は Docker コンテナを一つ用意し、この中に Heroku CLI、Postgres を入れています。

Docker イメージ作成

Heroku CLI, Postgres 用の Docker イメージは Docker Hub の Postgres をベースにして dockerfile でごにょごにょして Heroku CLI のインストール等を行います。

dockerfile は以下の通りです。

FROM postgres

RUN apt update && apt -y upgrade
RUN apt update && apt -y upgrade

RUN apt install -y \
  sudo \
  curl \
  wget \
  unzip \
  git \
  byobu \
  python3-pip \
  libpq-dev

RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

RUN curl https://cli-assets.heroku.com/install.sh | sh

RUN useradd \
  --create-home \
  --groups sudo \
  --shell /bin/bash \
  worker
RUN echo 'worker:worker' | chpasswd
RUN su - worker -c 'mkdir /home/worker/Desktop'

Docker イメージをビルド

$ docker build --tag heroku-postgres:1 ./

Docker コンテナを開始

$ docker run \
  -p 5000:5000 \
  --name test-heroku-postgres \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -d \
  heroku-postgres:1
  • -p 5000:5000」で Heroku ローカル開発環境 (heroku local web) で公開する 5000 番ポートに外部のブラウザからアクセスできるようにしてます。

  • コンテナ開始時に Postgres が初期化されるようなので「-e POSTGRES_PASSWORD=mysecretpassword」環境変数でパスワードを指定しています。ローカルのとりあえずの環境なのでパスワードそのまま書いていますが、気になる場合は https://hub.docker.com/_/postgres の「Docker Secrets」あたりを読むとパスワードをファイルに書いてそのファイル名を指定することもできるようです。(試していないので本当にできるかわかりません)

  • -d」を指定しているのでシェルのコマンドプロンプトにすぐ戻り、「$ docker ps -a」で確認するとバックグラウンドでコンテナが動いていることが確認できます。

  • PC 再起動時等、コンテナが停止している場合は以下でコンテナを起動できます。

    $ docker container start test-heroku-postgres
    

起動しているコンテナで bash を実行して worker ユーザーになります。

$ docker exec -it test-heroku-postgres /bin/bash
# su - worker
$ cd Desktop/

以下、コンテナの中の bash で実行しています。

Flask アプリのスクリプトをダウンロード

Postgres に接続するテスト用の Python Flask スクリプトを Gist (https://gist.github.com/nezuppo/c36bbb4c1f81da348ba74e8ccf3001bf) に貼り付けています。

Gist からこのスクリプトをダウンロード

$ wget https://gist.github.com/nezuppo/c36bbb4c1f81da348ba74e8ccf3001bf/archive/e1df36533ba102fd1494e06d23398f6f3d7b891e.zip
  • 別途新規で git init をする必要があるため、git clone ではなく wget で zip ファイルをダウンロードしています。

ダウンロードした zip を解凍してディレクトリに cd

$ unzip e1df36533ba102fd1494e06d23398f6f3d7b891e.zip
$ cd c36bbb4c1f81da348ba74e8ccf3001bf-e1df36533ba102fd1494e06d23398f6f3d7b891e/

ローカル開発環境で Flask アプリからローカルの Postgres に接続

localdb データベース作成

$ createdb -U postgres localdb

postgres ユーザーで localdb データベースを指定して psql 起動

$ psql -U postgres localdb

psql 内で testtable を作成してデータを挿入

localdb=# create table testtable(aaa text);
localdb=# insert into testtable values('test local');
localdb=# select * from testtable;
    aaa
------------
 test local
(1 row)

localdb=# exit

Python の必要なパッケージをインストール

$ sudo pip install -r requirements.txt

環境変数 DATABASE_URL で Postgres への接続情報を指定して heroku ローカル開発環境を起動

$ DATABASE_URL=postgresql://postgres@localhost/localdb \
  heroku local web

ブラウザで http://localhost:5000/ を開くと Flask アプリが Postgres に接続し、インサートされた「test local」をひっぱってきてブラウザに表示されます。

終了は ctrl-c です。

ネット上の Heroku サイトの Flask アプリから Heroku 上の Postgres に接続

git の初期設定

$ git config --global user.email "worker@localdomain"
$ git config --global user.name "worker"

git を初期化

$ git init

.gitignore 作成

$ echo "__pycache__/" > .gitignore

ファイルを git に追加して commit

$ git add .
$ git commit -m "first commit"

Heroku にログイン

$ heroku login

Heroku アプリを作成

$ heroku create

Heroku Postgres をアドオン

$ heroku addons:create heroku-postgresql:hobby-dev
  • 最初に git init しているので --app で Heroku アプリ名を指定する必要はありません

Heroku のアプリに Postgres がアドオンされたことを確認

$ heroku addons

Heroku Postges への接続 URL の確認

$ heroku config

Heroku Postgres に関する情報の確認

$ heroku pg

Heroku アプリにアドオンした Postgres (ローカルではなくリモート) に psql で接続

$ heroku pg:psql

psql 内で testtable を作成してデータを挿入

DATABASE=> create table testtable(aaa text);
DATABASE=> insert into testtable values('test remote');
DATABASE=> select * from testtable;
     aaa
-------------
 test remote
(1 row)

DATABASE=> exit

リモートが Heroku であることを確認

$ git remote
heroku

リモートに git push して Heroku アプリを初期化

$ git push heroku master

ブラウザでアクセスする URL を確認

$ heroku open
  • 本来はブラウザを立ち上げるためのコマンドだが、Docker コンテナが CUI 環境のためブラウザが起動されず URL が表示されるので、これをブラウザにコピペして開く
  • Postgres にインサートした ‘test remote’ が表示される

2022年1月17日月曜日

Docker の Ubuntu でよく使うコマンドやツールが一通りインストールされたイメージを作成

WSL の Ubuntu には sudo、wget、byobu 等の普段良く使うツールがあらかじめインストールされていますが、Docker のデフォルトの Ubuntu イメージは最小限必要な構成になっているため、これらはインストールされていません。

必要なパッケージをひとつひとつインストールするように dockerfile を書くのも大変なので、まとめてインストールできるよう調べてみました。

環境

  • Docker の Ubuntu イメージ: ubuntu:20.04

結論

いろいろ検討した結果、dockerfile は以下の通りです。

$ cat dockerfile
FROM ubuntu:20.04

RUN apt update && apt -y upgrade
RUN apt update && apt -y upgrade

ARG DEBIAN_FRONTEND=noninteractive
RUN apt install -y ubuntu-minimal
RUN apt install -y ubuntu-standard
RUN apt install -y ubuntu-server

RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

以下、この dockerfile を作成するために検討した内容です。

WSL のパッケージを確認

上述の通り、WSL の Ubuntu には sudo、wget、byobu 等の普段良く使うツールがインストールされているので、これらを依存関係として持っている大元のパッケージを確認します。以下、WSL にインストールした Ubuntu で実施しています。

まずはインストールされているパッケージ一覧を取得するため、`dpkg -l’ の出力を確認

$ dpkg -l
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                           Version                               Architecture Description
+++-==============================-=====================================-============-=================================================================>
ii  accountsservice                0.6.55-0ubuntu12~20.04.4              amd64        query and manipulate user account information
ii  adduser                        3.118ubuntu2                          all          add and remove users and groups
ii  alsa-topology-conf             1.2.2-1                               all          ALSA topology configuration files
... (省略) ...

というように、最初の 5行目まではヘッダ情報で、6行目以降の「ii」に続く 2項目がインストールされているパッケージのようなので、これを抜き取りパッケージ一覧を作ります。

$ dpkg -l | tail -n +6 | awk '{print $2}'
accountsservice
adduser
alsa-topology-conf
... (省略) ...

これらすべてのパッケージの依存関係一覧を取得します。

$ dpkg -l | tail -n +6 | awk '{print $2}'| xargs -I{} apt depends {} 2>/dev/null | less

出力の中から、sudo、wget、byobu 等に依存関係を持っているパッケージを見てみると、ubuntu-minimal、ubuntu-standard、ubuntu-server パッケージによく使うコマンドやツールが一通り依存関係に含まれていることがわかりました。

Docker の Ubuntu イメージに ubuntu-minimal、ubuntu-standard、ubuntu-server パッケージをインストールしようとすると予期しないエラーその 1が発生

本題とはそれますが、予期しないエラーが出ました。

まず、ubuntu-minimal、ubuntu-standard、ubuntu-server パッケージをインストールする dockerfile は以下のようににしてみました。

$ cat dockerfile
FROM ubuntu:20.04

RUN apt update && apt -y upgrade

RUN apt install -y ubuntu-minimal
RUN apt install -y ubuntu-standard
RUN apt install -y ubuntu-server

この docker ファイルでイメージを build すると、予期しないエラーが出ました。

$ docker build --tag test-with-tools:1 ./
... (省略) ...
#5 144.7 E: Failed to fetch http://archive.ubuntu.com/ubuntu/pool/main/s/systemd/systemd-timesyncd_245.4-4ubuntu3.14_amd64.deb  404  Not Found [IP: 91.189.88.142 80]
... (省略) ...
executor failed running [/bin/sh -c apt install -y ubuntu-minimal]: exit code: 100

例えば、「http://archive.ubuntu.com/ubuntu/pool/main/s/systemd/systemd-timesyncd_245.4-4ubuntu3.14_amd64.deb」がダウンロード出来ないとのことです。

ブラウザでこの URL を開いてみると「Not Found」と出ました。サーバーは動いているが、指定したファイルが存在しないようです。

そこで、「systemd-timesyncd_245.4-4ubuntu3.14_amd64.deb」が置かれているディレクトリ「http://archive.ubuntu.com/ubuntu/pool/main/s/systemd/」をブラウザで開いてみると

systemd-timesyncd_245.4-4ubuntu3.15_amd64.deb」は置かれているが
systemd-timesyncd_245.4-4ubuntu3.14_amd64.deb」は置かれていないことがわかります。

古いバージョンの「systemd-timesyncd_245.4-4ubuntu3.14_amd64.deb」をダウンロードしようとしてエラーになっているようです。

dockerfile を見てみると「RUN apt update && apt -y upgrade」というように古いリポジトリ情報で apt update し、その後 apt upgrade しているので、このタイミングでリポジトリ情報が新しいものに更新されていると想定されます。なので、もう一度 update すれば良いのではないかと思われます。(ついでに upgrade も実施)

この時点で dockerfile は以下のようになってます。

$ cat dockerfile
FROM ubuntu:20.04

RUN apt update && apt -y upgrade
RUN apt update && apt -y upgrade

RUN apt install -y ubuntu-minimal
RUN apt install -y ubuntu-standard
RUN apt install -y ubuntu-server

無事 Docker イメージのビルドが進みました。(が、以下の予期しないエラーその 2が発生しました)

Docker の Ubuntu イメージに ubuntu-minimal、ubuntu-standard、ubuntu-server パッケージをインストールしようとすると予期しないエラーその 2が発生

以下のエラーです。

#6 242.1 Errors were encountered while processing:
#6 242.1  console-setup
#6 242.1  ubuntu-minimal
#6 242.1 E: Sub-process /usr/bin/dpkg returned an error code (1)
------
executor failed running [/bin/sh -c apt install -y ubuntu-minimal]: exit code: 100

ubuntu-minimal と依存関係がある console-setup をインストールしうようとしていてエラーになっているっぽいです。

更にメッセージを見てみると、

#6 241.9 If you don't use a framebuffer, the choices that start with "." will reduce the
#6 241.9 number of available colors on the console.
#6 241.9
#6 241.9   1. . Arabic
#6 241.9   2. # Armenian
#6 241.9   3. # Cyrillic - KOI8-R and KOI8-U
... (省略) ...
#6 241.9   21. . Combined - Latin; Slavic Cyrillic; Greek
#6 241.9   22. . Combined - Latin; Slavic and non-Slavic Cyrillic
#6 241.9   23. Guess optimal character set
#6 241.9
#6 241.9 Character set to support:

と出ているので、コンソールから入力を求められているが、スクリプトでの処理のため入力できずエラーとなったようです。

ググると環境変数で「DEBIAN_FRONTEND=noninteractive」を指定すると良いようなので dockerfile は以下のようにしました。

コンテナの中に環境変数 DEBIAN_FRONTEND を残したくないので「ENV DEBIAN_FRONTEND=noninteractive」ではなく、「ARG DEBIAN_FRONTEND=noninteractive」としてます。

$ cat dockerfile
FROM ubuntu:20.04

RUN apt update && apt -y upgrade
RUN apt update && apt -y upgrade

ARG DEBIAN_FRONTEND=noninteractive
RUN apt install -y ubuntu-minimal
RUN apt install -y ubuntu-standard
RUN apt install -y ubuntu-server

apt インストールのキャッシュを削除してイメージのサイズを少しでも減らす

ググると apt install した時のキャッシュを削除してイメージのサイズを小さくすると良いとのことなので dockerfile に追加しました。

$ cat dockerfile
FROM ubuntu:20.04

RUN apt update && apt -y upgrade
RUN apt update && apt -y upgrade

ARG DEBIAN_FRONTEND=noninteractive
RUN apt install -y ubuntu-minimal
RUN apt install -y ubuntu-standard
RUN apt install -y ubuntu-server

RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

参考

2022年1月10日月曜日

WSL Ubuntu で Heroku with Python

Ubuntu への Heroku CLI のインストール、サンプル Python アプリのデプロイ、アプリ開発のためのローカル Heroku 環境の構築等は Heroku サイトのドキュメント「Getting Started on Heroku with Python」を参考にできますが、WSL Ubuntu に適用しようとするとこの通りに進めるだけではうまくいかなかったので、Heroku のドキュメントを実施するにあたり別途必要なことをここにまとめておきます。

環境

  • Windows11 Home
  • WSL2 Ubuntu 20.04.3

pip の代わりに pipenv を使うための準備

Heroku のドキュメントではローカル開発環境でのサンプルアプリ実行に必要な Python パッケージのインストールに pip を使っています。pip でも問題なく動作するかと思われますが、Python パッケージ環境を肥大化させたくないので pip の代わりに今回は pipenv を使います。

Heroku のドキュメントに書かれたことを実施する前にまずは以下の手順で pipenv 環境の準備をします。

pipenv をインストール

$ sudo apt install pipenv

作業用ディレクトリを作成して移動

$ mkdir /some/where/work
$ cd /some/where/work/

pipenv プロジェクトを作成し pipenv shell を起動

$ pipenv --python $(which python3)
$ pipenv shell

以降、全て pipenv shell で実施

Heroku CLI のインストールに snap を使えない

Ubuntu への Heroku CLI のインストールは「$ sudo snap install heroku --classic」を実行するとのことですが、WSL では snap を使うことは難しいようなのでインストール用のシェルスクリプトをダウンロードして実行。

$ wget https://cli-assets.heroku.com/install-ubuntu.sh
$ chmod +x install-ubuntu.sh
$ ./install-ubuntu.sh

アプリ開発のためのローカル Heroku 環境の構築に pipenv を使う

サンプルアプリに必要なパッケージをインストール。これを実施しないと、後述の pipenv install でエラーが出ます。

$ sudo apt install libpq-dev

このサンプルアプリを実行するために必要な python パッケージは requirements.txt で定義されています。

$ cat requirements.txt
django
gunicorn
django-heroku

ローカルにこれらの Python パッケージをインストール。今回は前述の通り pip の代わりに pipenv を使っています。

$ pipenv install -r requirements.txt

ローカル環境にアプリの変更を適用する時も pip ではなく pipenv

ドキュメントではローカル環境に Python の request パッケージをインストールするのに $ pip install requests としていますがこれも pipenv でやります。

$ pipenv install requests

2021年9月20日月曜日

github pages のドキュメントを sphinx で作成

github pages のドキュメントを sphinx で作成する方法はググるといろいろあるようですが、今回は以下を参考にしてみました。

github ではリポジトリの gh-pages ブランチを github pages として公開することができるので、main ブランチの sphinx の build/html/ に同リポジトリの gh-pages ブランチをサブモジュールとして割り当てます。


環境

  • Ubuntu 20.04.3 LTS
  • Sphinx 4.2.0

github でリポジトリを用意

github で新しくリポジトリを新規作成しました。リポジトリ名は test としています。

ローカルの Ubuntu にリポジトリをクローン

$ cd ~
$ git clone https://github.com/nezuppo/test.git

gh-pages ブランチを作成

git の test リポジトリのディレクトリに移動

$ cd ~/test/

–orphan オプションを付けて何も commit されていない gh-pages ブランチを作成してチェックアウト

$ git checkout --orphan gh-pages
Switched to a new branch 'gh-pages'

一応 git-branch で確認すると orphan だからなのか gh-pages ブランチは出てこず、以下のような出力となりました。

$ git branch -a
  main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main

ファイルは main ブランチと同じのものがありましたが、commit 前の状態でした。

$ ls -la
total 16
drwxr-xr-x 3 test test 4096 Sep 19 22:46 .
drwxr-xr-x 8 test test 4096 Sep 19 22:55 ..
drwxr-xr-x 8 test test 4096 Sep 19 23:16 .git
-rw-r--r-- 1 test test    6 Sep 19 22:46 README.md
$ git status
On branch gh-pages

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   README.md

まずは空の状態にしました。

$ rm README.md
$ git rm --cached README.md
rm 'README.md'
$ ls -la
total 12
drwxr-xr-x 3 test test 4096 Sep 19 23:20 .
drwxr-xr-x 8 test test 4096 Sep 19 22:55 ..
drwxr-xr-x 8 test test 4096 Sep 19 23:21 .git
$ git status
On branch gh-pages

No commits yet

nothing to commit (create/copy files and use "git add" to track)

空の状態だと github に push できないので .nojekyll ファイルを追加し push までしました。なお、github pages でデフォルトで有効になっている Jekyll ツールが sphinx ドキュメントとの相性が悪いようなので .nojekyll ファイルで無効にする必要があるとのことです。

$ touch .nojekyll
$ git add .nojekyll
$ git commit
$ git push --set-upstream origin gh-pages

この状態で github の test リポジトリの [Settings] - [Pages] で確認すると Source が gh-pages リポジトリの / (root) となっていました。


main ブランチ配下に gh-pages ブランチをサブモジュールとして割り当て

main ブランチの doc/ に sphinx を展開し、html ファイル一式が置かれる doc/build/html/ に gh-pages ブランチをサブモジュールとして割り当てます。これにより gh-pages ブランチ の / (root) に sphinx ドキュメントの html ファイル一式が置かれ github pages で公開されるようになります。

main ブランチの / (root) に移動

$ cd ~/test

$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

サブモジュール用のディレクトリを作成

$ mkdir -p doc/build

gh-pages ブランチをサブモジュールとして割り当て

$ git submodule add -b gh-pages https://github.com/nezuppo/test.git doc/build/html

doc/ に sphinx を展開

doc/ に移動

$ cd doc

sphinx を展開。以下のように source と build ディレクトリは分割させました。

$ sphinx-quickstart
(省略)
> Separate source and build directories (y/n) [n]: y
(省略)

この状態で make clean すると build 配下がサブモジュール化された状態も含め全て削除されてしまい都合が悪いので Makefile を以下のように修正。ついでに make clean と make html、make help 以外は使えないようにしました。

$ diff -U -1 Makefile{.org,}
--- Makefile.org        2021-09-20 00:14:36.590000000 +0900
+++ Makefile    2021-09-20 00:15:43.500000000 +0900
@@ -1,20 +1,25 @@
 # Minimal makefile for Sphinx documentation
 #

 # You can set these variables from the command line, and also
 # from the environment for the first two.
 SPHINXOPTS    ?=
 SPHINXBUILD   ?= sphinx-build
 SOURCEDIR     = source
 BUILDDIR      = build

 # Put it first so that "make" without argument is like "make help".
 help:
        @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

 .PHONY: help Makefile

+clean:
+       rm -rf $(BUILDDIR)/html/*
+       rm -rf $(BUILDDIR)/html/.buildinfo
+       rm -rf $(BUILDDIR)/doctrees
+
 # Catch-all target: route all unknown targets to Sphinx using the new
 # "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
+html:
        @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

また、sphix のテーマは bizstyle が好みなの以下のようにで変更しました。

$ diff -U 0 source/conf.py{.org,}
--- source/conf.py.org  2021-09-20 00:21:20.030000000 +0900
+++ source/conf.py      2021-09-20 00:21:58.500000000 +0900
@@ -54 +54 @@
-html_theme = 'alabaster'
+html_theme = 'bizstyle'

html ドキュメントを生成

$ make html

build/html/ 配下に github pages で公開する html ドキュメントが生成されました。

$ ls -la build/html/
total 48
drwxr-xr-x 4 test test 4096 Sep 20 00:23 .
drwxr-xr-x 4 test test 4096 Sep 20 00:23 ..
-rw-r--r-- 1 test test  230 Sep 20 00:23 .buildinfo
-rw-r--r-- 1 test test   45 Sep 19 23:59 .git
-rw-r--r-- 1 test test    0 Sep 19 23:59 .nojekyll
drwxr-xr-x 2 test test 4096 Sep 20 00:23 _sources
drwxr-xr-x 2 test test 4096 Sep 20 00:23 _static
-rw-r--r-- 1 test test 3070 Sep 20 00:23 genindex.html
-rw-r--r-- 1 test test 4484 Sep 20 00:23 index.html
-rw-r--r-- 1 test test  269 Sep 20 00:23 objects.inv
-rw-r--r-- 1 test test 3437 Sep 20 00:23 search.html
-rw-r--r-- 1 test test  598 Sep 20 00:23 searchindex.js

gh-pages ブランチを commit

main ブランチより先に gh-pages ブランチを commit する必要があるようです。

gh-pages ブランチを割り当てたサブモジュールディレクトリに移動

$ cd ~/test/doc/build/html/

ブランチが gh-pages であることを確認

$ git branch
* gh-pages
  main

git インデックスに追加

$ git add *
$ git add .buildinfo

commit

$ git commit

git status でコミット漏れがないことを確認して push

$ git status
On branch gh-pages
Your branch is ahead of 'origin/gh-pages' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
$ git push

main ブランチを commit

gh-pages ブランチの次に main ブランチを commit します。

test リポジトリの / (root) に移動

$ cd ~/test

ブランチが main であることを確認

$ git branch
  gh-pages
* main

git インデックスに追加

$ git add doc/build/html/
$ git add doc/

commit

$ git commit

git status でコミット漏れがないことを確認して push

$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
$ git push

github pages を確認

これで sphinx ドキュメントの html ファイル一式を github pages で公開することができました。


sphinx ドキュメントを更新する場合

以下の順序で実施します。

  1. doc/source/ 配下のドキュメントを更新
  2. doc/ で make html
  3. 上記の [gh-pages ブランチを commit] を実施
  4. 上記の [main ブランチを commit]

新たにリポジトリを clone する場合

gh-pages リポジトリをサブモジュールとして割り当てられた test リポジトリを新たに clone する場合は以下の順序で実施します。

  1. test リポジトリを clone
  2. サブモジュールを update
    $ cd test/
    $ git submodule update --init
    
  3. サブモジュールのディレクトリに移動して gh-pages ブランチにチェックアウト
    $ cd doc/build/html/
    $ git checkout gh-pages
    

参考

2021年7月18日日曜日

find コマンドでディレクトリを除外して列挙

はじめに

find コマンドでディレクトリを除外して列挙しようと思って調べてみたら

$ find /etc ! -type d

でよさそうなので、本当にいけてるか調べてみました。

まずは対象ディレクトリ配下のファイルやディレクトリの数を確認

対象ディレクトリとして /etc でやってみます。

$ find /etc
/etc
/etc/services
/etc/lsb-release
(以下省略)

普通に find できます。が。。。

$ find /etc > /dev/null
find: ‘/etc/polkit-1/localauthority: Permission denied
find: ‘/etc/ssl/private: Permission denied

というように標準エラー出力を確認すると、一般ユーザーではアクセスできないものがあるようなので以下、sudo つけてやります。

これらがファイルとかディレクトリとかを確認するために ls に渡します。

$ sudo find /etc | xargs sudo ls -lad
drwxr-xr-x 93 root root        4096 Jul 18 09:55 /etc
-rw-------  1 root root           0 Jun  4 06:39 /etc/.pwd.lock
drwxr-xr-x  3 root root        4096 Jun  4 06:40 /etc/NetworkManager
(以下省略)

drwxr-xr-x の先頭の文字でファイルとかディレクトリとか判断できます。 (この場合は d なのディレクトリ)

cut コマンドで先頭の文字だけ切り出します。

$ sudo find /etc | xargs sudo ls -lad | cut -b 1
d
-
d
d
-(以下省略)

ソートをかけて uniq コマンドでカウントします。

$ sudo find /etc | xargs sudo ls -lad | cut -b 1 | sort | uniq -c
    728 -
    213 d
    628 l

/etc 配下にはファイル (-) が 728個、ディレクトリ (d) が 213個、シンボリックリンク (l) が 628個あることがわかりました。

最終的にファイル名やディレクトリ名にスペースが含まれる場合でも対応できるように以下のようにします。(find に -print0、xargs に -0 を付けてます)

$ sudo find /etc -print0 | xargs -0 sudo ls -lad | cut -b 1 | sort | uniq -c
    728 -
    213 d
    628 l

find に ! -type d を付けてディレクトリが除外されているか確認

ディレクトリが除外され、その他 (今回の場合はファイルとシンボリックリンク) が期待通り列挙されているようです。

$ sudo find /etc ! -type d -print0 | xargs -0 sudo ls -lad | cut -b 1 | sort | uniq -c
    728 -
    628 l

念のため、!-type d だけに効くように () を付けます。

$ sudo find /etc \( ! -type d \) -print0 | xargs -0 sudo ls -lad | cut -b 1 | sort | uniq -c
    728 -
    628 l

結論

$ find /etc ! -type d

でディレクトリを除外して列挙できるみたいです。