AWS DevOps Blog

Part 1: Develop, Deploy, and Manage for Scale with Elastic Beanstalk and CloudFormation Series

Part 1: Modeling an Elastic Beanstalk Application and its Dependencies in CloudFormation

Welcome to the 1st part of a 5-part series where we’ll cover best-practices and practical tips & tricks for developing, deploying, and managing a web application with an eye for application performance and operational efficiency using AWS CloudFormation and Elastic Beanstalk. We’ll introduce a new part of the series as a blog post each Monday, and discuss the post as well as take questions during an Office Hours-style Hangout that Thursday. All application source and accompanying CloudFormation templates are available on GitHub at http://github.com/awslabs/amediamanager

The Hangout

We’ll be discussing this blog post – including your Q&A – during a live Office Hours Hangout at 9a Pacific on Thursday, April 10, 2014. Sign up at http://bit.ly/awsoh!

Part 1: Modeling an Elastic Beanstalk Application and its Dependencies in CloudFormation

In the first part of this series we’ll introduce our sample application – aMediaManager – and review its infrastructure and application architecture. As part of that review we’ll uncover the other AWS services our application depends on, including RDS, ElastiCache, S3, DynamoDB, etc.

We’ll then explore how to use CloudFormation to model and deploy the application with all of these dependencies in EC2 Classic. We’ll leverage embedded CloudFormation stacks to model the application and infrastructure in layers, and explore how to pass all of the configuration information (e.g., database and cache endpoints) to our application.

About the aMediaManager Application

Our sample application – aMediaManager – is a Java web app that allows users to upload, convert, and share videos via a web browser.

The application code uses the following frameworks and libraries:

  • AWS SDK for Java
  • Amazon ElastiCache Java Cluster Client
  • Spring Framework 3.2
  • Spring Security 3.1
  • Spring Web MVC
  • ThymeLeaf Template Engine
  • Apache commons-fileupload, commons-io, common-dbcp, and commons-lang

Core Functionality

User Login & Profile

The application stores user authentication and profile information in Amazon DynamoDB. Profile photos are stored in Amazon S3.

Video Upload, Conversion, and Management

Videos are uploaded and stored directly to S3, while Elastic Transcoder creates a video thumbnail and creates a streaming version. Video metadata, including tags, is stored in RDS and cached in ElastiCache.

Admin Console

An admin console exists to provision parts of the application that can’t be managed in CloudFormation (i.e., database schema and Elastic Transcoder resources). The console also provides information about the application configuration and AWS credentials being used by the app.

About the Infrastructure

The app uses a number of AWS services for data storage, video conversion, security, and messaging, including Amazon S3 for uploaded videos and app logs; Amazon RDS for storing searchable video metadata; Amazon ElastiCache for caching database queries; and Amazon DynamoDB for storing user profile information.

Here’s an overview of our Elastic Beanstalk application and the other AWS Resources it requires. We’ll model all of them in a CloudFormation template:

Deploy the Application

The complete application source code and CloudFormation templates are Apache licensed and available on GitHub at http://github.com/awslabs/amediamanager.

An Important Note About Cost

All defaults in the CloudFormation template fall within the AWS Free Usage Tier. If your account is not eligible for the free usage tier, or if you use non-default values, you will be charged for the resources provisioned by this template. To delete the application, navigate to the AWS CloudFormation Managemenet Console, choose the amediamanager stack and click the Delete Stack button.

Deploy From the Console

If you’d like to follow along with a running version of the application, you can deploy it now using CloudFormation by clicking this link.

Deploy From the Command Line

To deploy from your command line, you’ll need the AWS CLI installed and configured, as well as a git client:

  1. Download the source from GitHub:

    $> git clone https://github.com/awslabs/amediamanager
    
  2. Go into the source folder and copy the launch params template file:

    $> cd amediamanager
    $> cp cfn/classic/launch-params.json.example cfn/classic/launch-params.json
    
  3. Edit cfn/classic/launch-params.json and replace YOUR_EC2_KEY_PAIR with the name of a real EC2 Key Pair in your account.

  4. Run a stack with the aws CLI:

    $> aws cloudformation create-stack 
         --stack-name amediamanager 
         --template-body file://cfn/classic/amm-master.cfn.json 
         --parameters file://cfn/classic/launch-params.json 
         --capabilities CAPABILITY_IAM
    

Modeling the Application and Infrastructure with CloudFormation

By using CloudFormation templates to declare our application and all of its dependencies, we can quickly deploy the entire stack to any one of 8 Regions around the world. We can also allow developers to run and destroy their own isolated stacks for dev and test purposes, and we can version control our templates and use them to manage changes to stacks.

Although we could define model this entire application and all of its dependencies in a single template file, we’ll choose to compose everything into 3 separate templates.

Composing and Embedding Multiple Stacks

In a CloudFormation template you can declare many different AWS resources like EC2 Instances and RDS Database Servers. You can also declare another CloudFormatin stack! For this application, we compose our entire app and infrastructure from 3 templates:

  1. amm-master.cfn.json: This is the parent template and it only defines two Resources, both of them embedded CloudFormation stacks. To run the entire application, run this stack.
  2. amm-resources.cfn.json: This template defines all of the dependencies for our application, including RDS databse, ElastiCache cluster, DynamoDB table, S3 bucket, IAM roles, etc. It is the first child stack created by amm-master.cfn.json, and it outputs the IDs of everything it creates.
  3. amm-elasticbeanstalk.cfn.json: This template defines the Elastic Beanstalk Application and Environment that runs our app code. It takes as inputs the IDs of its resource dependencies (DB hostname, S3 bucket name, etc). It is a child stack defined in amm-master.cfn.json, and that parent template provides its inputs by referencing the outputs of amm-resources.cfn.json

Here’s a picture:

One Stack to Bring Them All and In the Template Bind Them

amm-master.cfn.json is the parent template. It defines several input parameters and just 2 resources. Those resources are both embedded CloudFormation stacks – AppResources and App1

"Resources": {
  "AppResources" : {
    "Type" : "AWS::CloudFormation::Stack",
    "Properties" : {
      "TemplateURL" : { "Fn::Join" : ["", [ "http://", { "Ref" : "AssetsBucketPrefix" }, { "Ref" : "AWS::Region" }, ".s3.amazonaws.com/", { "Ref" : "AppResourcesTemplateKey" }]]},
      "Parameters" : {
        "DatabaseUser"           : { "Ref" : "DatabaseUser"},
        "DatabasePassword"       : { "Ref" : "DatabasePassword" },
        "RemoteDBAccessSource"   : { "Ref" : "RemoteDBAccessSource" },
        "DBInstanceType"         : { "Ref" : "DBInstanceType"},
        "ApplicationName"        : { "Ref" : "ApplicationName"}
      }
    }
  },
  "App1" : {
    "Type" : "AWS::CloudFormation::Stack",
    "Properties" : {
      "TemplateURL" : { "Fn::Join" : ["", [ "http://", { "Ref" : "AssetsBucketPrefix" }, { "Ref" : "AWS::Region" }, ".s3.amazonaws.com/", { "Ref" : "AppTemplateKey" }]]},
      "Parameters" : {
        "RdsDbId"                : { "Fn::GetAtt" : [ "AppResources", "Outputs.RdsDbId" ]},
        "CacheEndpoint"          : { "Fn::GetAtt" : [ "AppResources", "Outputs.CacheEndpoint" ]},
        "CachePort"              : { "Fn::GetAtt" : [ "AppResources", "Outputs.CachePort" ]},
        "AppBucket"              : { "Fn::GetAtt" : [ "AppResources", "Outputs.AppBucket" ]},
        "TranscodeTopic"         : { "Fn::GetAtt" : [ "AppResources", "Outputs.TranscodeTopic" ]},
        "TranscodeQueue"         : { "Fn::GetAtt" : [ "AppResources", "Outputs.TranscodeQueue" ]},
        "TranscodeRoleArn"       : { "Fn::GetAtt" : [ "AppResources", "Outputs.TranscodeRoleArn" ]},
        "UsersTable"             : { "Fn::GetAtt" : [ "AppResources", "Outputs.UsersTable" ]},
        "InstanceSecurityGroup"  : { "Fn::GetAtt" : [ "AppResources", "Outputs.InstanceSecurityGroup" ]},
        "DatabaseUser"           : { "Ref" : "DatabaseUser"},
        "DatabasePassword"       : { "Ref" : "DatabasePassword" },
        "AssetsBucketPrefix"     : { "Ref" : "AssetsBucketPrefix" },
        "WarKey"                 : { "Ref" : "WarKey"},
        "KeyName"                : { "Ref" : "KeyName" },
        "InstanceType"           : { "Ref" : "InstanceType"},
        "ApplicationName"        : { "Ref" : "ApplicationName" }
      }
    }
  }
}

The AppResources resource in the parent template points to the amm-resources.cfn.json in some S3 bucket, while the App1 resource points to the amm-elasticbeanstalk.cfn.json template. The App1 resource depends on the AppResources template as we can see from the Fn::GetAtt in App1, so this means that CloudFormation will create the AppResources stack (i.e., the am-resources.cfn.json template) first. This will create all of the dependencies our application requires, including RDS, DynamoDB, SQS, Security Groups, IAM Role, etc, and the values for each of these resources will be included in the stacks Outputs:

"Outputs": {
  "InstanceSecurityGroup": {
    "Value": {"Ref": "InstanceSecurityGroup"}
  },
  "RdsDbId": {
     "Value" : { "Ref" : "Database" }
  },
  "CacheEndpoint": { 
    "Value": { "Fn::GetAtt" : [ "CacheCluster", "ConfigurationEndpoint.Address" ]}
  },
  "CachePort": { 
    "Value" : {"Fn::GetAtt" : [ "CacheCluster", "ConfigurationEndpoint.Port" ]}
  },
  "AppBucket": {
    "Value": { "Ref" : "AppBucket"}
  },
  "TranscodeTopic": {
    "Value": { "Ref" : "TranscodeTopic" }
  },
  "TranscodeQueue": {
    "Value": { "Ref" : "TranscodeQueue" }
  },
  "TranscodeRoleArn": {
    "Value": { "Fn::GetAtt": ["TranscodeRole", "Arn"]}
  },
  "UsersTable": {
    "Value": { "Ref" : "UsersTable" }
  },
  "DatabaseUser":{
    "Value": { "Ref" : "DatabaseUser"}
  },
  "DatabasePassword": {
    "Value": { "Ref" : "DatabasePassword" }
  },
  "InstanceSecurityGroup": {
    "Value": { "Ref" : "InstanceSecurityGroup" }
  }
}

After creating the AppResources stack, the App1 stack will be provisioned. We’ll pass in the required parameters to that stack by using Fn::GetAtt to retrieve outputs from the AppResources stack. For example, we pass in the ID of the RDS database created in AppResources like this:

"Parameters" : {
  "RdsDbId" : { "Fn::GetAtt" : [ "AppResources", "Outputs.RdsDbId" ]},
  ...
}

Working backwards, this retreives the RdsDbId value from the Outputs section of the AppResources resource.

Using Elastic Beanstalk with CloudFormation

Launching the amm-master.cfn.json template causes the amm-resources.cfn.json template to run, which creates all of the dependencies for our app. When it’s done, we’re finally ready to deploy and run our application code in Elastic Beanstalk. We model and define that Elastic Beanstalk environment in the amm-elasticbeanstalk.cfn.json template file.

The Elastic Beanstalk Application

In Elastic Beanstalk, an Application is a collection of Application Versions (i.e., versions of your code), Configuration Templates, and Environments. An Environment exists within a particular Application and is a running deployment of a specific Application Version, including all of the infrastructure (e.g., EC2, Auto Scaling, ELB, CloudWatch, etc) required to run the app.

In the amm-elasticbeanstalk.cfn.json template we define both an Application and an Environment resource. This snippet shows the Application:

"Resources": {
  "Application": {
    "Type": "AWS::ElasticBeanstalk::Application",
    "Properties": {
      "ApplicationName" : {"Ref": "ApplicationName"},
      "ConfigurationTemplates": [{
        "TemplateName": "DefaultConfiguration",
        "Description": "Default Configuration",
        "SolutionStackName": "64bit Amazon Linux running Tomcat 7",
        "OptionSettings": [
          {
            "Namespace": "aws:elasticbeanstalk:application:environment",
            "OptionName": "S3_CONFIG_BUCKET",
            "Value": {
              "Ref": "AppBucket"
            }
          },
          {
            "Namespace": "aws:elasticbeanstalk:application:environment",
            "OptionName": "AMM_AWS_REGION",
            "Value": {
              "Ref": "AWS::Region"
            }
          },
          {
            "Namespace": "aws:autoscaling:launchconfiguration",
            "OptionName": "EC2KeyName",
            "Value": {
               "Ref" : "KeyName"
            }
          },
          ...
        ]
      }],
      "ApplicationVersions": [{
        "VersionLabel": "Initial Version",
        "Description": "Initial Version",
        "SourceBundle": {...}
      }]
    }
  },
  ...

The Application’s Properties include:

  • ApplicationName: The name of the app as it will appear in the Elastic Beanstalk console
  • ConfigurationTemplates: Here we define one template named DefaultConfiguration. This defines configuration settings for an Environment within the Application.
  • SolutionStackName: This property of the DefaultConfiguration templates means that any Environment launched with this template will be of type 64bit Amazon Linux running Tomcat 7
  • OptionSettings: This collection of settings will be applied to any Environment launched with the DefaultConfiguration template. We’ve used the option settings here to define environment variables (they will be exported to all instances running in an environment); configure the EC2 Key Pair installed on running hosts; and more. You can look at all of the option settings defined in the default configuration by clicking this link

The Elastic Beanstalk Environment

Now that an Application has been defined, we can launch an Environment running our code. In this snippet we define an Environment resource that indicates which Application it is a part of, as well as which template and application version to deploy:

"Resources": {
  ...
  "Environment": {
    "Type": "AWS::ElasticBeanstalk::Environment",
    "Properties": {
      "ApplicationName": {
        "Ref": "Application"
      },
      "EnvironmentName" : "Development",
      "Description": "Default Environment",
      "VersionLabel": "Initial Version",
      "TemplateName": "DefaultConfiguration",
      "OptionSettings": [
        {
          "Namespace": "aws:elasticbeanstalk:application:environment",
          "OptionName": "AMM_RDS_INSTANCEID",
          "Value": {
            "Ref": "RdsDbId"
          }
        },
        {
          "Namespace": "aws:elasticbeanstalk:application:environment",
          "OptionName": "AMM_CACHE_ENDPOINT",
          "Value": {
            "Ref": "CacheEndpoint"
          }
        },
        ...
      ]
    }
  },

You’ll also notice that in addition to specifying the DefaultConfiguration as the ConfigurationTemplate, we’ve also defined explicit OptionSettings for this environment. In this case, we’re setting environment variables to the values of the resources (e.g., database, cache cluster) created in the amm-resources.cfn.json template and passed in via parameters. It’s important to note that option settings defined directly on an environment are specific to that environment (i.e., they’re not part of a configuraiton template). In the upcoming Part 3 of this series, we’ll talk explicitly about why we made this choice (hint: it has to do with how we handle application configuration in a centralized fashion).

Finally, the amm-elasticbeanstalk.cfn.json template outputs the URL of the environment that was created:

"Outputs": {
  "URL": {
    "Description": "URL of the AWS Elastic Beanstalk Environment",
    "Value": {
      "Fn::Join": ["", ["http://", {
        "Fn::GetAtt": ["Environment", "EndpointURL"]
      }]]
    }
  }
}

The Result

After you deploy the amm-master.cfn.json stack and it completes, head on over to the CloudFormation Management Console to checkout the result.

In my console I see the 3 stacks I expect, and the annotated stack numbers correspond to our previous diagram:

But what about that 4th stack that I didn’t annotate? The Elastic Beanstalk service has chosen to use CloudFormation as its engine: when you use Elastic Beanstalk to launch an Environment, it ultimately builds a CloudFormation template and uses it to provision your enviornment. This is an Elastic Beanstalk implementation detail; you can (and should) ignore this stack and use the Elastic Beanstalk APIs directly to manage your application and environment.

Meanwhile in the Elastic Beanstalk Management Console I can see my amediamanager application and the Environment named development inside of it:

Click on the development environment to drill into environment’s dashboard:

Finally, click on the Environment’s CNAME to access the application and create a new account:

If you want to play around with the app and upload videos, you’ll need to click the App Config link in the menu bar, then click the Create buttons to provision the Elastic Transcoder Pipeline and Database Schema resources. We’ll talk more about what’s happening here in Parts 3 and 4 of the series.

We’ll also focus a lot more on the internals of the application in Parts 3–5 of this series. In the first 2 parts we’re really focused on the provisioning and management of the infra required to runt the app.

CloudFormation ROI

We’ve committed to using CloudFormation, and the work we put into building the templates will start paying returns in short order. Let’s take a look at a few scenarios where CloudFormation makes it simple yet predictable to manage our application and its infrastructure.

Scenario 1: Another Developer Wants Her Own Sandbox

Let’s assume that what we’ve provisioned in this blog post is our dev/test environment. The Elastic Beanstalk Application and Environment we provisioned was for Henry. Lucy just joined the team, and she’d like her own space to deploy her branches of the code base. For cost and efficiency reasons, we’d like her and other devs to share the supporting resources (i.e., database, memcached, DynamoDB, etc), but give them their own Elastic Beanstalk Application and Environment.

No problem! Here’s how we could handle that quickly, reliably, and with an easy rollback path:

$> cd amediamanager
$> vi cfn/classic/amm-master.cfn.json

And then I’ll append the following resource for Lucy:

"AppLucy" : {
  "Type" : "AWS::CloudFormation::Stack",
  "Properties" : {
    "TemplateURL" : { "Fn::Join" : ["", [ "http://", { "Ref" : "AssetsBucketPrefix" }, { "Ref" : "AWS::Region" }, ".s3.amazonaws.com/", { "Ref" : "AppTemplateKey" }]]},
    "Parameters" : {
      "RdsDbId"                : { "Fn::GetAtt" : [ "AppResources", "Outputs.RdsDbId" ]},
      "CacheEndpoint"          : { "Fn::GetAtt" : [ "AppResources", "Outputs.CacheEndpoint" ]},
      "CachePort"              : { "Fn::GetAtt" : [ "AppResources", "Outputs.CachePort" ]},
      "AppBucket"              : { "Fn::GetAtt" : [ "AppResources", "Outputs.AppBucket" ]},
      "TranscodeTopic"         : { "Fn::GetAtt" : [ "AppResources", "Outputs.TranscodeTopic" ]},
      "TranscodeQueue"         : { "Fn::GetAtt" : [ "AppResources", "Outputs.TranscodeQueue" ]},
      "TranscodeRoleArn"       : { "Fn::GetAtt" : [ "AppResources", "Outputs.TranscodeRoleArn" ]},
      "UsersTable"             : { "Fn::GetAtt" : [ "AppResources", "Outputs.UsersTable" ]},
      "InstanceSecurityGroup"  : { "Fn::GetAtt" : [ "AppResources", "Outputs.InstanceSecurityGroup" ]},
      "DatabaseUser"           : { "Ref" : "DatabaseUser"},
      "DatabasePassword"       : { "Ref" : "DatabasePassword" },
      "AssetsBucketPrefix"     : { "Ref" : "AssetsBucketPrefix" },
      "WarKey"                 : { "Ref" : "WarKey"},
      "KeyName"                : { "Ref" : "KeyName" },
      "InstanceType"           : { "Ref" : "InstanceType"},
      "ApplicationName"        : { "Ref" : "ApplicationName" },
      "DeveloperName"          : "Lucy"
    }
  }
}

My git diff looks like:

Love it. Looks great. Let’s commit this change to git, then let CloudFormation update the stack with the changes:

$> git commit -am "Adding an Elastic Beanstalk App and Env for Lucy"
$> aws cloudformation update-stack 
  --stack-name amediamanager 
  --template-body cfn/classic/amm-master.cfn.json 
  --parameters file://cfn/classic/launch-params.json

Back in the Elastic Beanstalk console we can see that Lucy’s sandbox is being created:

Scenario 2: Control Security

One of our developers would like to connect MySQL Workbench from his laptop to the test RDS database to run a few ad-hoc queries. We’ve disallowed this by default (instead only allowing EC2 Instances in our Elastic Beanstalk Environment to connect), but we can use the same ‘modify template/commit/update’ workflow to allow this.

But first, a bit about how we’ve disallowed this. We’ve exposed a parameter RemoteDBAccessSource in the amm-master.cfn.json template with a default value of 1.1.1.1/1. The value of that parameter is passed to the amm-resources.cfn.json template where the RDS Security Group template is created. In that template we’ve defined a conditioned called RemoteDBAccess that inspects the value of the parameter:

"Conditions" : {
  "RemoteDBAccess" : { "Fn::Not" : [{ "Fn::Equals" : [ { "Ref" : "RemoteDBAccessSource" }, "1.1.1.1/1" ]}]}
},

This condition evaluates to false (i.e., disallow Remote DB Access) if the param value is 1.1.1.1/1. Otherwise it evaluates to true.

We then conditionally create a AWS::RDS::DBSecurityGroupIngress resource based on this condition:

"DBSecurityGroupRemoteAccess" : {
  "Type": "AWS::RDS::DBSecurityGroupIngress", 
  "Condition": "RemoteDBAccess",
  "Properties": {
    "DBSecurityGroupName": {"Ref": "DBSecurityGroup"},
    "CIDRIP": "RemoteDBAccessSource"
  }
}

In a nutshell, if we set the RemoteDBAccessSource to a valid CIDR that is not 1.1.1.1/1, the DBSecurityGroupRemoteAccess resource will be created, allowing access from that IP.

I’ll edit my cfn/classic/launch-params.json file and add the IP address of the developer’s laptop as a parameter:

[
  {
    "ParameterKey": "KeyName",
    "ParameterValue": "evbrown-amazon"
  },
  {
    "ParameterKey": "RemoteDBAccessSource",
    "ParameterValue": "67.170.86.146/32"
  }
]

And then I’ll update the stack from the CLI:

$> aws cloudformation update-stack 
     --stack-name amediamanager 
     --template-body cfn/classic/amm-master.cfn.json 
     --parameters file://cfn/classic/launch-params.json

I could also do the update stack workflow from the CloudFormation Management Console.

Scenario 3: Give Dev Team in Tokyo a Local Stack

We’re expanding our dev team to Japan, and they’d like to work on a stack that’s not 6,733 miles away. No problem! We’ll provision a complete stack for them in the ap-northeast-1 region:

$> aws cloudformation create-stack 
     --stack-name amediamanager 
     --template-body file://cfn/classic/amm-master.cfn.json 
     --parameters file://cfn/classic/launch-params.json 
     --capabilities CAPABILITY_IAM 
     --region ap-northeast-1

Delete Your Stack!

As we mentioned earlier, running this stack and application creates real AWS resources that will cost money if left on.

In the CloudFormation Management Console select the amediamanager stack and click Delete Stack:

Or from the CLI, run:

$> aws cloudformation delete-stack 
     --stack-name amediamanager

Coming Up: Part 2

First, don’t forget to join us for the live Office Hours Hangout later this week (or view the recording if it’s past April 10 2014 and you don’t have a time machine).

In Part 2 of this series (blog post and Office Hours links forthcoming at http://blogs.aws.amazon.com/application-management) we’ll take everything we learned in Part 1 and use it to provision a VPC and deploy our app and resources into that VPC. We’ll also dive into building and managing templates for multi-region deployments (i.e., the very thing that allowed us to deploy that stack to Tokyo in the last scenario.) Please join us next time for more!