それでもなお

それでも、頑張るしかないので

EC2インスタンスのユーザーデータを後から追加する際の躓きポイント

EC2インスタンスのユーザーデータは2種類の設定方法があるけど用途が異なるという話

先日、aws-cliでec2のユーザーデータを追加する際に引っかかった。 結論、ユーザーデータの追加方法は2種類あり、それぞれファイルのまま引き渡したりエンコードが必要だったりと違いがある。AWS公式ドキュメント

まずあとから追加する場合は、modify-instance-attribute を使用する。 ここで、指定する引数は次の2パターンだ

  1. --attribute --value userData
  2. --user-data

それぞれどちらでもユーザーデータをセットできる。具体的には以下のように指定する。 それぞれmacでの検証

case1:引数はスクリプト分そのまんま&かつ未エンコードの値をそのまま渡す

aws ec2 modify-instance-attribute --instance-id i-***** \
        --user-data echo Hello
case2: 引数はbase64でエンコード済みのファイルを指定

# base64 encode
base64 user_data.txt > encoded_user_data.txt

aws ec2 modify-instance-attribute --instance-id i-***** \
        --attribute userData --value file://encoded_user_data.txt

私の確認した限りでは未エンコのファイルはエラーになり、エンコ済みのスクリプト文はデコードされずにセットされてしまう…もしかしたら別の方法でも行けるのかもしれない。

今回の話をややこしくしている原因として、インスタンスを起動(生成)するコマンドrun-instancesがある。 この場合、引数--user-dataへ先程と同様にファイルもしくはスクリプト文字列を指定するのであるが、この場合base64エンコードは内部で実行されるため必要がない。

引数の指定の仕方も1種類となっているため、同じ要領でセットするとはまるので注意が必要。

CloudWatchLogsにログを保存する際に便利なこと

webサーバやアプリケーションサーバ等を構築する際に、ログをどう集め分析するかが課題になるが 単一のサーバでの分析であれば、CloudWatchLogsエージェントを使用すれば簡単なログの収集、分析、監視、可視化が可能になる。

本記事では詳細は紹介しないが、CloudWatchLogsではロググループごとに独自の検索(フィルタリング)が可能である。 aws公式ドキュメント

CloudWatchLogsはログをjsonで引き渡すとよいという話

ドキュメント見てもらうとわかると思うがいくつかのフィルタパターンを指定できる。そしてログがjson形式で出力されている場合、

{ $.Key = "Value" }
{ $.Key != "Value" }
{ $.Key[0] = "Value"}

などなどありがちなjson形式を指定でき便利である。

という事でpythonjson形式のログを出力したい場合、次のようにformatterjson.dumpsしてやればいい感じで出力される。

↓サンプルコード

import logging
import sys
import json

def set_logger(logger_name,log_file_name):

    logger = logging.getLogger(logger_name)
    # ルートロガー設定
    logger.setLevel(logging.DEBUG)
    # コンソール出力用のハンドラー
    to_stream = logging.StreamHandler(stream=sys.stdout)
    # ファイル出力用のハンドラー
    to_file = logging.handlers.FileHandler(filename=log_file_name)
    # ハンドラーごとの出力レベル定義
    to_stream.setLevel(logging.DEBUG)
    to_file.setLevel(logging.INFO)
    # ハンドラーにセットするformatterを設定(時刻以外)
    jsonFormat = json.dumps(dict(level="%(levelname)s", time="%(asctime)s",
                                 function="%(funcName)s", message="%(message)s"))
    # 引数:fmtにformatterを渡す&時刻の書式を好みの形になるように引数:datefmtへ
    log_format = logging.Formatter(fmt=jsonFormat,
                                   datefmt="%Y-%m-%d %H:%M:%S")
    # それぞれのハンドラへformatterをセットしてやる
    to_stream.setFormatter(log_format)
    to_file.setFormatter(log_format)
    # ロガーへハンドラーを追加する
    logger.addHandler(to_stream)
    logger.addHandler(to_file)
    logger.INFO("Hello World")
    return logger

私は基本上記のようにモジュール化しておき、使い回す形にしている。

 logger を利用する場合でもルートのレベルはちゃんと設定し直すこと。(default:warningのためログが出ない!?みたいなことに陥る。ルートは個人的には最も低レベルなDEBUGにしておき、ここのハンドラ、もしくは子loggerのレベルで調整してやるのが良い)

そしてこのコードだと以下の様、ログ出力が得られる。

{
    "level": "INFO",
    "time": "2019-06-24 02:11:18",
    "function": "<module>",
    "message": "Hello World"
}

具体的な値でフィルタリングをもう一度説明すると、例えばINFOレベルのログだけを表示させたければ…

{ $.level = "INFO" }

でフィルタリングが可能になる。

pythonでミリ秒のunixtime(utc基準)を生成する方法

よく使うけどdatetime周りは忘れやすいのでめも

なぜミリ秒?

awsのcloudwatchlogsのapifiter_log_events 等では取得範囲を時間で指定するが、 その時間はutc基準のunixパラメータで指定する必要がある。

実際のコード

from datetime import datetime
utc_time = datetime.utcnow()
float_unix_time = utc_time.timestamp()
mili_unix_time = int(float_unix_time*1000)

秒単位の場合は、 strftime('%s') で取得すると一発で取れるようなのでそっちの方が簡単。

エラー対応:sbt1.0系からsbt0.13系に変更したときにコンパイルエラーが発生するときの対応

やろうとしたこと

sbt compileを同一プロジェクトでsbt1.0で利用していたところsbt0.13系を使用してコンパイルしようとした (旧バージョンのプロダクトをビルドしたかった。) 当然sbtは1.0と0.13で定義とか色々設定の仕方が変わっていたりするので、そこのあたりはちゃんと設定できているものとする。 (先日はビルドできていたのになぜかできなくなった)

 エラーメッセージ

[error] (compile:copyResources) Error wrapping InputStream in GZIPInputStream: java.util.zip.ZipException: Not in GZIP format

ここには詳細は載せないが、こまかいスタックトレースを追っても原因にたどり着けなかった。

結論

依存性の更新ができていなかった

今回のケースでは、旧バージョンのプロダクションをビルドするものだったので、当然dependenciesは1.0系統と異なるバージョンが指定されていた。 で、これらはprojectフォルダー配下のtargetディレクトリにインストールされているのがデフォルトであるが、どうも1.0→0.13系にした場合はその依存性が更新されず、 1.0系のバージョンを利用してコンパイルしようとして、失敗していたように見える。 おそらく0.13→1.0系にした場合は、自動的に依存性が更新されると思われる。

対策

targetフォルダをすべて消す。

複数のsbtプロジェクトに分けるケースが多いと思うが、例えば以下コマンド等で消す。

find . -name target -exec rm -r "{}" \;

参考

github.com

Serverless-Frameworkでデプロイするpackageをfunctionごとに分ける

Serverless-Frameworkはaws lambdaへのデプロイを簡易化するデプロイフレームワークです。 先日ちょっとハマったので記載します。

公式は以下↓

serverless.com

このフレームワークではpackageキーでアップロードするファイルを選択できる。 基本的な使い方としては、以下のようになる

service: ***
package: ***

上は他のキーをかなり端折っているが、ポイントはserviceと同じレベルで記載しているところ。 複数の関数を載せるということは当然以下のようにfunction単位で載せたくなるだろう。

service: ****

    testfunction1
    name: test-function1
    package: *****
      


    testfunction2
    name: test-function2
    package: ******

しかし、この場合おそらく上のpackege設定で上書きされ、個別の設定ができない。 結果、以下の記述をserveceとレベルに記載することで、個別の設定が適用される。

package:
  individually: true