ruk·si

AWS
OpsWorks Push-to-Deploy

Updated at 2015-10-10 11:37

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

Sources