Closed41

lambdaでシンプルにmysqldump to s3

inomotoinomoto

定期mysqldumpをFargateやEC2じゃなくてlambdaでかるーくやりたいが、ググるとメンテが終わったnpmパッケージを使ったものや、ローカルとかで引っこ抜いたバイナリをlambdaに突っ込んでpythonでゴニョるみたいなのしか出てこない。

後者はまぁいいっちゃいいんだけど、そもそもshellだと5行くらいで終わるのにやたらコードを書かされている印象で、なんだかなぁ。と思っている。

ということでもっとスッキリした感じでやりたい

inomotoinomoto

いい感じのmysqldumpバイナリの取得方法やいい感じの言語のパッケージが無いかと探したが無い。

そこでshellの方向で模索すると、lambdaでカスタムランタイムならshellが使える(というかshellがデフォルト)ということに気付く。
かつベースはamazonlinux2なので、普通にyumで色々インストールすればok。いけそう。

inomotoinomoto

まずはローカルでモノを作る。

Lambdaのカスタムランタイムのベースイメージはこれらしい
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-images.html#runtimes-images-custom
https://gallery.ecr.aws/lambda/provided

inomotoinomoto

とりあえずbashで潜ってyumとかやってみよう。ということで

$ docker run -it public.ecr.aws/lambda/provided:al2.2022.11.15.11 bash
Unable to find image 'public.ecr.aws/lambda/provided:al2.2022.11.15.11' locally
al2.2022.11.15.11: Pulling from lambda/provided
625fcf1e5e5f: Pull complete
8df5433d9fb6: Pull complete
bae685131515: Pull complete
94130148b8a0: Pull complete
Digest: sha256:023dd5403feaffcb92f8585c664444111723cf89ca42b0ebbec50a7acd080581
Status: Downloaded newer image for public.ecr.aws/lambda/provided:al2.2022.11.15.11
23 Nov 2022 03:30:25,642 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)

^C23 Nov 2022 03:30:43,926 [INFO] (rapid) Received signal signal=interrupt
23 Nov 2022 03:30:43,926 [INFO] (rapid) Shutting down...
23 Nov 2022 03:30:43,926 [WARNING] (rapid) Reset initiated: SandboxTerminated

ん?

ってこれENTRYPOINT入ってんじゃん、雑にbashで入れないわ。
※途中でCtrl-Cで抜けた

でECR Publicのページをよく見ると、Usageのところにちゃんと触り方書いてある。ごめんなさい。

inomotoinomoto

考え直してentrypoint上書きで入った。

$ docker run -it --entrypoint bash public.ecr.aws/lambda/provided:al2.2022.11.15
.11
bash-4.2# pwd
/var/task
bash-4.2# cat /etc/os-release
NAME="Amazon Linux"
VERSION="2"
ID="amzn"
ID_LIKE="centos rhel fedora"
VERSION_ID="2"
PRETTY_NAME="Amazon Linux 2"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2"
HOME_URL="https://amazonlinux.com/"
VARIANT_ID="202211081638-2.0.979.0"

ヨシ!

ちなみに、カスタムで入れるべきbootstrapやfunction.shはデフォルトでは存在しないっぽい。
entrypointっぽいファイルとして/lambda-entrypoint.shがあるので覗くと

bash-4.2# cat /lambda-entrypoint.sh
#!/bin/sh
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.

if [ $# -ne 1 ]; then
  echo "entrypoint requires the handler name to be the first argument" 1>&2
  exit 142
fi
export _HANDLER="$1"

RUNTIME_ENTRYPOINT=/var/runtime/bootstrap
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/local/bin/aws-lambda-rie $RUNTIME_ENTRYPOINT
else
  exec $RUNTIME_ENTRYPOINT
fi

bootstrapがなければ/usr/local/bin/aws-lambda-rieをexecするらしい。ちなみにこのファイルはなんかバイナリだった。
めっちゃ見間違えた、ifでチェックしてるのはAWS_LAMBDA_RUNTIME_APIであってbootstrapではなかった。実lambda環境で動いてたらbootstrapをそのまま動かす、そうじゃなければ(ローカルのdockerなどでは)aws-lambda-rieラッパーをかますということっぽい。

inomotoinomoto

bashはできたので、必要そうなパッケージの所在を確認する。
欲しいのは

  • mysqldump
  • gzip
  • awscli

かな。

手始めにgzipから

bash-4.2# which gzip
bash: which: command not found
bash-4.2# gzip
bash: gzip: command not found
bash-4.2# yum search gzip
Loaded plugins: ovl
=========================================== N/S matched: gzip ============================================
gzip.x86_64 : The GNU data compression program
pigz.x86_64 : Parallel implementation of gzip

  Name and summary matches only, use "search all" for everything.

whichがなかった...けどgzipの入手はok

次はawscli

bash-4.2# yum search awscli
Loaded plugins: ovl
========================================== N/S matched: awscli ===========================================
awscli.noarch : Universal Command Line Environment for AWS

  Name and summary matches only, use "search all" for everything.
bash-4.2# yum info awscli
Loaded plugins: ovl
Available Packages
Name        : awscli
Arch        : noarch
Version     : 1.18.147
Release     : 1.amzn2.0.1
Size        : 2.1 M
Repo        : amzn2-core/2/x86_64
Summary     : Universal Command Line Environment for AWS
URL         : http://aws.amazon.com/cli
License     : ASL 2.0 and MIT
Description : This package provides a unified
            : command line interface to Amazon Web Services.

awscliの1系が入るらしい。まぁ大したことやらんので1系でいいです。

最後に大本命mysqldump

bash-4.2# yum search mysql
Loaded plugins: ovl
=========================================== N/S matched: mysql ===========================================
MySQL-python.x86_64 : An interface to MySQL
apr-util-mysql.x86_64 : APR utility library MySQL DBD driver
dovecot-mysql.x86_64 : MySQL back end for dovecot
freeradius-mysql.x86_64 : MySQL support for freeradius
libdbi-dbd-mysql.x86_64 : MySQL plugin for libdbi
mysql-connector-java.noarch : Official JDBC driver for MySQL
mysql-connector-odbc.x86_64 : ODBC driver for MySQL
pcp-pmda-mysql.x86_64 : Performance Co-Pilot (PCP) metrics for MySQL
perl-DBD-MySQL.x86_64 : A MySQL interface for Perl
php-mysql.x86_64 : A module for PHP applications that use MySQL databases
php-mysqlnd.x86_64 : A module for PHP applications that use MySQL databases
qt-mysql.x86_64 : MySQL driver for Qt's SQL classes
qt-mysql.i686 : MySQL driver for Qt's SQL classes
qt3-MySQL.x86_64 : MySQL drivers for Qt 3's SQL classes
qt3-MySQL.i686 : MySQL drivers for Qt 3's SQL classes
qt5-qtbase-mysql.x86_64 : MySQL driver for Qt5's SQL classes
qt5-qtbase-mysql.i686 : MySQL driver for Qt5's SQL classes
redland-mysql.x86_64 : MySQL storage support for Redland
rsyslog-mysql.x86_64 : MySQL support for rsyslog
mariadb.x86_64 : A community developed branch of MySQL
mariadb-devel.x86_64 : Files for development of MariaDB/MySQL applications
mariadb-libs.x86_64 : The shared libraries required for MariaDB/MySQL clients
mariadb-libs.i686 : The shared libraries required for MariaDB/MySQL clients

  Name and summary matches only, use "search all" for everything.

そういやmysqlはデフォルトではいないんだった。厳密にmysqlにしたければそれでもいいけど、正直dumpだけならmariadbのでいい気がする。
というわけで

bash-4.2# yum search mariadb
Loaded plugins: ovl
========================================== N/S matched: mariadb ==========================================
mariadb-bench.x86_64 : MariaDB benchmark scripts and data
mariadb-devel.x86_64 : Files for development of MariaDB/MySQL applications
mariadb-embedded.x86_64 : MariaDB as an embeddable library
mariadb-embedded-devel.x86_64 : Development files for MariaDB as an embeddable library
mariadb-libs.x86_64 : The shared libraries required for MariaDB/MySQL clients
mariadb-libs.i686 : The shared libraries required for MariaDB/MySQL clients
mariadb-server.x86_64 : The MariaDB server and related files
mariadb.x86_64 : A community developed branch of MySQL
mariadb-test.x86_64 : The test suite distributed with MariaD

  Name and summary matches only, use "search all" for everything.

どれだろ。

inomotoinomoto

パッケージで何が入るか知りたいので、下記を参考にrepoqueryを手配する。
https://ja.stackoverflow.com/questions/170/yumでインストールしたパッケージに含まれるファイルの一覧を表示するには

bash-4.2# repoquery --list mariadb-devel | grep mysqldump
bash-4.2# repoquery --list mariadb-libs | grep mysqldump
bash-4.2# repoquery --list mariadb-server | grep mysqldump
/usr/bin/mysqldumpslow
/usr/share/man/man1/mysqldumpslow.1.gz
bash-4.2# repoquery --list mariadb | grep mysqldump
/usr/bin/mysqldump
/usr/share/man/man1/mysqldump.1.gz

普通にmariadbのところに入ってるらしい。

てかmysqldumpslowって知らないな。調べると「スロークエリーログファイルの要約」とのこと。そうなんだ。豆知識。普通にログファイル見たことしかなかったわ。
https://dev.mysql.com/doc/refman/8.0/ja/mysqldumpslow.html

inomotoinomoto

ローカル環境を作ろうと思ったら、dockerイメージの探検で終わってしまった。
今度こそローカル環境を。

Usageの説明にあるbootstrapやfunction.shについては下記チュートリアルにサンプルがある。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-walkthrough.html

inomotoinomoto

まずは考えることをやめてサンプルを忠実にコピペする。

bootstrap
#!/bin/sh

set -euo pipefail

# Initialization - load function handler
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

# Processing
while true
do
  HEADERS="$(mktemp)"
  # Get an event. The HTTP request will block until one is received
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")

  # Extract request ID by scraping response headers received above
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # Run the handler function from the script
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # Send the response
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response"  -d "$RESPONSE"
done
function.sh
function handler () {
  EVENT_DATA=$1
  echo "$EVENT_DATA" 1>&2;
  RESPONSE="Echoing request: '$EVENT_DATA'"

  echo $RESPONSE
}
Dockerfile
FROM public.ecr.aws/lambda/provided:al2

# Copy custom runtime bootstrap
COPY bootstrap ${LAMBDA_RUNTIME_DIR}
# Copy function code
COPY function.sh ${LAMBDA_TASK_ROOT}

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "function.handler" ]

とファイルを用意して

$ docker build -t lambda-local .
(出力略)
$  docker run -p 9000:8080 lambda-local
23 Nov 2022 04:31:54,086 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)

で別shellから

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations"
-d '{"payload":"hello world!"}'

とすると、docker runしてたやつに出力が出る

$ docker run -p 9000:8080 lambda-local
23 Nov 2022 04:31:54,086 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)
23 Nov 2022 04:32:51,598 [INFO] (rapid) extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory
23 Nov 2022 04:32:51,598 [WARNING] (rapid) Cannot list external agents error=open /opt/extensions: no such file or directory
START RequestId: 4641b1fe-fc97-438d-b846-f5098c5b682f Version: $LATEST
23 Nov 2022 04:32:51,598 [WARNING] (rapid) First fatal error stored in appctx: Runtime.InvalidEntrypoint
23 Nov 2022 04:32:51,598 [ERROR] (rapid) Init failed error=fork/exec /var/runtime/bootstrap: permission denied InvokeID=
23 Nov 2022 04:32:51,599 [WARNING] (rapid) Reset initiated: ReserveFail
23 Nov 2022 04:32:51,599 [WARNING] (rapid) Cannot list external agents error=open /opt/extensions: no such file or directory
23 Nov 2022 04:32:51,599 [WARNING] (rapid) First fatal error stored in appctx: Runtime.InvalidEntrypoint

ヨシ...いやまて、ヨシではない、エラーしている。

inomotoinomoto

Init failed error=fork/exec /var/runtime/bootstrap: permission denied InvokeID=

ごめんなさい。
chmod +x bootstrapしてビルドからやりなおすと

$ docker run -p 9000:8080 lambda-local
23 Nov 2022 04:34:52,088 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)
23 Nov 2022 04:34:53,583 [INFO] (rapid) extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory
23 Nov 2022 04:34:53,583 [WARNING] (rapid) Cannot list external agents error=open /opt/extensions: no such file or directory
START RequestId: 8958d653-4fd5-4083-a4a7-fb03400dbc25 Version: $LATEST
{"payload":"hello world!"}
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    61  100    16  100    45  22598  63559 --:--:-- --:--:-- --:--:-- 61000
{"status":"OK"}
END RequestId: 8958d653-4fd5-4083-a4a7-fb03400dbc25
REPORT RequestId: 8958d653-4fd5-4083-a4a7-fb03400dbc25  Init Duration: 0.28 ms Duration: 23.11 ms       Billed Duration: 24 ms    Memory Size: 3008 MB    Max Memory Used: 3008 MB
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations"
-d '{"payload":"hello world!"}'
Echoing request: '{"payload":"hello world!"}'

よさそう。なんかno such file or directoryが見えるけど、それ以外の出力は見慣れたlambdaなので大丈夫なのでしょう。

inomotoinomoto

ここからいい感じにスクリプトをmountして更新されるようにしたり、curlを楽に発火できるように整えてもいいのだけど、今回作りたいのはあんまり試行錯誤の無いやつなので、そこの整備は我慢するやらない。

都度ビルドしてrunしてcurlすることにしよう。

inomotoinomoto

結局作ってしまった...
まぁmysqldumpのテストにmysqlもほしいからね。仕方ないね。

docker-compose.yml
version: "3"

services:
  lambda:
    image: public.ecr.aws/lambda/provided:al2.2022.11.15.11
    command: function.handler
    volumes:
      - ./runtime:/var/runtime
      - ./task:/var/task

  invoker:
    image: curlimages/curl
    command: sh -c 'curl -s -XPOST "$$URL" -d "$$DATA"; echo ""'
    environment:
      - URL=http://lambda:8080/2015-03-31/functions/function/invocations
      - DATA={"payload":"hello world!"}
    depends_on:
      - lambda

  db:
    image: mysql:8.0
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    volumes:
      - db:/var/lib/mysql

volumes:
  db:

Dockerfileは無し、bootstrapとfunction.shをそれぞれ./runtime./taskディレクトリに移した。

これでdocker compose upすればそれぞれ実行されてログが出る。現実的にはdbがうるさいのでdocker compose up --attach lambda --attach invokerとなる。なんかいつからかlogging: driver: noneが動かなくなってた

ちなみにdocker compose start invokerすれば再実行できるけど、lambda側のコードは再読み込みされないので、現実的にはlambdaも再起動することになりそう。

またcurlコマンドの指定が変なことになっているのは、curlのレスポンスが末尾改行なしなのでdocker composeのログにflushされない問題の回避。

inomotoinomoto

サクッと動作確認する。

まずbootstrapでコマンド群を入れる

bootstrap
 # Initialization - load function handler
 source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

+yum install -y gzip awscli mariadb >/dev/null
+
 # Processing
 while true

devnullしてるのはdocker composeでログが鬱陶しいから。試行錯誤が終わったら消す予定。

で、docker compose upしてlambdaコンテナにbashで入り、疎通テストがてらmysqladmin ping -h db -u rootすると

bash-4.2# mysqladmin ping -h db -u root
mysqladmin: connect to server at 'db' failed
error: 'Authentication plugin 'caching_sha2_password' cannot be loaded: /usr/lib64/mysql/plugin/caching_sha2_password.so: cannot open shared object file: No such file or directory'

どうやらmysql8からのデフォルト設定により、雑につなぐにはひと工夫いるようだ。mysql8は実運用したことなくて知らなかった。

docker-compose.yml
   db:
     image: mysql:8.0
+    command: --default-authentication-plugin=mysql_native_password
     environment:
       MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
     volumes:

これでok

bash-4.2# mysqladmin ping -h db -u root
mysqld is alive
inomotoinomoto

次はawscliを動かす。
正直docker compose環境で ~/.aws/ をねじ込むのはアレなので、環境変数で渡すことにする。

手元にaws sts assume-roleしてsecretsを環境変数に展開するスクリプトがあるので、これを使う想定で

docker-compose.yml
  lambda:
    image: public.ecr.aws/lambda/provided:al2.2022.11.15.11
    command: function.handler
     volumes:
       - ./runtime:/var/runtime
       - ./task:/var/task
+    environment:
+      - AWS_ACCESS_KEY_ID
+      - AWS_SECRET_ACCESS_KEY
+      - AWS_SESSION_TOKEN

こう。MFA強制のIAM Role運用をしていてterraformとかしてる人にはお馴染みのやつな気がする。

で、bashで入ってaws sts get-caller-identityしたらいい感じに通ってた。ヨシ!

inomotoinomoto

ところでbootstrapのコードを見るとlambdaのアーキテクチャがなんとなく見える。

「イベント」をランタイムサーバ?へのロングポーリングで待ち受け、イベントが来たらハンドラを呼び出し、結果をランタイムサーバにHTTPで送り返す。つまりインスタンス的な成分はハンドラを実行していなくてもしばらく起動しっぱなしということになる。

実際、下記ページにあるライフサイクルを見るとそうなっていることがわかる。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtime-environment.html

Shutdown: このフェーズは、Lambda 関数が一定期間呼び出しを受け入れないと、トリガーされます。

とのことなので、実行間隔が空いたときにいわゆる「コールドスタート」になるのはShutdownされてInitが走っているとき、ということか。なるほどなー。おもしろい。

inomotoinomoto

ローカル環境ができたので、本命のスクリプトを書いていく。毎度のことながら環境構築をエンジョイしすぎた

まず前提環境として、dumpされるDBの中身と、dumpがアップロードされるS3バケットを用意する。

S3は適当にAWSコンソールから作る。名前は sbox-lambda-simple-mysqldump-s3 にした。設定もデフォルト。なんでもいい。

DBの中身は適当にググるとそれっぽい記事があったので、そのまま使わせてもらう。
https://qiita.com/tayasu/items/c5ddfc481d6b7cd8866d

ぶっちゃけ1行でもレコードあればよいので、適当にinsertして32レコードほど作成。中身は本当になんでもいい。

一応ここでmysqldumpのコマンドを試しておく。lambdaのコンテナにbashで入り

bash-4.2# mysqldump -u root -h db db
-- MySQL dump 10.14  Distrib 5.5.68-MariaDB, for Linux (x86_64)
--
-- Host: db    Database: db
-- ------------------------------------------------------
-- Server version       8.0.31

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
...

よし。ちなみにcreate database db;としたのでコマンドの最後がdb dbとなっているが、正しい。

inomotoinomoto

mysqldumpして圧縮してs3に投げつけるやつは、とてもシンプルにやってる記事があったので参考にする。
https://rooter.jp/infra-ops/mysql_dump_s3_directly/

ファイルに吐かずにawscliのstdinに投げるのは考えたことなかった。まぁそれでいいな。

これを見つつ、それっぽくfunction.shを書く。

function.sh
function handler () {
  filename=$(date '+%Y-%m-%d_%H%M%S').sql.gz

  mysqldump -u root -h db db | \
    gzip | \
    aws s3 cp - s3://sbox-lambda-simple-mysqldump-s3/dump/$filename

  echo $filename
}

docker composeを起動し、lambdaを発火させると

ヨシ!

良いんだけど、なんらかdumpに失敗したときに気付けない。失敗するとどうなるかというと、スクショの1つ目のファイルみたいなのが出来上がる。これは一応対処したい。

というのと、DB接続情報を直接貼っているのはなんか考える必要がある。

inomotoinomoto

失敗については、多分失敗扱いにはなっている。ゴミファイルは出来上がるけど、まぁ失敗検知さえできればあとは上の運用レイヤで考えればいいか。

接続情報については、ハンドラとしては環境変数で受けつつ、bootstrapではパラメータストアを読むようにしてみた。

docker-compose.yml
     image: mysql:8.0
     command: --default-authentication-plugin=mysql_native_password
     environment:
-      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
+      MYSQL_ROOT_PASSWORD: password
     volumes:
       - db:/var/lib/mysql
function.sh
 function handler () {
   filename=$(date '+%Y-%m-%d_%H%M%S').sql.gz

-  mysqldump -u root -h db db | \
+  mysqldump -u $DBUSER -h $DBHOST $DBNAME -p$DBPASS | \
     gzip | \
     aws s3 cp - s3://sbox-lambda-simple-mysqldump-s3/dump/$filename
bootstrap
 yum install -y gzip awscli mariadb >/dev/null

+function getparam() {
+  aws ssm get-parameter --with-decryption --region ap-northeast-1 --name $1 --query 'Parameter.Value' --output text
+}
+export DBUSER=$(getparam sbox-mysqldump-dbuser)
+export DBHOST=$(getparam sbox-mysqldump-dbhost)
+export DBNAME=$(getparam sbox-mysqldump-dbname)
+export DBPASS=$(getparam sbox-mysqldump-dbpass)
+
 # Processing
 while true
 do

これでもちゃんと動いた。ヨシ!

ハンドラとしては環境変数を読めば良いだけなので、パラメータストアを使うつもりがなければlambdaの環境変数に直接入れてもいいはず。

inomotoinomoto

ところで、当初の動機に

後者はまぁいいっちゃいいんだけど、そもそもshellだと5行くらいで終わるのにやたらコードを書かされている印象で、なんだかなぁ。と思っている。

ということでもっとスッキリした感じでやりたい

とあるんだけど、bootstrapは結構ごちゃごちゃ書いてあるんだよな...半分以上テンプレとはいえ。
でも処理本体はパイプ繋がりの1行(?)で終わってるし、まぁいいか。

inomotoinomoto

あとはAWS上の実環境で動作を試していく。

やることは

  • lambdaを作り、コードをデプロイする
    • 実行ロールも作らないといけない。S3とSSMとKMSかな
  • Aurora serverless v1でも作り、実行してみる
inomotoinomoto

まずはDBを手配。scale to zeroな仕様でお砂場テストに使いやすいAurora serverless v1を使う。
※使ったことないけど...

AWSコンソールからポチポチしていくが、右に出ているトピックサイドバー的なやつを見ると、serverless v1対応のエンジンバージョンはmysql5.7系しかない。最初何も考えずに最新にしたらv2しか選択できなかった。
仕方ないのでv1対応最新の 2.07.1 にする。ローカル環境はノリでちょっと苦労しながらmysql8で作ったのに...まぁいいか。

設定はv1にすること以外はだいたいデフォルト。 クラスターがアイドル状態のときに容量を 0 ACU にスケールする を有効にしておくくらい。

inomotoinomoto

おぉ、セキュリティグループはとりあえず新規に作っていじろうと思って「新規作成」にしたら、自動でweb閲覧元のグローバルIPから3306が通るような設定で作られた。ありがたい。
手元から初期データ作成のクエリ打とうと思ってたんだ。

ということを見ながら作成完了を待つ。RDSではあるのでそこそこ時間かかるっぽい。

inomotoinomoto

セキュリティグループがいい感じだったので勘違いしてたけど、冷静に考えてパブリックIPなかった。
先にlambdaをセットアップしてそこからやろう。

なおインスタンスのreadyは2~3分ってところだった。

inomotoinomoto

まずハコのlambdaをAWSコンソールで適当に作る。ランタイムをカスタムのamzn2にするのと、VPCの有効化をする。それ以外はデフォルト。

カスタムランタイムで作成すると、見慣れたエディタ画面に bootstrap.samplehello.sh.sample というファイルができていた。これをリネームして編集すればGUIでテストできそう。
手元に動く(はず)のコードはあるので、それをコピペする。

bootstrapはそのままコピペとして、functionは先にテストデータを入れる必要があるので、dumpは動かない。
のでまず疎通テストで下記。

function handler () {
  mysqladmin ping -u $DBUSER -h $DBHOST -p$DBPASS
}

あとSSMのパラメータはAuroraのインスタンスに合わせて調整。

で実行すると

Function Logs
/var/task/bootstrap: line 10: yum: command not found
/var/task/bootstrap: line 10: yum: command not found
START RequestId: f0df7134-0df7-4e69-954d-26e015d78fce Version: $LATEST
RequestId: f0df7134-0df7-4e69-954d-26e015d78fce Error: Runtime exited with error: exit status 127
Runtime.ExitError

そんなあ。

inomotoinomoto

調べた感じ、たぶんlambdaのこのイメージにyumは無い。いや無いという情報な無いのだけど、誰もやってないのである気配が無い。
いや公式に提供してるdockerイメージにあるからあると思ったんだけど...普通に別物じゃん...それベースイメージと呼ばないんだけど...

仕方ないのでyumはrun timeでは使わない方向を模索。

inomotoinomoto

lambdaでyumを使えない前提のデプロイパックの生成を模索

inomotoinomoto

色々試したけど全くもってシンプルにならないことがわかった。とりあえず記録だけ残す。

まずyumを使わない成果物の生成は以下のようにできる。

FROM public.ecr.aws/lambda/provided:al2.2022.11.15.11 as build

RUN yum install -y gzip mariadb unzip
ADD https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip /opt/awscli.zip
RUN cd /opt && unzip awscli.zip

FROM public.ecr.aws/lambda/provided:al2.2022.11.15.11

COPY --from=build /usr/bin/gzip /usr/bin/mysqldump /usr/local/bin/
COPY --from=build /opt/aws /opt/aws
RUN ln -s /opt/aws/dist/aws /usr/local/bin/

dockerになっているが、要するにビルド用イメージでインストールした成果物をコピーしただけ。これならランタイムではyumをしなくて済む。

で、デプロイ用のzipは下記のようにして作れる。

#!/bin/bash

set -eu

rm -rf dist
mkdir dist
cp runtime/bootstrap task/function.sh dist/

tag=$(docker build --quiet --target build . | cut -d: -f2)
container=$(docker create $tag)
docker cp $container:/usr/bin/gzip dist/gzip
docker cp $container:/usr/bin/mysqldump dist/mysqldump
docker cp $container:/opt/aws dist/aws
docker rm $container

cd dist
zip -qr ../function.zip ./
inomotoinomoto

ちなみにawscliはランタイムでpipを入れてインストールする手がある。
https://blog.serverworks.co.jp/tech/2019/04/25/lambda-awscli/

が、どうせビルドタイムで他のコマンドを用意するのであれば、全部ビルドタイムでやってしまおうという観点でのawscli v2の採用。

yumならともかく、そうでもないホストのスクリプトなインストーラを都度実行するのは信頼性の観点からやりたくない。

inomotoinomoto

前述のスクリプトで必要なものは揃うが、気になるポイントがいくつか。

  • lambda実行はほとんどのディレクトリはread-only
    • 実行ファイルのsymlinkを貼りたいが、cwdにもbinにも貼れない。特にawscliは毎回./aws/dist/aws みたいにしないと実行できない
  • awscliのdistがデカく、コンソールからzipをアップロードする際のサイズ制限(50MB)に対してかなりギリギリ(48MBくらい)

それぞれそこまでの障害ではないのだが、既に面倒なビルド・デプロイフローを組んでおり、カオス度が高い。
もともと「ネットに転がってるやつよりもっとシンプルに実現できないか」が焦点であり、既にシンプルさを達成できてない。

というわけでこの方向は不採用。

inomotoinomoto

今回はコードをシンプルにすることを目標に置くので不採用だが、コードを書いてでもインフラ側のシンプルさを取りたいのであれば、aws s3 cp相当をgoなりrustなりで実装すれば気になりは削減できてアリかもしれない。

inomotoinomoto

カスタムランタイムのdocker image版でいってみる。

inomotoinomoto

一応、できればlambda起動時にpullするイメージのサイズは最小化したい。
下記記事によると、lambdaのベースイメージ的なやつはキャッシュされているようなので、ベースは(alpineなどより)そっちのほうがよいみたい。
https://aws.amazon.com/jp/blogs/news/optimizing-lambda-functions-packaged-as-container-images/

というわけで、その上でパターンをいくつか試してサイズを見る。

まずビルドイメージから必要なやつだけ取り出してくるパターン。

FROM public.ecr.aws/lambda/provided:al2 as build

RUN yum install -y gzip mariadb unzip

ADD https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip /opt/awscli.zip
RUN cd /opt && unzip -q awscli.zip

FROM public.ecr.aws/lambda/provided:al2

COPY --from=build /usr/bin/gzip /usr/bin/mysqldump /usr/local/bin/
COPY --from=build /opt/aws /opt/aws
RUN ln -s /opt/aws/dist/aws /usr/local/bin/

レイヤのリスト

$ docker history lambda-local
IMAGE          CREATED             CREATED BY                                      SIZE      COMMENT
3b5f2bd5f2ad   About an hour ago   /bin/sh -c ln -s /opt/aws/dist/aws /usr/loca…   17B
d32fee162833   About an hour ago   /bin/sh -c #(nop) COPY dir:71e7a6526e5cb2598…   168MB
028557407f88   About an hour ago   /bin/sh -c #(nop) COPY multi:b3e5c2b0ab7627b…   3.27MB
bdd84b649c1d   11 days ago         ENTRYPOINT [ "/lambda-entrypoint.sh" ]          0B
...
inomotoinomoto

続いてシンプルにyumで全部入れるパターン

FROM public.ecr.aws/lambda/provided:al2

RUN yum install -y gzip mariadb awscli && \
  yum clean all && rm -rf /var/cache/yum
$ docker history lambda-local
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
a0c7ad338830   3 seconds ago   /bin/sh -c yum install -y gzip mariadb awscl…   250MB
bdd84b649c1d   11 days ago     ENTRYPOINT [ "/lambda-entrypoint.sh" ]          0B
...

前者のほうがだいぶ小さい。やっぱawscliをパッケージで入れるとpythonシリーズがゴロッと含まれてしまうからかな。

inomotoinomoto

前者のやつで適当にECRにpushする。適当にリポジトリを作り、「プッシュコマンドを表示」のところを見ながらビルドしてpushする。

ECR上でのサイズが153.77MBとなっている。ここに表示されるのは確かストレージ使用量で圧縮済サイズだったはずだが、それにしてもローカルで見えるサイズ(480MB)と比べて小さすぎる。
ここにもAWSのベースイメージはキャッシュされてる的な成分が関係してるんだろうか。

inomotoinomoto

lambda関数は作り直す。古いのを削除して作り直すが、まぁコンソールに従って入れる。
イメージはタグなどではなくshaを参照しているもよう。

また、作成画面ではVPC設置にできないので、設置後に設定する。

inomotoinomoto

テストすると何をやってもタイムアウトする。エラーではなく。
で、print debugによるとタイムアウトしているのはawscli。ここで思い至ったのはVPC内からだとssmやらs3やらに直接アクセスできないのではないかという点。

VPCエンドポイントを作ってやってみる

inomotoinomoto

エンドポイントだとssmとs3と...あとkmsっているのかな...とめんどくさいのでNATゲートウェイにした。
ひとまずパラメータ取得は乗り越えた

inomotoinomoto

そしてそのままコードをもとに戻したら通った。Auroraの初期データ投入は面倒なのでEC2立てた(lambdaのイメージはmysqldumpしかない)
権限周りは少々あったが、特に難しいことはなく成功した。

長かった...

このスクラップは2022/12/07にクローズされました