Profile picture Schedule a Meeting
c a n d l a n d . n e t

Lucky Framework deployment with Apex Up

Dusty Candland | | lucky, luckyframework, apexup, aws, crystal

Hosting a Lucky Framework application on AWS Lambda using Apex Up.

Crystal Version: 0.35.1 Lucky Version: 0.25

Building

The app needs to be build in a docker container. Crystal provides an image that we can use.

I used a make file to run on the docker image. I'm building an API and don't need Node/Yarn. Might need a custom docker image with those installed.

# Makefile
.PHONY: help

IS_PROD := $(filter prod, $(MAKECMDGOALS))
STAGE := $(if $(IS_PROD),prod,development)

help:
	@perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

prod: ## Set the deploy target to prod
	@echo "Setting Prod"

build: ## Build server.go
	shards install
	crystal build -o server src/start_server.cr --release --static

clean: ## Remove exec
	rm -f server

Apex Up Configuration

Apex docs are good, but you need to setup your VPC info before the first up run.

There are environment secrets in this file with the OSS version. The Pro version allows encrypted environment variables.

up.json configuration file

{
  "name": "my-app",
  "profile": "my_aws_profile",
  "regions": [
    "us-west-2"
  ],
  "environment": {
    "LUCKY_ENV": "production",
    "SECRET_KEY_BASE": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=",
    "DATABASE_URL": "postgresql://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.us-west-2.rds.amazonaws.com/my_app_staging",
    "SEND_GRID_KEY": "unused",
    "APP_DOMAIN": "https://xxxxxxxxxxxxxxxxxxxxxx.us-west-2.amazonaws.com"
  },
  "hooks": {
    "build": "docker run --rm -v $(pwd):/src -w /src crystallang/crystal make prod clean build",
    "clean": "rm server"
  },
  "lambda": {
    "memory": 512,
    "vpc": {
      "subnets": [
        "subnet-xxxxxxxx",
        "subnet-xxxxxxxx",
        "subnet-xxxxxxxx",
        "subnet-xxxxxxxx"
      ],
      "security_groups": [
        "sg-xxxxxxxxxxxxxxxxx"
      ]
    }
  }
}

The APP_DOMAIN environment variable won't be known until after the first deployment, update and then redeploy.

Add an .upignore file to exclude everything except the server binary and the ./config/watch.yml file. The yml file shouldn't be needed, but it seemed to error without it.

*
!./server
!./config/watch.yml

Database setup

I created a DB using AWS RDS service. Not sure why it didn't create a database, but I had to do that manually.

  • Create an EC2 server and SSH to it.
  • Install postgres-client
  • Make sure the postgres security group allows connections from the VPC ip range.
  • Connect to the postgres database & create the database
sudo apt-get update
sudo apt-get install postgresql-client

psql "postgresql://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.us-west-2.rds.amazonaws.com/postgres"
create database my_app_staging;

Lucky setup

Need to setup the DB when the app starts. Not the most effienct way, but I added the DB setup to the start_server.cr file.

  • Setup migration tracking tables
  • Run migrations
  • Ensure everything is good
  • Run the required data seeds
# src/start_server.cr
require "./app"
require "../tasks/db/seed/required_data.cr"

Habitat.raise_if_missing_settings!

if Lucky::Env.development?
  Avram::Migrator::Runner.new.ensure_migrated!
  Avram::SchemaEnforcer.ensure_correct_column_mappings!
end

if Lucky::Env.production?
  Avram::Migrator::Runner.setup_migration_tracking_tables
  Db::Migrate.new(quiet: true).call
  Avram::Migrator::Runner.new.ensure_migrated!
  Db::Seed::RequiredData.new.call
end

app_server = AppServer.new
puts "Listening on http://#{app_server.host}:#{app_server.port}"

Signal::INT.trap do
  app_server.close
end

app_server.listen

Deploy

Run the up command to deploy.

Webmentions

These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: