AWS Lambda をコンテナイメージで動かした話


My Redmine

こんにちは。今年こそは AWS SA プロフェッショナルを受験するぞ!と息巻いてる吉岡です。(受かるとは言ってない)

さて、かれこれ一年以上前になりますが、AWS Lambda でコンテナイメージが使えるようになったようです。
AWS Lambda の新機能 – コンテナイメージのサポート

今更ですが、先日、Ruby を使ったカスタムコンテナ(自分で書いた Dockerfile)で AWS Lambda(以下 Lambda)を動かしてみたので、実行方法を紹介したいと思います。

Lambda をコンテナで実行できると何が良いのか?

これまでは例えば、Lambda(Ruby runtime)で DB(MySQL, PostgreSQL) を扱うために Gem をインストールしようとすると、合わせてミドルウェアも必要になるため、設定や管理が非常に手間がかかりました。
参照: AWS Lambda Ruby 2.7 で PostgreSQL のコマンドを実行する方法

今回の新機能のおかげで、ミドルウェアを含めた環境ごとコンテナイメージにすることで簡単に DB を扱うことができるようになりました。もう少しわかりやすく書くと、Lambda で Rails を動かすことも割と簡単にできるようになりました!これによって rails コマンド(rails task など)を利用した処理などもサーバーレスで簡単に実行できるようになります!!

やること

そこで今回は Lambda を Redmine の Docker イメージで起動し、rails コマンドを実行してみます。

※ 今回、ベースイメージは DockerHub 公式の redmine:4.2.3 を使用します。
参照: Redmine - Official Image | Docker Hub(リンク)
また、 DB は SQLite3 を使用し、コンテナイメージに含めています。(通常ではあり得ない運用ですが、動作確認ということで簡略化しています。)

環境

以下、必要になります。

AWS SAM
バージョン: 1.37.0

インストール方法は以下のドキュメントをご確認ください。
参照: AWS SAM CLI のインストール

また、AWS SAM のインストールの際に AWS アカウントの作成や Docker のインストールなど必要に応じて作業してください。

作業内容

それでは早速作業していきます。

sam init による初期化

sam init を実行して必要ファイルの生成をします。

以下、実行サンプルです。

$ sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1

Cloning from https://github.com/aws/aws-sam-cli-app-templates

Choose an AWS Quick Start application template
        1 - Hello World Example
        2 - Multi-step workflow
        3 - Serverless API
        4 - Scheduled task
        5 - Standalone function
        6 - Data processing
        7 - Infrastructure event management
        8 - Machine Learning
Template: 1

 Use the most popular runtime and package type? (Nodejs and zip) [y/N]:

Which runtime would you like to use?
        1 - dotnet5.0
        2 - dotnetcore3.1
        3 - dotnetcore2.1
        4 - go1.x
        5 - java11
        6 - java8.al2
        7 - java8
        8 - nodejs14.x
        9 - nodejs12.x
        10 - nodejs10.x
        11 - python3.9
        12 - python3.8
        13 - python3.7
        14 - python3.6
        15 - python2.7
        16 - ruby2.7
        17 - ruby2.5
Runtime: 16

What package type would you like to use?
        1 - Zip
        2 - Image
Package type: 2

Based on your selections, the only dependency manager available is bundler.
We will proceed copying the template using bundler.

Project name [sam-app]: rails-sample

    -----------------------
    Generating application:
    -----------------------
    Name: rails-sample
    Base Image: amazon/ruby2.7-base
    Architectures: x86_64
    Dependency Manager: bundler
    Output Directory: .
    Next steps can be found in the README file at ./rails-sample/README.md

    Commands you can use next
    =========================
    [*] Create pipeline: cd rails-sample && sam pipeline init --bootstrap
    [*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch

以下、実行時の選択肢です。

Which template source would you like to use?
1 - AWS Quick Start Templates

Choose an AWS Quick Start application template
1 - Hello World Example

Which runtime would you like to use?
16 - ruby2.7
こちらは後で書き換えますが、とりあえず ruby2.7 を選択します。

What package type would you like to use?
2 - Image
今回の1番の肝です。2 の Image を選択します。

Project name [sam-app]: rails-sample
名前は任意です。
上記実行後、以下のようなファイルが作成されていることを確認します。

無事に実行が終わりますと、以下のようにファイルとディレクトリが作成されます。

.
├── events
│   └── event.json
├── Gemfile
├── hello_world
│   ├── app.rb
│   ├── Dockerfile
│   └── Gemfile
├── README.md
├── template.yaml
└── tests
    └── unit
        └── test_handler.rb

続けて作成されたファイルを編集していきます。

ファイル修正作業

template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  rails-sample

  Sample SAM Template for rails-sample

Globals:
  Function:
    Timeout: 120
    MemorySize: 512

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      PackageType: Image
      Architectures:
        - x86_64
    Metadata:
      DockerTag: rails-v1
      DockerContext: ./hello_world
      Dockerfile: Dockerfile

Outputs:
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

template.yamlのポイントとして、Rails の起動と処理の実行に少し時間がかかるため、Timeout(lambda のタイムアウト)を少し長めにしておく必要があります。

Globals:
  Function:
    Timeout: 120
    MemorySize: 512

hello_world/Dockerfile

FROM redmine:4.2.3

RUN gem install aws_lambda_ric

ARG FUNCTION_DIR="/usr/src/redmine"
WORKDIR ${FUNCTION_DIR}
COPY app.rb ${FUNCTION_DIR}

RUN : "database.ymlを作成" && { \
    echo "production:"; \
    echo "  adapter: 'sqlite3'"; \
    echo "  host: 'localhost'"; \
    echo "  database: 'sqlite/redmine.db'"; \
    echo "  username: 'redmine'"; \
    echo "  password: ''"; \
    echo "  port: ''"; \
    echo "  encoding: 'utf8'"; \
} | tee ${FUNCTION_DIR}/config/database.yml

RUN echo 'config.logger = Logger.new("tmp/production.log")' > config/additional_environment.rb
RUN bundle install
RUN rails generate_secret_token

# 注)今回はサンプルのためコンテナイメージにDBを含めています。
RUN rails db:create
RUN rails db:migrate

ENTRYPOINT ["/usr/local/bundle/bin/aws_lambda_ric"]
CMD ["app.lambda_handler"]

Dockerfileのポイントとしては lambda で起動するための Gem をインストールして、ENTORYPOINTaws_lambda_ric を実行してから、CMDapp.rb に記述されている lambda_handler を実行します。

FROM redmine:4.2.3

# lambdaでコンテナを実行するために必要なGemをインストール
RUN gem install aws_lambda_ric

...

# lambdaでは/tmp以外でファイルの書き込みができないため、ログの出力先の変更必要。
# 以下はRedmine独自の書き方なので、通常のRailsの場合は書き方が違います。
RUN echo 'config.logger = Logger.new("/tmp/production.log")' > config/additional_environment.rb

...

# 注)今回はサンプルのためコンテナイメージにDBを含めています。
RUN rails db:create
RUN rails db:migrate

# 実行準備の処理
ENTRYPOINT ["/usr/local/bundle/bin/aws_lambda_ric"]
# ファイル名とメソッド名を記述
CMD ["app.lambda_handler"]

hello_world/app.rb

require 'json'
require 'open3'

def lambda_handler(event:, context:)

  # rails db:migrate:status を実行してMigrationの状態を確認
  command = %*rails db:migrate:status*
  p command
  res = Open3.capture3(command)
  p res

  {
    statusCode: 200,
    body: {
      message: "success!",
    }.to_json
  }
end

以上、設定が終わりましたら、ローカルで実行してみます。

ローカルでテスト

sam build を実行してローカルでイメージのビルドします。

$ sam build
# コンテナイメージのビルドが実行されます(省略)

ビルドが無事に終わりましたら、sam local invoke を実行してローカルでの動作確認します。

$ sam local invoke
Invoking Container created from helloworldfunction:rails-v1
Building image.................
Skip pulling image and use local one: helloworldfunction:rapid-1.37.0-x86_64.

START RequestId: 6cd1485f-4e7a-4a95-b184-b7cf1387f182 Version: $LATEST
Skipped bootstraping TelemetryLog
Executing 'app.lambda_handler' in function directory '/usr/src/redmine'
"rails db:migrate:status"
["database: /usr/src/redmine/sqlite/redmine.db

 Status   Migration ID    Migration Name
--------------------------------------------------
   up     001             Setup
   up     002             Issue move
   up     003             Issue add note
   up     004             Export pdf
     ...(省略)
   up     20180923091603  Change sqlite booleans default
   up     20190315094151  Change custom values value limit
   up     20190315102101  Add trackers description
   up     20190510070108  Add unique id to import items
   up     20190620135549  Change roles name limit
   up     20200826153401  Add twofa scheme to user
   up     20200826153402  Add totp to user", "", #<Process::Status: pid 16 exit 0>]
{"statusCode":200,"body":"{\"message\":\"success!\"}"}END RequestId: 6cd1485f-4e7a-4a95-b184-b7cf1387f182
REPORT RequestId: 6cd1485f-4e7a-4a95-b184-b7cf1387f182  Init Duration: 1.88 ms  Duration: 4743.01 ms    Billed Duration: 4744 ms        Memory Size: 512 MB     Max Memory Used: 512 MB

上記のように rails db:migrate:status の出力されれば、設定完了です。

デプロイ

続いて、sam depelop を実行して、実際に AWS にデプロイして動作確認をします。

$ sam deploy --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: rails-sample
        AWS Region [ap-northeast-1]: ap-northeast-1
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]:
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]:
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]:
        Save arguments to configuration file [Y/n]:
        SAM configuration file [samconfig.toml]:
        SAM configuration environment [default]:

        Looking for resources needed for deployment:
         Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1dix87p43yhyp
         A different default S3 bucket can be set in samconfig.toml
         Image repositories: Not found.
         #Managed repositories will be deleted when their functions are removed from the template and deployed
         Create managed ECR repositories for all functions? [Y/n]: Y

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

d55f284a5036: Pushed
...
# イメージのプッシュ(省略)
...

helloworldfunction-7b5365a72273-rails-v1: digest: sha256:2386cb0e3828b8f3a0f3d97a33d02acc9ddd3f88969889224430c8649a799df2 size: 4078

        Deploying with following values
        ===============================
        Stack name                   : rails-sample
        Region                       : ap-northeast-1
        Confirm changeset            : False
        Disable rollback             : False
        Deployment image repository  :
                                       {
                                           "HelloWorldFunction": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/railssample4b62256f/helloworldfunction19d43fc4repo"
                                       }
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-1dix87p43yhyp
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles             : {}

Initiating deployment
=====================
Uploading to rails-sample/08aa59625af12cd81a3f45d9157a1969.template  972 / 972  (100.00%)

Waiting for changeset to be created..

...
# cloudformationを利用したデプロイの実行ログが表示されます。(省略)
...

CloudFormation outputs from deployed stack
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 HelloWorldFunctionIamRole
Description         Implicit IAM Role created for Hello World function
Value               arn:aws:iam::xxxxxxxxxxxx:role/rails-sample-HelloWorldFunctionRole-1HJZLKA9J0ZKD

Key                 HelloWorldFunction
Description         Hello World Lambda Function ARN
Value               arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:rails-sample-HelloWorldFunction-M9azD5kDY5Cl
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - rails-sample in ap-northeast-1

上記のように Successfully created/updated stack 出力されれば、OK です。

ポイントとしては、初回のみ、以下のように設定の確認がありますが、Stack Name [sam-app]: rails-sample 以外はデフォルトの値で実行できると思います。

Looking for config file [samconfig.toml] :  Not found

Setting default arguments for 'sam deploy'
=========================================
Stack Name [sam-app]: rails-sample
AWS Region [ap-northeast-1]: ap-northeast-1
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]:
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]:
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]:
Save arguments to configuration file [Y/n]:
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:

Looking for resources needed for deployment:
 Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1dix87p43yhyp
 A different default S3 bucket can be set in samconfig.toml
 Image repositories: Not found.
 #Managed repositories will be deleted when their functions are removed from the template and deployed
 Create managed ECR repositories for all functions? [Y/n]: Y

Saved arguments to config file
Running 'sam deploy' for future deployments will use the parameters saved above.
The above parameters can be changed by modifying samconfig.toml
Learn more about samconfig.toml syntax at
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html

ちなみに 2 回目以降は

$ sam build
$ sam deploy

の順で実行すればデプロイされます。

AWS で動作確認

最後に AWS にログインして lambda を実行してみます。

デプロイされている Lambda でテストを実行します。
今回は event を利用していないので、デフォルトのままで OK です。

数十秒待つと実行結果が出力されます。
rails db:migrate:status の出力結果が確認できれば、今回の作業は無事に完了です。

まとめ

今回紹介した新機能を利用すれば、複数の DB の対して同時に rails db:migration を実行することも簡単(短時間)です。 弊社でも DB の migration を実行する際にはこちらで紹介した技術を利用することで短時間で、大量の DB に対してアップデートを行うことが可能になりました。

実際には AWS CodePipeline や StepFunctions なども利用しており、アーキテクチャは少し複雑になりますが、それでもスクリプトを書いて EC2 などを利用して実行するより簡潔かつ安全に実行できるのではと思っております(EC2の場合Pipelineなどに組み込みにくい)。 また機会がありましたらそのアーキテクチャの紹介もしたいと思います。

注 1)Lambda から RDS へアクセスする際は、AWS RDS Proxy の利用が推奨されています。
注 2)Lambda を複数同時実行し、migration を実行する際は DB のリソースが枯渇する可能性が高いので、それぞれの用途に合わせて検証は必要です。

こちらの記事もオススメです!
AWS Lambda Ruby 2.7 でPostgreSQLのコマンドを実行する方法
Lambda ruby 2.7を利用してRDS(postgresql)と通信をできるようにする方法の紹介。
最近使っているAmazon Echoなどのスマートデバイス
最近使っているスマートデバイスを紹介。Amazon Echoの複数台利用で便利に。
久しぶりに印象に残ったRedmineの改善
My Redmineのお客様から作業時間一覧についてお問い合わせをいただきRedmineを改善。
解析素人がRとMeCabを使って形態素解析を始めてみました
統計分析の学びとしてR言語を使い始めました。R言語は利用までのコストが低く、情報も多いのでお勧めです。
シンプルなUIでRedmineのチケットを作成・更新できる「RedMica Bridge」ベータ版をリリース
「RedMica Bridge」はシンプルなUIで簡単にチケットに入力でき、複数のRedmineのチケットを表示することができます。
ファーエンドテクノロジーからのお知らせ(2024/04/24更新)
入門Redmine 第6版 出版記念企画セミナー「Redmineのアクセス制御」【2024/5/30開催】
入門Redmine 第6版(2024年3月23日発売)の書籍から「Redmineのアクセス制御」について解説します。
My Redmine 初回ご契約で「入門Redmine 第6版」プレゼントのお知らせ
Redmineのクラウドサービス「My Redmine」を初めてご契約いただいたお客様にRedmine解説書「入門Redmine 第6版」を進呈いたします。
2024年度ブランドパートナーに島根県在住のモデル ユイさんを継続起用
ユイさん(モデルスタジオミューズ所属)をファーエンドテクノロジーの2024年度ブランドパートナーとして継続して起用します。
My Redmine スタンダードプランおよびAdminサポートデスクプランの料金改定のお知らせ【2024年4月ご利用分より】
2024年4月ご利用分より、My Redmine スタンダードプラン(民間企業・個人向け及び官公庁向け)とAdminサポートデスクプランの料金を改定いたします。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け