Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@
# You can customize the priority of any check
# Priority values are: `low, normal, high, higher`
#
{Credo.Check.Design.AliasUsage, [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.AliasUsage,
[priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]},
{Credo.Check.Design.TagFIXME, []},
# You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just
Expand Down
3 changes: 2 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Used by "mix format"
[
plugins: [Styler],
inputs: ["{mix,.formatter,.iex,.credo}.exs", "{config,lib,test,bench,scripts}/**/*.{ex,exs,eex}"]
inputs: ["{mix,.formatter,.iex,.credo}.exs", "{config,lib,test,bench,scripts}/**/*.{ex,exs,eex}"],
line_length: 100
]
4 changes: 2 additions & 2 deletions .github/workflows/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
otp: ["26.2"]
elixir: ["1.18.1", "1.17.3", "1.16.3"]
otp: ["28.3", "27.3"]
elixir: ["1.19.x", "1.18.x"]
steps:
- name: Git clone the repository
uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir ref:v1.18.1
erlang 27.2
elixir 1.19.4-otp-28
erlang 28.3
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

This changelog follows the same style that I have seen LiveView, Phoenix, and Elixir use in the past. I'll try to make sure that I maintain it - probably should create some sort of automated process for it... who knows. For now - there's only one release so this should be good enough!

## 1.1.0

### Enhancements

- Add `:release_dir` configuration option to customize the deployment directory for Elixir releases. This allows matching the directory path expected by your release bundle. Defaults to `/srv/<app>/release` for backward compatibility.

- Add ARM architecture support for AWS CLI installation in the bootstrap script. The startup script now automatically detects the instance architecture (x86_64 or aarch64/arm64) and downloads the appropriate AWS CLI version, enabling support for Graviton-based EC2 instances.

## 1.0.0

This is the first official release! So everything is empty. Read the docs to get started - have fun!
Expand Down
19 changes: 14 additions & 5 deletions lib/flame_ec2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@

@impl true
# The following TODO is from `FLAME.FlyBackend`. We should track it to ensure that we mirror the behavior properly.
# TODO explore spawn_request

Check warning on line 191 in lib/flame_ec2.ex

View workflow job for this annotation

GitHub Actions / pipeline (27.3, 1.19.x)

Found a TODO tag in a comment: # TODO explore spawn_request

Check warning on line 191 in lib/flame_ec2.ex

View workflow job for this annotation

GitHub Actions / pipeline (28.3, 1.18.x)

Found a TODO tag in a comment: # TODO explore spawn_request

Check warning on line 191 in lib/flame_ec2.ex

View workflow job for this annotation

GitHub Actions / pipeline (28.3, 1.19.x)

Found a TODO tag in a comment: # TODO explore spawn_request

Check warning on line 191 in lib/flame_ec2.ex

View workflow job for this annotation

GitHub Actions / pipeline (27.3, 1.18.x)

Found a TODO tag in a comment: # TODO explore spawn_request
def remote_spawn_monitor(%BackendState{} = state, term) do
case term do
func when is_function(func, 0) ->
Expand Down Expand Up @@ -222,14 +222,17 @@
EC2Api.run_instances!(state)
end)

Utils.log(state.config, "#{inspect(__MODULE__)} #{inspect(node())} EC2 instance created in #{req_connect_time}ms")
Utils.log(
state.config,
"#{inspect(__MODULE__)} #{inspect(node())} EC2 instance created in #{req_connect_time}ms"
)

remaining_connect_window = state.config.boot_timeout - req_connect_time

case resp do
%{"instanceId" => instance_id, "privateIpAddress" => ip} ->
new_state =
%BackendState{
%{
state
| runner_instance_id: instance_id,
runner_instance_ip: ip
Expand All @@ -241,12 +244,14 @@
remote_terminator_pid
after
remaining_connect_window ->
Logger.error("failed to connect to EC2 instance within #{state.config.boot_timeout}ms")
Logger.error(
"failed to connect to EC2 instance within #{state.config.boot_timeout}ms"
)

exit(:timeout)
end

new_state = %BackendState{
new_state = %{
new_state
| remote_terminator_pid: remote_terminator_pid,
runner_node_name: node(remote_terminator_pid)
Expand All @@ -261,7 +266,11 @@

@impl true
def handle_info(msg, %BackendState{} = state) do
Utils.log(state.config, "Missed message sent to FlameEC2 Process #{self()}: #{inspect(msg)}")
Utils.log(
state.config,
"Missed message sent to FlameEC2 Process #{inspect(self())}: #{inspect(msg)}"
)

{:noreply, state}
end
end
13 changes: 10 additions & 3 deletions lib/flame_ec2/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule FlameEC2.Config do
:boot_timeout,
:app,
:s3_bundle_url,
:release_dir,
:instance_metadata_url,
:instance_metadata_token_url,
:ec2_service_endpoint,
Expand All @@ -47,6 +48,7 @@ defmodule FlameEC2.Config do
:app,
:s3_bundle_url,
:s3_bundle_compressed?,
:release_dir,
:local_ip
]}

Expand All @@ -66,6 +68,7 @@ defmodule FlameEC2.Config do
app: nil,
s3_bundle_url: nil,
s3_bundle_compressed?: false,
release_dir: nil,
local_ip: nil,
instance_metadata_url: nil,
instance_metadata_token_url: nil,
Expand Down Expand Up @@ -164,7 +167,7 @@ defmodule FlameEC2.Config do
end

defp validate_s3_bundle_url!(%Config{} = config) do
%Config{config | s3_bundle_compressed?: String.ends_with?(config.s3_bundle_url, ".tar.gz")}
%{config | s3_bundle_compressed?: String.ends_with?(config.s3_bundle_url, ".tar.gz")}
end

defp validate_local_ip!(%Config{local_ip: nil}) do
Expand All @@ -180,11 +183,15 @@ defmodule FlameEC2.Config do
"You must specify either the image_id or the launch_template_id for the FlameEC2 backend"
end

defp validate_instance_creation_details!(%Config{image_id: _image_id, launch_template_id: nil} = config) do
defp validate_instance_creation_details!(
%Config{image_id: _image_id, launch_template_id: nil} = config
) do
config
end

defp validate_instance_creation_details!(%Config{image_id: nil, launch_template_id: _launch_template_id} = config) do
defp validate_instance_creation_details!(
%Config{image_id: nil, launch_template_id: _launch_template_id} = config
) do
config
end

Expand Down
11 changes: 7 additions & 4 deletions lib/flame_ec2/ec2_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ defmodule FlameEC2.EC2Api do
form: params,
aws_sigv4: Map.put_new(credentials, :service, "ec2")
]
|> Req.new()
|> Req.request()
|> raise_or_response!()
|> Map.fetch!(:body)
Expand Down Expand Up @@ -71,7 +70,9 @@ defmodule FlameEC2.EC2Api do
end

defp params_from_config(%Config{} = config, env) do
systemd_service = Templates.systemd_service(app: config.app)
systemd_service =
Templates.systemd_service(app: config.app, release_dir: config.release_dir)

env = Templates.env(vars: env)

start_script =
Expand All @@ -81,7 +82,8 @@ defmodule FlameEC2.EC2Api do
env: env,
aws_region: config.aws_region,
s3_bundle_url: config.s3_bundle_url,
s3_bundle_compressed?: config.s3_bundle_compressed?
s3_bundle_compressed?: config.s3_bundle_compressed?,
release_dir: config.release_dir
)

base_params = %{
Expand Down Expand Up @@ -121,7 +123,8 @@ defmodule FlameEC2.EC2Api do
}
end

defp creation_details_params(%Config{image_id: image_id}) when is_binary(image_id) and image_id != "" do
defp creation_details_params(%Config{image_id: image_id})
when is_binary(image_id) and image_id != "" do
%{
"ImageId" => image_id
}
Expand Down
22 changes: 19 additions & 3 deletions lib/flame_ec2/templates.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ defmodule FlameEC2.Templates do
@templates_path Path.absname("./templates", __DIR__)

@type systemd_assign() ::
{:app, atom() | String.t()} | {:custom_start_command, String.t()} | {:custom_stop_command, String.t()}
{:app, atom() | String.t()}
| {:release_dir, String.t() | nil}
| {:custom_start_command, String.t()}
| {:custom_stop_command, String.t()}
@spec systemd_service([systemd_assign()]) :: String.t()
def systemd_service(assigns) do
systemd_service_template(assigns)
Expand All @@ -25,12 +28,25 @@ defmodule FlameEC2.Templates do
| {:aws_region, String.t()}
| {:s3_bundle_url, String.t()}
| {:s3_bundle_compressed?, boolean()}
| {:release_dir, String.t() | nil}
@spec start_script([start_script_assign()]) :: String.t()
def start_script(assigns) do
start_script_template(assigns)
end

EEx.function_from_file(:defp, :systemd_service_template, Path.join(@templates_path, "systemd.service.eex"), [:assigns])
EEx.function_from_file(
:defp,
:systemd_service_template,
Path.join(@templates_path, "systemd.service.eex"),
[:assigns]
)

EEx.function_from_file(:defp, :env_template, Path.join(@templates_path, "env.eex"), [:assigns])
EEx.function_from_file(:defp, :start_script_template, Path.join(@templates_path, "start.sh.eex"), [:assigns])

EEx.function_from_file(
:defp,
:start_script_template,
Path.join(@templates_path, "start.sh.eex"),
[:assigns]
)
end
23 changes: 21 additions & 2 deletions lib/flame_ec2/templates/start.sh.eex
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,23 @@ install_aws_cli() {
return 0
fi

log "Installing AWS CLI..."
curl -s "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
# Detect architecture
ARCH=$(uname -m)
case $ARCH in
x86_64)
AWS_CLI_ARCH="x86_64"
;;
aarch64|arm64)
AWS_CLI_ARCH="aarch64"
;;
*)
log "Unsupported architecture: $ARCH"
exit 1
;;
esac

log "Installing AWS CLI for architecture: $AWS_CLI_ARCH..."
curl -s "https://awscli.amazonaws.com/awscli-exe-linux-${AWS_CLI_ARCH}.zip" -o "awscliv2.zip"
unzip -q awscliv2.zip
./aws/install --update >/dev/null 2>&1
rm -rf aws awscliv2.zip
Expand All @@ -94,7 +109,11 @@ aws configure set default.region <%= @aws_region %>
S3_URL=<%= @s3_bundle_url %>
APP_DIR="/srv/<%= @app %>"
SERVICE_NAME=<%= @app %>
<%= if assigns[:release_dir] do %>
RELEASE_DIR="<%= @release_dir %>"
<% else %>
RELEASE_DIR="${APP_DIR}/release"
<% end %>

mkdir -p "${APP_DIR}" "${RELEASE_DIR}" || {
log "Failed to create required directories"
Expand Down
7 changes: 4 additions & 3 deletions lib/flame_ec2/templates/systemd.service.eex
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
<% release_dir = if assigns[:release_dir], do: @release_dir, else: "/srv/#{@app}/release" %>
[Unit]
Description=<%= @app %> service
After=local-fs.target network.target

[Service]
Type=simple
WorkingDirectory=/srv/<%= @app %>/release
WorkingDirectory=<%= release_dir %>

<%= if assigns[:custom_start_command] do %>
ExecStart=<%= @custom_start_command %>
<% else %>
ExecStart=/srv/<%= @app %>/release/bin/<%= @app %> start
ExecStart=<%= release_dir %>/bin/<%= @app %> start
<% end %>

<%= if assigns[:custom_stop_command] do %>
ExecStop=<%= @custom_stop_command %>
<% else %>
ExecStop=/srv/<%= @app %>/release/bin/<%= @app %> stop
ExecStop=<%= release_dir %>/bin/<%= @app %> stop
<% end %>

Environment=LANG=en_US.utf8
Expand Down
13 changes: 10 additions & 3 deletions mix.exs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
defmodule FlameEC2.MixProject do
use Mix.Project

@version "1.1.0"

def project do
[
app: :flame_ec2,
version: "1.0.0",
version: @version,
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_path(Mix.env()),
Expand All @@ -22,8 +24,13 @@ defmodule FlameEC2.MixProject do
aliases: aliases(),
docs: docs(),
dialyzer: [plt_file: {:no_warn, "priv/plts/dialyzer.plt"}, plt_add_deps: :app_tree],
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
test_coverage: [tool: ExCoveralls]
]
end

def cli do
[
preferred_envs: [
ci: :test,
coveralls: :test,
"coveralls.detail": :test,
Expand Down
Loading