Simple Rails deployment using CDK
10 mins
-
"2021-04-12T19:05:27.220Z"
Introduction
In this post I'm going to create a very simple Ruby on Rails application and deploy it using AWS CDK. This has been done in a few places but I have a few difference goals here:
- Create something that can be deployed as cheaply as possible. I really want to just be able to create a quick rails app and get it on the web, somewhat like Heroku, but using AWS, more for bootstrapping an MVP or something as quickly as possible.
- I don't care about High Availability, redundancy etc. If things break, we'll just redeploy.
- To learn more about CDK and few AWS features I haven't played with much at work, like SSM.
Setup
Create a directory called cdk-blog
or whatever you want to be the base directory of the application and deploy code. The structure I'll use will look like:
cdk-blog- blog # the actual application we're deploying- deploy # the cdk deploy code
For a great introduction to CDK see: https://dev.to/emmanuelnk/series/11689
Create the Rails application
$ rails new blogcreatecreate README.mdcreate Rakefilecreate .ruby-versioncreate config.rucreate .gitignorecreate .gitattributes...├─ websocket-extensions@0.1.4└─ ws@6.2.1✨ Done in 8.17s.Webpacker successfully installed 🎉 🍰
Now lets get this thing to run on docker. If you're looking for a great guide on 'Dockerizing' Rails, check out this post by Evil Martian. I'm not using exactly what the Evil Martians created, but this works for me for the purposes of this post:
ARG BASE_IMAGE_VERSION=2.7FROM ruby:$BASE_IMAGE_VERSION-alpine as builderARG BUNDLE_WITHOUTARG RAILS_ENVENV BUNDLE_WITHOUT ${BUNDLE_WITHOUT}ENV RAILS_ENV ${RAILS_ENV}ENV SECRET_KEY_BASE=foobarENV RAILS_SERVE_STATIC_FILES=trueRUN apk add --update --no-cache \build-base \postgresql-dev \git \nodejs \yarn \tzdataWORKDIR /app# Install gemsRUN gem install bundlerADD Gemfile* /app/RUN bundle install -j4 --retry 3 \# Remove unneeded files (cached *.gem, *.o, *.c)&& rm -rf /usr/local/bundle/cache/*.gem \&& find /usr/local/bundle/gems/ -name "*.c" -delete \&& find /usr/local/bundle/gems/ -name "*.o" -delete# Add the Rails appADD . /app# Precompile assets# RUN bundle exec rake assets:precompile################################ Stage finalFROM ruby:$BASE_IMAGE_VERSION-alpineMAINTAINER brendan.grainger@gmail.comARG ADDITIONAL_PACKAGES# Add Alpine packagesRUN apk add --update --no-cache \postgresql-client \$ADDITIONAL_PACKAGES \tzdata \file# Add userRUN addgroup -g 1000 -S app \&& adduser -u 1000 -S app -G appUSER app# Copy app with gems from former build stageCOPY --from=builder /usr/local/bundle/ /usr/local/bundle/COPY --from=builder --chown=app:app /app /app# Set Rails envENV RAILS_LOG_TO_STDOUT trueENV RAILS_SERVE_STATIC_FILES trueWORKDIR /app# Expose Puma portEXPOSE 3000# Save timestamp of image buildingRUN date -u > BUILD_TIME
I like to make sure it's all running ok locally. Create docker-compose.yml
file and add the following:
# See: https://codetales.io/blog/publishing-ports-the-right-way#version: '3.7'services:db:image: postgres:11-alpineenvironment:- POSTGRES_PASSWORD=passwordvolumes:- db-data:/var/lib/postgresql/dataports:- 127.0.0.1:${PG_PORT}:5432app:#user: rootimage: rainkinz/blog-app:latestbuild:context: .args:# Install dev and test gems- BUNDLE_WITHOUT=productiontty: true # do we need this?stdin_open: true # do we need thisvolumes:- type: bindsource: ./target: /appconsistency: cachedenvironment:- RAILS_ENV- DB_USER=postgres- DB_PASSWORD=password- POSTGRES_PASSWORD=password- DB_HOST=db- SECRET_KEY_BASE=we-dont-need-a-secret-hereports:- 127.0.0.1:${APP_PORT:-3000}:3000links:- dbcommand: ["rails", "server", "-b", "0.0.0.0", "-p", "${MY_APP_PORT:-3000}"]#volumes:db-data:
$ docker-compose upWARNING: The PG_PORT variable is not set. Defaulting to a blank string.Creating network "blog_default" with the default driverCreating volume "blog_db-data" with default driverPulling db (postgres:11-alpine)...11-alpine: Pulling from library/postgresca3cd42a7c95: Pull completea0d003399a5b: Pull complete0a08de1ad3ba: Pull completed339911efe38: Extracting [> ] 557.1kB/57.72MB223f635ad1dd: Download complete
Creating the deploy
I am using AWS CDK to deploy the application.
First up install CDK:
$ npm install -g aws-cdk
Now create the application in our cdk-rails
base directory:
$ mkdir deploy && cd deploy$ cdk init --language=typescript# Welcome to your CDK TypeScript project!This is a blank project for TypeScript development with CDK.The `cdk.json` file tells the CDK Toolkit how to execute your app....
This will create a 'blank' Typescript based CDK application, with the following level folder structure (ignoring node_modules
):
.├── README.md├── bin│ └── deploy.ts├── cdk.json├── jest.config.js├── lib│ └── deploy-stack.ts├── node_modules├── package-lock.json├── package.json├── test│ └── tmp.test.ts└── tsconfig.json
For not the 2 files we'll work with mostly are:
bin/deploy.ts
This contains the deployment 'glue' code.
#!/usr/bin/env nodeimport 'source-map-support/register';import * as cdk from '@aws-cdk/core';import { DeployStack } from '../lib/deploy-stack';const app = new cdk.App();new DeployStack(app, 'DeployStack', {/* If you don't specify 'env', this stack will be environment-agnostic.* Account/Region-dependent features and context lookups will not work,* but a single synthesized template can be deployed anywhere. *//* Uncomment the next line to specialize this stack for the AWS Account* and Region that are implied by the current CLI configuration. */// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },/* Uncomment the next line if you know exactly what Account and Region you* want to deploy the stack to. */// env: { account: '123456789012', region: 'us-east-1' },/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */});
lib/deploy-stack.ts
Which is where we'll write most of the deployment code for now. We will refactor later though. It looks like this:
import * as cdk from '@aws-cdk/core';export class DeployStack extends cdk.Stack {constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {super(scope, id, props);// The code that defines your stack goes here}}
Install some dependencies we'll need:
npm install --save @aws-cdk/aws-iam @aws-cdk/aws-ecr-assets
AWS_PROFILE=kuripai_dev cdk deploy --all
Notice, I always explicitly use a profile or set it using .envrc
. I prefer to do this than accidentally deploy to production or something...