Deploying an AWS Lambda Ruby Function
25 Jul 2024 - Eric Chrobak
I needed a CD/CI pipeline to push Ruby apps to version control and deploy to AWS Lambda. The documentation out there is disjointed. I dealt with many issues and lots of trial and error trying to setup this process. It was crucial to have this pipeline setup so I can work on the applications no matter where I am.
I do development in a variety of places including locally on a Mac, AWS Cloud9, GitHub Codespaces, or in the AWS Lambda UI. Now that the pipeline is setup I just setup the environment, clone the repo and get to work. This article is the shortcut for others setting this pipeline up.
In this article I’ll go over:
- Benefits of Lambda Functions
- Setting up your Ruby environment for AWS Lambda
- Setting up your AWS Lambda Function
- Developing Ruby Function
- Making Changes and Deploying Function
- Testing your Ruby Function
- Common Issues / Troubleshooting
Benefits of Lambda Functions
Lambda gives you lots of options to manage, deploy, monitor, and execute your functions. I find the options overwhelming and it has a bit of a learning curve. However, you have a lot power with Lambda.
- Setup deployment pipelines with AWS, GitHub, BitBucket, or GitLab
- Trigger your functions via a URL or a schedule
- Store your function, packages, dependencies, files, etc in an S3 bucket
- Develop your code in the Cloud9 IDE right in the Function UI, build it in Cloud9, develop locally
- Access to various runtimes: Node.js, Python, Ruby, Java, .NET, and custom runtimes
- Develop your own packages, libraries, dependencies and upload them for use in your functions
- Version control built in
- Build test versions to try out without affecting the production version
I enjoy using serverless tools like Lambda as it allows me to get right to work building the application.
How to setup Your Ruby Environment for AWS Lambda
AWS Lambda supports 2 runtimes: Ruby 3.2.0 and Ruby 3.3.0. We’re going to use Rbenv as the version manager tool for Ruby and Ruby 3.3.0.
It is crucial that you setup your Ruby environment as it is not possible to use any out of the box Ruby version installed on your machine. It will not work. Additionally, the steps are sequential and you will run into problems doing them out of order.
Your first step is to setup your repository on either GitHub, BitBucket, or GitLab.
Mac, Cloud9, Codespaces
- Install Ruby version manager tool Rbenv and Ruby Build. Examples of how to install on different systems:
- Mac using Homebrew - Ruby Build included
$ brew install rbenv $ brew install ruby build $ brew update ruby build
- Rbenv install on Cloud9, Codespaces using yum, apt, apt-get - Ruby Build excluded
$ sudo yum install -y rbenv $ sudo apt install rbenv $ sudo apt-get install rbenv
- Mac using Homebrew - Ruby Build included
- Set up your shell to load rbenv:
$ rbenv init
- Install Ruby 3.3.0 with Rbenv:
$ rbenv install 3.3.0
- Clone repo:
$ git clone https://github.com/username/repository_name.git
- Set Ruby version for folder to 3.3.0:
$ rbenv local 3.3.0
- Install Bundler - Gem manager (JavaScript npm equivalent)
$ gem install bundler
- Install gems
$ bundle install
How to Setup Your AWS Lambda Function
- Create a new Lambda Function in the AWS Console
- Choose the Ruby 3.3 runtime (Use 3.2 if your environment is setup for 3.2)
- Create security credentials for your environment variables
- Click your user name in the top right corner
- Click Security Credentials
- Click Create Access Key in Access Keys section
- Copy the Access Key ID and Secret Access Key
- In your version control software add the credentials and any API tokens as environment variables
- GitHub: Settings > Secrets and Variables > Actions > New Repository Secret
- BitBucket: Repository Settings > Repository Variables > Add
Developing Your Function
- Add .env file to your project
$ touch .env
- Add your AWS variables to the .env file
AWS_ACCESS_KEY_ID=your_access_key_id AWS_SECRET_ACCESS_KEY=your_secret_access_key AWS_REGION=your_region FUNCTION_NAME=your_function_name
- Get API Tokens and add them to the .env file
CLIENT_ID=your_client_id CLIENT_SECRET=your_client_secret REFRESH_TOKEN=your_refresh_token
- Add the .env file to your .gitignore file
$ echo '.env' >> .gitignore
- Create a Ruby file for your function
$ touch lambda_function.rb
- Add function to the Ruby file - Lambda requires lambda_handler as the function name. This article won’t go into what passes through event or context. You can read more about it here
def lambda_handler(event:, context:) # Your code here end
- Add dontenv gem to your Gemfile. This allows pulling in the environment variables from the .env file
gem 'dotenv'
Making Changes and Deploying
I’m assuming you will implement your own testing methods. The only testing I’m using here is running the function.
- Make your changes
- Run application to test
$ ruby lambda_function.rb
- Add yaml file for deployment
- GitHub
- Add yaml file to .github/workflows folder
$ mkdir -p .github/workflows $ touch .github/workflows/main.yml
- Build yaml file - It needs steps to checkout source code, zip files, and deploy ```yaml name: Deploy Lambda Function
on: [push]
jobs:
deploy_zip: name: deploy lambda function runs-on: ubuntu-latest steps: - name: checkout source code uses: actions/checkout@v3 - name: Build binary run: | sudo apt update && sudo apt install -y zip && sudo zip -r code.zip * - name: default deploy uses: appleboy/lambda-action@v0.2.0 with: aws_access_key_id: $ aws_secret_access_key: $ aws_region: $ function_name: $ zip_file: code.zip ```
- Add yaml file to .github/workflows folder
- BitBucket
- Add yaml file to root folder
$ touch bitbucket-pipelines.yml
- Build yaml file - It needs steps to zip the files and a step to deploy to AWS Lambda
```yaml
pipelines:
default:
- step: name: Build and package script: - apt-get update && apt-get install -y zip - zip -r code.zip * artifacts: - code.zip
- step: name: Update Lambda Code script: - pipe: atlassian/aws-lambda-deploy:0.2.1 variables: AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION: $region FUNCTION_NAME: $FUNCTION_NAME COMMAND: ‘update’ ZIP_FILE: ‘code.zip’ ```
- Add yaml file to root folder
- GitHub
- Bundle gems for deployment - Gems have to be bundled in the vendor/bundle folder in order for Lambda to use them
bundle config set --local path 'vendor/bundle' && bundle install
- Push changes to repo
- Add to staging
$ git add --all
- Commit Changes
$ git commit -m 'your message'
- Push to GitHub or Atlassian (you may be prompted to enter your GitHub app password)
$ git push origin main
GitHub and Atlassian will automatically deploy the changes to AWS Lambda.
- Add to staging
Testing Deployment
- Go to the AWS Console
- Click on the Lambda Function
- Click on test
- Add a test event
- Click test
- Check the logs for the function
Common issues & Troubleshooting
- No Using The Right Ruby version - Error - “Cannot load such file” - Local Ruby version doesn’t match Lambda Ruby version
- You will see an error saying “cannot load such file”. Here you can see that the Lambda Ruby version is set to 3.3.0. If you are using a different version, you will need to set your local Ruby version to 3.3.0, bundle the gems again, and push the changes to version control.
/var/lang/lib/ruby/
3.3.0
/rubygems/core_ext/kernel_require.rb:59:in `require{ "errorMessage": "cannot load such file -- lambda_function (LoadError)", "errorType": "Init<LoadError>", "stackTrace": [ "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'", "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'", "/var/task/hello_ruby_record.rb:1:in `<top (required)>'", "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'", "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'" ] }
- Gems Not Installed In The Vendor Folder - Error message: “Cannot load such file - gemname”
- The error message is similar to above but, it will list one of the gems in your gem file or that you are requiring in your app. Make sure you bundle the gems in the vendor/bundle folder.
{ "errorMessage": "cannot load such file -- mysql2", "errorType": "Init<LoadError>", "stackTrace": [ "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'", "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'", "/var/task/hello_ruby_record.rb:3:in `<top (required)>'", "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'", "/var/lang/lib/ruby/3.3.0/rubygems/core_ext/kernel_require.rb:59:in `require'" ] }
- Environment Variables Missing/Not Set/Incorrect
- You will see a variety of errors depending on how you are handling exceptions. Make sure you set the environment variables in Lambda Configuration
- Application is Running Twice
- AWS has an init phase that runs before the function is called. Once it runs successfully then it will execute all the code in the function. It has a timeout limit of 10 seconds.
You can see below I am keeping the code in the Lambda Handler to a minimum and calling a method that processes the response. When the init process runs it will only initialize the ApiName class and variable and check the Lambda_Handler. It will not call the process_array method. You can read more about the init and invoke process here.
This is important as any code that can be run within 10 seconds of the init phase will run. Then it will start the invoke phase and execute all the code. So if you are doing any data transformations, Database read/write, API read/write in the Lambda_handler it will run in the init phase and the invoke phase.
You can end up with duplicate data in the database, API calls, etc. So keep the code in the Lambda_handler to a minimum and call methods that do the processing.
require 'faraday' require 'dotenv/load' require_relative 'helpers/helper-methods' $logger = Logger.new($stdout) $api_name = ApiName.new $variable1 = variable $variable2 = variable def lambda_handler(event:, context:) $logger.info('Lambda Handler') response = $api_name.method_name if(response.class == "Array") process_array(response) else $logger.info(response) end end def process_array(response) response.each do |item| $logger.info(item) # Do something with the item end end
Conclusion
This should cover the bulk of what you need to know to setup a deployment pipeline from your Ruby environment to AWS Lambda. A lot of the process will be the same for JavaScript, Python, Java, etc. You will need to keep in mind the init and invoke phases, using the correct runtime in your development and production environments, setting environment variables, installing packages, and configuring your yaml file for deployment.