Techカテゴリの記事

技術系の備忘録を書き始めます.

詳細

技術系の記事を書き始めることにしました. 検索機能,月別アーカイブ,マークダウン,シンタックスハイライト等を実装したのでなんかいい感じに見れるようになると思います. 試しにPython形式でかいたコードの見栄えを確認してみましょう.

def func(param):
    return param**2

いい感じですね.

DjangoでMarkdown形式のエディタを実装,HTML形式で表示する

詳細

Djangoでマークダウン形式のエディタを爆速で実装します.参考にしたサイトは以下です.

mdeditorとは

Markdown形式のエディタをDjangoに爆速で実装できるライブラリです.ここ 実際に動作しているところはリンク先をご覧ください.結構有能なのでびっくりします.

環境

  • Ubuntu18.04 LTS(さくらVPS)
  • Python3.6.8
  • Django==3.0
  • django-mdeditor==0.1.18
  • Markdown==3.3.4

mdeditorの実装

まず,pipで必要なライブラリを入れましょう

pip install django-mdeditor
pip install Markdown

mdeditor自体の設定をしていきます.settings.pyに以下を追記します.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'mdeditor' #これを追記
]
X_FRAME_OPTIONS = 'SAMEORIGIN'
MDEDITOR_CONFIGS = {
    'default': {
        'language': 'en',
    }
}

次に,models.pyを設定していきましょう.app内で定義するModelを以下のように定義します. 普段使っているmodels.TextFieldが Markdown形式のオブジェクトになります

#models.py
from django.db import models
from mdeditor.fields import MDTextField

# Create your models here.
class Article(models.Model):
    title = models.TextField(max_length=200)
    content = MDTextField() #ここがMarkdown形式のTextFieldになる

    def __str__(self):
        return self.title

また,{project_name}/urls.pyに以下を追記します.

urlpatterns = [
    path('admin/', admin.site.urls),
    path('mdeditor/', include('mdeditor.urls')) #これを追記
]

admin.pyへの追記を忘れずに!

# admin.py
from django.contrib import admin
from .models import Article

# Register your models here.
admin.site.register(Article)

現時点でDjango Adminの管理画面からMarkdown形式で編集できるようになっているはずです.

Markdown_to_htmlの実装

次に,MarkdownをHTMLに変換していい感じに表示されるようにしていきましょう. 先ほど定義したArticleモデルに次を追記していきます.

#models.py
from django.db import models
from mdeditor.fields import MDTextField
import markdown #ここを追記

# Create your models here.
class Article(models.Model):
    title = models.TextField(max_length=200)
    content = MDTextField() #ここがMarkdown形式のTextFieldになる

    def __str__(self):
        return self.title

    # 以下を追記
    def markdown_to_html(self):
        md = markdown.Markdown(
            extensions=['extra', 'admonition', 'sane_lists', 'toc']
        )
        html = md.convert(self.content)
        return html

このメソッドをHTML内のDjangoテンプレートから呼び出していきます. 以下は関連する部分のHTMLコードです.

<!doctype html>
{% extends 'base.html' %}
{% load static %}

{% block content %}

{% for item in object_list %}
<article class="blog-post">
  <h4 class="blog-post-title">{{ item.title }}</h4>
  {{ item.markdown_to_html|safe }}
</article>
{% endfor %}
{% endblock %}

これだけだとコード整形のみが為されてお洒落なシンタックスハイライトがつきません. いい感じの見栄えにするために以下をHTML内に追記します.

<head>
  ...
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/a11y-dark.min.css">
  ...
</head>

<body>
  ...
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
  <script>hljs.initHighlightingOnLoad();</script>
</body>

これで完了です.試しにサーバーを立ち上げて確かめてみてください.

python3 manage.py runserver

Django + Ace(django_ace) + DockerでオンラインPython実行環境を実装する

詳細

タイトルの通り,Django + Ace + DockerでオンラインPython実行環境を実装します. 一回で収まらないかもしれないので「その1」としています.

参考にしたサイト

環境

  • Django==3.0
  • django_ace==1.0.11
  • Python==3.6.8
  • Docker Pythonイメージ: python:3.7-slim-buster

構成の説明

  • Django: フレームワークとして使用
  • Ace: Webページ上でテキスト編集する際のシンタックスハイライト等に使用
  • django-ace: AceをDjango内のView定義から呼び出せるようにしたもの
  • Docker: 実行環境をサーバー内で隔離しないと悪意あるコードが実行できてしまうので,Dockerコンテナを使用

環境構築&必要ライブラリ群のインストール

$ python3 -m venv venv
$ source venv/bin/activate
$ pip install Django==3.0
$ pip install django-ace==1.0.11

プロジェクトの作成

$ django-admin startproject project
$ cd project
$ django-admin startapp editor
$ mkdir history #実行するスクリプトの保存先

この時点で,ディレクトリは以下のような構成になっているはずです.

project/
 ├ editor/
 ├ project/
 ├ history/
 └ manage.py

フォームビューの作成

エディタ部分を実装するために,フォームビューを実装します. CDN等で提供されるAceを埋め込んで利用しても良いですが,ここではdjango-aceを使用します.

#forms.py
from django import forms
from django_ace import AceWidget

class EditorForm(forms.Form):
    code = forms.CharField(
        widget = AceWidget(
            mode="python",
            theme="twilight",
            width="100%"
        )
    )

EditorFormを作成したら,Viewを追加していきます. 下記のコードでは,index.htmlをテンプレートとして,HTML中のformから実行コードを取得します. 取得したコードは,historyフォルダ内に保存され,dockerコンテナ内で実行されます.

  • start_docker(code): dockerコンテナに実行するコードを投げ,実行するコード.
  • Home(views.FormVIew): html内のフォームから取得したコードを,start_dockerに投げます.
# views.py
from .forms import EditorForm

FILE_DIR = os.path.join(settings.BASE_DIR, 'history')
DOCKER_CMD = 'docker run -i --rm --name tmp_container -v {}:/usr/src/myapp -w /usr/src/myapp python:3.7 python {}'

def start_docker(code):
    # dockerコンテナ内でコードを実行する
    file_name = '{}.py'.format(datetime.datetime.now().isoformat())
    file_path = os.path.join(FILE_DIR, file_name)

    with open(file_path, 'w', encoding='utf-8') as file:
        file.write(code)

    cmd = DOCKER_CMD.format(FILE_DIR, file_name)
    ret = subprocess.run(
        cmd, timeout=15, shell=True,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT
    )

    return ret.stdout.decode()

class Home(generic.FormView):
    template_name = 'index.html'
    form_class = EditorForm
    sucess_url = reverse_lazy('home')

    def form_valid(self, form):
        # 送信ボタンで呼ばれる
        code = form.cleaned_data['code']
        output = start_docker(code)
        context = self.get_context_data(form=form, output=output)
        return self.render_to_response(context)
<!--index.htmlの一部のみ抜粋-->
<head>
....
{{ form.media}}
</head>

<body>
....
<div class="col-12">
    <form action="" method="POST">{% csrf_token %}
        {{ form.code }}
        <button class="btn btn-primary" type="submit">Run</button>
    </form>
</div>

<div class="col-12">
    <label for="exampleFormControlTextarea1" class="form-label">実行結果</label>
    <textarea class="form-control" id="exampleFormControlTextarea1" rows="3" disabled>{{ output }}</textarea>
</div>
...
</body>

以上をいい感じにまとめると,下記のようなコード編集画面が現れます.(あとで追記)

docker上でdjango+mysqlで環境構築すると、access deniedエラーになる件

詳細

Docker上でdjango+mysqlの環境構築をするとエラーになる件

環境

  • Macbook Air(M1)
  • Docker Desktop for Mac (ver.20.10.6)
  • Django == 3.0
  • Python3 (3.6-slim-buster)
  • MySQL 5.7

結論から言うと

MySQLイメージを使う場合は、{既存のDBファイルを含むローカルフォルダ}:/var/lib/mysqlとしてバインドするとハマります。 新しい名前付きボリュームor空ディレクトリをバインドすることで、このエラーを回避できます。

やりたいこと

Pythonイメージ内でdjangoプロジェクトをビルドし、djangoからMySQLをDBとして使用したいです。 その際、MySQLのデータを永続化させるために既にDB関連のデータを含んでいるローカルのフォルダ(下記のmysql/data_db)をマウントしようとしてハマりました。

はじめに試したディレクトリ構造は以下のようになっています。

my-docker-project/
|__django-project/
|     |__src/...
|     |__Dockerfile
|
|__mysql/
|    |__data_db/...
|    |__init_db/...
|
|__docker-compose.yml

また、docker-compose.ymlは下記の通りです。

version: "3"

services:
  mysql-db:
    image: amd64/mysql:5.7
    environment:
      - MYSQL_DATABASE=python_db
      - MYSQL_USER={ユーザー名}
      - MYSQL_PASSWORD={パスワード}
      - MYSQL_ROOT_PASSWORD=password
    volumes:
      - ./mysql/data_db:/var/lib/mysql
      - ./mysql/init_db:/docker-entrypoint-initdb.d
    privileged: true
    ports:
      - 3306:3306

  django-project:
    build: ./django-project
    restart: always
    command: bash -c "python3 /mysite/local_manage.py makemigrations && python3 /mysite/local_manage.py migrate && python3 /mysite/local_manage.py runserver 0.0.0.0:8000"
    ports:
      - 8000:8000
    volumes:
      - ./django-project/mysite/history:/mysite/history
      - ./django-project/mysite/media:/mysite/media
    depends_on:
      - mysql-db

以上の情報をもとに、django側のsettings.pyを以下の通りに設定しました。

# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'python_db',
        'USER': 'root',
        'HOST': 'mysql-db',
        'PASSWORD': 'password',
        'PORT': '3306'
    }
}

症状

表記の通りの環境構築をDocker上で行いdocker compose up --buildすると、mysqlにアクセスできず以下のエラーに見舞われます。

django.db.utils.OperationalError: (1045, "Access denied for user 'root'@'192.168.32.3' (using password: YES)")

原因

調べて見ると以下の記事に行き当たりました。 stackoverflowの記事

上記の記事によると、 Do note that none of the variables below will have any effect if you start the container with a data directory that already contains a database: any pre-existing database will always be left untouched on container startup. とのことでした。すなわち、一度docker buildなどでDBファイルシステムが構成されてしまったディレクトリをバインドしようとすると、DBコンテナの初期化に失敗するらしいです。

対処法

docker-compose.yml内で/var/lib/mysqlに紐付けるフォルダを、docker compose up --buildする前に空にしておくことでうまくいきます。すなわち、

  1. $ rm -r ./mysql/data_dbでdata_dbを削除
  2. $ mkdir ./mysql/data_dbで再度ディレクトリ作成
  3. $ docker compose up --buildでビルド

という手順を踏むことになります。これはうまくいくことを確認しました。

【Docker】alpineイメージをproxy環境下で使用する場合

詳細

alpine linuxイメージをプロキシ環境下で使用する場合、様々な障壁に阻まれてコケることがあります。 以下では、その回避策を列挙していきます。(問題の切り分けができていないので一部おかしいことを言っているかもしれません)

1. プロキシの環境変数への登録

Dockerfile内でプロキシを登録します。 ここでRUN export http_proxy={プロキシ:ポート}とすると、なぜか環境変数に反映されておらずハマりました。 解決策としては

ENV http_proxy=http://{プロキシのIPアドレス}:{ポート}/
ENV https_proxy=http://{プロキシのIPアドレス}:{ポート}/

とすることで環境変数に反映することができました。

2. レポジトリの追加

プロキシを経由する関係でapkレポジトリからライブラリを引っ張ってこれない場合があります。 これは、デフォルトでapkがインデックスを探しに行くレポジトリURLがhttps://~となっている場合に起こります。 下記のissueにはdl-cdn.alpinelinux.org does not support TLS at all. (中略) APK repositories should be http onlyとあります。 そのため、/etc/apk/repositoriesにプレーンなhttpのURLの記述とする必要があります。

Githubのissue

すなわち、

RUN rm /etc/apk/repositories
RUN echo "http://cd-cdn.alpinelinux.org/alpine/v3.13/main" >> /etc/apk/repositories
RUN echo "http://cd-cdn.alpinelinux.org/alpine/v3.13/community" >> /etc/apk/repositories

とし、httpでapkのレポを探しに行きます。

pymodbusでModbus/TCPスレーブ(サーバー)を構築する

詳細

お久しぶりです。IMAXおじさんです。 今日は今までのWEB系の話とは少し変わって、PyModbusでModbus/TCPのサーバーを構築する話をします。

参考文献

HMSのサイト M-system技研のModbus/TCP通信仕様書

Modbus/TCPとは

Modbus/TCPとは、Modicon社(現Schneider Electric社)が1979年に策定した産業用Ethernetプロトコルです。 主にPLCやリモートIOとの通信に使用される産業用Ethernetプロトコルであり、世界的なシェアとしては5%程度を占めています(2020年現在)。

Ethernet系フィールドバスのシェア

通信の概要

サーバー(フィールド側,  PLCやリモートIO)
xxx.yyy.zzz.100
    |
    |
クライアント(PC)
xxx.yyy.zzz.101

Modbus/TCPはサーバー/クライアント方式の通信プロトコルです。 PLCやリモートIOなどがスレーブ(==サーバー)、データ収集を行うPCなどがマスター(==クライアント)としてデータのやり取りを行います。 名前の通り、各サーバーとクライアントがTCPでペイロードをやり取りします。 物理レイヤーの仕様(端子形状など)は指定されておらず、あくまで通信のレイヤーのみの仕様となります。

ペイロードの仕様に関しては、下記のM-system技研発行の通信仕様書がよくまとまっており参考になります。

M-system技研 Modbus通信仕様書

PyModbusとは

PyModbusとは上記のModbusサーバー・クライアントを擬似的にPythonで構築できるライブラリです。 対向機をわざわざ購入するとなると、数万円かかってしまいますが、PyModbusを使えば無料で手軽に対向機を用意できます。

PyPIのPyModbusページ

Modbus/TCPによる通信デモ

さて、Modbus/TCPの説明が終わったところで早速デモの構築に移りましょう。 PC上にPyModbusで構築したサーバーまでデータを取りに行くデモを構築します。

検証環境

  1. PC: DELL Inspiron 7370
  2. OS: Ubuntu20.04(LTS)
  3. Python: 3.8.10
  4. PyModbus: 2.5.3

Modbus/TCPスレーブ(サーバー)のPyModbusによる構築

PyModbusのサーバーを早速構築します。ローカルホストにサーバーを立てることにするため、特にIPアドレスは気にしなくてもよいです。 実際にデバイス間通信を試したい人は、Ethernetポートを備えたデバイスを2個用意して固定IPを振ってあげるとよいかと思います。

今回はinput_registerに値を入れることにします。 サーバーがレジスタ(input_register)に保持する値は、以下の通りとしましょう。

----------------------------
register1 |   int16
----------------------------
register2 |   float
register3 |   (32bit)
----------------------------

Modbusでは、1レジスタ=2byte(16bit)で扱われることに注意してください。なので、32bitのfloatなど16bitを超えるようなデータはレジスタをまたがって保持されることになります。

さて、このような値を返してくれるサーバーをPyModbusで構築していきます。まずは必要ライブラリのimportを。

# sample_server.py
from pymodbus.constants import Endian
from pymodbus.version import version
from pymodbus.server.asynchronous import StartTcpServer

from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.payload import BinaryPayloadBuilder

サーバー側でアクセス・送受信しているパケットの内容を確認するため、ログの設定をします。

import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s'
          ' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
log = logging.getLogger()
log.setLevel(logging.DEBUG)

次に、レジスタに入れる値を作っていきます。 PyModbusではpymodbus.BinaryPayloadBuilderを使ってペイロードのオブジェクトを作ることになります。

builder = BinaryPayloadBuilder(byteorder=Endian.Big) #ペイロードビルダーのインスタンスを作成。バイトオーダーは再現したいリモートIOの仕様に準ずること。

# 最初のint_16のペイロードを作成
builder.add_16bit_int(100)

# 2つ目のfloatのペイロードを作成
builder.add_32bit_float(123.45)

# ペイロードをModbusSequentialDataBlockへ変換
block = ModbusSequentialDataBlock(1, builder.to_registers())

# レジスタへの登録を行う
store = ModbusSlaveContext(ir=block, zero_mode=False)
context = ModbusServerContext(slaves=store, single=True)

最後に、サーバーを起動しておしまいです。

StartTcpServer(context, address=("0.0.0.0, 502")) # ローカルホストで起動

Modbus/TCPマスター(クライアント)のPyModbusによる構築

サーバーの構築はできたので、サーバーにためている情報を取りに行くクライアントを作ります。

Modbusではクライアントからのみ通信を始めることができます。 クライアントから打てるリクエストにはいくつか種類(ファンクションコード)があります。 詳しくはModbus/TCPの通信仕様書を見ていただきたいのですが、今回はファンクションコード4(FC4)を使います。 FC4はread_input_registersというリクエストになっており、その名の通りinput_registersの値を読み取るものになります。

(余談ですが、Modbus/TCPではread_input_registersで読まれるレジスタアドレスは300001から始まると決まっているようです。FC4なのに開始アドレスが3xxxなのは気持ちが悪い...)

サーバーの設定では、レジスタ3つにまたがってデータが溜まっているのでread_input_registersで3つ分レジスタを読んであげましょう。

from pymodbus.client.sync import ModbusTcpClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder

# Modbusクライアントのインスタンス。ローカルホストにサーバーが立っているので、アクセス先をlocalhostと指定。
client = ModbusTcpClient("localhost", port=502)

# FC4のread_input_registersでレジスタアドレス300001+0 ~ 300001+2を読み取る。
# 第一引数が開始アドレス(相対)、第二引数がレジスタ数
res = client.read_input_registers(0, 3, unit=1)

# 読み取ったレジスタ値をデコーダインスタンスへ渡す
# サーバー側がビッグエンディアン設定なので、こちらも合わせる。
decoder = BinaryPayloadDecoder.fromRegisters(res.registers, byteorder=Endian.Big)

# 最初のint_16bitをデコード
int16 = decoder.decode_16bit_int()

# 次のfloat_32bitをデコード
float32 = decoder.decode_32bit_float()

print(int16) # -> 100
print(float32) # -> 123.45...

これで、サーバーのinput_registersに溜まったデータをModbus/TCPで読み取ることができました。

まとめ

産業用ネットワーク難しい

自作競プロジャッジサーバの構成について

詳細

簡単な自作競プロジャッジサーバーを作った話をします。(競プロとは?:こちらを参照) デプロイしてから結構経つのに、詳細な構成に関して何も記事を書いていなかったので、ここらで文章に落とそうと思います。 サーバは一時期まで公開していましたが、Dockerで実行環境を隔離しているとはいえ任意のコードが実行できてしまうのは怖いので最近は公開していません。

1. ジャッジサーバの構成に関して

ジャッジサーバ内のコンポーネント間の構成とデータフローを示します。 図1.1.

回答提出から正誤判定までのジャッジサーバ内での処理の流れ

  1. 回答者からコードが提出されると、一旦Djangoがそれを受け取ります。
  2. DjangoはこれをRedis+Celeryで実装された非同期処理ジャッジキューに突っ込みます。その後、ユーザに対しジャッジ処理中である旨を示します。
  3. ジャッジキューは順に提出コードの正誤判定を行ない、判定終了したコードに対し完了フラグを立てます。
  4. ジャッジ完了後、各コードの正誤判定の詳細をユーザに返します。

2. 難しかったところ

非同期処理の実装

コードを提出してから、ジャッジが完了するまでにある程度時間がかかります。 実行に時間がかかる場合やテストケースが多い場合は、POSTしてからページが表示されるまで何も表示されない状態になってしまいます。 また、実行時間があまりに長い場合はブラウザにリクエストタイムアウトと判定されてしまいま す。 そのため、今回は下記のようなフローで非同期処理を実装することにしました。

バックエンドではジャッジがcelery+redisで実装されたFIFOキューを保持し、提出されたコードに順次正誤判定を行なっています。 ジャッジ未完了の状態ではブラウザ更新を促し、ジャッジ完了の状態では結果を通知します。

TLEの検知

競技プログラミングにおいては、コードの実行時間が非常に重要です。多くの場合、2秒程度の実行時間内に問題を解けるコードを書く必要があります。 制限時間内に解けなかったコードにはTLE(Time Limit Exceeded)判定が与えられ、不正解扱いとなります。

最初は簡単なジャッジサーバができれば良い程度に考えていたので、コードの正解・不正解しか判定しないロジックを書いていました。 ところが、段々と完成度を追求したくなってくるもので、やはりTLEの判定がしたくなってきます。

色々実行時間を計測する術を検討しました。例えば、下記のようにコード実行前後で時刻を計測し、その差分を測るようなやり方です。

start = time.time()
ret = subprocess.run(cmd, shell=True, ...) #提出コードを実行
end = time.time()

timeTaken = end - start # 実行時間を計測

しかし、このやり方ではwhile True: passのような無限ループが発生した場合にTLE判定を出せなくなってしまいます。 一定時間以上実行している場合には途中で実行を打ち切るような動作が必要になるわけです。

最終的にはコードを実行しているsubprocessモジュールで実行時間を計測し、オーバーした場合にTLE判定を出すようにしました。 subprocessモジュールには、実行時間を制限するオプションtimeoutがあるため、ここに実行制限時間の秒数を渡せば上記の問題に対応しつつTLEを判定できます。

# subprocessモジュールによるコードの実行
try:
    ret = subprocess.run(
        cmd, timeout=2, shell=True, stdin=fin, #  timeoutに制限時間を指定
        stdout=subprocess.PIPE, stderr=subprocess.PIPE
    )
except (CalledProcessError, TimeoutExpired) as e:
    if TimeoutExpired:
        tle += 1  # 実行時間が超過した場合はTLE判定を出す
        continue

    elif CalledProcessError:
        ...

3 今後の予定

  • フロントエンドをVue.jsで作り直す。
  • ajaxによる非同期通信を実装し、ユーザがブラウザ更新をする必要をなくす
  • 対応言語を増やす(現在Java, C++, Python3, PyPy3に対応)
  • etc....

HerokuにLINE webhookをデプロイする(heroku/line-bot-sdk/python3)

詳細

HerokuにLINE Webhookをデプロイするまで

今回は無料で使えるホスティングサービスであるHerokuにLINE Webhookをデプロイするまでの手順を備忘録的に記します。

作りたいもの

実家でデジタル難民と化している祖母が他の家族からのLINEメッセージを見れるように、↓のようなシステムを作ろうと思い立ちました。

このシステムを作るにあたり、LINEメッセージを送ると自作のAPIにデータをPOSTするようなWebhookをつくりたいです。 今回はゴールとして、まずはLINE bot(Python)に送ったメッセージをそのままLINEで返信してくるようなエコーボットをデプロイすることにします。

本記事の構成

  1. 前提(heroku/line developerのアカウント作成等)
  2. Webhookの構成
  3. line-bot-sdkについて
  4. herokuへのデプロイ手順

1. 前提

1.1. herokuのアカウント作成

herokuの利用にはアカウント作成が必要です。今回は無料枠の範疇のことしかしないので、Freeアカウントで作成します。 こちらからアカウントを作成することができます。

また、heroku CLIを使うためインストールが必要です。こちらの手順に従い、インストールしてください。

1.2. LINE Developerアカウントの作成

今回のwebhook作成に関しては、LINEbotが必要となります。LINEbotの作成にはLINE developerアカウントが必要となるため、アカウント登録をおこないます。 こちらからアカウント作成をしてください。

2. webhookの構成

今回のwebhookを作成するにあたり、ローカル上で下記のようなフォルダ構成を作成します。

*/
|__main.py              # webhookの処理を記述する
|__requirements.txt  # 使用するライブラリの一覧
|__runtime.txt          # herokuで使用する言語の諸元
|__Procfile              # herokuで実行するコマンド
|__.env                   # herokuで使用する環境変数

2.1. main.py

main.pyにはline-bot-sdkを使って行う処理を記述することになります。 詳細は3節で説明するため、ここでは説明を省きます。

2.2. requirements.txt

requirements.txtには今回使用するライブラリを記述します。これをプロジェクトフォルダに含めることで、デプロイ時に自動でライブラリをインストールしてくれます。

Flask==2.0.3
line-bot-sdk==2.2.1
requests==2.27.1

2.3. runtime.txt

runtime.txtにはherokuで使用するPythonのバージョンを記述します。その際、herokuが対応しているバージョンを選択しなければならないため、現在のherokuが何に対応しているかを確認するようにします。 今回は以下の内容を記述します。一行のみです。

python-3.9.0

2.4. Procfile

Procfileはheroku上でデプロイ時に実行するコマンドを記述します。今回はmain.pyを実行することでサーバーを立ち上げたいので、下記の内容とします。

web: python3 main.py

2.5. .env

.envファイルはherokuの環境に登録したい環境変数を記述します。main.pyなどに直接記載したくない情報(例えばline-bot-sdkのシークレットキーなど)を記述し、herokuの環境に反映します。

3. line-bot-sdkについて

今回は開発者公式が公開しているline-bot-sdkのサンプルプログラムを使用します。サイトはこちら(GitHub)から見れます。

元のサンプルプログラムはアクセストークンとシークレットキーをベタ書きしているため、この二つだけは環境変数から読み込むように変更を加えています。

from flask import Flask, request, abort
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
import os

# Flaskでwebhookを立ち上げ
app = Flask(__name__)

# アクセストークンとシークレットキーは環境変数から読み込む
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']

    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("invalid signatrue. please check channel info")
        abort(400)

    return 'OK'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(event.reply_token, TextSendMessage(text=event.message.text))


if __name__ == "__main__":
    port = int(os.getenv("PORT"))
    app.run(host="0.0.0.0", port=port)

4. herokuへのデプロイに関して

ここまでのプログラムの準備ができたところで、herokuへのデプロイを行います。大まかな手順は下記の通りです。

  1. heroku上でのプロジェクト作成
  2. gitリポジトリの作成・登録
  3. heroku環境変数の反映
  4. herokuへのpush

4.1. heroku上でのプロジェクト作成

まずは、herokuの個人コンソールから「create new app」を選択し、新しいアプリケーションを作成します。

名前を選ぶように促されるので、適当な名前をつけます。「hogehoge.herokuapp.com」というURLを与えられますが、世界で一意な名前である必要があるため、適度に長い名前にしてください。

4.2. gitリポジトリの作成・登録

ローカルのプロジェクトをgitリポジトリとして登録し、herokuにpushします。

ローカルで

git init
git add *
git commit -m "first commit"

とし、まずはgitのローカルリポジトリを作成します。

次に、herokuのリモートリポを紐付けます。heroku CLIから以下を実行します。

heroku git:remote -a {自分のアプリの名前}

このままgit pushすると、環境変数を設定しないままビルドが始まってしまうので、git pushする前に.envファイルで設定した環境変数をherokuに反映します。

4.3. 環境変数の反映

heroku CLIを用いて、.envファイル内に記述した環境変数をherokuのアプリに反映します。 プロジェクトディレクトリ内でheroku config:pushを実施します。成功すると「Successfully wrote settings to Heroku!」とログが出ます。

venv) hogehoge:~/Desktop/line-webhook-app$ heroku config:push
Successfully wrote settings to Heroku!

4.4. herokuへのpush

ここまで実施できたら、git push heroku masterでheroku上のリモートリポジトリにプロジェクトをpushします。 pushが成功すると自動でビルド→デプロイが行われます。

LINE botにメッセージを送って、全く同じ内容の返事が来れば成功です。

プロジェクト内でheroku logs --tailとすると、デプロイしたアプリのログを確認することができます。 うまくLINE botが返事を返してくれなければ、このログを見てトラブルシュートを行います。

まとめ

これまでVPSをメインで使っていたので、herokuのようなPaaSを使えば無料で簡単にデプロイできるんだなと感心しました。

【AWS】VPSからAWSにサイトを移行した件

詳細

お久しぶりです。IMAXおじさんです。

今日はこのサイトをさくらVPSからAWSに移行した話をします。現在はさくらVPSに戻しております。

元の構成(さくらVPS)に関して

元々はさくらVPSで小さめのサーバを借りてnginx + django + mysqlの構成でサイトを構成していました。詳細はこちらの記事に掲載しています。

この構成でやりたいことはだいたいできており、不満は全くなかったのですがお仕事でAWSを使い始めたこともあり、こちらに移行することにしました。

AWSでの構成に関して

個人サイトなのであまりお金がかかるのも困りものです。そこで、下の要求を満たすように構成を考えました。

  • 月間予算20$ ~ 25$に収まるようにする
  • さくらVPSで構築していたコンテナをそのままEC2に移行するのではなく、S3やALBなどを使って、なるべくAWSの機能性を活かした構成にする
  • それぞれのサーバのログをS3に保管し、分析できるようにする

まだ全てを満たす構成にはできていませんが、現在は下のような形でデプロイをしています。

やりのこしていること

  • ログの分析
  • お部屋の気温分析アプリをIoT Core + IoT Analytics + Grafanaで実装

【AWS】AWS Solutions Architect - Associateに合格しました。

詳細

ご無沙汰しております。IMAXおじさんです。

11.28〜12.1まで開催されていたAWS re:Invent 2023に参加してきました。これに行く前にAWSの資格を一つくらい取得しておきたいと思い立ち、AWS Solutions Architect - Associateを受験しました。 結果は合格(760/1000)でした。次はDeveloper Associateの取得を目指します。

About

IMAXおじさんが(主に)技術系記事を備忘録として残していくブログです.

Category

  1. Tech
  2. Daily
  3. Job
  4. Other