7839

雑草魂エンジニアブログ

【EC2】Rails5 環境構築(Ruby + MySQL5.7 + Node.js + Nginx)

最近、全く更新できていなかった。。。
今年やったことは今年のうちに。自分の棚卸しも兼ねて、一気にアウトプットしていきたいと思う。

今回は、AWSのEC2に、Railsアプリケーションをデプロイするにあたり、毎回調べていたので、備忘録として環境構築方法を残しておく。

環境構築

  • EC2 (t2.micro)
  • Amazon Linux release 2 (Karoo)
  • Ruby Ver 2.6.3
  • MySQL Ver 5.7.32
  • Node.js Ver 14.15.2
  • Nginx Ver 1.18.0

EC2が起動した後に、SSH接続する

まず全ての更新処理を実行する

$ sudo yum update

git インストール

$ sudo yum install git
$ git --version
git version 2.23.3

rbenv インストール

デフォルトでは、/usr/local/rbenv にセットアップされるが、どのユーザーも使えるように、/opt/rbenv にセットアップする。

1. ディレクトリを作成して、そのパスを環境変数 RBENV_ROOT に設定する。

$ sudo mkdir /opt/rbenv
$ export RBENV_ROOT=/opt/rbenv

2. git を用いて、rbenv を /opt/rbenv に clone する。

$ sudo git clone https://github.com/sstephenson/rbenv.git /opt/rbenv
$ sudo mkdir /opt/rbenv/versions
$ sudo mkdir /opt/rbenv/shims

3. すべてのユーザが rbenv を利用できるように、PATHを追加する。(/etc/profile.d に設定ファイルを作成する。)

$ sudo vi /etc/profile.d/rbenv.sh
export RBENV_ROOT="/opt/rbenv"
export PATH="$RBENV_ROOT/bin:$PATH"
eval "$(rbenv init -)"

4. リロードさせて、バージョンが確認できれば、インストール完了である。

$ source /etc/profile
$ rbenv -v
rbenv 1.1.2-40-g62d7798

5. さらに、Ruby を rbenv 経由でインストールする時に必要なプラグイン ruby-build も clone する。

$ sudo git clone https://github.com/sstephenson/ruby-build.git /opt/rbenv/plugins/ruby-build

6. インストールを実行する。(PREFIX でインストール先を指定しておく。)

$ PREFIX=/opt/rbenv sudo /opt/rbenv/plugins/ruby-build/install.sh

7. インストール可能な Ruby のバージョン一覧が表示されれば、ruby-buildのインストールは完了である。

$ rbenv install -l
2.5.8
2.6.6
2.7.2
Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all / -L' to show all local versions.

Ruby インストール

Rubyをインストールする際に、必要なパッケージを事前にインストールしておく。(必要なパッケージに関しては、ruby-build_suggested-build-environment参照。ただし、gcc-6 は利用できないため、gcc-c++に変更。)

$ sudo yum install -y gcc-c++ bzip2 openssl-devel libyaml-devel libffi-devel readline-devel zlib-devel gdbm-devel ncurses-devel

1. 今回インストールしたいのは、ver2.6.3 なので、バージョンを指定してインストールする。(root ユーザーでインストールを行った。root権限がないと、2.6.3のフォルダを作成する際にエラーが発生する。)

$ sudo -s
[root@]$ rbenv install 2.6.3

2. rbenv で使用する Ruby のバージョンを指定する

[root@]$ rbenv global 2.6.3
[root@]$ rbenv rehash

3. Ruby のバージョンを確認する

[root@]$ ruby -v
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]

4. Gem は個別に bundler で管理するので、システムにインストールする Gem は bundler だけなので、bundler のみをインストールしておく。

[root@]$ gem install bundler
Fetching bundler-2.2.1.gem
Successfully installed bundler-2.2.1
Parsing documentation for bundler-2.2.1
Installing ri documentation for bundler-2.2.1
Done installing documentation for bundler after 3 seconds
1 gem installed

5. rootユーザーから、一般ユーザに戻る。

[root@]$ exit

MySQL5.7 インストール

1. 既存のパッケージがインストールされているか、事前に確認する

$ yum list installed | grep mariadb
$ yum list installed | grep mysql

2. 初期からインストールされている MariaDB 用パッケージを削除する。

$ sudo yum remove mariadb-libs

3. MySQLリポジトリyumに追加する。

$ sudo yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm

4. 今回は、MySQL5.7を使いたいので、MySQL8.0の無効化をして、MySQL5.7を有効化する。最後に、インストール可能なパッケージを確認する。

$ sudo yum-config-manager --disable mysql80-community
$ sudo yum-config-manager --enable mysql57-community
$ yum info mysql-community-server mysql-community-devel

5. MySQL5.7が利用可能であることが確認できたので、インストールする。

$ sudo yum install mysql-community-server mysql-community-devel

6. MySQLのバージョンが確認できたら、インストールは完了。(追加で、ログファイルを作成しておく。)

$ mysqld --version
mysqld  Ver 5.7.32 for Linux on x86_64 (MySQL Community Server (GPL))
$ sudo touch /var/log/mysqld.log

7. mysqld を起動する。

$ sudo service mysqld start

8. 起動状況の確認をする。

$ systemctl status mysqld.service

9. インスタンスの起動と同時に起動するように設定する。

$ sudo chkconfig mysqld on

10. 初期パスワードを確認する。

$ sudo less /var/log/mysqld.log | grep root@localhost
2020-12-17T03:16:27.232843Z 1 [Note] A temporary password is generated for root@localhost: (2<hB;Mwa(+X

末尾の部分がrootユーザの初期パスワードとなる。

11. mysqlにrootユーザでログインする。(パスワードを求められるので、上記のパスワードでログインする。)

$ mysql -u root -p
Enter password: 

12. パスワードの再設定を行う。(ただし、パスワードには、大文字、小文字、特殊文字(アスタリスク等)を含む必要がある。)

mysql> ALTER USER 'root'@'localhost' identified BY '<-新パスワード->';
mysql> exit

これでMySQLの設定は完了である。

Node.js(nvm) インストール

Node.jsの開発環境を構築するにあたって、バージョン管理できるライブラリとして nvm を使うこととする。 基本的には、rbenv と同じような流れでインストールを行う。

1. ディレクトリを作成して、そのパスを環境変数 NVM_ROOT に設定する。

$ sudo mkdir /opt/nvm
$ export NVM_ROOT=/opt/nvm

2. git を用いて、nvm を /opt/nvm に clone する。

$ sudo git clone https://github.com/nvm-sh/nvm.git /opt/nvm
$ sudo mkdir /opt/nvm/versions

3. すべてのユーザが nvm を利用できるように、PATHを追加する。(/etc/profile.d に設定ファイルを作成する。)

$ sudo vi /etc/profile.d/nvm.sh
export NVM_ROOT="/opt/nvm"
export PATH="$NVM_ROOT/bin:$PATH"
[ -s "$NVM_ROOT/nvm.sh" ] && \. "$NVM_ROOT/nvm.sh"  # This loads nvm
[ -s "$NVM_ROOT/bash_completion" ] && \. "$NVM_ROOT/bash_completion"  # This loads nvm bash_completion

4. リロードさせて、バージョンが確認できれば、インストール完了である。

$ source /etc/profile
$ nvm -v
0.37.2

5. rootユーザーになり、インストール可能な Node.js のバージョン一覧を表示する。

$ sudo -s
[root@]$ nvm ls-remote

6. Node.js のインストールする。(/opt/nvm/versions/node/[version No.] にインストールされる)

[root@]$ nvm install 14.15.2

7. 使用するバージョンを指定する。

[root@]$ nvm use v14.15.2

8. Node.js のバージョンが確認できれば、完了である。

[root@]$ node -v
v14.15.2
[root@]$  npm -v
6.14.9

9. また、yarn を使いたいので、yarn をインストールしておく。(yum でのインストールも可能であるが、nvm の node のバージョンと紐付けて管理したいので、npm でのインストールを行う。)

[root@]$ npm install -g yarn

10. yarn のバージョンを確認する。

[root@]$ which yarn
/opt/nvm/versions/node/v14.15.2/bin/yarn
[root@]$ yarn -v
1.22.10

11. rootユーザーから、一般ユーザに戻る。

[root@]$ exit

Nginx インストール

今回は、amazon-linux-extras リポジトリからインストールする。

1. インストール可能なパッケージ一覧を確認する。

$ amazon-linux-extras

 37  mono                     available    [ =5.x  =stable ]
 38  nginx1                   available    [ =stable ]
 39  ruby2.6                  available    [ =2.6  =stable ]

2. Nginx が存在するので、インストールする。

$ sudo amazon-linux-extras install nginx1

3. 設定ファイルのバックアップをしておく。

$ sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.back

4. Nginx を起動し、自動起動有効化を設定し、最後にステータスを確認する。

$ sudo systemctl start nginx
$ sudo systemctl enable nginx
$ systemctl status nginx

5. EC2 のIPアドレスにアクセスして、Nginx の画面が表示されれば動作確認も完了となる。

その他、よく使うコマンドは以下の通りである。

# 設定を再読み込み
$ sudo systemctl reload nginx
# Nginx 停止
$ sudo systemctl stop nginx

Railsのサンプルアプリ作成

動作確認のために、Rails のサンプルアプリケーションを作成する。

1. ディレクトリを作成し、bundle init して、Gemfileを編集する。

$ mkdir sample
$ cd sample
$ bundle init
$ vim Gemfile

2. 今回、Rails v5.2.3 をインストールするように Gemfile を変更する。

# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem "rails", "~> 5.2.3"

3. 必要なGemをインストールする。ただし、システムには入れず、現在のパス配下の「vendor/bundle」にインストールさせる。(--jobs=4と指定することで、並列処理が可能となり、高速化される。)

$ bundle install --path vendor/bundle --jobs=4

4. 現在のディレクトリに、Rails ファイルを生成する。(Gemfileを上書きしていいか確認されるので、Yで続行する。)

$ bundle exec rails new . -d mysql -BJT

オプションでの指定は以下の通りである。(詳細に関しては、 bundle exec rails new -h で確認できる。)

  • -d mysql:データベースを MySQL に指定する。
  • -B:(--skip-bundle)Rails 作成時に bundle install を行わない。
  • -J:(--skip-javascript, --skip-turbolinks)JavaScript ファイルを作成しない。
  • -T:(--skip-test)railsのデフォルトのテストminitestを使わない。

5. 追加したい Gem を Gemfile に追加して、インストールを行う。(上記で Gemfile を上書きしているため、conflict が発生するので、bundle install ではなく、bundle updateを実行する。)

$ bundle update

6. MySQLサーバーに接続するために、config/database.yml のpasswordが空欄となっているので、先ほど設定した MySQL のrootユーザーの password を追記する。

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: <-- パスワードを追記する -->
  host: localhost

7. Rails 用のデータベースを作成する。

$ bundle exec rake db:create

8. 次に、Nginx で表示できるように、今回のsampleアプリ用の Nginx の設定ファイルを追加する。

$ sudo vim /etc/nginx/conf.d/sample.conf

server {
  # リクエストを受けるポートを設定する
  listen   80;
  server_name localhost;
  location / {
    # 80 ポートで受けたリクエストを 3000 ポートに中継する
    proxy_pass http://127.0.0.1:3000;
  }
}

9. Nginx を再起動する。

$ sudo systemctl reload nginx

10. 最後に、Rails サーバーを起動する。

$ bundle exec rails s

11. EC2 のIPアドレスにアクセスして、無事に Rails の起動画面が表示されれば、完了である。

https://railsguides.jp/railsguides/images/getting_started/rails_welcome.png

長々とお疲れ様でした。

まとめ

無事に Rails アプリケーションの環境を構築することができた。環境構築方法は様々あるので、これからも最適解を探していきたい。

それでは、ステキな開発ライフを。

【Python/Django】logging を設定して、リモート syslog サービスにログを出力する(システム監視)

今回、Djangoのアプリケーション運用監視において、Zabbix を用いることになり、監視サーバーにアプリケーション上のエラーログなどを出力する必要があった。Python の標準モジュールである logging を用いて簡単に設定できたので紹介する。

動作環境

logging に関して

docs.python.org

ログを取ること(出力すること)を、ロギング(logging)と言う。Pythonでは、 print() でログを出力することもできるが、logging モジュールでロギングをすることで以下のような利点がある。

  1. ログの種類を区別できる(Error / Debug / Info 等)
  2. フォーマットを指定すれば、簡単に統一されたログ出力ができる
  3. ログ出力先を設定できる(コンソール、テキスト等)

(1)ログの種別を区別することで、ただのDEBUG用のログ出力なのか、ユーザ(プログラム実行者)に伝えたい情報としての出力なのかを切り分けることができ、有用である。

Django の設定・使い方

docs.djangoproject.com

Django の logging では、独自のモジュールではなく、Pythonの標準モジュール logging を用いている。

Python の logging は、以下の4つで構成されている。

名前 役割(概要)
Logger ロギングシステムのエントリーポイント。すなわち、出力したいログの受け入れ口であり、ログ出力のトリガーとなる部分。
Handler どのようなログをどのように出力するか、設定する。
Filter ログレベルとは別に、ログデータにフィルタを設定する。
Formatter ログに出力する文字列の形式を設定する。

Djangoでは、 settings.pyLOGGING に設定を行う。

Django のデフォルトのロギング設定 は、 django/utils/log.py に定義されている。

カスタムする場合は、DEFAULTをコピーして貼り付けて、編集するのが一番楽かつ間違いなく設定できるかもしれない。以下はデフォルトの設定である。

DEBUG = True

ALLOWED_HOSTS = ["your_prod_host"]

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'formatters': {
        'django.server': {
            '()': 'django.utils.log.ServerFormatter',
            'format': '[{server_time}] {message}',
            'style': '{',
        }
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
        },
        'django.server': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'django.server',
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console', 'mail_admins'],
            'level': 'INFO',
        },
        'django.server': {
            'handlers': ['django.server'],
            'level': 'INFO',
            'propagate': False,
        },
    }
}

ログを出力したい場合は、以下のように呼び出して、適切なログを選択して出力する。

import logging

logger = logging.getLogger("django")

logger.info("info-message")
logger.error("error-message")
logger.warning("warning-message")
logger.critical("critical-message")

開発時には、SQLをみたい場合が多いと思うので、以下を追加することでconsoleで確認することができる。(django.db.backends

'loggers': {
    'django.db.backends': {
          'handlers': ['console'],
          'level': 'DEBUG',
    },
},

本番時に、runserver では運用しないと思うので、以下も追加しておくといいと思う。(django.request )handlersに関しては、statusのエラーを検知して、ファイルに書き込むや通知するなどの設定が適宜必要かと思われる。

'loggers': {
    'django.request': {
          'handlers': ['console'],
          'level': 'ERROR',
    },
},

リモート syslog サービスにログを出力する

本題のリモート syslog サービスへのログ出力では、Python の標準モジュールに含まれている SysLogHandler を用いる。

SysLogHandler(address=('localhost', SYSLOG_UDP_PORT), facility=LOG_USER, socktype=socket.SOCK_DGRAM)

address の部分に、hostとportを指定することでリモートの syslog に出力することができる。

LOGGING = {
    <-- 省略 -->
    "formatters": {
        "simple": {
            "format": "{asctime} {message}",
            "style": "{",
        },
    },
    "handlers": {
        <-- 省略 -->
        "syslog": {
            "level": "INFO",
            "class": "logging.handlers.SysLogHandler",
            "formatter": "simple",
            "facility": "local1",
            "address": ("<-- IP address -->", 514),
        },
    },
    "loggers": {
        <-- 省略 -->
        "syslog": {
            "handlers": ["syslog"],
            "level": "INFO", # 
            "propagate": False, # 上位のロガーに伝達しない
        },
    },
}

使う場合は、以下のようにする。

import logging

syslog_logger = logging.getLogger("syslog")

syslog_logger.info("info-message")

まとめ

簡単にログ出力が実装できて、とても便利であった。実際には、本番環境では、DEBUGをFALSEにする必要があったりするため、settingsのファイルを分割するなどした方が運用としてはいいのではないかと思っている。

それでは、ステキな開発ライフを。

【Python】リスト内包表記の使い方まとめ

Python を使って、2次元配列を 1次元配列に変換したり、連想配列の要素である配列要素を抽出して 1つの配列にしたい場合に、for文を使わずに、リスト内包表記でできないんだっけ?と思ったことが、今回の記事を書くきっかけである。そこで、今回はリスト内包表記の理解を深めるべく、自分なりにまとめてみた。

リスト内包表記とは

リスト内包表記では、リスト(配列)やタプルなどのイテラブルオブジェクト(繰り返し可能なオブジェクト)の各要素を、任意の変数名で取り出し、で評価した結果を、新たなリスト(配列)の要素として返却する。基本的には、以下のように書く。

[ 式 for 任意の変数名 in イテラブルオブジェクト ]

リストの要素を取り出して、処理して新しい配列を作りたい場合などによく利用される。(JavaScript をやっている人であれば、map と同じと思ってもらえるといいかもしれない。)for 文との対比することで、書き方のシンプルさを理解してもらえると思う。

# for文の場合
doubles = []
for num in [1, 2, 3, 4]:
    doubles.append( num*2 )

# リスト内包表記の場合
doubles = [ num*2 for num in [1, 2, 3, 4] ]

また、処理速度に関しても、for文と比べてリスト内包表記の方が速いと言われている。

さらに、書き方はほとんど同じであるが、返り値の型を任意の型にしたい場合は、以下のような内包表記が存在する。適宜使い分けていきたいと思えた。

  • 辞書内包表記{ key: value for 変数 in 配列など }
    • 辞書なので、key: value の組み合わせを作る必要がある
  • ジャネレータ内包表記( 式 for 変数 in 配列など )
    • ジェネレータなので、for文などで回さないと要素を取得できない。(ざっくりいうと、式の return をyield に置き換えたようなもの)
    • 順番に要素を取り出すことができる
  • 集合内包表記{ 式 for 変数 in 配列など }
    • set型のオブジェクトである(重複する値を持つことができず、一意な値のみが要素となる。)

if文 + リスト内包表記

if 文と組み合わせることで、リストのフィルターを実現することができる。

[ 式 for 任意の変数名 in イテラブルオブジェクト if 条件式 ]

条件式を満たした任意の変数のみを式で評価して、新しいリストを作ることができる。

members = [
  { "name": "Ken", "age": 18 },
  { "name": "Taro", "age": 20 },
  { "name": "Jiro", "age": 23 },
]
adult = [ member["name"] for member in members if 20 <= member["age"] ]

上記の例では、大人の名前のみを抽出している。

三項演算子 + リスト内包表記

上記の if 文との組み合わせの場合、リストのフィルターとして要素を除外することはできるが、条件に一致しない要素に対して別の処理などをすることができなかった。そこで、別の処理をしたい場合には、三項演算子を使うことで実装できる。

[ 真のときの値 if 条件式 else 偽のときの値 for 任意の変数名 in イテラブルオブジェクト ]

Python三項演算子は、条件式より先に真の結果を書く必要がある。(個人的に、慣れない。。。)

age_class = [ "adult" if 20 <= member["age"] else "child"  for member in members ]

例では、上記と同じ members に対して、年齢から判断して、大人か子供か文字列を返却している。

ネストしたリスト内包表記

冒頭で書いた、2次元配列を 1次元配列に変換したりするには、リスト内包表記をネストさせる必要がある。リスト内包表記の中に for や if が複数回現れる場合、 左側 の for や if が、for 文で書いた場合の外側のブロックになる。 (私は最初、右から読んでいくのかと思い、勘違いしてしまったのでご注意を。)

[ item for inner_list in outer_list for item in inner_list ]

使用例を以下に示す。

matrix = [[0,1,2],[3,4,5],[6,7,8]]
list = [ item for sublist in matrix for item in sublist]
print(list)
# [0,1,2,3,4,5,6,7,8]
 members=[
  {"name": "Ken", "age": 18, "goods": [{"name": "power1", "price": 100}]}, 
  {"name": "Taro", "age": 20, "goods": []}, 
  {"name": "Jiro", "age": 23, "goods": [{"name": "power2", "price": 10}]}
]
items = [item for member in members for item in member["goods"]]
print(items)
# [{'name': 'power1', 'price': 100}, {'name': 'power2', 'price': 10}]

また、複数の変数をまとめて処理することも可能である。

cells = [(row, col) for row in range(3) for col in range(3)]
print(cells)
# [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

縦 3 x 横 3 の点をプロットする座標を生成することができる。

まとめ

配列操作をどれだけシンプルに書けるかが、コードのシンプルさにも影響してくるので、リスト内包表記はマスターしておきたいところである。また、処理速度も for文より速いとなると、シンプルさと高速化で一石二鳥である。今回、整理してみて、頭の中がクリアになった気がする。

それでは、ステキな開発ライフを。

【VSCode】Docker の Python 環境でリモート開発を行う(Remote - Containers)

VSCodePython開発環境で、Pylint を使っている時に、以下のような Lint エラーが出た。

Unable to import '****' pylint(import-error)

問題なくモジュールを使うことができるのに、VSCode 上で Lintエラーが出てしまっていた。この点に関して、どのような開発環境を用意すべきか調べたので、備忘録として残しておく。

開発環境概要

f:id:serip39:20201029131457p:plain

今回の開発は Docker 環境を用いて行っていた。そして、コーディングはローカルのPCで VSCode を使っていた。実装したコードを実行する場合は、以下のコマンドでコンテナに入って、コマンドを実行していた。

$ docker-compose exec python bash
bash-5.0#

そして、docker上の Python には pip でコンテナ起動時にモジュールをインストールしていた。

Lint エラーの原因

今回、Lint エラーが出ていたのは、pip でインストールしているはずのモジュールが認識されていないことが問題で発生していた。(コードを実行した場合には、問題なくモジュールを使うことができていたので、VSCode 上の問題であることが発覚した。)

そして、VSCode上では、setting.jsonで以下のような設定になっていた。

{
  "python.pythonPath": "/usr/local/opt/python@3.8/bin/python3.8",
}

モジュールは Docker の Python 上にインストールしているにも関わらず、VSCode はローカルの Python を参照しているので、モジュールを参照できずに Lint エラーが出ていた。

Remote - Containers の導入

VS Code上でどうやって Docker を参照させるのかと思い、検索すると、拡張機能で「Remote - Containers」があったので使ってみた。

marketplace.visualstudio.com

動作確認をした環境は以下の通りである。

  • macOS Catalina Version 10.15.7
  • Docker Desktop for Mac Version 2.3.0.5 (48029)
  • Visual Studio Code Version 1.50.1
  • Remote - Containers Version 0.147.0

インストール

VSCode上の拡張機能ビューで、「Remote Containers」と検索してインストールする。 インストールが完了すると、VSCodeの下にアイコンが表示される。

f:id:serip39:20201029145022p:plain

設定

今回、ディレクトリ構造は以下のようになっている。

├── docker-compose.yml
├── docker/python
│   ├── Dockerfile
│   └── requirements.txt
├── .devcontainer
│   ├── devcontainer.json
│   └── docker-compose.extend.yml
├── src
│   ├── manage.py
│   └── app/

リモート開発環境の設定は、 .devcontainer フォルダの中に設定ファイルを置いている。今回は、pythonの環境で、Django + MySQL のアプリケーション開発を行う環境となっている。docker/python/requirements.txt に関しては、pipでインストールしたいモジュールを列挙して記載している。

version: '3'

services:
  db:
    container_name: nora_mysql
    image: mysql:latest
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: django_project
      MYSQL_USER: django
      MYSQL_PASSWORD: django
      TZ: Asia/Tokyo
    ports:
      - 3306:3306
    expose:
      - 3306
    volumes:
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/mysql_data:/var/lib/mysql
    security_opt:
      - seccomp:unconfined

  python:
    container_name: django
    build: ./docker/python
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./src:/code
    ports:
      - 8000:8000
    depends_on:
      - db
FROM python:3.9-alpine3.12
ENV PYTHONUNBUFFERED 1

RUN apk --update add tzdata bash vim && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata && \
    rm -rf /var/cache/apk/*

RUN mkdir /code

WORKDIR /code
ADD ./requirements.txt /code/

RUN pip3 install -r requirements.txt
ADD . /code/

EXPOSE 8000

docker-compose.yml で Django の開発を行うための設定を記載し、リモート開発用の設定を「docker-compose.extend.yml」で追加する。

version: '3'
services:
  python:
    environment:
      - HOME=/code/remote-dev

environment環境変数 HOME を明示的に指定し、リモート開発サーバーや拡張機能等の環境をインストールする先を設定している。

次に、リモート開発サーバーや拡張機能等の設定を「devcontainer.json」に記述する。

{
  "name": "Docker-Python",
  "dockerComposeFile": [
    "../docker-compose.yml",
    "docker-compose.extend.yml"
  ],
  "service": "python",
  "workspaceFolder": "/code",
  "settings": {
    "editor.tabSize": 4,
    "files.insertFinalNewline": true,
    "files.trimFinalNewlines": true,
    "terminal.integrated.shell.linux": "/bin/bash",
    "python.pythonPath": "/usr/local/bin/python",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint": true
    },
    "python.formatting.provider": "black",
    "python.linting.pylintEnabled": true,
    "python.linting.pylintArgs": [
      "--load-plugins",
      "pylint_Django",
    ]
  },
  "extensions": [
    "ms-python.python",
    "dbaeumer.vscode-eslint",
  ],
  "shutdownAction": "stopCompose"
}
  • name:VS Code に表示されるワークスペース
  • dockerComposeFile:docker-compose.ymlのファイルパスを指定する
  • service:Docker Containerのサービス名
  • workspaceFolder:VS Code で開くワークスペースディレクト
  • settings:VSCodeのの設定を記述(保存時に、blackで自動的にフォーマットされるようにしている)
  • extensions:リモート開発環境にインストールする拡張機能pythonとeslintをインストールに指定。)
  • shutdownAction:「stopCompose」の場合、VS Code を閉じた際に、コンテナが自動的に停止する。「none」の場合、コンテナは起動したままになる。

settingsの部分で、「"python.pythonPath": "/usr/local/bin/python"」を指定して、Docker 内の Python のパスを指定している。

また、black は、Pythonのコードフォーマッターである。pip で事前にインストールが必要である。本当に便利なので、是非活用してみて欲しい。

動作確認

左下のアイコンをクリックすると、メニューが表示される。

f:id:serip39:20201029162820p:plain

「Remote-Containers: Open Folder in Container...」を選択し、本ルートディレクトリを指定する。すると、自動的にビルドが行われ、コンテナ内の開発環境が起動し、VSCode が開かれる。

f:id:serip39:20201029163422p:plain

左下のアイコンには、ワークスペース名が表示されている。

これで、Lintエラーもなくなり、Docker の Python 環境を用いたリモート開発環境を用意することができた。

まとめ

VSCode で、Docker の Python 環境を用いたリモート開発環境を用意することができた。「Remote Containers」はとても便利な拡張機能であった。

それでは、ステキな開発ライフを。

【Python】API を用いてファイルを分割アップロードする

PythonDjango)のアプリケーションを開発している中で、クラウドストレージに、写真 や Excel 、PDF などのデータを API 経由でアップロードを行ったので、備忘録として残しておく。

今回データをアップロードする API では、バイナリーデータに 512KB の制限があったので、分割してアップロードを行えるように実装を行った。また、今回使用したpythonのバージョンは以下の通りである。

Python 3.9.0 (default, Oct 22 2020, 05:03:39) 

APIリクエスト〜urllibライブラリ

docs.python.org

urllib ライブラリは「URLを扱うモジュールを集めたパッケージ」である。

  • urllib.request:HTTPリクエストを行う
  • urllib.error:HTTPリクエストのエラーハンドリングを行う
  • urllib.parse:URLに日本語が含まれていた場合に、URLエンコード/デコードを行う
  • urllib.robotparser:robots.txt ファイルをパースする

今回も、上記の 3 つのライブラリを用いて、API リクエスト(POST)を行う場合は以下のように実装できる。

import urllib.request
import urllib.parse

def post_api(Blob_data, upload_path):
    url = f"http://api.com/{urllib.parse.quote(upload_path)}"
    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/octet-stream",
    }
    data = Blob_data
    req = urllib.request.Request(
        url,
        data,
        headers,
    )
    try:
        with urllib.request.urlopen(req) as res:
            body = res.read()
            return body
    except urllib.error.URLError as e:
        print(e)

URLに、クラウドストレージのフォルダパスを含める必要があり、「フォルダパス」に日本語が含まれることがあるため、urllib.parse.quote でURLエンコードを行っている。

例)テスト → %E3%83%86%E3%82%B9%E3%83%88

URLエンコードに関しては、以下の2つのメソッドがある。(違いは、空文字の取り扱いと、変換しない除外文字(safe)のデフォルト指定である。)

1. urllib.parse.quote(string, safe='/', encoding='UTF-8')

  • 空文字(スペース): %20 に変換する
  • 変換しない文字(safe):デフォルトが / となっている

2. urllib.parse.quote_plus(string, safe='', encoding='UTF-8')

  • 空文字(スペース): + に変換する
  • 変換しない文字(safe):デフォルトは設定されていない

実際に、HTTPリクエストは、 urllib.request.urlopen() メソッドで行われ、引数に Request オブジェクト urllib.request.Request を設定する。(urlopen の引数は、Request オブジェクトではなく、直接 url を指定することで単純なリクエストであれば可能である。ただし、headerなどの詳細な設定が必要な場合は、Request オブジェクトを指定する必要がある。)また、urlopen の返り値は HTTPResponse オブジェクト なので、res.read() メソッドを使うことでレスポンスのボディの中身を確認したり、res. headers() でヘッダー情報を確認することができる。

エラーハンドリングに関しては、二種類の例外を投げることができる。

1. urllib.error.URLError :HTTP通信に失敗した場合

2. urllib.error.HTTPError :HTTP ステータスコードが4xxまたは5xxだった場合

ファイル分割

Pythonでファイルの読み込みをする場合は、 open() を用いる。引数としては、読み込みたいファイルのパスとモードを設定する。モードは以下の通りである。

  • r:読み込み用(デフォルト)
  • w:書き込み用
  • x :新規作成用

末尾に、「b」をつけることでバイナリファイルとして取り扱うことができる
例)「rb」: バイナリファイル読み込み

以下が、ファイルをバイナリファイルとして読み込むコードである。

with open(file_path, "rb") as f:  // ファイルを開く
        data = f.read(partialSize)  // ファイルの内容を読み出す

withブロックを使うことで、ブロックの終了時に自動でファイルオブジェクトをクローズすることができるので、 f.close() を記載する必要がなくなり、便利である。

また、今回ファイルを分割して読み込む必要があり、以下のメソッドを用いる。

f.read(size)

引数にsizeに従い、ファイルの内容をテキストまたはBlobオブジェクトとして読み出す。これを使うことで、ファイルの中身を分割して読み込むことができる。ただ、これだけでは先頭からのみしか取得できなくなってしまうので、ファイルオブジェクトのファイル位置を変更するために、seek を用いる。

f.seek(offset, whence)

whenceで設定した基準点からのオフセット値(offset)でファイル位置を変更することができる。

whence値 設定
0 ファイルの先頭から(デフォルト)
1 現在のファイルの位置から
2 ファイルの末端から

よって、以下のようなファイルを分割するメソッドを実装することができる。

def divideFile(file_path, start, end):
    partialSize = end - start + 1
    try:
        with open(file_path, "rb") as f:
            f.seek(start)
            data = f.read(partialSize)
            return data
    except Exception as e:
        print('Error' + e)

サンプルコード

上記をまとめることによって、ファイルを分割して API を用いてアップロードすることができた。

import urllib.request
import urllib.parse
import hashlib
import os
from copy import copy
import math

BLOCK_SIZE = 524288


def getDividedFile(file_path, start, end):
    partialSize = end - start + 1
    try:
        with open(file_path, "rb") as f:
            f.seek(start)
            data = f.read(partialSize)
            return data
    except Exception as e:
        print("Error" + e)


def uploadFile(file_path, upload_path):
    file_size = os.path.getsize(file_path)

    h = hashlib.new("md5")
    with open(file_path, "rb") as f:
        binary_data = f.read()
        h.update(binary_data)

    url = f"http://api.com/{urllib.parse.quote(upload_path)}?md5={h.hexdigest()}&size={file_size}"

    req_header = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/octet-stream",
    }

    start_byte = 0
    end_byte = BLOCK_SIZE - 1
    loop = math.ceil(file_size / BLOCK_SIZE)
    count = 0

    while count < loop:
        headers = copy(req_header)
        start_byte = count * BLOCK_SIZE
        end_byte = (count + 1) * BLOCK_SIZE - 1
        if end_byte > file_size:
            end_byte = file_size - 1
        count += 1

        headers["Content-Length"] = end_byte - start_byte + 1
        headers["file-range"] = f"{start_byte}-{end_byte}"

        req = urllib.request.Request(
            url=url,
            data=getDividedFile(file_path, start_byte, end_byte),
            method="POST",
            headers=headers,
        )

        try:
            urllib.request.urlopen(req)
            print(f"success upload file volume:{start_byte}-{end_byte}")
        except urllib.error.URLError as e:
            print(e)

サンプルでは、MD5ハッシュ値も計算して付与して、分割後のファイルと一致しているか確認ができるようリクエストに付加している。

まとめ

今回は、Pythonの標準ライブラリのみを用いて、ファイルを分割してアップロードする処理を実装できた。

それでは、ステキな開発ライフを。