kakakakakku blog

Weekly Tech Blog: Keep on Learning!

読んだ本を振り返る(2024年1-8月)

2024年に読んだ本を年末の振り返りでまとめようと思っていたけど,2024年2月から「毎日10分間読書」という習慣化を始めたことをキッカケにコツコツ読み進められるようになった.中途半端な時期ではあるけど,2024年8月までに読んだ本とその感想(書評記事 or X ポスト)をまとめておこうと思う📝

8ヶ月で「計26冊」読めた❗️技術的な本と仕事で必要なドメイン知識を獲得する本を中心に読んでいた.もともと本を読むのが遅いのにブログに書評記事を書くまでをセットに考えてしまっていて全然進まなかった過去があるけど,2023年から無理に書評記事を書かずに X ポストでも OK という運用に変えて身軽になったのが良かったと思う.

📕 2024年1月: 0冊😇

まったく読めなかったことを反省して,2024年2月12日から「毎日10分間読書」を始めた📅

📕 2024年2月: 2冊😃

現場のプロがわかりやすく教える位置情報エンジニア養成講座

kakakakakku.hatenablog.com

AWS コスト最適化ガイドブック

📕 2024年3月: 1冊😃

Good Code, Bad Code

📕 2024年4月: 3冊😃

Web API テスト技法

テスト駆動 Python 第2版

kakakakakku.hatenablog.com

データエンジニアリングの基礎

kakakakakku.hatenablog.com

📕 2024年5月: 4冊😃

Terraform の教科書

kakakakakku.hatenablog.com

AWS コンピュータービジョン開発の教科書

kakakakakku.hatenablog.com

組織を変える5つの対話

Team Guide to Software Testability

kakakakakku.hatenablog.com

📕 2024年6月: 3冊😃

ソフトウェアアーキテクチャメトリクス

スモール・リーダーシップ

認証と認可 Keycloak 入門

📕 2024年7月: 6冊😃

農業のしくみとビジネスがこれ1冊でしっかりわかる教科書

スマート農業のきほん

エンジニアリングが好きな私たちのためのエンジニアリングマネジャー入門

kakakakakku.hatenablog.com

図解よくわかるスマート農業

データ農業が日本を救う

農業と環境調査のためのリモートセンシング・GIS・GPS 活用ガイド

📕 2024年8月: 7冊😃

図解でよくわかる 病害虫のきほん

ランサムウエアから会社を守る

何もしない習慣

GitHub CI/CD 実践ガイド

kakakakakku.hatenablog.com

エンジニア組織を強くする開発生産性の教科書

朝イチの「ひとり時間」が人生を変える

AWS クラウドネイティブデザインパターン

AWS CDK で Amazon API Gateway にカスタムドメインを設定する

AWS CDK で Amazon API Gateway に Amazon Route 53 のカスタムドメインを設定してみた💡

実際に試したログをまとめておく \( 'ω')/

サンプルコード

今回は Amazon Route 53 でドメインを取得してある前提とする.サンプルコード上では xxxxx.com にしておく📝

👾 api-gateway-custom-domain.ts

import {
    Stack,
    StackProps,
    aws_apigateway,
    aws_certificatemanager,
    aws_route53,
    aws_route53_targets,
} from 'aws-cdk-lib'
import { Construct } from 'constructs'

export class ApiGatewayCustomDomainStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props)

        const hostedZone = aws_route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
            hostedZoneId: 'xxxxx',
            zoneName: 'xxxxx.com',
        })

        const certificate = new aws_certificatemanager.Certificate(this, 'Certificate', {
            domainName: 'api.xxxxx.com',
            validation: aws_certificatemanager.CertificateValidation.fromDns(hostedZone),
        })

        const api = new aws_apigateway.RestApi(this, 'ApiGateway', {
            restApiName: 'sandbox-api-gateway-custom-domain',
            domainName: {
                domainName: 'api.xxxxx.com',
                certificate: certificate,
            },
            disableExecuteApiEndpoint: true,
        })

        api.root.addMethod('GET', new aws_apigateway.HttpIntegration('https://dog.ceo/api/breeds/image/random'))

        new aws_route53.ARecord(this, 'ARecod', {
            zone: hostedZone,
            recordName: 'api',
            target: aws_route53.RecordTarget.fromAlias(new aws_route53_targets.ApiGateway(api)),
        })
    }
}

ポイント

AWS Certificate Manager

AWS Certificate Manager で証明書を取得するために aws_certificatemanager.Certificate を使う.そして今回は Amazon Route 53 で取得したドメインを使うため,DNS 検証で使う CNAME レコードも自動的に作ってくれて便利👌 ちなみに aws_certificatemanager.DnsValidatedCertificate は既に Deprecated になっているので注意🚨

docs.aws.amazon.com

Amazon API Gateway

addDomainName() でカスタムドメインの設定を詳細に実装することもできるけど,今回はシンプルに aws_apigateway.RestApidomainName を使った.さらに disableExecuteApiEndpoint も設定しておくと Amazon API Gateway のデフォルトドメインのアクセスを拒否できる👌

docs.aws.amazon.com

Amazon API Gateway にカスタムドメインを設定するときの解説は aws_apigateway モジュールの Overview にも詳しく載っているからあわせて見ておくと良いかと❗️

docs.aws.amazon.com

Amazon Route 53

最後は aws_route53.ARecord を使って Amazon API Gateway に対して A レコード(エイリアスレコード)を設定する.デフォルトだと Apex ドメインになるため,今回は recordName を設定して api サブドメインにした.

docs.aws.amazon.com

デプロイ

さっそく cdk diffcdk deploy を実行する❗️証明書の DNS 検証待ちもあって多少デプロイ時間は長かった.

$ cdk diff ApiGatewayCustomDomainStack

Resources
[+] AWS::CertificateManager::Certificate Certificate Certificate4E7ABB08 
[+] AWS::ApiGateway::RestApi ApiGateway ApiGateway11E7F47B 
[+] AWS::ApiGateway::Deployment ApiGateway/Deployment ApiGatewayDeploymentA26796E824a5989fccba92ddf3192941a8d88d82 
[+] AWS::ApiGateway::Stage ApiGateway/DeploymentStage.prod ApiGatewayDeploymentStageprod1C6D5CD6 
[+] AWS::ApiGateway::DomainName ApiGateway/CustomDomain ApiGatewayCustomDomainDD503C48 
[+] AWS::ApiGateway::BasePathMapping ApiGateway/CustomDomain/Map:--=>ApiGatewayCustomDomainStackApiGateway93CA03C8 ApiGatewayCustomDomainMapApiGatewayCustomDomainStackApiGateway93CA03C8B11D3EE6 
[+] AWS::ApiGateway::Method ApiGateway/Default/GET ApiGatewayGET25EBFEA3 
[+] AWS::Route53::RecordSet ARecod ARecod5038ED2E 

$ cdk deploy ApiGatewayCustomDomainStack

✨  Synthesis time: 2.98s
✨  Deployment time: 88.42s
✨  Total time: 91.4s

動作確認

今回は Amazon API Gateway の「HTTP 統合」を使って Dog API を呼び出すようにしている.実際に試したところ,カスタムドメインで Amazon API Gateway を呼び出すことができた❗️さらにデフォルトドメインだと Forbidden になるのを確認できた.

$ curl -s https://api.xxxxx.com | jq .
{
  "message": "https://images.dog.ceo/breeds/hound-blood/n02088466_9237.jpg",
  "status": "success"
}

$ curl -s https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/ | jq .
{
  "message": "Forbidden"
}

関連ドキュメント

docs.aws.amazon.com

docs.aws.amazon.com

Auth0 Terraform Provider: Auth0 の設定を Terraform で管理しよう

Auth0 Terraform Provider を使うと Auth0 の設定を Terraform で宣言的に管理できる.もちろん Terraform の仕組みに沿って terraform plan コマンドで確認してから terraform apply コマンドでデプロイできる❗️Auth0 Terraform Provider を試したログをまとめる.今回は「ゼロから実装」「既存リソースをインポートして実装」を試した \( 'ω')/

github.com

Auth0 Deploy CLI

Auth0 の設定を自動デプロイするときの選択肢として,Auth0 Terraform Provider 以外に YAML ベースの Auth0 Deploy CLI もある💡 a0deploy コマンドを使って Auth0 の設定をエクスポート・インポートできて,Keyword Replacement 機能を使えば YAML ファイルに環境変数を挿入できる拡張性もある.Auth0 Terraform Provider と Auth0 Deploy CLI の比較は以下に載っている📝

auth0.com

ドキュメントにも載っている通り,普段から Terraform を使っているなら Auth0 Terraform Provider を採用するのが良さそう.Auth0 のために Terraform 環境をゼロから構築するのは学習コストも含めて大変さはあるとは思うけど,Auth0 Deploy CLI には terraform plan コマンドに相当するドライラン機能(変更検出)がなく,ドライランのために Auth0 Terraform Provider を採用するという意思決定もあると思う.ちなみに Auth0 Deploy CLI のドライラン機能は2018年から解決されてなく厳しそう💨

github.com

Auth0 Terraform Provider を試す(ゼロから実装)

さっそく Quickstart ドキュメントを参考に Auth0 Terraform Provider を試す❗️

1. M2M Application を追加する

まずは Auth0 を操作するための M2M (Machine-to-Machine) Application を Auth0 コンソールで追加する.今回は Auth0 Terraform Provider という名前にした.そして Domain / Client ID / Client Secret を取得して環境変数に設定しておく.

$ export AUTH0_DOMAIN=''
$ export AUTH0_CLIENT_ID=''
$ export AUTH0_CLIENT_SECRET=''

2. Application: terraform init コマンドを実行する

providers.tf を実装して Auth0 Terraform Provider を取得する.

👾 providers.tf

terraform {
  required_providers {
    auth0 = {
      source  = "auth0/auth0"
      version = ">= 1.4.0"
    }
  }
}

provider "auth0" {}

3. Application: terraform plan コマンドを実行する

次に clients.tf を実装する.まずは Auth0 Application を auth0_client リソースを使って追加する.設定は必要最低限にして,今回は SPA タイプで,URL 関連は http://localhost:3000 にした👌後ほど動作確認で使う.

👾 clients.tf

resource "auth0_client" "sample" {
  name                = "ReactSamples"
  app_type            = "spa"
  callbacks           = ["http://localhost:3000"]
  allowed_origins     = ["http://localhost:3000"]
  allowed_logout_urls = ["http://localhost:3000"]
  web_origins         = ["http://localhost:3000"]
}

そして terraform plan コマンドを実行する.Auth0 Deploy CLI に慣れていると「plan できるの最高じゃん」という気持ちになる😀 実際にオペレーションに自信が持てるようになる.

$ terraform plan
Terraform will perform the following actions:

  # auth0_client.sample will be created
  + resource "auth0_client" "sample" {
      + allowed_logout_urls                 = [
          + "http://localhost:3000",
        ]
      + allowed_origins                     = [
          + "http://localhost:3000",
        ]
      + app_type                            = "spa"
      + callbacks                           = [
          + "http://localhost:3000",
        ]
      + client_id                           = (known after apply)
      + custom_login_page_on                = (known after apply)
      + grant_types                         = (known after apply)
      + id                                  = (known after apply)
      + is_first_party                      = (known after apply)
      + is_token_endpoint_ip_header_trusted = (known after apply)
      + name                                = "ReactSamples"
      + oidc_conformant                     = (known after apply)
      + signing_keys                        = (sensitive value)
      + web_origins                         = [
          + "http://localhost:3000",
        ]
    }

Plan: 1 to add, 0 to change, 0 to destroy.

4. Application: terraform apply コマンドを実行する

最後は terraform apply コマンドを実行する.

$ terraform apply

auth0_client.sample: Creating...
auth0_client.sample: Creation complete after 0s [id=xxxxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

できた❗️最高 \( 'ω')/

ReactSamples Application を追加できた

5. API: terraform plan コマンドと terraform apply コマンドを実行する

今度は resource-servers.tf を実装する.Auth0 API を auth0_resource_server リソースを使って追加する.設定は必要最低限にした👌

👾 resource-servers.tf

resource "auth0_resource_server" "sample" {
  name        = "ReactSamples"
  identifier  = "https://react.samples.com"
  signing_alg = "RS256"
}

同じく terraform plan コマンドと terraform apply コマンドを実行する.

$ terraform plan
Terraform will perform the following actions:

  # auth0_resource_server.sample will be created
  + resource "auth0_resource_server" "sample" {
      + enforce_policies                                = (known after apply)
      + id                                              = (known after apply)
      + identifier                                      = "https://react.samples.com"
      + name                                            = "ReactSamples"
      + signing_alg                                     = "RS256"
      + signing_secret                                  = (known after apply)
      + skip_consent_for_verifiable_first_party_clients = (known after apply)
      + token_dialect                                   = (known after apply)
      + token_lifetime                                  = (known after apply)
      + token_lifetime_for_web                          = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

$ terraform apply
auth0_resource_server.sample: Creating...
auth0_resource_server.sample: Creation complete after 1s [id=xxxxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

おー \( 'ω')/

ReactSamples API を追加できた

6. 動作確認をする

追加した Auth0 Application と Auth0 API を使って動作確認をする.今回はサクッと試せる Auth0 React Samples を使う.

github.com

フロントエンドを起動して,Log in ボタンを押すと Auth0 の認証画面が出て,Sign up ボタンからサインアップするとログインできた❗️Auth0 Terraform Provider で追加したリソースを期待通りに使えた.

Auth0 React Samples: 初期画面

Auth0 React Samples: ログイン画面

Auth0 React Samples: ログイン後

7. Google 認証を無効化する

デフォルトでは Google 認証が有効化されていて,ログイン画面に Continue with Google が表示されている.今度は Auth0 Terraform Provider で Google 認証を無効化する.

さっそく connections.tf を実装する.Auth0 Terraform Provider には auth0_connection_client リソースと auth0_connection_clients リソースがあって,併用はしないようにとドキュメントに書かれていた.今回は auth0_connection データソースと auth0_connection_client リソースを使って Auth0 Application と Auth0 Connection を紐付ける🔗

ただし Auth0 Connection は自動的に設定されてしまっていて,一度 Terraform リソースを作ってから削除してみた😇(もしくはリソースをインポートする案もあると思う)

👾 connections.tf

connection_idUsername-Password-AuthenticationIdentifiergoogle-oauth2Identifier を Auth0 コンソールから取得しておく.

data "auth0_connection" "username_password_authentication" {
  connection_id = "con_xxxxx"
}

data "auth0_connection" "google_oauth2" {
  connection_id = "con_xxxxx"
}

resource "auth0_connection_client" "sample_username_password_authentication" {
  client_id     = auth0_client.sample.id
  connection_id = data.auth0_connection.username_password_authentication.connection_id
}

resource "auth0_connection_client" "sample_google_oauth2" {
  client_id     = auth0_client.sample.id
  connection_id = data.auth0_connection.google_oauth2.connection_id
}

terraform plan コマンドと terraform apply コマンドを実行する.

$ terraform plan
Terraform will perform the following actions:

  # auth0_connection_client.sample_google_oauth2 will be created
  + resource "auth0_connection_client" "sample_google_oauth2" {
      + client_id     = "xxxxx"
      + connection_id = "con_xxxxx"
      + id            = (known after apply)
      + name          = (known after apply)
      + strategy      = (known after apply)
    }

  # auth0_connection_client.sample_username_password_authentication will be created
  + resource "auth0_connection_client" "sample_username_password_authentication" {
      + client_id     = "xxxxx"
      + connection_id = "con_xxxxx"
      + id            = (known after apply)
      + name          = (known after apply)
      + strategy      = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

$ terraform apply
auth0_connection_client.sample_google_oauth2: Creating...
auth0_connection_client.sample_username_password_authentication: Creating...
auth0_connection_client.sample_google_oauth2: Creation complete after 0s [id=con_xxxxx::xxxxx]
auth0_connection_client.sample_username_password_authentication: Creation complete after 0s [id=con_xxxxx::xxxxx]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

次に無効化する Google 認証を削除する(今回はコメントアウトにした).

👾 connections.tf

# resource "auth0_connection_client" "sample_google_oauth2" {
#   client_id     = auth0_client.sample.id
#   connection_id = data.auth0_connection.google_oauth2.connection_id
# }

もう一度 terraform plan コマンドと terraform apply コマンドを実行すると Google 認証を削除できた❗️

$ terraform plan
Terraform will perform the following actions:

  # auth0_connection_client.sample_google_oauth2 will be destroyed
  # (because auth0_connection_client.sample_google_oauth2 is not in configuration)
  - resource "auth0_connection_client" "sample_google_oauth2" {
      - client_id     = "xxxxx" -> null
      - connection_id = "con_xxxxx" -> null
      - id            = "con_xxxxx::xxxxx" -> null
      - name          = "google-oauth2" -> null
      - strategy      = "google-oauth2" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

$ terraform apply
auth0_connection_client.sample_google_oauth2: Destroying... [id=con_xxxxx::xxxxx]
auth0_connection_client.sample_google_oauth2: Destruction complete after 0s

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.

Google 認証を削除できた

Auth0 React Samples: ログイン画面(Google 認証なし)

イイ感じ❗️

Auth0 Terraform Provider を試す(既存リソースをインポートして実装)

現状まだ experimental(実験的機能)ではあるけど,Auth0 の既存設定を Terraform の import ブロックとしてエクスポートする機能がある👀 さっそく Auto-generating Terraform config from Auth0 tenant ドキュメントを参考に試す❗️

1. GenerateSamples Application を追加する

インポート対象として Auth0 コンソールで Auth0 Application を追加しておく❗️

GenerateSamples Application を追加した

2. Auth0 CLI をセットアップする

ドキュメントを参考に Auth0 CLI(auth0 コマンド)をセットアップする.

$ auth0 --version
auth0 version 1.4.0 54e9a30eeb58a4a7e40e04dc19af6869036bfb32

auth0.github.io

3. auth0 tf generate コマンドを実行する

そして auth0 tf generate コマンドを実行すると,指定した tmp-auth0-tf ディレクトリに auth0_generated.tfauth0_import.tf などがエクスポートされる👌 auth0_generated.tf ファイルにはリソース実装が含まれていて,auth0_import.tf には import ブロックが含まれている.対象のリソースが多くコード量はそこそこ多かった.

$ auth0 tf generate --output-dir tmp-auth0-tf
Fetching data from Auth0... done
Generating Terraform configuration... done

4. 既存リソースを Terraform 管理にする

エクスポートされた auth0_generated.tfauth0_import.tf などをそのままデプロイして紐付けることもできるけど,今回は GenerateSamples Application の import ブロックとリソース実装を抜き出して clients.tf に追加してみた.

👾 clients.tf

import {
  id = "xxxxx"
  to = auth0_client.generatesamples
}

resource "auth0_client" "generatesamples" {
  allowed_clients                       = []
  allowed_logout_urls                   = ["http://localhost:3000"]
  allowed_origins                       = ["http://localhost:3000"]
  app_type                              = "spa"
  callbacks                             = ["http://localhost:3000"]
  client_aliases                        = []
  client_metadata                       = {}
  cross_origin_auth                     = false
  cross_origin_loc                      = null
  custom_login_page                     = null
  custom_login_page_on                  = true
  description                           = null
  encryption_key                        = {}
  form_template                         = null
  grant_types                           = ["authorization_code", "implicit", "refresh_token"]
  initiate_login_uri                    = null
  is_first_party                        = true
  is_token_endpoint_ip_header_trusted   = false
  logo_uri                              = null
  name                                  = "GenerateSamples"
  oidc_backchannel_logout_urls          = []
  oidc_conformant                       = true
  organization_require_behavior         = null
  organization_usage                    = null
  require_pushed_authorization_requests = false
  sso                                   = false
  sso_disabled                          = false
  web_origins                           = ["http://localhost:3000"]
  jwt_configuration {
    alg                 = "RS256"
    lifetime_in_seconds = 36000
    scopes              = {}
    secret_encoded      = false
  }
  native_social_login {
    apple {
      enabled = false
    }
    facebook {
      enabled = false
    }
  }
  refresh_token {
    expiration_type              = "expiring"
    idle_token_lifetime          = 1296000
    infinite_idle_token_lifetime = false
    infinite_token_lifetime      = false
    leeway                       = 0
    rotation_type                = "rotating"
    token_lifetime               = 2592000
  }
}

5. terraform plan コマンドと terraform apply コマンドを実行する

そして terraform plan コマンドと terraform apply コマンドを実行して既存リソースをインポートできた👌

$ terraform plan
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

$ terraform apply
auth0_client.generatesamples: Importing... [id=xxxxx]
auth0_client.generatesamples: Import complete [id=xxxxx]

Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed.

6. 既存リソースを更新する

インポートしたリソースの更新を試すため,GenerateSamples Application の description を更新した.ちなみに encryption_key はエクスポートした状態だと Payload validation error: 'Too few properties defined (0), minimum 1' on property encryption_key (The client's encryption key). というエラーになったため,行ごと削除した.

👾 clients.tf

-  description                           = ""
-  encryption_key                        = {}
+  description                           = "This is an application to try the `auth0 tf generate` command"

terraform plan コマンドと terraform apply コマンドを実行すると description を更新できた❗️

$ terraform plan
Terraform will perform the following actions:

  # auth0_client.generatesamples will be updated in-place
  ~ resource "auth0_client" "generatesamples" {
      + description                           = "This is an application to try the `auth0 tf generate` command"
        id                                    = "xxxxx"
        name                                  = "GenerateSamples"
        # (21 unchanged attributes hidden)

        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

$ terraform apply
auth0_client.generatesamples: Modifying... [id=xxxxx]
auth0_client.generatesamples: Modifications complete after 0s [id=xxxxx]

description を更新できた

まとめ

Auth0 Terraform Provider を使って Auth0 の設定を Terraform で宣言的に管理しよう❗️

Lambda 関数(コンテナ)をテストできる testcontainers-python v4.8.0 の新機能「AWSLambdaContainer」

2024年8月14日にリリースされた testcontainers-python v4.8.0 の新機能を確認していたら new: Added AWS Lambda module と書いてあって,これは何だろう〜と気になって試してみた❗️

github.com

簡単に言えば,テスト実行時に testcontainers-python で AWS Lambda 関数(コンテナ)を起動して,AWS Lambda RIE (Runtime Interface Emulator) エンドポイント /2015-03-31/functions/function/invocations を呼び出したレスポンスを assert できる機能だった💡AWS Lambda 関数(コンテナ)の振る舞いをデプロイする前にテストできる \( 'ω')/

さっそく AWSLambdaContainer を試す

ディレクトリ構成

.
├── Dockerfile
├── requirements-test.txt
├── src
│   └── app.py
└── tests
    └── test_app.py

👾 Dockerfile

まず Dockerfile を作る.Python ベースイメージを使えば AWS Lambda RIC (Runtime Interface Clients)AWS Lambda RIE (Runtime Interface Emulator) をセットアップしなくて OK👌 Ubuntu などをベースイメージにする場合は別途セットアップする必要がある.

docs.aws.amazon.com

FROM public.ecr.aws/lambda/python:3.12

COPY src/app.py ${LAMBDA_TASK_ROOT}

CMD [ "app.handler" ]

👾 src/app.py

AWS Lambda 関数で実行するコードはサンプルとして受け取った event をそのまま返す実装にした.

def handler(event, context):
    return event

👾 tests/test_app.py

以下のドキュメントを参考にしつつテストコードを書いてみた❗️

testcontainers-python.readthedocs.io

まず DockerImageDockerfile からコンテナイメージをビルドしつつ,AWSLambdaContainer で AWS Lambda 関数(コンテナ)を起動する💡そして send_request() で AWS Lambda RIE (Runtime Interface Emulator) のエンドポイント /2015-03-31/functions/function/invocations を呼び出す.

今回は4種類の assert を実装した👌

  • エンドポイント
  • ステータスコード
  • レスポンス (title)
  • レスポンス (url)
import re

from testcontainers.aws import AWSLambdaContainer
from testcontainers.core.image import DockerImage


def test_function():
    with DockerImage(path='.') as image:
        with AWSLambdaContainer(image=image, port=8080) as func:
            pattern = r'http://localhost:\d+/2015-03-31/functions/function/invocations'
            assert re.match(pattern, func.get_api_url())

            response = func.send_request(
                {
                    'title': 'kakakakakku blog',
                    'url': 'https://kakakakakku.hatenablog.com/',
                }
            )
            body = response.json()

            assert response.status_code == 200
            assert body['title'] == 'kakakakakku blog'
            assert body['url'] == 'https://kakakakakku.hatenablog.com/'

ちなみに get_api_url() という関数はドキュメントには載ってなかったけど,実装を確認しているときに発見した.ポート部分は毎回変わるけど http://localhost:61522/2015-03-31/functions/function/invocations という値が返ってくる😀 まさに RIE エンドポイント \( 'ω')/

github.com

👾 requirements-test.txt

AWSLambdaContainer の依存する ServerContainer は HTTP リクエストを操作するライブラリとして内部的に HTTPX を使っているようだった.

www.python-httpx.org

よって,AWSLambdaContainer のレスポンスは HTTPX オブジェクトになるため,セットアップを忘れると ModuleNotFoundError: No module named 'httpx' というエラーが出る.今回 requirements-test.txt は以下のようにした.

httpx==0.27.0
pytest==8.3.2
testcontainers==4.8.0

✅ 動作確認

OK👌

$ python -m pytest --verbose

tests/test_app.py::test_function PASSED                                                                                                                                     [100%]

まとめ

testcontainers-python v4.8.0 の新機能「AWSLambdaContainer」を試してみた❗️

ちなみに個人的には AWS Lambda 関数を実装するときは handler()main() を分割して,main() をテストしている.ドキュメントにもベストプラクティス「Lambda ハンドラーをコアロジックから分離します」と紹介されていたりする📝

docs.aws.amazon.com

今回の AWSLambdaContainer では AWS Lambda RIE (Runtime Interface Emulator) を使って AWS Lambda 関数(コンテナ)の振る舞いをテストできるため,あくまで個人的には単体テストとしてではなく統合テストとして活用できそうかなと思った.

関連記事

kakakakakku.hatenablog.com

フォルダオブジェクトの有無によって aws s3api list-objects-v2 コマンドの結果が異なる

AWS CLI で Amazon S3 の aws s3api list-objects-v2 コマンドを使ってオブジェクト数をカウントしてたときに少しハマったことがあって簡単にメモしておこうと思う👌

awscli.amazonaws.com

起きたこと

例えば Amazon S3 バケットに folder/file.txt というオブジェクトが保存されているときに aws s3api list-objects-v2 コマンドを実行すると「オブジェクト数1」の場合と「オブジェクト数2」の場合があって,オブジェクト数をカウントするコードが期待通りに動かなかったという場面があった👀(検証用の Amazon S3 バケットは削除済)

オブジェクト数1

Key: folder/file.txt

オブジェクト数2

Key: folder/
Key: folder/file.txt

結論

結論としては「フォルダオブジェクト」の有無によって結果が異なっていた.よって,aws s3api list-objects-v2 コマンドを使ってオブジェクト数をカウントするときはフォルダオブジェクトの有無を考慮すると良さそう.アプリケーション実装によっても異なると思う.

ちなみに Amazon S3 には厳密には「フォルダ」という概念はなく,キーの末尾が / となる 0 Byte のオブジェクトがあるとマネジメントコンソールではフォルダとして表示してくれるという前提がある👌以下のドキュメントに詳しく載っている.

docs.aws.amazon.com

検証1: オブジェクト数1

オブジェクト数: 0

$ aws s3api list-objects-v2 \
    --bucket kakakakakku-sandbox-list-objects-v2
{
    "RequestCharged": null
}

オブジェクト folder/file.txt を追加する.

$ aws s3api put-object \
    --bucket kakakakakku-sandbox-list-objects-v2 \
    --key folder/file.txt \
    --body file.txt

オブジェクト数: 1(LastModified の値は修正している)

$ aws s3api list-objects-v2 \
    --bucket kakakakakku-sandbox-list-objects-v2 \
    --prefix folder/
{
    "Contents": [
        {
            "Key": "folder/file.txt",
            "LastModified": "2024-07-30T00:00:00+00:00",
            "ETag": "\"4c21212908803d3bf45f5a162bed13f1\"",
            "Size": 27,
            "StorageClass": "STANDARD"
        }
    ],
    "RequestCharged": null
}

検証2: オブジェクト数2

オブジェクト数: 0

$ aws s3api list-objects-v2 \
    --bucket kakakakakku-sandbox-list-objects-v2
{
    "RequestCharged": null
}

フォルダオブジェクト folder/ を追加する.

$ aws s3api put-object \
    --bucket kakakakakku-sandbox-list-objects-v2 \
    --key folder/

オブジェクト数: 1(LastModified の値は修正している)

$ aws s3api list-objects-v2 \
    --bucket kakakakakku-sandbox-list-objects-v2 \
    --prefix folder/
{
    "Contents": [
        {
            "Key": "folder/",
            "LastModified": "2024-07-30T00:00:00+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD"
        }
    ],
    "RequestCharged": null
}

オブジェクト folder/file.txt を追加する.

$ aws s3api put-object \
    --bucket kakakakakku-sandbox-list-objects-v2 \
    --key folder/file.txt \
    --body file.txt

オブジェクト数: 2(LastModified の値は修正している)

$ aws s3api list-objects-v2 \
    --bucket kakakakakku-sandbox-list-objects-v2 \
    --prefix folder/
{
    "Contents": [
        {
            "Key": "folder/",
            "LastModified": "2024-07-30T00:00:00+00:00",
            "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
            "Size": 0,
            "StorageClass": "STANDARD"
        },
        {
            "Key": "folder/file.txt",
            "LastModified": "2024-07-30T00:00:00+00:00",
            "ETag": "\"4c21212908803d3bf45f5a162bed13f1\"",
            "Size": 27,
            "StorageClass": "STANDARD"
        }
    ],
    "RequestCharged": null
}