MOテクノロジー

技術をメモしていくブログ

Lambda(Java)で外部Jarや設定ファイルの読み込み方法

Lambda(Java)で外部jarを使いたい用件が出てきたので、やってみた。
課題として、外部jarを読み込めるか、設定ファイルを読み込めるか、の2つがあった。(外部jarは設定ファイルを読み込むことが必須のため)
結論として、jarは普通にビルドパスに含めればいい。
設定ファイルはちょっと工夫が必要。

まずプロジェクトのディレクトリ構成はこんな形にする。

$ tree
.
├── lib
│   └── external.jar  # 外部jar
├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       └── amazonaws
│   │   │           └── lambda
│   │   │               └── demo
│   │   │                   └── Helloworld.java
│   │   └── resources
│   │       └── setting.conf  # 設定ファイル
・・・

これをZipにしてS3にデプロイする。今回はAWS Toolkit for Eclipseを使って楽々デプロイした。
Zipを展開するとこのような構成になっている。

$ tree
.
├── com
│   └── amazonaws
│       └── lambda
│           └── demo
│               └── Helloworld.class
├── lib
│   ├── external.jar
│   └── ・・・
└── resources
    └── setting.conf

最後に、この設定ファイルはLambda動作時には/var/task/resources/setting.confに置かれるので、読み込む場合はそのパスを指定する。

参考

AWS Toolkit for Eclipseチュートリアル。思いの外スムーズに連携できていたのでとても便利だった。 docs.aws.amazon.com

Pythonのデコレータは算数っぽく考えるとわかりやすかった

デコレータはなかなか理解するのが難しかったんだが、算数っぽく考えると割とすんなり飲み込めたのでメモする。

前提としてPythonは、関数内で関数を呼び出せる、関数を引数や関数の戻り値とできることを確認しつつ、ちょっとずつ考えていく。

関数内で関数を呼び出す

main関数の中にsub関数を作って、main()の実行結果を出力する。

>>> def main():
...     def sub():
...         print('This is sub.')
...     return sub
...
>>> main()
<function main.<locals>.sub at 0x10ee6a280>

結果は関数オブジェクトが返ってくる。これは納得、main()でsubをリターンしているんだから。
ではsub関数の内部のprint()を実行するにはどうするか。
main()の戻り値=subの関数オブジェクトになるんだから、算数っぽく考えると
main() = subの両辺に()をプラスして、 main()() = sub()となるはず。

>>> main()()
This is sub.

おk。

関数に引数として関数を渡す

関数の引数に関数を指定できることを確認する。

>>> def main(func):
...     return func
...
>>> def sub(text):
...     return text
...
>>> main(sub)
<function sub at 0x10ee6a1f0>

main関数の引数をしてsub関数を指定すると、return funcで引数にしたオブジェクトがそのまま返ってくる。これも納得。
ではsub関数の処理を実行するには、算数っぽく考えると
main(sub) = sub の両辺に()をプラスして、 main(sub)('あつい') = sub('あつい')となるはず。

>>> main(sub)('あつい')
'あつい'

デコレータにしてみる

ここまで理解できればデコレータも余裕。
call関数をデコレートして、処理の前後に別の処理を入れてみる。

# デコレートする側
>>> def main(func):
...     def sub(text):
...         print('sub 1.')
...         func(text)
...         print('sub 2.')
...     return sub
...
# デコレートされる側
>>> def call(text):
...     print(text)
...
>>> main(call)
<function main.<locals>.sub at 0x102cef4c0>

main関数の引数にcall関数オブジェクトを指定する。
main(call) = subになるから、両辺に()をプラスして、 main(call)('あつい') = sub('あつい')となるはず。

>>> main(call)('あつい')
sub 1.
あつい
sub 2.

これによりcall関数の処理の前後に別の文字列を表示することができている。 これがデコレータ。
最後に、Pythonicに@マークを使って書き換えてみる。

>>> def main(func):
...     def sub(text):
...         print('sub 1.')
...         func(text)
...         print('sub 2.')
...     return sub
...
>>> @main
... def call(text):
...     print(text)
...
>>> call('なんでこんなにあついねん')
sub 1.
なんでこんなにあついねん
sub 2.

今までのことを踏まえると、call('text')はmain(call)('text')と同じということになる。
callの前後にmainの処理を差し込むイメージ。この考え方が適切なのかどうかは不明だが、なんとなく理解できた。

算数っぽくとかって書いたけど、そもそも算数って両辺に同じ数足しても問題ないんだっけ?とかなってる。何も覚えていない。

sedの使い方忘れないように

sedを使いこなしたいと思いつつ、多機能すぎてついつい忘れてしまう。
基本の使い方のチートシート的な。使っているのはMacの標準で入っているBSD系のsed

# パイプつなぎ
~ $ echo It is sunny today. | sed 's/sunny/rainy/g'
It is rainy today.

# 複数つなげる時は -e
~ $ echo It is sunny today. | sed -e 's/sunny/rainy/g' -e 's/rainy/cloudy/g'
It is cloudy today.

# ファイルをインプットとする
~ $ cat weather.txt
It is sunny today.
~ $ sed -e 's/sunny/rainy/g' weather.txt
It is rainy today.

ちょっと便利な使い方
特定の文字列が出てきた時、それを抽出したい。
例えば、uniqueIDという文字列が出てきた時、それに続くIDを取得したいケース。

  • 前提1:行によって要素数が異なるため、単純にカンマ区切りで抽出とかはできない
  • 前提2:uniqueIDは10桁の固定とし、1行内にuniqueIDは1回しか出てこないとする
# パイプつなぎ
~ $ echo \"time:2020-08-01\",\"uniqueID\":\"ABCDEFGHIJ\",\"name\":\"hoge\" | sed -e 's/^.*uniqueID":"\(.\{10\}\).*$/\"uniqueID\":\"\1\"/'
"uniqueID":"ABCDEFGHIJ"

# ファイルをインプットとする(マッチしないする行も出力してしまうので最後にgrepしている。grepせんでもsedだけでできそうな気もするが...)
~ $ cat log
"time:2020-08-01","uniqueID":"ABCDEFGHIJ","name":"hoge"
"time:2020-08-01","wanwan":"inu","nyannyan":"neko"
"time:2020-08-01","uniqueID":"WANNYANWAN","wanwan":"inu"
~ $ sed -e 's/^.*uniqueID":"\(.\{10\}\).*$/\"uniqueID\":\"\1\"/' log | grep uniqueID
"uniqueID":"ABCDEFGHIJ"
"uniqueID":"WANNYANWAN"

# やってること
# "uniqueID:"という文字列が出てきた時、その後に続く10文字を取得する
# "uniqueID:"という文字列以降全てを、"uniqueID:" + 取得した10文字に置換する

()でグループ化して\1でマッチしてる部分を取得しているとことが肝。例えばこんなふうに使える。

# グループに一致した値が`\1`に入る
~ $ echo abcdefghij | sed -e 's/^.*def\(.\{1\}\).*$/\1/g'
g

# グループが複数ある場合、採番が振られる
~ $ echo abcdefghij | sed -e 's/^.*def\(.\{1\}\)\(.\{1\}\).*$/\1 \2/g'
g h

これだけ覚えていればいろいろできそうだぞ(できない)

sarのログ保存期間の延長&記録間隔の変更

sarのログ保存期間はデフォルトで1ヶ月くらいになっている。
半年ほど記録を伸ばす必要が出てきたので、対応してみた。
osはAmazon Linux 2。

ログ保存期間の延長

修正ファイルはこちらのHISTORY
デフォルトだとHISTORY=28になっているので、これを保存したい日数に変える。
今回は約半年の183にしてみた。

[ec2-user@ip-10-0-1-69 ~]$ cat /etc/sysconfig/sysstat
# sysstat-10.1.5 configuration file.

# How long to keep log files (in days).
# If value is greater than 28, then log files are kept in
# multiple directories, one for each month.
HISTORY=183

# Compress (using gzip or bzip2) sa and sar files older than (in days):
COMPRESSAFTER=31

# Parameters for the system activity data collector (see sadc manual page)
# which are used for the generation of log files.
SADC_OPTIONS="-S DISK"

# Compression program to use.
ZIP="bzip2"

これだけで、特になんかのプロセスを再起動したりとかは必要ない。
で、ログの出力形式はどうなるかというと

[ec2-user@ip-10-0-1-69 ~]$ ll /var/log/sa/
合計 0
drwxr-xr-x 2 root root 68  726 00:00 202007
lrwxrwxrwx 1 root root 11  724 23:50 sa24 -> 202007/sa24
lrwxrwxrwx 1 root root 11  725 23:50 sa25 -> 202007/sa25
lrwxrwxrwx 1 root root 11  726 01:00 sa26 -> 202007/sa26
lrwxrwxrwx 1 root root 12  724 23:53 sar24 -> 202007/sar24
lrwxrwxrwx 1 root root 12  725 23:53 sar25 -> 202007/sar25

なるほど。YYYYMMディレクトリが切られてsymlinkが貼られるのね。

ログの記録間隔の変更

合わせて、ログの記録がデフォルトで10分単位になってるのも変更できるようなので変えてみた。
設定ファイルはここ。
デフォルトでは*/10 * * * * root /usr/lib64/sa/sa1 1 1となっているところを、好きな間隔に設定する。
1分ごとに設定してみた。

[ec2-user@ip-10-0-1-69 ~]$ sudo cat /etc/cron.d/sysstat
# Run system activity accounting tool every 10 minutes
*/1 * * * * root /usr/lib64/sa/sa1 1 1
# 0 * * * * root /usr/lib64/sa/sa1 600 6 &
# Generate a daily summary of process accounting at 23:53
53 23 * * * root /usr/lib64/sa/sa2 -A

プロセスの再起動とかは必要なし。 ログを見てみると、途中から1分間隔に変更されたことがわかる。

[ec2-user@ip-10-0-1-69 ~]$ sar -f /var/log/sa/sa26
Linux 4.14.165-131.185.amzn2.x86_64 (ip-10-0-1-69.ap-northeast-1.compute.internal)   20200726日    _x86_64_    (2 CPU)

000001秒     CPU     %user     %nice   %system   %iowait    %steal     %idle
001001秒     all      0.00      0.00      0.01      0.00      0.01     99.97
002001秒     all      0.01      0.00      0.01      0.00      0.01     99.97
003001秒     all      0.00      0.00      0.00      0.00      0.01     99.98
004001秒     all      0.00      0.00      0.00      0.00      0.01     99.98
005001秒     all      0.01      0.00      0.01      0.00      0.01     99.97
010001秒     all      0.00      0.00      0.01      0.00      0.02     99.97
010801秒     all      0.02      0.00      0.01      0.00      0.02     99.95
010901秒     all      0.01      0.00      0.02      0.01      0.02     99.95
011001秒     all      0.03      0.00      0.02      0.01      0.01     99.93
011101秒     all      0.00      0.00      0.02      0.01      0.02     99.96
平均値:      all      0.01      0.00      0.01      0.00      0.01     99.97

間隔を狭めたことによる負荷とかは気にしないといけないが、場合によっては有用な設定になりそう。

単体テストの考え方メモ

単体テスト、そんなしょっちゅう書いていないし、ついつい考え方がぶれたりしてしまうのでポイントだけメモ。

実行に0.1秒もかかる単体テストは遅い。早く走らないとしたら、それは単体テストではない。

単体テストと他の種類のテストをごっちゃにしない。例えば以下の内容は単体テストとは分けるべき。

  • データベースとやり取りする
  • ネットワークを介した通信をする
  • ファイルシステムにアクセスする
  • 実行するために特別な環境設定を必要とする(環境設定ファイルの編集など)

参考
レガシーコード改善ガイド 保守開発のためのリファクタリング マイケル・C・フェザース著

Pythonで超簡易プログレスバー的な

Pythonプログレスバーを出したい用件があったので調べてみた。
それ用のライブラリもあるみたいだけど、事情によりpip installすることの敷居が高かったので標準ライブラリだけで実装できないか調べてみた。
とりあえずキャリッジリターンとprint関数だけで1行で行ける。

import time

max = 22 # 全体の数
for i in range(1+max):
    print(f'\r{i/max*100:.3f} %', end='')
    time.sleep(0.1)
print()

f:id:komepea:20200615224550g:plain:w500
progress_bar

ちな2系バージョンの一例もメモ。

import sys
import time

max = 22
for i in range(1+max):
    sys.stdout.write('\r{:.3f} %'.format(float(i)/max*100))
    sys.stdout.flush()
    time.sleep(0.1)
print('')

X-Forwarded-Protoを学んだ

クライアント --> ALB --> EC2(nginx)という構成のサービスの話。
ALBでは80,443で受けたリクエストを全て80でEC2に送信している。
このとき、同期からnginxの設定で80できたリクエストは443にリダイレクトするから、無限ループになっちゃわない?と質問された。確かに感。虚を突かれた感。
ということで調べてみた。

X-Forwarded-Protoとは

ここに全て書いてあった。
HTTP ヘッダーおよび クラシックロードバランサー - Elastic Load Balancing

クライアントがロードバランサーへの接続に使用したプロトコル (HTTP または HTTPS) を識別することができます。
次の例には、HTTPS リクエストとしてクライアントから発信されたリクエストの X-Forwarded-Proto リクエストヘッダーが含まれています。
X-Forwarded-Proto: https

つまりX-Forwarded-Protoを見ればクライアント --> ALBで使われたプロトコルがわかるので、そこがhttpかhttpsなのか、でリダイレクトされるかされないかを判別している。
例えば下のコードだと、https以外で来たリクエストはhttpsにリダイレクトするが、httpsできたリクエストはリダイレクトされないといった仕組み。

    if ($http_x_forwarded_proto != https) {
        return 301 https://$host$request_uri;
    }

参考

ELB を使用して HTTP トラフィックを HTTPS にリダイレクトする