AWSの料金をSlackに通知するLambdaをRustに移植してみた

はじめに

お仕事でAWS Lambdaを使って実装する処理が出てきました。
この処理はクリティカルな部分ではない、補助的な処理だったため日頃から使う機会を窺っていたRustで実装してみることにしました。

まずはRust+Lambdaの肩慣らしのために、プライベートなアカウントで利用している毎日のAWSの料金をSlackに通知する処理をRustに移植してみることにしました。

元ネタ: LambdaでAWSの料金を毎日Slackに通知する(Python3) qiita.com

今回移植したコード一式はこちらにおきました。 github.com

Rustへの移植

基本的には元のPythonの処理をそのまま移植していく方針としました。
関数の作成やIAMロールへのポリシーのアタッチなども元ネタとほぼ同じやり方にしています。

関数の作成はこんな感じでカスタムランタイムを選択しておきます。アーキテクチャx86_64にしました。

f:id:febc_yamamoto:20211216213606p:plain

コードはローカルでzipファイルを作ってアップロードする形にします。

f:id:febc_yamamoto:20211216213826p:plain

プロジェクト構成

全体的な構成はこんな感じにしました。

$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── Makefile
├── bootstrap // エントリーポイント用のバイナリクレート
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── event.json // テスト用のダミーインプット

Lambdaのカスタムランタイムの仕様としてエントリーポイントはbootstrapという実行可能ファイルにする必要があるとのことでした。 docs.aws.amazon.com

今回はCargoのワークスペースを作成しその中にバイナリクレートとしてbootstrapというクレートを配置する形にしました。

ビルド〜zip作成

こちらに従ってビルドしていくようなMakefileを用意しました。

github.com

あらかじめrustup target add x86_64-unknown-linux-gnuしておいた上でmake zipするとDocker上でクロスコンパイル〜zip作成が行われるようになっています。 Makefileはこんな感じです。

DOCKER_PLATFORM ?= linux/amd64
RUST_VERSION    ?= 1.57
RUST_ARCH       ?= x86_64-unknown-linux-gnu
DEPS            ?= bootstrap/src/*.rs bootstrap/Cargo.toml Cargo.toml Cargo.lock

RELEASE_DIR     := ${PWD}/target/${RUST_ARCH}/release
TARGET_BIN      := ${RELEASE_DIR}/bootstrap
TARGET_ZIP      := lambda.zip

build:
  cargo build --release --target ${RUST_ARCH}

.PHONY: buildx
buildx: $(TARGET_BIN)

$(TARGET_BIN): $(DEPS)
  docker run -it --rm --platform ${DOCKER_PLATFORM} \
    -v "$${PWD}":/usr/src/myapp -w /usr/src/myapp rust:${RUST_VERSION} \
    make build
    
.PHONY: zip
zip:  $(TARGET_ZIP)
$(TARGET_ZIP): $(TARGET_BIN)
  zip -j "$(TARGET_ZIP)" "$(TARGET_BIN)"

PythonからRustへの移植

移植したコード全体はこちらです。 github.com

ロギング

pythonではloggerをこんな感じで用意していました。

logger = logging.getLogger()
logger.setLevel(logging.INFO)

print!println!でも良かったのですが、ログレベルをサポートするためにこちらを使うことにしました。

docs.rs docs.rs

こんな感じで使います。

// 初期化
simplelog::SimpleLogger::init(LevelFilter::Info, Config::default()).unwrap();

// ログ出力
log::info!("hello {}", "world");

AWS SDK

boto3の代わりにrusotoを使います。

github.com

⚠️ Rusoto is in maintenance mode. ⚠️と書かれてますが今回は気にせず使うことにしました。 こんな感じになります。

    let client = CloudWatchClient::new(Region::UsEast1);

    client.get_metric_statistics(GetMetricStatisticsInput {
        namespace: String::from("AWS/Billing"),
        metric_name: String::from("EstimatedCharges"),

        dimensions: Some(vec![Dimension {
            name: String::from("Currency"),
            value: String::from("USD"),
        }]),

        start_time: (Utc::today().and_hms(0, 0, 0) - Duration::days(1))
            .format("%+")
            .to_string(),
        end_time: Utc::today().and_hms(0, 0, 0).format("%+").to_string(),

        period: 86400,
        statistics: Some(vec![String::from("Maximum")]),
        ..GetMetricStatisticsInput::default()
    }).await

SlackへのPost(WebHook)

requestsの代わりにreqwestを使います。

docs.rs

こんな感じにになりました。

    let client = reqwest::Client::new();
    client.post(slack_post_url.to_string()).body(body.to_string()).send().await?;

実行!

ということでmake zipし、作成されるzipファイルをマネジメントコンソールからアップロードしテスト実行してみます。

f:id:febc_yamamoto:20211216214138p:plain

いい感じですね!

残課題

Dockerでのビルド周りが遅いのは要改善です。 また、LocalStackやlambci/lambdaを用いてローカル実行できるようにしてあげる必要もあると思います。

この辺は追々対応していきます。

ということで今回は以上です。