AWS - OpsWorks Push-to-Deploy
Terminology:
- Stack: a collection of instances and related AWS resources
- Layer: a blueprint for a set of instances
- OpsWorks Layer: one or more EC2 instances e.g. webserver
- Service Layer: AWS service e.g. AWS RDS database
- Instance: EC2 instance, belonging to one or more layers
- Time-based Instance: start/stop instances using a schedule
- Load-based Instance: start/stop instances in response to usage metrics
- App: code stored in a repository that is installed to instances
- Deployments: deploy apps or run commands on all instances
- Permissions: configure AWS user access rights in this stack
Supported app types are Java, Node.js, PHP, Rails and Static. There are also blueprints for stuff like Memcached and ECS cluster.
This stack will cost around $40 per month. That is more than Heroku of the same level of complexity, but the price scales up much less drastically than Heroku pricing. When you have an ~$1000 / month app running on Heroku, it will cost closer to $500 on AWS and the difference will keep on growing. I'd advice starting with Heroku and when you need to upgrade the plan, move production to AWS but keep all staging servers in Heroku. Using AWS Route 53 DNS to control your Heroku address makes it painless to switch, just change one DNS record when you have the OpsWorks setup ready. Or at the very least, move your databases to AWS after the $50 / month Heroku PostgreSQL plan become insufficient.
1 t2.micro = ~$10
1 load balancer = ~$20, good to have for fast extensibility
5 GitHub private repos = $7
+ some marginal additions e.g. Route 53 and traffic.
Before Creating the Stack
Decide project name. I'll use mink
.
Create the VPC for OpsWorks stack.
VPC Dashboard > Start VPC Wizard
Step 1:
VPC with a Single Public Subnet
Step 2:
IP CIDR block: 10.0.0.0/16
VPC Name: mink-vpc
Public Subnet: 10.0.0.0/24
Subnet Name: mink-subnet-public
S3 Endpoint: Public subnet
Optionally rename all the generated resources. I usually go around the resources and give them better names at this point.
VPC: mink-vpc
Subnet: mink-subnet-public
Routing Tables: mink-rtb-main, mink-rtb-public
Internet Gateway: mink-igw
Network ACL: mink-acl-main
Security Group: mink-vpc-sg
Create a key pair to access the instances.
EC2 > Key Pairs > Create New > mink-kp
Create security groups.
For load balancer:
EC2 > Security Groups > Create Security Group
Security Group Name: mink-node-main-elb-sg
Description: mink-node-main-elb-sg
VPC: mink-vpc
Inbound:
HTTP TCP 80 Anywhere
For EC2 instances:
EC2 > Security Groups > Create Security Group
Security Group Name: mink-node-main-app-sg
Description: mink-node-main-app-sg
VPC: mink-vpc
Inbound:
HTTP TCP 80 Anywhere
SSH TCP 22 Anywhere (or My IP, your pick)
Create load balancer for the Node app.
Step 1:
Load Balancer Name: mink-node-main-elb
Create LB Inside: mink-vpc
Subnets: mink-subnet-public
Step 2:
Security Group: mink-node-main-elb-sg
Step 4:
Ping Protocol: HTTP
Ping Port: 80
Ping Path: / (switch to /health after implementing it)
Step 5:
Don't add any instances.
Enable Cross-Zone Load Balancing
Enable Connection Draining
Step 6:
Name: mink-node-main-elb
Creating the Stack
Create the OpsWorks stack.
Name: mink-stack
Region: US West (Oregon) (us-west-1)
VPC: mink-vpc
Subnet: mink-subnet-public
OS: Amazon Linux 2015.09
Root Device Type: EBS / Instance Store
EBS: the usual volumes attached to instances
separate from the instance
can be detached and attached to another instance
one instance can have multiple EBSes attached, by only 1 root
you can create snapshots and images from EBSes
Instance: physically attached to the instance
temporary, cleared on instance stop
only available in certain instance types
IAM role: generate
SSH key: mink-kp
IAM instance profile: generate
Hostname theme: Fruits
Stack Color: Purple
Configuration Management:
Default should be fine now.
You can configure Chef here,
overwrite or customize built-in recipes
add your custom script cookbook Git repository
Security:
Use OpsWorks security groups: No, I like setting these myself.
aws-opsworks-service-role
policy will become:
{
"Statement": [
{
"Action": [
"ec2:*",
"iam:PassRole",
"cloudwatch:GetMetricStatistics",
"cloudwatch:DescribeAlarms",
"ecs:*",
"elasticloadbalancing:*",
"rds:*"
],
"Effect": "Allow",
"Resource": [
"*"
]
}
]
}
aws-opsworks-service-role
trust will become:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"Service": "opsworks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
aws-opsworks-ec2-role
trust will become:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Create Node.js app layer.
Layers > Add a layer
Layer Type: Node.js App Server
Node.js Version: 0.12.7
Security Group: mink-node-main-app-sg
Elastic Load Balancer: mink-node-main-elb
Instace Shutdown: Wait for...
Add Layer
Layers > Node.js App Server > Network >
Automatically Assign IP Addresses
Public IP addresses: Yes
They are behind load balancer so they don't need elastic IP,
but they still need public IP for Internet communication.
Now you will have 2 layers, Elastic Load Balancer and Node.js App Server.
Add instance.
Layers > Add Instance >
Hostname: *
Size: t2.micro
Subnet: mink-subnet-public
Add Instance
Instances >
Start
New instance is starting up, time to define the app that it runs.
The instance will get sensible name `mink-stack - orcus` after boot
Create the app code.
mkdir mink
cd mink
npm init # enter enter enter server.js enter enter...
touch server.js # OpsWorks expect this file in root
open server.js
var http = require('http');
var server = http.createServer();
server.on('request', function(request, response) {
response.writeHead(200);
response.write('Hello, this is mink.');
response.end();
})
server.on('close', function() {
console.log('Shutting down...')
});
// OpsWorks sets PORT env variable.
server.listen(process.env.PORT);
git init
git add -A
git commit -m "Init commit"
# https://github.com/new
# mink, private. copy the git URL
git remote add origin git@github.com:ruksi/mink.git
git push -u origin master
# You can use either personal (user/orgnization) access tokens or deploy keys.
# I'll setup access tokens as the webhook for push to deploy requires that.
# GitHub User > Settings > Personal access token > Generate new token
# Select only `repo` and `public_repo`, name it `aws-mink-stack`
https://<token>@github.com/ruksi/mink.git
Define OpsWorks app.
Apps > Add an app
Name: mink-node-main
Type: Node.js
Data Source Type: None
Repository Type: Git
Repository URL: Paste the URL with the token
Add App
Instances
Wait for the instance to come online.
Deployments > Deploy an App
App: mink-node-main
Command: Deploy
Deploy
Deployments
Wait for the deployments to finish.
Instances
Navigate to the shown public IP
Should open the Node app.
Layers > ELB
Check that the Health is 1 green, can take couple of minutes.
Navigate to the shown ELB link.
Should open the Node app.
Auto-scaling is under Instance
. If you add a new instance to the app, it receives latest deployment automatically. You can test this at Instances > +Instance > Start
. You can add time-based or load-based auto-scaling when required.
Heroku-like push-to-deploy
GitHub allows creating service hooks for OpsWorks. You will have an additional private repository per deployment target.
Get information about the stack and app.
OpsWorks
Stack > Stack Settings
ARN: arn:aws:opsworks:<REGION>:<ACCOUNT_ID>:stack/<STACK_ID>/
You need this for the permissions.
You need the stack ID for GitHub service hook.
Apps > Click the name
ID: UUID (OpsWorks ID)
You need the app ID for GitHub service hook.
Create user for push deployment.
IAM > Users > Create New Users
Name: mink-node-main-git-deployer
Generate access key: Yes
IAM > Users > mink-node-main-git-deployer > Inline Policies > Create One
Policy Generator
Effect: Allow
Service: AWS OpsWorks
Actions: CreateDeployment, UpdateApp
Resource: arn:aws:opsworks:<REGION>:<ACCOUNT_ID>:stack/<STACK_ID>/
Will generate policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Action": [
"opsworks:CreateDeployment",
"opsworks:UpdateApp"
],
"Resource": [
"arn:aws:opsworks:<REGION>:<ACCOUNT_ID>:stack/<STACK_ID>/"
]
}
]
}
Create new repository for deployment. You can also just make a branch but I like separate repository like in Heroku.
# https://github.com/new
# mink-production, private. copy the git URL
git remote add production git@github.com:ruksi/mink-production.git
git push production master:master
GitHub > Settings > Webhooks > Add Service > AWS OpsWorks
App: UUID, we got this in the first step.
Stack: UUID, we got this in the first step.
Branch: master
GitHub API URL: -
AWS Access Key: Key for the deployer we created a while back.
AWS Secret: Secret of the deployer we created a while back.
GitHub Token: The personal token we created in normal deployment config.
Add service
Change the deployment source.
Apps > Edit
Repository URL: https://<TOKEN>@github.com/ruksi/mink-production.git
Save
Try it out.
# edit the server.js rendered string
git add -A
git commit -m "Change server test response"
git push production master:master
# Stack > Deployments, you should see a running deployment.
Command Line Usage
aws \
--region us-east-1 \
opsworks create-deployment \
--stack-id <STACK_ID> \
--app-id <APP_ID> \
--command "{\"Name\":\"deploy\"}"
aws \
--region us-east-1 \
opsworks create-deployment \
--stack-id <STACK_ID> \
--app-id <APP_ID> \
--command "{\"Name\":\"rollback\"}"
# Multiple rollbacks do property revert multiple revisions.
Connecting to Instances
ssh -i ./mink-kp.pem ec2-user@<INSTANCE_PUBLIC_IP>
The running application is at /srv/www/mink_node_main/current/
.
sudo su
cd /srv/www/mink_node_main/current
ls
# config* log* opsworks.js* package.json public* server.js tmp*
# * = added by opsworks
cd /srv/www/mink_node_main/current/log
ls
# node.stderr.log node.stdout.log
Domain
TODO
HTTPS
TODO