WilliamSpain

WilliamSpain

How to properly build an Elixir custom runtime that is deployed in a ZIP archive to AWS Lambda

Hello,
I am currently trying to create an AWS Lambda function using a custom runtime built in Elixir. I had been able to replicate the following Haskell guide AWS lambda in any language using custom runtimes | Tooploox and wanted to do something similar in Elixir.

To start off I am very new to Elixir so I am likely to miss some obvious things. I was able to get a very basic hello world function running, however the zip package I send its around 25mb vs 4-7mb in haskell, the structure of my zip is as follows. I have seen suggestions of removing things like iex that might not be used.

  • /bin
  • /erts-15.1.2
  • /lib
  • /releases
  • bootstrap

When I run invoke the function it it uses a max memory of around 150mb vs 40mb for the Haskell approach. I am perfectly fine if this is the way it should be, however as a new elixir programmer I was just worried a mistake on my end has increased requirements.

In my attempt to get an approach similar to the haskell one working I ran into issues with releases and dependencies like burrito/distillery etc. The common issue I ran into was similar to the following.

/var/task/erts-15.1.2/bin/epmd: /lib64/libc.so.6: version `GLIBC_2.38' not found (required by /var/task/erts-15.1.2/bin/epmd)
/var/task/erts-15.1.2/bin/beam.smp: /lib64/libgcc_s.so.1: version `GCC_12.0.0' not found (required by /var/task/erts-15.1.2/bin/beam.smp)

I fixed this by using docker and building from the amazonlinux:2023 base image so that everything had the correct version and then just copying the function.zip to my machine to verify its correct. I had also considered some hacky approach of just patching these into the zip file.

DOCKERFILE:

# Use Amazon Linux 2023 as the base image
FROM amazonlinux:2023

# Install system dependencies with --allowerasing to handle conflicts
RUN yum update -y && \
    yum install -y \
    git \
    curl \
    tar \
    gzip \
    zip \
    gcc \
    gcc-c++ \
    make \
    autoconf \
    openssl \
    openssl-devel \
    ncurses-devel \
    wget \
    gnupg \
    nano \
    glibc-langpack-en --allowerasing

# Install ASDF
RUN git clone https://github.com/asdf-vm/asdf.git ~/.asdf && \
    echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc && \
    echo '. "$HOME/.asdf/completions/asdf.bash"' >> ~/.bashrc

# Source ASDF scripts
SHELL ["/bin/bash", "-c"]
RUN source ~/.asdf/asdf.sh && \
    source ~/.asdf/completions/asdf.bash

# Install Erlang plugin and latest version
RUN source ~/.asdf/asdf.sh && \
    asdf plugin-add erlang https://github.com/asdf-vm/asdf-erlang.git && \
    ERLANG_VERSION=27.1.2 && \
    asdf install erlang $ERLANG_VERSION && \
    asdf global erlang $ERLANG_VERSION

# Install Elixir plugin and latest version
RUN source ~/.asdf/asdf.sh && \
    asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git && \
    ELIXIR_VERSION=1.17.3 && \
    asdf install elixir $ELIXIR_VERSION && \
    asdf global elixir $ELIXIR_VERSION

# Set the working directory inside the container
WORKDIR /app

# Copy the current directory into the container
COPY . /app

# Install project dependencies
RUN source ~/.asdf/asdf.sh && \
    mix local.hex --force && \
    mix local.rebar --force && \
    mix deps.get

# Clean up any previous build artifacts
RUN rm -rf /app/_build/prod/rel/hello_lambda

# Build the Elixir release
RUN source ~/.asdf/asdf.sh && \
    MIX_ENV=prod mix release

# Package the release into function.zip after moving the bootstrap file
RUN cd _build/prod/rel/hello_lambda && \
    mv /app/bootstrap . && \
    zip -r /app/function.zip *

# Default entry point to start shell for testing
CMD ["/bin/bash"]

I am mainly just wondering if anyone can see some obviously superior approach within the bounds I set for myself, perhaps configuring a dependency like burrito correctly. Im sure I could, and probably should, use a different approach if I was trying to accomplish something important, but for this I just want to experiment with AWS and custom runtimes.

I will attach the files from my project here for anyone to examine/replicate if they see fit.

mix.exs:

defmodule HelloLambda.MixProject do
  use Mix.Project

  def project do
    [
      app: :hello_lambda,
      version: "0.1.0",
      elixir: "~> 1.17",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      releases: [
        hello_lambda: [
          include_erts: true,
          include_executables_for: [:unix],
          applications: [runtime_tools: :permanent]
        ]
      ]
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger, :inets, :crypto, :public_key, :ssl],
      mod: {HelloLambda.Application, []}
    ]
  end


  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:jason, "~> 1.2"}
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end
end

application.ex:

defmodule HelloLambda.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Task, fn -> HelloLambda.Runtime.start() end}
    ]

    opts = [strategy: :one_for_one, name: HelloLambda.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

runtime.ex:

defmodule HelloLambda.Runtime do
  require Logger

  def start do
    {:ok, _} = Application.ensure_all_started(:inets)
    loop()
  end

  defp loop do
    case get_invocation() do
      {:ok, request_id, event} ->
        response = HelloLambda.Handler.handle(event)
        send_response(request_id, response)
        loop()

      {:error, reason} ->
        Logger.error("Failed to get invocation: #{inspect(reason)}")
        :timer.sleep(1000)
        loop()
    end
  end

  defp get_invocation do
    runtime_api = System.get_env("AWS_LAMBDA_RUNTIME_API")
    url = "http://#{runtime_api}/2018-06-01/runtime/invocation/next"

    case :httpc.request(:get, {String.to_charlist(url), []}, [], []) do
      {:ok, {{'HTTP/1.1', 200, 'OK'}, headers, body}} ->
        request_id = get_header_value(headers, 'lambda-runtime-aws-request-id')
        event = Jason.decode!(body)
        {:ok, request_id, event}

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp send_response(request_id, response) do
    runtime_api = System.get_env("AWS_LAMBDA_RUNTIME_API")
    url = "http://#{runtime_api}/2018-06-01/runtime/invocation/#{request_id}/response"
    body = Jason.encode!(response)

    :httpc.request(
      :post,
      {String.to_charlist(url), [], 'application/json', body},
      [],
      []
    )
  end

  defp get_header_value(headers, key) do
    headers
    |> Enum.find(fn {k, _} -> String.downcase(to_string(k)) == String.downcase(to_string(key)) end)
    |> elem(1)
    |> to_string()
  end
end

handler:

defmodule HelloLambda.Handler do
  def handle(event) do
    %{"message" => "Hello, World!", "event" => event}
  end
end

Where Next?

Popular in Questions Top

JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
Kagamiiiii
Student & New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement