Djangoのコマンドにするほどじゃないけど意外と使う処理をどうするか


こんにちは。LAPRASでテクニカルサポートを担当しているdenzowです。

この記事は、[LAPRAS Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lapras) 5日目の記事です。1週間以内の遅刻は遅刻じゃないのでセーフです。

## 前置き

LAPRASではDjangoを使用してサービスを開発しています。Djangoには`managements`配下に`Commandクラス`を継承したクラスを定義することで独自のコマンドを実装することができます。

```shell
$ python manage.py hogehoge_command
```

LAPRASでも当然、多くのコマンドが実装されておりバッチ処理や有事の際のオペレーション等に利用されています。現在は独自に実装したコマンドが70程あります。

一方で、できるだけ避けたいことですが日々リリースされ続けているため、意図しない挙動でお客様にご迷惑をおかけしてしまいデータの修正等のオペレーションが発生することがあります。
これらを対処するときには通常、アプリケーションが稼働しているpodにログインし`manage.py shell`でDjangoShellを起動した上で手作業をします。しかし、修正までに時間がかかる場合はこれらの処理を簡略化、あるいは誰でも出来るように共通化したい要望がでてきます。

## それってコマンドじゃないの

たしかに現在やっている処理をDjangoのコマンドとして定義できれば一番ですが、実際は難しいことが多いです。

- 今すぐ必要な処理であることが多くリリースまで待てない
- リリース後にメインコードの変更によりそのまま使うことができない可能性が高い
- そもそもバグ修正により不要になるコマンドであるためライフサイクルも短い

このような問題があります。そのため、理想としては

- メインコードとは独立した、より早いリリースサイクル
- 書き換えが容易
- 他のメンバーも共有できる

これらを満たす方法が必要でした。


## で、どうしたか

メインコードとは別の`support-commands`というリポジトリを用意しました。以下はディレクトリ構成の一部抜粋です。

```shell
support-commands
└── bin
    ├── .common
    │   └── scout.sh        -- podログインのための環境情報等
    ├── .nanika_syori
    │   └── script.py       -- 実際に行う処理内容
    ├── scoutshell           -- kubectl経由でmanage.py shellに処理を流し込むスクリプト
    └── nanika_syori         -- 実際に行いたい処理のエントリーポイント

```

LAPRASではアプリケーションの実行基盤にk8sを使用しており、テクニカルサポートを行うメンバーはいずれもpodにログインする権限を持っています。そこで`kubectl`の標準入力経由でスクリプトを`manage.py shell` に流し込む方針を録りました。`direnv`を利用しているなら`.envrc`に`export PATH=$PATH:$(pwd)/bin`を書いておけば、このリポジトリ配下に移動すると以下の様にそのままコマンドを実行することができます。

```shell
$ nanika_syori 12345
```

## もう少し詳細

### scout.sh
scout.shは作業を行うpodを選択するためのシェルスクリプトです。

```shell:scout.sh
#!/bin/bash

POD_NAMESPACE="k8sのネームスペース"
SCOUT_POD_PREFIX='podのprefix'
MANAGE='poetry run ./manage.py'
SHELL_PLUS='poetry run ./manage.py shell_plus'
POD_NAME=$(...miserarenaiyo...)
if [[ -z "${POD_NAME}" ]]; then
    echo '[ERROR] POD not found.'
    exit 2
fi

```

### scoutshell

```shell:scoutshell
#!/bin/bash

SCRIPT_DIR=$(cd $(dirname ${BASH_SOURCE:-$0}); pwd)
source ${SCRIPT_DIR}/.common/scout.sh

echo "EXEC: kubectl exec -n ${POD_NAMESPACE} -it ${POD_NAME} ${SHELL_PLUS}"
envsubst < $1 | kubectl exec -n ${POD_NAMESPACE} -it ${POD_NAME} ${SHELL_PLUS}
```

引数で渡されたファイルに`envsubst`を使って一部書き換えた上で`manage.py shell`に流し込みます。`envsubst`を使っているのは、処理に与える引数が必要な場合に動的に書き換える必要があるためです。


### nanika_syori

これはサンプルなので実際には違いますが、大抵このような処理になっています。

```shell
#!/bin/bash

SCRIPT_DIR=$(cd $(dirname ${BASH_SOURCE:-$0}); pwd)
SCRIPT_NAME=`basename "$0"`

export ARG1=$1

if [[ -z "${ARG1}" ]]; then
    echo '[ERROR] ARG1 not found.'
    exit 2
fi

scoutshell ${SCRIPT_DIR}/.${SCRIPT_NAME}/script.py

```

引数を`ARG1`といった変数に詰め替えとチェックを行い、実際の処理がかかれた`script.py`を先程の`scoutshell`に渡して起動します。`script.py`をこのスクリプトに内包させることもできますが管理画面道なので分ける形式を取っています。

### .${SCRIPT_NAME}/script.py


```python
hoge_id = ${ARG1}
print(f'HELLO {hoge_id}')
```

処理内容はあくまで一例ですが、`${ARG1}` は最終的に`scoutshell`の`envsubst`によって置換されてから`manage.py shell`に流し込まれます。

以下の様に実行した場合を例とします。

```shell
$ nanika_syori 12345
```

そうすると、`12345`は`ARG1`という変数に詰め替えが行われ、最終的に`manage.py shell`に渡されるスクリプトは以下になります。


```python
hoge_id = 12345
print(f'HELLO {hoge_id}')
```

これで任意の引数を取らせつつ、処理が実行できるようになっています。


## まとめ

LAPRASでのテクニカルサポートが利用するリポジトリの一部をご紹介しました。なかなか刺さる人もいない内容ですがどなたかに伝われば幸いです。また、このリポジトリのblameの結果に自分以外の名前を刻んでくれるメンバーが増えればなお幸いです。