【Python】smtplibでメール送信

f:id:tm200:20210829222902j:plain

Pythonでメール通知の実装メモ。
ホスト名などは適宜変更してください。

実装

ライブラリは不要です。

# -*- coding: utf-8 -*-
import getpass
import platform
import smtplib
from datetime import datetime
from email.mime.text import MIMEText
from email.utils import formatdate


def send_mail(to: str, mail_text: str):
    """
    メール送信
    :param to: 送信先
    :param mail_text: 本文
    """
    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    host_name = platform.uname()[1]

    message = MIMEText(mail_text)
    message['Subject'] = f'[{now}][{host_name}]'
    message['From'] = f'{getpass.getuser()}@{host_name}'
    message['To'] = to
    message['Date'] = formatdate()
    with smtplib.SMTP('localhost') as smtp:
        smtp.send_message(message)

【Kubernetes】kubectl の便利コマンド

f:id:tm200:20210826215705p:plain

使わなくなると忘れそうなので、kubectlコマンドをメモ。
alias k=kubectl 実行済の想定です。
随時、追加します。

入門ならこの一冊😊

トークン取得

$ (k describe secret $(k get secrets | grep <サービスアカウント名> | cut -f1 -d ' ') | grep -E '^token' | cut -f2 -d':' | tr -d '\t')

ネームスペース切り替え

$ k config set-context $(k config current-context) --namespace=<ネームスペース>

# namespace一覧
$ k config view | grep namespace

tls用secret作成

base64デコード時のオプションを--decodeとすると、macでも動きます。

$ k create secret tls <secret名> --key=xxx.key --cert=xxx.crt

# 確認コマンド
$ k get secret/<secret名> -o yaml

# jqが使える場合、項目指定も
$ k get secret/<secret名> -o json | jq -r '.data."<項目名>" | base64 --decode'

# historyに出力させたくない場合
$ k get secret/<secret名> -o yaml > secret.yaml
$ grep "^data:" -A <secretの件数> secret.yaml | awk -F '{print $2}' | base64 --decode

リソースを更新して再起動

# コンフィグマップの編集
$ k edit cm <ConfigMap名>

# 変更を反映させる為、再起動
$ k rollout restart <deployment名>

ポートフォワードでデバッグ

以下のコマンドで対象Podの80番ポートがlocalhost:8080でアクセス可能となる。

$ k port-forward <pod名> 8080:80

【Python】FastAPIのディレクトリ構成

f:id:tm200:20210725133513p:plain

クラウドソーシングでFastAPIの実装を行う機会があったので、調べた事などをメモ。
作成したソースコードGit Hubに上げてあります。

ドキュメント

公式ドキュメントは分かりやすく書かれていた印象です。

fastapi.tiangolo.com

ディレクトリ構成

公式ドキュメントや現場のPJを参考に、以下の構成にしました。
※ Docker関連ファイルなどは除外してあります。

.
├── __init__.py
├── app.py
├── cli.py
├── database.py
├── db_config.py
├── dependencies
│   ├── __init__.py
│   ├── auth.py
│   └── request.py
├── exception.py
├── gunicorn_conf.py
├── handlers
│   ├── __init__.py
│   └── exception.py
├── midlewares
│   ├── __init__.py
│   └── timer.py
├── model
│   ├── __init__.py
│   └── users.py
├── plugins
│   └── __init__.py
├── requirements.txt
├── response.py
└──  routers
    ├── __init__.py
    ├── index.py
    └── users.py

app.py

create_appルーターやDB、ミドルウェア、エラーハンドリングの設定を行っています。
uvicorngunicornからこちらのモジュールを指定して起動します。

cli.py

開発時の動作確認用。
uvicorn.runで起動する他、ArgumentParserでホストとポートを指定可能です。
起動コマンドはpython cli.py

database.py

SQLiteMySQL単体テスト用のデータベース設定を定義しています。
ORMはsqlalchemy

exception.py

独自のExceptionクラスを定義しています。

response.py

JSONResponseを継承した、APIのレスポンスクラスを定義しています。
orjsonを組み込むだけならORJSONResponseクラスを直接利用すれば良いと思います。

gunicorn_conf.py

本番起動時のgunicornに関する設定を記載しています。
詳細は以下を参照してください。

docs.gunicorn.org

dependencies/*

fastapi.Dependsrouterモジュール内のエンドポイント定義関数の引数に設定するモジュール群です。
公式で言うと以下の辺りです。

fastapi.tiangolo.com

handlers/exception.py

handlers配下に定義したエラーハンドリング関数をapp.pyadd_exception_handlerする事により、エラーハンドリングを集約する事ができます。

midlewares/*

ミドルウェア用関数を定義しています。
app.pyadd_middlewareする事により使用可能となります。
各エンドポイントのレスポンス返却前に、何かしらの処理を追加できます。
公式に記載があるのですが、yieldを使った依存関係をもつ場合の終了コードはミドルウェアの後に実行されるようです。

fastapi.tiangolo.com

model/*

model配下にsqlalchemyのモデルクラスを定義します。

plugins/*

PJ独自のライブラリを使用した共通処理がある場合などで、実装モジュールを分けられる場合に使用します。

routers/*

fastapi.APIRouterで各エンドポイントを定義します。
app.pyでinclude時にsqlalchemy.orm.Sessionを注入してエンドポイントで利用できるようにしています。

まとめ

FastAPIを利用したい時にサッと引っ張ってこれる雛形が欲しいという事もあり、この記事の執筆に至りました。
おかしな点などありましたら、教えて頂けると嬉しいです😊

関係ないのですが、クラウドソーシングをしていると動けば良いというスタンスのコードをよく見かけるので、こういった記事が増える事により、〇〇コードが少しでも減ってくれれば良いなと思います。
(構成以前に謎文法な事も多いので、まずは公開されているpythonコードで自分が実装する分野と近しい実装を見て欲しいです、、、😂 )

【Python】pyhive で Presto 接続

f:id:tm200:20210704143739p:plain


PyHiveでPresto接続方法のメモ。
認証方法はプロダクト毎に違うと思いますので、適宜変更。
この辺りを見れば実装方法が分かると思います。

github.com

インストール

$ pip3 install pyhive[presto]==0.6.4

実装

# -*- coding: utf-8 -*-
import base64
from pyhive import presto

def execute():
    cursor = presto.connect(
        '<ホスト名>',
        port=443,
        catalog='<カタログ名>',
        username='<ユーザー名>',
        protocol='https',
    ).cursor()

    # パラメータは辞書・リスト・tupleのいずれか
    cursor.execute('SELECT * FROM sample WHERE test_column = %s', parameters=('param_value',))
    print(cursor.fetchall())


if __name__ == '__main__':
    execute()

参考

逐次処理等がしたい場合はこちらを参考にすると良いかも。
機会があれば実装しようと思います。

github.com

www.python.org

オススメ書籍

色々と考えさせられる一冊です!
ページ数も少なく、時間がない人でも読める量だと思います(`・∀・´)

【Kubernetes】Job・CronJobの設定方法

f:id:tm200:20210611174228p:plain

Job・CronJobの実装方法をメモ。
kubernetes環境上で実行するjobは並列起動してもエラーとならない様な設計にすると良いそうです。
後はdockerイメージ更新はタグをベースに行う事により、imagePullPolicy: IfNotPresentで更新の必要がない時はpullをskipさせる事ができます。
タグがlatestだと更新漏れ等事故の元となる為、お勧めしません。

Jobの定義

マニフェストに定義してkubectl apply -f job.yamlで実行できます。
jobの確認方法はkubectl get jobなど、他のリソースと同じです。

apiVersion: batch/v1
kind: Job
metadata:
  name: test-job
spec:
  completions: 5
  parallelism: 3
  backoffLimit: 1
  activeDeadlineSeconds: 3600  # この秒数を超える処理はエラーとなるので注意
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: test-job-container
         image: test-image:0.0.1
         imagePullPolicy: IfNotPresent
         command:
           - sh
           - -c
           - "python3 main.py"
         env:
         - name: MYSQL_PASSWORD  #jsonをsecretに格納した場合
           valueFrom:
             secretKeyRef:
               name: test.secret.mysql
               key: passwd

CronJobの定義

実行環境によりますが毎時起動などで0分もしくは5の倍数分を指定するPJが多い為、若干ずらして指定すると環境に優しいです。
実行後のPOD保管数をsuccessfulJobsHistoryLimitfailedJobsHistoryLimitで指定できます。
もしPOD内にクレデンシャル情報などが生成される場合、不要になった時点で削除しておくと安全かもしれません。

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: test-cronjob
spec:
  schedule: "02 * * * *"
  concurrencyPolicy: Allow
  startingDeadlineSeconds: 2000  # 開始時刻 + startingDeadlineSeconds までの間に起動可能
  successfulJobsHistoryLimit: 3 # 成功したジョブのPOD保管数
  failedJobsHistoryLimit: 3 # 失敗したジョブのPOD保管数
  jobTemplate:
    spec:
      backoffLimit: 1
      activeDeadlineSeconds: 3600  # この秒数を超える処理はエラーとなるので注意
      template:
        spec:
          containers:
          - name: test-cronjob-container
            image: test-image:0.0.1
            imagePullPolicy: IfNotPresent
            command:
              - sh
              - -c
              - "python3 main.py"
            env:
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: test.secret.mysql
                  key: passwd

【Kubernetes】PODの安全な起動・終了について

f:id:tm200:20210611174228p:plain

KubernetesのPODを安全に起動・終了する方法をメモ。
アプリケーションのGraceful shutdownは言語によって異なる為、都度実装する事になります。

The Twelve-Factor App

現在のPJではThe Twelve-Factor Appに沿った開発が推奨されています。
PODの安全な起動・停止に関する考慮も重要です。
12factor.net

安全に停止させるには

deploymentのマニフェストPreStopを入れる事で、PODの破棄命令〜SIGTERMまでの時間を稼ぐ事ができます。
下記は単純にsleepしているだけですが、何かしらの処理を入れる事も可能です。

deployment.yaml
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 20"]

起動時にアプリケーションのスタンバイが間に合わない場合

アプリケーションがReady状態になったかを確認するエンドポイントを設定する事ができます。
こちらもマニフェストに入れる事が可能です。

deployment.yaml
readinessProbe:
  initialDelaySeconds: 10
  timeoutSeconds: 1
  httpGet:
    path: /status
  port: 443
  scheme: HTTPS

ローリングアップデート時のPOD数

PodDisruptionBudgetを作成する事により、クラスタ内に必ずPODが残るように退避しながらノードが入れ替わっていきます。
これにより、ローリングアップデート時にクラスタ内にPODが存在しないという状態を回避できます。

PDB設定例
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: <任意のPDB名>
spec:
  maxUnavaliable: 1
  selector:
    matchLabels:
      app: <PODのラベル名>

【curl】通信時間の計測

f:id:tm200:20210610165136p:plain

curlのオプションで通信時にかかる時間の計測ができます。  
どの時間帯が長引いているか等で、不具合の原因切り分けができるかもしれません。

manページ

curl.se  

コマンド

$ curl -w "\ntime_namelookup: %{time_namelookup}\ntime_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_pretransfer: %{time_pretransfer}\ntime_redirect: %{time_redirect}\ntime_starttransfer: %{time_starttransfer}\ntime_total: %{time_total}\n" https://curl.se/docs/manpage.html

<レスポンス内容...>

time_namelookup: 0.004318
time_connect: 0.061376
time_appconnect: 0.180256
time_pretransfer: 0.180681
time_redirect: 0.000000
time_starttransfer: 0.217821
time_total: 0.375091

参考

time_namelookupが大きい場合...名前解決に時間がかかっている
time_pretransfertime_starttransferの差が大きい場合...サーバー側の処理に時間がかかっている

time_namelookup: <開始から名前解決完了までにかかった時間>
time_connect: <開始からTCP接続完了までにかかった時間>
time_appconnect: <開始からSSLハンドシェイク成功までにかかった時間>
time_pretransfer: <開始からファイル転送開始までにかかった時間>
time_redirect: <リダイレクトにかかった時間>
time_starttransfer: <開始からレスポンスの最初の1byteが到達するまでにかかった時間>
time_total: <開始から終了まで>