Skip to content

Commit

Permalink
Add an AWS EC2 Ruby on Rails example
Browse files Browse the repository at this point in the history
This ports the official AWS CloudFormation Application Framework
template for a Ruby on Rails server that uses EC2 and a MySQL local
database. I've implemented some experimental support for
AWS::CloudFormation::Init-like config-sets, that install files and
packages, and run scripts and services, on the VM during startup.
  • Loading branch information
joeduffy committed Jul 29, 2018
1 parent 1f59c71 commit c658a76
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 0 deletions.
3 changes: 3 additions & 0 deletions aws-ts-ruby-on-rails/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: rails
runtime: nodejs
description: A Ruby on Rails stack using a single EC2 instance with a local MySQL database for storage.
5 changes: 5 additions & 0 deletions aws-ts-ruby-on-rails/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# AWS EC2 Ruby on Rails

This is a conversion of the AWS CloudFormation Application Framework template for a basic Ruby on Rails server.
It creates a single EC2 instance and uses a local MySQL database for storage.
See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/sample-templates-appframeworks-us-west-2.html.
95 changes: 95 additions & 0 deletions aws-ts-ruby-on-rails/ami.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as aws from "@pulumi/aws";

const instanceTypeToArch: {[instanceType: string]: string} = {
"t1.micro" : "PV64" ,
"t2.nano" : "HVM64",
"t2.micro" : "HVM64",
"t2.small" : "HVM64",
"t2.medium" : "HVM64",
"t2.large" : "HVM64",
"m1.small" : "PV64" ,
"m1.medium" : "PV64" ,
"m1.large" : "PV64" ,
"m1.xlarge" : "PV64" ,
"m2.xlarge" : "PV64" ,
"m2.2xlarge" : "PV64" ,
"m2.4xlarge" : "PV64" ,
"m3.medium" : "HVM64",
"m3.large" : "HVM64",
"m3.xlarge" : "HVM64",
"m3.2xlarge" : "HVM64",
"m4.large" : "HVM64",
"m4.xlarge" : "HVM64",
"m4.2xlarge" : "HVM64",
"m4.4xlarge" : "HVM64",
"m4.10xlarge" : "HVM64",
"c1.medium" : "PV64" ,
"c1.xlarge" : "PV64" ,
"c3.large" : "HVM64",
"c3.xlarge" : "HVM64",
"c3.2xlarge" : "HVM64",
"c3.4xlarge" : "HVM64",
"c3.8xlarge" : "HVM64",
"c4.large" : "HVM64",
"c4.xlarge" : "HVM64",
"c4.2xlarge" : "HVM64",
"c4.4xlarge" : "HVM64",
"c4.8xlarge" : "HVM64",
"g2.2xlarge" : "HVMG2",
"g2.8xlarge" : "HVMG2",
"r3.large" : "HVM64",
"r3.xlarge" : "HVM64",
"r3.2xlarge" : "HVM64",
"r3.4xlarge" : "HVM64",
"r3.8xlarge" : "HVM64",
"i2.xlarge" : "HVM64",
"i2.2xlarge" : "HVM64",
"i2.4xlarge" : "HVM64",
"i2.8xlarge" : "HVM64",
"d2.xlarge" : "HVM64",
"d2.2xlarge" : "HVM64",
"d2.4xlarge" : "HVM64",
"d2.8xlarge" : "HVM64",
"hi1.4xlarge" : "HVM64",
"hs1.8xlarge" : "HVM64",
"cr1.8xlarge" : "HVM64",
"cc2.8xlarge" : "HVM64",
};

const regionArchToAmi: {[region: string]: {[arch: string]: string}} = {
"us-east-1" : {"PV64" : "ami-2a69aa47", "HVM64" : "ami-97785bed", "HVMG2" : "ami-0a6e3770"},
"us-west-2" : {"PV64" : "ami-7f77b31f", "HVM64" : "ami-f2d3638a", "HVMG2" : "ami-ee15a196"},
"us-west-1" : {"PV64" : "ami-a2490dc2", "HVM64" : "ami-824c4ee2", "HVMG2" : "ami-0da4a46d"},
"eu-west-1" : {"PV64" : "ami-4cdd453f", "HVM64" : "ami-d834aba1", "HVMG2" : "ami-af8013d6"},
"eu-west-2" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-403e2524", "HVMG2" : "NOT_SUPPORTED"},
"eu-west-3" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-8ee056f3", "HVMG2" : "NOT_SUPPORTED"},
"eu-central-1" : {"PV64" : "ami-6527cf0a", "HVM64" : "ami-5652ce39", "HVMG2" : "ami-1d58ca72"},
"ap-northeast-1" : {"PV64" : "ami-3e42b65f", "HVM64" : "ami-ceafcba8", "HVMG2" : "ami-edfd658b"},
"ap-northeast-2" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-863090e8", "HVMG2" : "NOT_SUPPORTED"},
"ap-northeast-3" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-83444afe", "HVMG2" : "NOT_SUPPORTED"},
"ap-southeast-1" : {"PV64" : "ami-df9e4cbc", "HVM64" : "ami-68097514", "HVMG2" : "ami-c06013bc"},
"ap-southeast-2" : {"PV64" : "ami-63351d00", "HVM64" : "ami-942dd1f6", "HVMG2" : "ami-85ef12e7"},
"ap-south-1" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-531a4c3c", "HVMG2" : "ami-411e492e"},
"us-east-2" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-f63b1193", "HVMG2" : "NOT_SUPPORTED"},
"ca-central-1" : {"PV64" : "NOT_SUPPORTED", "HVM64" : "ami-a954d1cd", "HVMG2" : "NOT_SUPPORTED"},
"sa-east-1" : {"PV64" : "ami-1ad34676", "HVM64" : "ami-84175ae8", "HVMG2" : "NOT_SUPPORTED"},
"cn-north-1" : {"PV64" : "ami-77559f1a", "HVM64" : "ami-cb19c4a6", "HVMG2" : "NOT_SUPPORTED"},
"cn-northwest-1" : {"PV64" : "ami-80707be2", "HVM64" : "ami-3e60745c", "HVMG2" : "NOT_SUPPORTED"},
};

// get looks up the appropriate AMI for the given region and instance type.
export function get(region: aws.Region, instanceType: aws.ec2.InstanceType): string {
let arch = instanceTypeToArch[instanceType];
if (!arch) {
throw new Error(`Unsupported instance type: ${instanceType}`);
}
let archToAmi = regionArchToAmi[region];
if (!archToAmi) {
throw new Error(`Unsupported region: ${region}`);
}
let ami = archToAmi[arch];
if (!ami || ami === "NOT_SUPPORTED") {
throw new Error(`Unsupported region and instance type combination: ${region} / ${instanceType} (${arch})`);
}
return ami;
}
54 changes: 54 additions & 0 deletions aws-ts-ruby-on-rails/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config(pulumi.getProject());

// keyName is the name of an existing EC2 KeyPair to enable SSH access to the instances.
export const keyName = config.get("keyName");

// dbName is a MySQL database name.
export const dbName = config.get("dbName") || "MyDatabase";
if (!/[a-zA-Z][a-zA-Z0-9]*/.test(dbName)) {
throw new Error("dbName must begin with a letter and contain only alphanumeric characters");
} else if (dbName.length < 1 || dbName.length > 64) {
throw new Error("dbName must between 1-64 characters, inclusively")
}

// dbUser is the username for MySQL database access.
export const dbUser = config.require("dbUser");
if (!/[a-zA-Z][a-zA-Z0-9]*/.test(dbUser)) {
throw new Error("dbUser must begin with a letter and contain only alphanumeric characters");
} else if (dbUser.length < 1 || dbUser.length > 16) {
throw new Error("dbUser must between 1-16 characters, inclusively");
}

// dbPassword is the password for MySQL database access.
export const dbPassword = config.require("dbPassword");
if (!/[a-zA-Z0-9]*/.test(dbPassword)) {
throw new Error("dbPassword must only alphanumeric characters");
} else if (dbPassword.length < 1 || dbPassword.length > 41) {
throw new Error("dbPassword must between 1-41 characters, inclusively");
}

// dbRootPassword is the root password for MySQL.
export const dbRootPassword = config.require("dbRootPassword");
if (!/[a-zA-Z0-9]*/.test(dbRootPassword)) {
throw new Error("dbRootPassword must only alphanumeric characters");
} else if (dbRootPassword.length < 1 || dbRootPassword.length > 41) {
throw new Error("dbRootPassword is must between 1-41 characters, inclusively");
}

// instanceType is the WebServer EC2 instance type.
export const instanceType: aws.ec2.InstanceType = <aws.ec2.InstanceType>config.get("instanceType") || "t2.small";
if (false) {
// TODO: dynamically verify the values.
throw new Error("instanceType must be a valid EC2 instance type");
}

// sshLocation is the IP address range that can be used to SSH to the EC2 instances.
export const sshLocation = config.get("sshLocation") || "0.0.0.0/0";
if (!new RegExp("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})").test(sshLocation)) {
throw new Error("sshLocation must be a valid IP CIDR range of the form x.x.x.x/x");
} else if (dbName.length < 1 || dbName.length > 41) {
throw new Error("sshLocation is must between 9-18 characters, inclusively");
}
9 changes: 9 additions & 0 deletions aws-ts-ruby-on-rails/files/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
development:
adapter: mysql2
encoding: utf8
reconnect: false
pool: 5
database: {{dbName}}
username: {{dbUser}}
password: {{dbPassword}}
socket: /var/lib/mysql/mysql.sock
26 changes: 26 additions & 0 deletions aws-ts-ruby-on-rails/files/install_application
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/bin/bash -e
source /etc/profile.d/rvm.sh
rvm use 2.3.1
export HOME=/home/ec2-user
export PATH=$PATH:/usr/local/bin
cd /home/ec2-user

# Kill the rails server if it is running to allow update
if pgrep ruby &> /dev/null ; then pkill -TERM ruby ; fi

# This sample template creates a new application inline
# Typically you would use files and/or sources to download
# your application package and perform any configuration here.

# Create a new application, with therubyracer javascript library
rails new sample -d mysql --skip-spring --skip-bundle --force
cd /home/ec2-user/sample
sed -i 's/^# \\(.*therubyracer.*$\\)/\\1/' Gemfile
bundle install

# Create a sample scoffold
rails generate scaffold Note title:string body:text --force

# Configure the database connection
mv /tmp/database.yml config
rake db:create db:migrate
6 changes: 6 additions & 0 deletions aws-ts-ruby-on-rails/files/install_ruby
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
curl -sSL https://get.rvm.io | bash
source /etc/profile.d/rvm.sh
rvm install 2.3.1
rvm --default use 2.3.1
gem install rails
3 changes: 3 additions & 0 deletions aws-ts-ruby-on-rails/files/setup.mysql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
CREATE USER '{{dbUser}}'@'localhost' IDENTIFIED BY '{{dbPassword}}';
GRANT ALL ON {{dbName}}.* TO '{{dbUser}}'@'localhost';
FLUSH PRIVILEGES;
9 changes: 9 additions & 0 deletions aws-ts-ruby-on-rails/files/start-application
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash -e
source /etc/profile.d/rvm.sh
rvm use 2.3.1
export HOME=/home/ec2-user
export PATH=$PATH:/usr/local/bin
cd /home/ec2-user/sample

# Startup the application
rails server --binding 0.0.0.0 -p 80 -d
114 changes: 114 additions & 0 deletions aws-ts-ruby-on-rails/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as aws from "@pulumi/aws";
import * as ami from "./ami";
import * as config from "./config";
import { createUserData, renderConfigFile } from "./init";

const webSg = new aws.ec2.SecurityGroup("webServerSecurityGroup", {
description: "Enable HTTP and SSH access",
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: [ "0.0.0.0/0" ] },
],
ingress: [
{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: [ "0.0.0.0/0" ] },
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: [ config.sshLocation ] },
],
});

const webServer = new aws.ec2.Instance("webServer", {
ami: ami.get(aws.config.requireRegion(), config.instanceType),
instanceType: config.instanceType,
securityGroups: [ webSg.name ],
keyName: config.keyName,
userData: createUserData(
[ "install_ruby_2_3_1", "install_mysql", "configure_mysql", "install_application" ],
{
"install_ruby_2_3_1": {
files: {
"/tmp/install_ruby": {
content: renderConfigFile("./files/install_ruby", config),
mode: "000500",
owner: "root",
group: "root",
},
},
commands: {
"01_install_ruby": {
command: "/tmp/install_ruby > /var/log/install_ruby.log",
},
},
},
"install_mysql": {
packages: {
yum: [ "mysql", "mysql-server", "mysql-devel", "mysql-libs" ],
},
files: {
"/tmp/setup.mysql": {
content: renderConfigFile("./files/setup.mysql", config),
mode: "000400",
owner: "root",
group: "root",
},
},
services: {
"sysvinit": {
"mysqld": { enabled: true, ensureRunning: true },
},
},
},
"configure_mysql": {
commands: {
"01_set_mysql_root_password": {
command: `mysqladmin -u root password '${config.dbRootPassword}'`,
test: `$(mysql ${config.dbName} -u root --password='${config.dbRootPassword}' >/dev/null 2>&1 </dev/null); (( $? != 0 ))`,
},
"02_create_database": {
command: `mysql -u root --password='${config.dbRootPassword}' < /tmp/setup.mysql`,
test: `$(mysql ${config.dbName} -u root --password='${config.dbRootPassword}' >/dev/null 2>&1 </dev/null); (( $? != 0 ))`,
},
"03_cleanup": {
command: "rm /tmp/setup.mysql",
},
},
},
"install_application": {
files: {
"/tmp/database.yml": {
content: renderConfigFile("./files/database.yml", config),
mode: "000400",
owner: "root",
group: "root",
},
"/tmp/install_application": {
content: renderConfigFile("./files/install_application", config),
mode: "000500",
owner: "root",
group: "root",
},
"/home/ec2-user/start-application": {
content: renderConfigFile("./files/start-application", config),
mode: "000500",
owner: "root",
group: "root",
},
},
commands: {
"01_install_application": {
command: "/tmp/install_application > /var/log/install_application.log",
},
"02_configure_reboot": {
command: "echo /home/ec2-user/start-application >> /etc/rc.local",
},
"03_start_application": {
command: "/home/ec2-user/start-application",
},
"04_cleanup": {
command: "rm /tmp/install_application",
},
},
},
},
),
});

// Expor the URL for our newly created Rails application.
export let websiteURL = webServer.publicDns.apply(url => `https://${url}/notes`);
Loading

0 comments on commit c658a76

Please sign in to comment.