├── .gitignore ├── LICENSE ├── README.md ├── lib └── mix │ └── tasks │ └── erllambda.ex ├── mix.exs └── priv └── templates └── bootstrap /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | mix_erllambda-*.tar 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alert Logic, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MixErllambda 2 | 3 | Elixir OTP release packaging for AWS Lambda. 4 | 5 | ## Installation 6 | 7 | The package can be installed by adding `mix_erllambda` to your list of 8 | dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:mix_erllambda, "~> 1.1"} 14 | ] 15 | end 16 | ``` 17 | 18 | ## Usage 19 | 20 | Just add as a mix dependency and use `mix erllambda.release`. 21 | 22 | ### erllambda.release 23 | 24 | Build a release package suitable for AWS Lambda deployment. 25 | 26 | This task heavily utilises distillery to build application. Once release is 27 | built, task applies additional overlays that are necessary to bootstrap 28 | release as AWS Lambda environment. At the end it creates a zip package 29 | suitable for AWS lambda deployment with a provided environment. 30 | 31 | Follow distillery release usage examples to init release for the project: 32 | 33 | # init release 34 | mix distillery.init 35 | 36 | mkdir config 37 | echo "use Mix.Config" > config/config.exs 38 | 39 | To create package run erllambda.release with MIX_ENV set to the Mix 40 | environment you are targeting: 41 | 42 | # Builds a release package with MIX_ENV=dev (the default) 43 | mix erllambda.release 44 | 45 | # Builds a release package with MIX_ENV=prod 46 | MIX_ENV=prod mix erllambda.release 47 | 48 | # Builds a release package for a specific release environment 49 | MIX_ENV=prod mix erllambda.release --env=dev 50 | 51 | ### Details 52 | 53 | Task is built on top of distillery release and has the same command line 54 | interface. Please note that some of use cases (such as release upgrades) are 55 | not supported as they don't make sense in AWS lambda universe. 56 | 57 | For full list of available options please read release documentation: 58 | 59 | # mix help release 60 | 61 | See [Elixir example](https://github.com/alertlogic/erllambda_elixir_example) for details. 62 | -------------------------------------------------------------------------------- /lib/mix/tasks/erllambda.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Erllambda.Release do 2 | use Mix.Task 3 | 4 | @shortdoc "Build lambda release of project" 5 | @moduledoc """ 6 | Build a release package suitable for AWS Lambda deployment. 7 | 8 | This task heavily utilises distillery to build application. Once release is 9 | built, task applies additional overlays that are necessary to bootstrap 10 | release as AWS Lambda environment. At the end it creates a zip package 11 | suitable for AWS lambda deployment with a provided environment. 12 | 13 | ## Usage 14 | 15 | Follow distillery release usage examples to init release for the project: 16 | 17 | # init release 18 | mix distillery.init 19 | mkdir config 20 | echo "use Mix.Config" > config/config.exs 21 | 22 | To create package run erllambda.release with MIX_ENV set to the Mix 23 | environment you are targeting: 24 | 25 | # Builds a release package with MIX_ENV=dev (the default) 26 | mix erllambda.release 27 | 28 | # Builds a release package with MIX_ENV=prod 29 | MIX_ENV=prod mix erllambda.release 30 | 31 | # Builds a release package for a specific release environment 32 | MIX_ENV=prod mix erllambda.release --env=dev 33 | 34 | ## Details 35 | 36 | Task is built on top of distillery release and has the same command line 37 | interface. Please note that some of use cases (such as release upgrades) are 38 | not supported as they don't make sense in AWS lambda universe. 39 | 40 | For full list of available options please read release documentation: 41 | 42 | # mix help release 43 | """ 44 | alias Distillery.Releases.{Release, Config, Assembler, Overlays, Shell, Utils, Errors} 45 | 46 | def run(args) do 47 | # parse options 48 | opts = Mix.Tasks.Distillery.Release.parse_args(args) 49 | verbosity = Keyword.get(opts, :verbosity) 50 | Shell.configure(verbosity) 51 | 52 | # make sure we've compiled latest 53 | Mix.Task.run("compile", []) 54 | # make sure loadpaths are updated 55 | Mix.Task.run("loadpaths", []) 56 | 57 | # load release configuration 58 | Shell.debug("Loading configuration..") 59 | 60 | case Config.get(opts) do 61 | {:error, {:config, :not_found}} -> 62 | Shell.error("You are missing a release config file. Run the release.init task first") 63 | System.halt(1) 64 | 65 | {:error, {:config, reason}} -> 66 | Shell.error("Failed to load config:\n #{reason}") 67 | System.halt(1) 68 | 69 | {:ok, config} -> 70 | do_release(config) 71 | end 72 | end 73 | 74 | defp do_release(config) do 75 | with {:ok, release} <- assemble(config), 76 | {:ok, release} <- apply_overlays(release), 77 | {:ok, _release} <- package(release) do 78 | :ok 79 | else 80 | {:error, _reason} = error -> 81 | Shell.error(format_error(error)) 82 | System.halt(1) 83 | end 84 | rescue 85 | e -> 86 | Shell.error( 87 | "Release failed: #{Exception.message(e)}\n" <> 88 | Exception.format_stacktrace(System.stacktrace()) 89 | ) 90 | 91 | System.halt(1) 92 | end 93 | 94 | @spec assemble(Config.t()) :: {:ok, Release.t()} | {:error, term} 95 | defp assemble(config) do 96 | Shell.info("Assembling release..") 97 | Assembler.assemble(config) 98 | end 99 | 100 | @spec apply_overlays(Release.t()) :: {:ok, Release.t()} | {:error, term} 101 | defp apply_overlays(release) do 102 | Shell.info("Applying lambda specific overlays..") 103 | 104 | lambda_overlays = 105 | [{:template, template_path("bootstrap"), "bootstrap"}] 106 | 107 | overlays = lambda_overlays ++ release.profile.overlays 108 | output_dir = release.profile.output_dir 109 | overlay_vars = release.profile.overlay_vars 110 | 111 | with {:ok, _paths} <- Overlays.apply(output_dir, overlays, overlay_vars), 112 | # distillery overlays do not preserve files flags 113 | :ok <- make_executable(Path.join(output_dir, "bootstrap")), 114 | do: {:ok, release} 115 | end 116 | 117 | defp make_executable(path) do 118 | case File.chmod(path, 0o755) do 119 | :ok -> 120 | :ok 121 | {:error, reason} -> 122 | {:error, {:chmod, path, reason}} 123 | end 124 | end 125 | 126 | @spec package(Release.t()) :: {:ok, Release.t()} | {:error, term} 127 | defp package(release) do 128 | Shell.info("Packaging release..") 129 | with {:ok, tmpdir} <- Utils.insecure_mkdir_temp(), 130 | tmp_package_path = package_path(release, tmpdir), 131 | :ok <- make_package(release, tmp_package_path), 132 | :ok <- file_cp(tmp_package_path, package_path(release)), 133 | _ <- File.rm_rf(tmpdir), 134 | do: {:ok, release} 135 | end 136 | 137 | defp file_cp(src, dst) do 138 | case File.cp(src, dst) do 139 | :ok -> 140 | :ok 141 | {:error, reason} -> 142 | {:error, {:file_copy, {src, dst}, reason}} 143 | end 144 | end 145 | 146 | defp make_package(release, zip_path) do 147 | release_dir = Path.expand(release.profile.output_dir) 148 | targets = targets(release) 149 | exclusions = 150 | archive_paths(release) 151 | |> Enum.map(&(Path.relative_to(&1, release_dir))) 152 | 153 | case make_zip(zip_path, release_dir, targets, exclusions) do 154 | :ok -> 155 | Shell.debug("Successfully built zip package: #{zip_path}") 156 | {:error, reason} -> 157 | {:error, {:make_zip, reason}} 158 | end 159 | end 160 | 161 | defp make_zip(zip_path, cwd, targets, exclusions) do 162 | args = ["-q", "-r", zip_path] ++ targets ++ ["-x" | exclusions] 163 | command = "zip #{args |> Enum.join(" ")}" 164 | Shell.debug("$ #{command}") 165 | case System.cmd("zip", args, cd: cwd) do 166 | {_output, 0} -> 167 | :ok 168 | {output, exit_code} -> 169 | {:error, {command, exit_code, output}} 170 | end 171 | end 172 | 173 | defp targets(release), do: [ 174 | "erts-#{release.profile.erts_version}", 175 | "bin", 176 | "lib", 177 | "releases", 178 | "bootstrap" 179 | ] 180 | 181 | defp archive_paths(release), do: [ 182 | Path.join(Release.version_path(release), "*.tar.gz"), 183 | Path.join(Release.version_path(release), "*.zip"), 184 | Path.join(Release.bin_path(release), "*.run") 185 | ] 186 | 187 | @spec package_path(Release.t()) :: String.t() 188 | defp package_path(release), do: package_path(release, Release.version_path(release)) 189 | 190 | defp package_path(release, base_dir), do: Path.join(base_dir, package_name(release)) 191 | 192 | defp package_name(release), do: "#{release.name}.zip" 193 | 194 | defp priv_file(path), do: Path.join("#{:code.priv_dir(:mix_erllambda)}", path) 195 | 196 | defp template_path(name), do: priv_file(Path.join("templates", name)) 197 | 198 | @spec format_error(term()) :: String.t() 199 | defp format_error(error) 200 | 201 | defp format_error({:error, {:chmod, path, reason}}) do 202 | "Failed to change mode of a file #{path}\n #{:file.format_error(reason)}" 203 | end 204 | 205 | defp format_error({:error, {:file_copy, {src, dst}, reason}}) do 206 | "Failed to copy file: #{:file.format_error(reason)}\n" <> 207 | " source: #{src}\n" <> 208 | " destination: #{dst}" 209 | end 210 | 211 | defp format_error({:error, {:make_zip, {command, exit_code, output}}}) do 212 | "Zip packaging exited with code #{exit_code}:\n" <> 213 | " $ #{command}\n" <> 214 | " " <> String.trim(output) 215 | end 216 | 217 | defp format_error(err), do: Errors.format_error(err) 218 | end 219 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule MixErllambda.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :mix_erllambda, 7 | version: "1.1.0", 8 | elixir: "~> 1.7", 9 | start_permanent: Mix.env() == :prod, 10 | description: "Elixir OTP release packaging for AWS Lambda", 11 | package: package(), 12 | deps: deps() 13 | ] 14 | end 15 | 16 | def application do 17 | [ 18 | ] 19 | end 20 | 21 | defp deps do 22 | [ 23 | {:distillery, "~> 2.1"}, 24 | {:ex_doc, ">= 0.0.0", only: :dev} 25 | ] 26 | end 27 | 28 | defp package() do 29 | [ 30 | licenses: ["MIT"], 31 | links: %{"GitHub" => "https://github.com/alertlogic/mix_erllambda"} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /priv/templates/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | unset CDPATH 6 | 7 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" 8 | 9 | export HOME=$SCRIPT_DIR 10 | export RELEASE_READ_ONLY=1 11 | export RELEASE_MUTABLE_DIR=/tmp 12 | 13 | exec "$SCRIPT_DIR/bin/<%= release_name %>" foreground 14 | --------------------------------------------------------------------------------