In this post, we will show you how to deploy application on Amazon EC2 with AWS CloudFormation and why we should using CreationPolicy attribute instead of wait conditions. An important, For Amazon EC2 and Auto Scaling resources, we recommend that you use a CreationPolicy attribute instead of wait conditions. Add a CreationPolicy attribute to those resources, and use the cfn-signal helper script to signal when an instance creation process has completed successfully.
You can use AWS CloudFormation to automatically install, configure, and start applications on Amazon EC2 instances. Doing so enables you to easily duplicate deployments and update existing installations without connecting directly to the instance, which can save you a lot of time and effort.
AWS CloudFormation includes a set of helper scripts (cfn-init, cfn-signal, cfn-get-metadata, and cfn-hup) that are based on cloud-init. You call these helper scripts from your AWS CloudFormation templates to install, configure, and update applications on Amazon EC2 instances that are in the same template.
In example you can use the following template below to launches a LAMP stack by using cfn helper scripts to install, configure and start Apache, MySQL, and PHP.
AWSTemplateFormatVersion: 2010-09-09 Description: A CFN Template to launches LAMP Stack Parameters: KeyName: Description: Name of an existing EC2 KeyPair to enable SSH access to the instance Type: 'AWS::EC2::KeyPair::KeyName' ConstraintDescription: must be the name of an existing EC2 KeyPair. DBName: Default: MyDatabase Description: MySQL database name Type: String MinLength: '1' MaxLength: '64' AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' ConstraintDescription: must begin with a letter and contain only alphanumeric characters. DBUser: NoEcho: 'true' Description: Username for MySQL database access Type: String MinLength: '1' MaxLength: '16' AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' ConstraintDescription: must begin with a letter and contain only alphanumeric characters. DBPassword: NoEcho: 'true' Description: Password for MySQL database access Type: String MinLength: '1' MaxLength: '41' AllowedPattern: '[a-zA-Z0-9]*' ConstraintDescription: must contain only alphanumeric characters. DBRootPassword: NoEcho: 'true' Description: Root password for MySQL Type: String MinLength: '1' MaxLength: '41' AllowedPattern: '[a-zA-Z0-9]*' ConstraintDescription: must contain only alphanumeric characters. InstanceType: Description: WebServer EC2 instance type Type: String Default: t2.small AllowedValues: - t1.micro - t2.nano - t2.micro - t2.small - t2.medium - t2.large ConstraintDescription: must be a valid EC2 instance type. SSHLocation: Description: ' The IP address range that can be used to SSH to the EC2 instances' Type: String MinLength: '9' MaxLength: '18' Default: 0.0.0.0/0 AllowedPattern: '(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})' ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x. Mappings: AWSInstanceType2Arch: t1.micro: Arch: HVM64 t2.nano: Arch: HVM64 t2.micro: Arch: HVM64 t2.small: Arch: HVM64 t2.medium: Arch: HVM64 t2.large: Arch: HVM64 AWSInstanceType2NATArch: t1.micro: Arch: NATHVM64 t2.nano: Arch: NATHVM64 t2.micro: Arch: NATHVM64 t2.small: Arch: NATHVM64 t2.medium: Arch: NATHVM64 t2.large: Arch: NATHVM64 AWSRegionArch2AMI: us-east-1: HVM64: ami-0080e4c5bc078760e HVMG2: ami-0aeb704d503081ea6 us-west-2: HVM64: ami-01e24be29428c15b2 HVMG2: ami-0fe84a5b4563d8f27 us-west-1: HVM64: ami-0ec6517f6edbf8044 HVMG2: ami-0a7fc72dc0e51aa77 eu-west-1: HVM64: ami-08935252a36e25f85 HVMG2: ami-0d5299b1c6112c3c7 eu-west-2: HVM64: ami-01419b804382064e4 HVMG2: NOT_SUPPORTED eu-west-3: HVM64: ami-0dd7e7ed60da8fb83 HVMG2: NOT_SUPPORTED eu-central-1: HVM64: ami-0cfbf4f6db41068ac HVMG2: ami-0aa1822e3eb913a11 Resources: WebServerInstance: Type: 'AWS::EC2::Instance' Metadata: 'AWS::CloudFormation::Init': configSets: InstallAndRun: - Install - Configure Install: packages: yum: mysql: [] mysql-server: [] mysql-libs: [] httpd: [] php: [] php-mysql: [] files: /var/www/html/index.php: content: !Sub <html> <head> <title>AWS CloudFormation PHP Sample</title> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> </head> <body> <h1>Welcome to the AWS CloudFormation PHP Sample</h1> <p/> <?php // Print out the current data and time print "The Current Date and Time is: <br/>"; print date("g:i A l, F j Y."); ?> <p/> <?php // Setup a handle for CURL $curl_handle=curl_init(); curl_setopt($curl_handle,CURLOPT_CONNECTTIMEOUT,2); curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER,1); // Get the hostname of the intance from the instance metadata curl_setopt($curl_handle,CURLOPT_URL,'http://169.254.169.254/latest/meta-data/public-hostname'); $hostname = curl_exec($curl_handle); if (empty($hostname)) { print "Sorry, for some reason, we got no hostname back <br />"; } else { print "Server = " . $hostname . "<br />"; } // Get the instance-id of the intance from the instance metadata curl_setopt($curl_handle,CURLOPT_URL,'http://169.254.169.254/latest/meta-data/instance-id'); $instanceid = curl_exec($curl_handle); if (empty($instanceid)) { print "Sorry, for some reason, we got no instance id back <br />"; } else { print "EC2 instance-id = " . $instanceid . "<br />"; } $Database = "localhost"; - ' $DBUser = "' - !Ref DBUser - | "; - ' $DBPassword = "' - !Ref DBPassword - | "; print "Database = " . $Database . "<br />"; $dbconnection = mysql_connect($Database, $DBUser, $DBPassword) or die("Could not connect: " . mysql_error()); print ("Connected to $Database successfully"); mysql_close($dbconnection); ?> <h2>PHP Information</h2> <p/> <?php phpinfo(); ?> </body> - | </html> mode: '000600' owner: apache group: apache /tmp/setup.mysql: content: !Join - '' - - 'CREATE DATABASE ' - !Ref DBName - | ; - 'GRANT ALL ON ' - !Ref DBName - .* TO ' - !Ref DBUser - '''@localhost IDENTIFIED BY ''' - !Ref DBPassword - | '; mode: '000400' owner: root group: root /etc/cfn/cfn-hup.conf: content: !Join - '' - - | [main] - stack= - !Ref 'AWS::StackId' - |+ - region= - !Ref 'AWS::Region' - |+ mode: '000400' owner: root group: root /etc/cfn/hooks.d/cfn-auto-reloader.conf: content: !Join - '' - - | [cfn-auto-reloader-hook] - | triggers=post.update - > path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init - 'action=/opt/aws/bin/cfn-init -v ' - ' --stack ' - !Ref 'AWS::StackName' - ' --resource WebServerInstance ' - ' --configsets InstallAndRun ' - ' --region ' - !Ref 'AWS::Region' - |+ - | runas=root mode: '000400' owner: root group: root services: sysvinit: mysqld: enabled: 'true' ensureRunning: 'true' httpd: enabled: 'true' ensureRunning: 'true' cfn-hup: enabled: 'true' ensureRunning: 'true' files: - /etc/cfn/cfn-hup.conf - /etc/cfn/hooks.d/cfn-auto-reloader.conf Configure: commands: 01_set_mysql_root_password: command: !Join - '' - - mysqladmin -u root password ' - !Ref DBRootPassword - '''' test: !Join - '' - - '$(mysql ' - !Ref DBName - ' -u root --password=''' - !Ref DBRootPassword - ''' >/dev/null 2>&1 </dev/null); (( $? != 0 ))' 02_create_database: command: !Join - '' - - mysql -u root --password=' - !Ref DBRootPassword - ''' < /tmp/setup.mysql' test: !Join - '' - - '$(mysql ' - !Ref DBName - ' -u root --password=''' - !Ref DBRootPassword - ''' >/dev/null 2>&1 </dev/null); (( $? != 0 ))' Properties: ImageId: !FindInMap - AWSRegionArch2AMI - !Ref 'AWS::Region' - !FindInMap - AWSInstanceType2Arch - !Ref InstanceType - Arch InstanceType: !Ref InstanceType SecurityGroups: - !Ref WebServerSecurityGroup KeyName: !Ref KeyName UserData: !Base64 'Fn::Join': - '' - - | #!/bin/bash -xe - | yum update -y aws-cfn-bootstrap - | # Install the files and packages from the metadata - '/opt/aws/bin/cfn-init -v ' - ' --stack ' - !Ref 'AWS::StackName' - ' --resource WebServerInstance ' - ' --configsets InstallAndRun ' - ' --region ' - !Ref 'AWS::Region' - |+ - | # Signal the status from cfn-init - '/opt/aws/bin/cfn-signal -e $? ' - ' --stack ' - !Ref 'AWS::StackName' - ' --resource WebServerInstance ' - ' --region ' - !Ref 'AWS::Region' - |+ CreationPolicy: ResourceSignal: Timeout: PT5M WebServerSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: Enable HTTP access via port 80 SecurityGroupIngress: - IpProtocol: tcp FromPort: '80' ToPort: '80' CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: !Ref SSHLocation Outputs: WebsiteURL: Description: URL for newly created LAMP stack Value: !Join - '' - - 'http://' - !GetAtt - WebServerInstance - PublicDnsName
In addition to the Amazon EC2 instance and security group, we create three input parameters that specify the instance type, an Amazon EC2 key pair to use for SSH access, and an IP address range that can be used to SSH to the instance. The mapping section ensures that AWS CloudFormation uses the correct AMI ID for the stack’s region and the Amazon EC2 instance type. Finally, the output section outputs the public URL of the web server.
LAMP Configuration
Now that we have a template that installs Linux, Apache, MySQL, and PHP, we’ll need to expand the template so that it automatically configures and runs Apache, MySQL, and PHP. In the following example, we expand on the Parameters
section, AWS::CloudFormation::Init
resource, and UserData
property to complete the configuration. As with the previous template, sections marked with an ellipsis (…) are omitted for brevity. Additions to the template are shown in red italic text.
Note that the example defines the DBUsername
and DBPassword
parameters with their NoEcho
property set to true
. If you set the NoEcho
attribute to true
, CloudFormation returns the parameter value masked as asterisks (*****) for any calls that describe the stack or stack events.
The example adds more parameters to obtain information for configuring the MySQL database, such as the database name, user name, password, and root password. The parameters also contain constraints that catch incorrectly formatted values before AWS CloudFormation creates the stack.
In the AWS::CloudFormation::Init
resource, we added a MySQL setup file, containing the database name, user name, and password. The example also adds a services
property to ensure that the httpd and mysqld services are running (ensureRunning
set to true
) and to ensure that the services are restarted if the instance is rebooted (enabled
set to true
). A good practice is to also include the cfn-hup helper script, with which you can make configuration updates to running instances by updating the stack template. For example, you could change the sample PHP application and then run a stack update to deploy the change.
In order to run the MySQL commands after the installation is complete, the example adds another configuration set to run the commands. Configuration sets are useful when you have a series of tasks that must be completed in a specific order. The example first runs the Install
configuration set and then the Configure
configuration set. The Configure
configuration set specifies the database root password and then creates a database. In the commands section, the commands are processed in alphabetical order by name, so the example adds a number before each command name to indicate its desired run order.
LAMP Installation
You’ll build on the previous basic Amazon EC2 template to automatically install Apache, MySQL, and PHP. To install the applications, you’ll add a UserData
property and Metadata
property. However, the template won’t configure and start the applications until the next section.
The UserData
property runs two shell commands: install the AWS CloudFormation helper scripts and then run the cfn-init helper script. Because the helper scripts are updated periodically, running the yum install -y aws-cfn-bootstrap
command ensures that you get the latest helper scripts. When you run cfn-init, it reads metadata from the AWS::CloudFormation::Init resource, which describes the actions to be carried out by cfn-init. For example, you can use cfn-init and AWS::CloudFormation::Init to install packages, write files to disk, or start a service. In our case, cfn-init installs the listed packages (httpd, mysql, and php) and creates the /var/www/html/index.php
file (a sample PHP application).
CreationPolicy Attribute
Finally, you need a way to instruct AWS CloudFormation to complete stack creation only after all the services (such as Apache and MySQL) are running and not after all the stack resources are created. In other words, if you use the template from the previous section to launch a stack, AWS CloudFormation sets the status of the stack as CREATE_COMPLETE
after it successfully creates all the resources. However, if one or more services failed to start, AWS CloudFormation still sets the stack status as CREATE_COMPLETE
. To prevent the status from changing to CREATE_COMPLETE
until all the services have successfully started, you can add a CreationPolicy attribute to the instance. This attribute puts the instance’s status in CREATE_IN_PROGRESS
until AWS CloudFormation receives the required number of success signals or the timeout period is exceeded, so you can control when the instance has been successfully created.
The following example adds a creation policy to the Amazon EC2 instance to ensure that cfn-init completes the LAMP installation and configuration before the stack creation is completed. In conjunction with the creation policy, the example needs to run the cfn-signal helper script to signal AWS CloudFormation when all the applications are installed and configured.