LayerにGemを格納してAWS Lambda関数を成功させる方法(2)

はじめに

先日、Layerにgemを格納してAWS Lambda関数を動かす記事を書きました。

miyohide.hatenablog.com

この記事では、Rubyで実装されたgemを使ってみたのですが、この記事ではネイティブ拡張を使ったgemを使う方法を記します。具体的には、PostgreSQLと接続するためのGemであるpg gemを使う方法を記します。

gemをzipで固めるだけではダメ?

任意の環境でbundle config set --local path 'vendor/bundle' && bundle installを実行し、zip -r my_ruby_package.zip vendorにてzipファイルを作成してLayerとして登録、関数にも紐付けして実行するとlibpq.so.5: cannot open shared object file: No such file or directoryのようなエラーメッセージが出力されます。

このエラーメッセージは、今回試したpg gemではOSが持っている共通ライブラリlibpq.so.5に依存しており、Lambda関数が動作する環境にはこれらの共通ライブラリが入っていないために発生しているのが原因です。

このため、動作環境に対してlibpq.so.5などをLD_LIBRARY_PATHに格納されるようにファイルをコピーします。具体的には、libディレクトリを作成し、libディレクトリ以下にlibpq.so.5などを格納、zip -r my_ruby_package.zip vendor libを実行してlibディレクトリもzipファイル化しておきます。

このzipファイルがLayerとして登録された場合、Lambdaの環境変数LD_LIBRARY_PATHに含まれるパス/opt/liblibpq.so.5などのファイルが格納されることになります。

docs.aws.amazon.com

ただ、この手法を続けても/lib64/libm.so.6: version 'GLIB_2.29' not foundというエラーメッセージが出て結局動きませんでした。

Layerを作るためのコンテナイメージを作る

Layerを作るには環境依存が大きいため、Layerを作るためのコンテナイメージを作ることにします。以下のようなDockerfileを用意します。

FROM public.ecr.aws/sam/build-ruby3.2:latest-x86_64

WORKDIR /var/task

RUN amazon-linux-extras install -y postgresql14
RUN yum -y install postgresql-devel postgresql
RUN gem update bundler
CMD ["bash", "make_layer.sh"]

pg gemを動かすために必要なライブラリ等をコピーするためのshell scriptmake_layer.shを以下のような中身で用意します。

bundle config --local silence_root_warning true
bundle config set clean true
bundle config set path '/var/task'

bundle install

mkdir -p /var/task/lib

cp -a /usr/lib64/libpq.so.5.14 /var/task/lib/libpq.so.5
cp -a /usr/lib64/liblber-2.4.so.2.10.7 /var/task/lib/liblber-2.4.so.2
cp -a /usr/lib64/libldap_r-2.4.so.2.10.7 /var/task/lib/libldap_r-2.4.so.2
cp -a /usr/lib64/libsasl2.so.3.0.0 /var/task/lib/libsasl2.so.3
cp -a /usr/lib64/libssl3.so /var/task/lib/libssl3.so
cp -a /usr/lib64/libsmime3.so /var/task/lib/libsmime3.so
cp -a /usr/lib64/libnss3.so /var/task/lib/libnss3.so
cp -a /usr/lib64/libnssutil3.so /var/task/lib/libnssutil3.so

cd /var/task
zip -r layer.zip .

この作り方は以下の記事を参考にしました。

www.farend.co.jp

neenet-pro.com

あとは、docker build -t ruby-lambda-layer .を実行してコンテナイメージを作成し、docker run --rm -it -v $PWD:/var/task ruby-lambda-layerを実行するとzipファイルが作成されます。

環境変数LD_PRELOADの設定

これでLayerに登録するzipファイルが作成できましたので、登録して実行してみます。すると、/usr/lib64/libnssutil3.so: version 'NSSUTIL_3.82' not foundというエラーメッセージが出ました。

このエラーメッセージの解決にかなり悩みましたが、以下のブログ記事の記載で解決しました。

carrfane.medium.com

具体的には、環境変数LD_PRELOAD/opt/lib/libnssutil3.soを設定します。

結果、単純にrequire 'pg'としただけの以下のプログラムですが無事動作しました。

動作結果。

考察

ここまできてようやくpg gemを動作することができました。動作には、Lambdaの動作環境に強く依存したライブラリのコピーが必要となり、動作させるだけでも一苦労です。また、将来的な動作環境の変更の際にまた試行錯誤するのは大変かと思います。

このため、以下の記事で書いたようにLambdaで動くコンテナイメージを作っておいた方が考えることが少なくて良さそうです。

miyohide.hatenablog.com

LayerにGemを格納してAWS Lambda関数を成功させる方法

はじめに

AWS LambdaでRubyを使って実装しようとしたとき、Gemを使いたいことがよくあります。Gemを使う際にちょっとハマったので、解決方法を記します。

とりあえずGemの使用例としてActiveSupportを使った以下のプログラムを動かしてみたいと思います。

require 'active_support/all'

def lambda_handler(event:, context:)
    p 4.month.from_now
    "Hello Ruby Lambda"
end

これを動かそうとすると、ActiveSupportを読み込めずにLambda関数は失敗します。

Layerの作成

本件を解決するために、Layerを作成します。公式ドキュメントに以下の記述がありますのでそれを参考にします。

docs.aws.amazon.com

Gemfileを以下のように実装します。

# frozen_string_literal: true

source "https://rubygems.org"

gem 'activesupport'

任意の環境(Cloud9とか)で以下のコマンドを実行します。

bundle config set --local path 'vendor/bundle' && bundle install

これでvendor/bundle以下にGemファイルがインストールされるので、これをzipファイルとしてまとめます。

zip -r my_ruby_package.zip vendor

このzipファイルをLayerとして登録しておきます。関数にも紐付けしておきます。

環境変数の設定

上記のドキュメントに記載がありますが、Lambdaはレイヤーのコンテンツをその実行環境の/optディレクトリに読み込むようです。実際に中身を覗いてみると、/opt/vendor/bundle/ruby/3.2.0以下にGemが入っています。

このため、環境変数GEM_PATH/opt/vendor/bundle/ruby/3.2.0を指定しておきます。

実行

これで準備が整いました。関数を実行してみると成功します。

考察

以上の作業を行うことでGemをLayerに格納しつつ、Gemを使ったLambda関数を実行することができました。ちょっと前準備が多いのが面倒ですね。

今回はActiveSupportを試しましたが、RubyのGemにはC言語で実装したものなどが存在します。例えばMySQLPostgreSQLと接続するために使うものが該当します。これらのGemを使うときにはさらに作業が必要となりそうです。この解決方法については後日記します。

RDS(PostgreSQL)におけるデータベース所有者設定でのエラー回避方法

はじめに

RDSにてPostgreSQLを使っているとき、マスターユーザーとは別のユーザーを作ってそのユーザがオーナーのデータベースを作ろうとするとERROR: must be member of role "xxxxx"というメッセージが出ることがあります。例を以下に記します。

postgres=> CREATE ROLE testuser001 WITH PASSWORD 'hogehogehoge' LOGIN;
CREATE ROLE
postgres=> CREATE DATABASE mytestdb001 WITH OWNER testuser001;
ERROR:  must be member of role "testuser001"
postgres=> 

この状態の原因と対策を以下に記します。

原因

RDSのマスターユーザー(ここではpostgres)にSuperuserが付与されていないのが原因です。\duにて権限を見てみます。

postgres=> \du
                                                                                        List of roles
    Role name    |                         Attributes                         |                                                  Member of

-----------------+------------------------------------------------------------+--------------------------------------------------------------------------------------------------------
------
 postgres        | Create role, Create DB                                    +| {rds_superuser}
                 | Password valid until infinity                              |
 rds_ad          | Cannot login                                               | {}
 rds_iam         | Cannot login                                               | {}
 rds_password    | Cannot login                                               | {}
 rds_replication | Cannot login                                               | {}
 rds_superuser   | Cannot login                                               | {pg_read_all_data,pg_write_all_data,pg_monitor,pg_signal_backend,pg_checkpoint,rds_replication,rds_pass
word}
 rdsadmin        | Superuser, Create role, Create DB, Replication, Bypass RLS+| {}
                 | Password valid until infinity                              |
 rdsrepladmin    | No inheritance, Cannot login, Replication                  | {}
 rdstopmgr       |                                                            | {pg_monitor,pg_checkpoint}

postgres=>

このようにマスターユーザー(postgres)にはSuperuserが付与されておらず、RDS固有のロールであるrdsadminにSuperuserが付与されています。

対策

マスターユーザー(postgres)が新たに作成したユーザー(testuser001)のメンバにしてあげればOKです。

postgres=> GRANT testuser001 TO postgres;
GRANT ROLE
postgres=>

これで、当初の目的であるマスターユーザーとは別のユーザーを作ってそのユーザがオーナーのデータベースを作ることができます。

postgres=> CREATE DATABASE mytestdb001 WITH OWNER testuser001;
CREATE DATABASE
postgres=>

\lにて確認してみます。

postgres=> \l
                                                    List of databases
    Name     |    Owner    | Encoding |   Collate   |    Ctype    | ICU Locale | Locale Provider |   Access privileges
-------------+-------------+----------+-------------+-------------+------------+-----------------+-----------------------
 mytestdb001 | testuser001 | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            |
 postgres    | postgres    | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            |
 rdsadmin    | rdsadmin    | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | rdsadmin=CTc/rdsadmin+
             |             |          |             |             |            |                 | rdstopmgr=Tc/rdsadmin
 template0   | rdsadmin    | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/rdsadmin          +
             |             |          |             |             |            |                 | rdsadmin=CTc/rdsadmin
 template1   | postgres    | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/postgres          +
             |             |          |             |             |            |                 | postgres=CTc/postgres
(6 rows)

postgres=>

オンプレミスのPostgreSQLですとこのようなことはなかった気がしますので、念の為記録しておきます。

考察

上記ではrdsadminの詳細についてスルーしましたが、詳細はAWSの以下のドキュメントに記されています。

docs.aws.amazon.com

オンプレミスのPostgreSQLと同じノリで操作するとこういうところでつまづくので、上記のようなドキュメントには目を通しておいた方が良いかなと考えています。

参考

この記事を書くためにAWS re:Postを検索すると以下の記事がヒットしました。

repost.aws

むやみに権限を付与しなくてもよいかなと思いますが、対策の一つとしてよいかなと考えています。

AWS CDKを使ったEC2 Instance Connect Endpointの実装

はじめに

プライベートサブネットにおいてあるEC2に対して、Instance Connect Endpointを使って接続するということを実施しています。

docs.aws.amazon.com

ただ、セキュリティグループの設定などを毎回誤ったりするので、同じミスを繰り返さないようにCDKをもって実装することにしました。

実装

まずは、aws-cdkのバージョンを最新に上げておきます。これは、EC2 Instance Connect Endpointをつくるためのコンストラクタを使えるようにするためです。この記事を書いている段階でaws-cdkのバージョンは2.130.0なので、npm -g install aws-cdk@2.130.0としておきます。

その後、cdk init app --language typescriptでテンプレートを作成し、libディレクトリ以下にできているTypeScriptファイルを編集します。

まずはVPCを作成します。サンプルとして、プライベートサブネットだけにします。

    const vpc = new Vpc(this, 'VPC', {
      maxAzs: 1,
      subnetConfiguration: [
        {
          name: 'private',
          subnetType: SubnetType.PRIVATE_ISOLATED,
        },
      ],
    });

セキュリティグループを作成します。SSHだけを許可します。

    // EC2用のセキュリティグループ
    const securityGroupForEC2 = new SecurityGroup(this, 'SecurityGroupForEC2', {
      vpc,
    });
    // EIC用のセキュリティグループ
    const securityGroupForEIC = new SecurityGroup(this, 'SecurityGroupForEIC', {
      vpc,
      allowAllOutbound: false, // 指定のEC2のみに通信を許可するためfalseを指定
    });
    securityGroupForEC2.addIngressRule(securityGroupForEIC, Port.tcp(22));
    securityGroupForEIC.addEgressRule(securityGroupForEC2, Port.tcp(22));

Instance Connect Endpointを作成します。

    // EC2 Instance Connectを作成する
    new CfnInstanceConnectEndpoint(this, 'InstanceConnect', {
      subnetId: vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_ISOLATED }).subnetIds[0],
      securityGroupIds: [securityGroupForEIC.securityGroupId],
    });

使用したL1コンストラクタのドキュメントは以下を。

docs.aws.amazon.com

実装が終わればcdk deployをすればOKです。

EC2の作成

EC2を作成します。ネットワークの設定で、サブネットを上記のCDKで作成したプライベートサブネットに配置することが注意点です。

確認

cdk deployを実行後、コンソールで確認します。

VPCが作成されています。

EC2の接続から「EC2 Instance Connect」を選択すると、エンドポイントも作成されていることが確認できます。

接続してみると、無事接続できます。

考察

CDKはAWSの更新に伴い、頻繁にバージョンアップされます。そのため、aws-cdkを定期的にバージョンアップしておかないと使用したいコンストラクタが使用できないということに陥ります。

Node.jsではnpm-check-updatesというパッケージが存在し、ncuコマンドでパッケージの更新有無が確認できるので、利用すると良いかなと思います。

www.npmjs.com

今回はEC2 Instance Connect EndpointをCDKを使って実装することをやりました。EC2 Instance Connect Endpointは簡単に設定できますが、VPCやサブネットごとに1つしか作成できないというクォーターが存在します(2024年2月現在)。

docs.aws.amazon.com

今回みたいなサンプルですと十分ですが、より複雑なネットワーク構成ですとEC2 Instance Connect Endpointのこのクォーター制限はかなり厳しいのかなと思われます。その際は別の接続手段を取ることが必要になると思われます。

Container Insightsを活用したECSクラスターの監視

はじめに

先日、Amazon Elastic Container ServiceにてSpring Bootで作ったアプリを動かすことをしました。

miyohide.hatenablog.com

今回は、このアプリに対してContainer Insights機能を試してみます。

Container Insightsとは

コンテナ化されたアプリのメトリクスやログを収集・集計・要約できるCloudWatchの機能のようです。詳細は以下を参照。

docs.aws.amazon.com

タスクやコンテナレベルでメトリクスやログを収集することが可能ということで、実際にやっています。

実装

実装と言っても、Amazon Elastic Container Serviceのクラスタを作成時にContainer Insightsのチェックを入れるだけです。

これだけでCloudWatchのContainer InsightsにてCPU使用率やメモリ使用率などが表示可能となります。

「コンテナマップ」ではクラスタのしたにサービスとタスク定義が表現されます。今回は大変単純なのであまりありがたみがわかりませんが、数が多くなると有用なのかもしれません。

リソースごとの表示をするとそれぞれのCPU利用率やメモリ使用率が表示できるので、これは有用なのかもしれません。

CloudWatchのロググループを見ると、「/aws/ecs/containerinsights/クラスタ名/performance」というロググループが作られていました。この中身がContainer Insightsの実態なのかなと思われます。

料金

お手軽に導入できるContainer Insightsですが、料金については以下のページを参照します。

aws.amazon.com

ただ、あまりわかりやすいものではないので、同ページにある例に目を通しておきます。この記事を書いているときには例12が該当します。

メトリクスの量に依存するので、以下のページに記載されているメトリクス一覧で数を確認します。

docs.aws.amazon.com

何かの方法で取得するメトリクスを選択できれば節約には良さそうなのですが、どうも設定できなさそうです。

メトリクスの月額コストは2024年2月現在、0.30USD(東京リージョン)です。結構高額です。

あわせてServiceが少なめのECSクラスターにて、Container Insightsをオンにして料金を確認すると良いかなと思います。

考察

Container Insightsの利用にアプリケーション上の設定などは必要ないので、利用できるものであれば利用すれば良いかなと思います。

唯一の懸念点は料金です。Serviceの数が増えると必然的にメトリクスの数が増えてしまうので、最初は少ないServiceしかないクラスターで有効化するとよいのかなと思います。

Spring BootのコンテナアプリをAmazon Elastic Container Serviceで動かす

はじめに

先日まではApp Runner上でコンテナアプリを動かすことをしていました。今日からは、Amazon Elastic Container Serviceを使ってX-Rayなどを試してみようかと思います。

まずはSpring Bootで作ったコンテナアプリをAmazon Elastic Container Serviceで動かすことをやってみます。

参考資料

Amazon Elastic Container Serviceを使うのは久しぶりだったので、何かよい題材はないか探してみます。「AWS Hands-on for Beginners シリーズ一覧」にAmazon Elastic Container Serviceのハンズオンがあったのでそれに目を通します。

pages.awscloud.com

このハンズオンではApache HTTPdを使ったコンテナアプリを動かしています。

Spring Bootアプリをコンテナ化する

まずはSpring Bootアプリをコンテナ化します。コンテナ化する方法は色々とあります。詳細は以下を参照してください。

spring.io

今回は、私が慣れているという理由でJibを使います。

github.com

コンテナ化については、上記のリンク先を参照してください。アプリは8080ポートで動きます。このポート番号が後々重要です。

Amazon Elastic Container Serviceの設定

Amazon Elastic Container Serviceの設定は上記ハンズオンをもとに実施します。コンソール画面が少々違いますが、2024年2月時点で実施しても十分通じる内容でした。

ハンズオンとの1番の違いはタスク定義です。Spring Bootで作ったコンテナアプリは8080ポートで動くので、「ポートマッピング」の「コンテナポート」に8080を設定することが注意点です。

これさえ間違わなければ、あとはハンズオン通りに進めていけばSpring Bootで作ったコンテナアプリは動きました。

考察

久しぶりにAmazon Elastic Container Serviceを触ることになり若干不安を感じましたが、AWSのハンズオン資料が充実していたのでかなり参考になりました。

aws.amazon.com

一方で、あくまでスムーズに行くように作られたハンズオンですので、いくつか説明が省略されているところやデフォルト値でうまく行くように設定されているところがあります。このため、ちょっと応用しようとするとつまづくところがあるのは注意点です。今回で言えばポートマッピングの部分でした。

これでSpring Bootで作ったコンテナアプリをAmazon Elastic Container Service上で動かすことができましたので、次からはX-Rayなどを組み込んでいきます。

App Runner上でのメトリックス監視のためのSpring BootとCloudWatchの設定

はじめに

先日、AWS App Runner上で動くコンテナ化したSpring Bootアプリケーションの可観測性に関する実装の一環でトレースをX-Rayに送ることを実装しました。

miyohide.hatenablog.com

ただ本番環境においてはトレースだけでは足りず、リクエスト数やJVMの状態などさまざまな情報が必要になります。App Runnerにおいては、リクエスト数などは特に何もしなくてもメトリックスタブから参照できます。

今回はJVMなどの各種情報をCloudWatchに送信するようにしてみます。

前提

今回の検証では以下のバージョンを使いました。

  • Java 21
  • Gradle 8.5
  • Spring Boot 3.2.2
  • Jib 3.4.0
  • AWS OpenTelemetry Agent 1.32.0

ライブラリの選定

Spring関連プロジェクトにて、AWSのマネージドサービスを扱うためのライブラリとしてSpring Cloud AWSというものがあります。

awspring.io

今回はこのSpring Cloud AWSを使います。

Spring Cloud AWSはSpring BootやSpring Frameworkのバージョンに応じて使用するバージョンが異なります。詳細は以下GitHubに記載があるので確認します。

github.com

今回はSpring Boot 3.2.2を使っているので、Spring Cloud AWS 3.1.xを使います。

実装

build.gradleでの実装方法はSpring Cloud AWSのドキュメントに書いている通りに従います。

docs.awspring.io

CloudWatchにメトリックスを送信するためには下記ドキュメントの通りio.micrometer:micrometer-registry-cloudwatch2も必要となります。

docs.awspring.io

具体的な実装は以下の通り。

// 省略
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // Spring Cloud AWSを使用するための記述 ここから
    implementation platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.1.0")
    implementation 'io.awspring.cloud:spring-cloud-aws-starter'
    implementation 'io.micrometer:micrometer-registry-cloudwatch2'
    // Spring Cloud AWSを使用するための記述 ここまで
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    javaAgent('software.amazon.opentelemetry:aws-opentelemetry-agent:1.32.0')
}

CloudWatchにメトリックスを送信するためには以下のドキュメントを参考にしながらapplication.propertiesに設定を記述します。

docs.awspring.io

今回は以下のように記述しました。

management.cloudwatch.metrics.export.enabled=true
management.cloudwatch.metrics.export.namespace=app/micrometer

なお、ネット上ではSpring Cloud AWS 2.xの情報が目につくのですが、Spring Cloud AWS 2.xと3.xにて幾らか変更点があるようです。以下のMigration Guideを確認すると良いかと思われます。

docs.awspring.io

AWSの設定

AWS App RunnerからCloudWatchにメトリックスを送信するためには、cloudwatch:PutMetricData権限が必要となります。今回、AWS App Runnerに付与しているIAMロールにインラインポリシーにて追加しました。

動作

以上の内容で各種メトリックスがCloudWatchに送信されます。「カスタム名前空間」にmanagement.cloudwatch.metrics.export.namespaceで設定していた値のものが作られていることが確認できます。

中身を確認すると、いろいろな情報が送信されています。

Javaアプリケーションに欠かせないヒープなどの情報は「area,id」のところにでていました。

GCの情報などは「action,cause,gc」のところにありました。

考察

ライブラリと少しの設定で各種メトリックスをCloudWatchに送信できるのは非常に有用です。何かトラブルが発生した時に情報が足りないとトラブル解決ができなくなることもあるので、設定しておいて損はないかと考えます。

一方で、CloudWatchは無料で使えるわけではないです。

aws.amazon.com

料金を踏まえて、取るメトリックを制限したい場合はapplication.propertiesに以下の記述をします。

# いったん全てのメトリックスの送信を止める
management.metrics.enable.all=false
# JVM関連だけを送信する
management.metrics.enable.jvm=true

また、Spring Cloud AWSの設定が間違っていてもSpring Bootアプリケーション自身は正常に動くのも注意が必要です。正常に動いているのでメトリックスも取れているだろうと思い込まずに実際に画面を見た方が良いと考えます。