[Rust] SAMをつかってLambda Authorizerのビルド&デプロイ

2024.01.25

Introduction

ここのblogにもあるように、
(betaですが)cargo-lambdaをつかってSAMでRustコードのビルドが可能です。

本稿では、SAMを使用して、Rustで実装したAWS Lambdaをビルドおよびデプロイする
方法について解説します。
最初にSAMの基本について簡単に説明し、RustでLambdaを実装して
Lambda Authorizerも設定してみます。

SAM?

サーバーレスアーキテクチャはインフラの管理をシンプルにしながら、
スケーラブルなアプリを構築することを可能にします。

AWS Serverless Application Model(SAM)は、
AWSでサーバーレスアプリを簡単に開発するためのツールです。
SAMはAWS CloudFormationの拡張しており、YAML形式の設定ファイルを記述することで
アプリのインフラ設定やリソース定義が可能です。

Environments

  • MacBook Pro (13-inch, M1, 2020)
  • OS : MacOS 13.5.2
  • Rust : 1.75.0
  • aws-cli : 2.2.35

AWSアカウントはセットアップ済みとします。

Setup

まずはCargo Lambdaをインストールします。

% cargo install cargo-lambda

SAMはHomebrewでインストールできるのでbrew installを実行。

% brew tap aws/tap
% brew install aws-sam-cli 

% sam --version
SAM CLI, version 1.107.0

ビルドコマンド実行時にSAMはCargo Lambdaを使うようにするため、
↓のように環境変数を設定しておきましょう。

% export SAM_CLI_BETA_RUST_CARGO_LAMBDA=1

Try

ではSAMをつかってRust用テンプレートを作成します。

% sam init

AWS Quick StartからHello Worldを選択して、
rust (provided.al2)のrumtimeを指定しましょう。

samconfig.tomlファイルに下記パラメータを追加します。

[default.build.parameters]
・
・
beta_features = true

[default.sync.parameters]
・
・
beta_features = true

そのままビルドとデプロイができる状態になってますので、
とりあえず確認してみましょう。

sam buildでビルドします。

% cd /path/your/example-project
% sam build --beta-features

デプロイして動作確認してみましょう。

% sam deploy --guided

途中いくつか質問されますが、↓の認証に関する質問だけYesで回答して、
他はデフォルトでOK。
API Gatewayも作成されているので、発行されたURLにアクセスすれば
Lambdaが実行されてメッセージが表示されます。
※ dockerをいれてればsam invokeコマンド使ってローカルで動かすことも可能

HelloWorldFunction has no authentication. Is this okay? [y/N]: Y

必要なくなったらきれいにしておきましょう。

% sam delete

impliment Lambda Authorizer

Lambda Authorizerを使えばLambdaに簡単に認証機能を実装することができます。
今回はリクエストパラメータベースのLambda Authorizerを使って
先程のHelloWorldApi Lambdaに認証機能を追加してみましょう。

まずはtemplate.yamlにAuthorizerの設定を追加します。

  Resources:
    AuthApi:
      Type: AWS::Serverless::Api
      Properties:
        StageName: Prod
        Auth:
          DefaultAuthorizer: ProxyAuthorizer
          Authorizers:
            ProxyAuthorizer:
              FunctionArn: !GetAtt AuthorizerFunction.Arn

    AuthorizerFunction:
      Type: AWS::Serverless::Function
      Metadata:
        BuildMethod: rust-cargolambda
      Properties:
        CodeUri: ./proxy_authorizer
        Handler: bootstrap
        Runtime: provided.al2023
        Environment:
          Variables:
            RUST_BACKTRACE: "1"
・
・

そして、template.yamlのHelloWorldFunction部分に RestApiIdで認証用Lambdaを関連付けます。

  Events:
    HelloWorld:
      Type: Api
      Properties:
        Path: /hello
        Method: get
        RestApiId: !Ref AuthApi #←追加

次は認証用Lambdaを作成します。
プロジェクトディレクトリにauthorizerディレクトリを作成し、
src/main.rsを以下の内容で作成します。

//authorizer/src/main.rs
use aws_lambda_events::event::apigw::{
    ApiGatewayCustomAuthorizerPolicy, ApiGatewayCustomAuthorizerRequest,
    ApiGatewayCustomAuthorizerResponse, IamPolicyStatement,
};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde_json::json;
use tokio;

// API Gatewayのポリシーに関する定数
const POLICY_VERSION: &str = "2012-10-17";
const EXECUTE_API_ACTION: &str = "execute-api:Invoke";
const POLICY_EFFECT_ALLOW: &str = "Allow";
const PRINCIPAL_ID: &str = "user";

// 許可されたトークンの値を定義する定数
const AUTHORIZED_TOKEN: &str = "allow";

/// Lambda関数のエントリーポイント。
///
/// AWS Lambdaランタイムを起動し、カスタム認証ハンドラーを登録します。
#[tokio::main]
async fn main() -> Result<(), Error> {
    run(service_fn(my_auth_handler)).await
}

/// カスタム認証handler
///
/// API Gatewayからの認証リクエストを処理し、適切なIAMポリシーを返す。
///
/// # Arguments
///
/// * `event` - API Gatewayからのカスタム認証リクエスト
///
/// # Returns
///
/// 認証が成功した場合は`ApiGatewayCustomAuthorizerResponse`を、失敗した場合は`Error`を返す
async fn my_auth_handler(
    event: LambdaEvent<ApiGatewayCustomAuthorizerRequest>,
) -> Result<ApiGatewayCustomAuthorizerResponse, Error> {
    let token = event.payload.authorization_token;

    match token {
        Some(ref token_value) if token_value == AUTHORIZED_TOKEN => {
            let policy = create_allow_policy(event.payload.method_arn.unwrap());
            Ok(create_authorizer_response(policy))
        }
        Some(_) => Err(Error::from("Unauthorized")),
        None => Err(Error::from("Missing token")),
    }
}

/// 認証成功時に許可ポリシードキュメントを生成する。
///
/// # Arguments
///
/// * `method_arn` - API GatewayのリソースARN
///
/// # Returns
///
/// `ApiGatewayCustomAuthorizerPolicy`のインスタンスを返す。
fn create_allow_policy(method_arn: String) -> ApiGatewayCustomAuthorizerPolicy {
    let stmt = IamPolicyStatement {
        action: vec![EXECUTE_API_ACTION.to_string()],
        resource: vec![method_arn],
        effect: Some(POLICY_EFFECT_ALLOW.to_owned()),
    };

    ApiGatewayCustomAuthorizerPolicy {
        version: Some(POLICY_VERSION.to_string()),
        statement: vec![stmt],
    }
}

/// 認証成功時のレスポンスを生成する。
///
/// # Arguments
///
/// * `policy` - 認証に成功したときに使用するIAMポリシー
///
/// # Returns
///
/// `ApiGatewayCustomAuthorizerResponse`のインスタンスを返する。
fn create_authorizer_response(policy: ApiGatewayCustomAuthorizerPolicy) -> ApiGatewayCustomAuthorizerResponse {
    let context = json!({ "simpleKey": "simpleValue" });
    ApiGatewayCustomAuthorizerResponse {
        principal_id: Some(PRINCIPAL_ID.to_string()),
        policy_document: policy,
        context,
        usage_identifier_key: None,
    }
}

Lambdaへのリクエスト時にAuthorizationヘッダにallowと指定していれば
実行を許可します。
Cargo.tomlは↓のようにします。

[package]
name = "lambda-authorizer"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

lambda_runtime = "0.6.0"
serde = "1.0.136"
tokio = { version = "1", features = ["macros"] }
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
aws-config = { version = "1.1.1", features = ["behavior-version-latest"] }
serde_json = "1.0.108"

再度ビルド&デプロイで動作確認してみましょう。
ちなみにこの状態だと、デプロイ時に認証関連の確認は出ません。

% sam build --beta-features
% sam deploy --guided

curlで適当なAuthorizationヘッダを指定すると、
認証用Lambdaによりrejectされます。

% curl -X GET <発行されたendpoint URL> -H "Authorization:hoo"/

{"message":"Unauthorized"}%

正しいヘッダを渡せば、HelloWorldApi Lambdaが実行されます。

% curl -X GET <発行されたendpoint URL> -H "Authorization:allow"

{"message":"Hello World!"}%

Summary

今回はSAMを使って、RustでLambdaの実装をしてみました。
(まだbetaですが)ビルドもデプロイも簡単なのでぜひお試しください。

References