├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── chapter-10 └── update_route53.py ├── chapter-11 └── scaling_metric.json ├── chapter-2 ├── delete-retired-amis.py └── example-stack.json ├── chapter-3 ├── image-resizing.py ├── openvpn.json └── protected_instance_cloudformation.json ├── chapter-4 ├── install_puppet.sh ├── packer_image.json ├── puppet_client_cloudformation.json ├── puppet_master_cloudformation.json ├── puppet_masterless.json ├── puppet_stack.json └── site.pp ├── chapter-5 ├── __init__.py ├── __init__.py.erb ├── celery.py ├── celery.py.erb ├── create_project.pp ├── init.pp ├── local_settings.new ├── local_settings.py.erb ├── myblog.conf ├── myblog.json ├── myblog_web.conf ├── mynginx.pp ├── requirements.pp ├── site.pp ├── tasks.py ├── tasks.py.erb ├── web.json └── web.pp ├── chapter-6 ├── autoscaling.json ├── myblog.json └── notification.json ├── chapter-7 ├── fabfile.py.1 ├── fabfile.py.2 └── fabfile.py.3 └── chapter-8 ├── roles.pp ├── tags.py └── tags.rb /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Submitting an Idea or Correction 2 | 3 | Corrections to "AWS System Administration" need to be processed through the O'REILLY production chain to make it to publication. 4 | While we appreciate reports in any format including e-mail, Twitter, and carrier pidgeon, the best way to report a correction or make a suggestion in through the [Peccary Book's Errata page](https://www.oreilly.com/catalog/errata.csp?isbn=0636920027638). Feel free to reference a code fork or pull request to clearly indentify the issue in your report, but the GitHub workflow will not be used for merging as we make edits in our Atlas repo first, and propagate out from there. 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Using Code Examples 2 | 3 | This book is here to help you get your job done. Major examples can be downloaded 4 | from our GitHub repository. Many other small examples are scattered through the 5 | book; we have not bothered to include them in the repository because they are fairly 6 | easy to type in. 7 | 8 | In general, you may use the code in your programs and documentation. You do not 9 | need to contact us for permission unless you’re reproducing a significant portion of 10 | the code. For example, writing a program that uses several chunks of code from this 11 | book does not require permission. Selling or distributing a CD-ROM of examples 12 | from O’Reilly books does require permission. Answering a question by citing this 13 | book and quoting example code does not require permission. Incorporating a 14 | significant amount of example code from this book into your product’s documentation 15 | does require permission. 16 | 17 | We appreciate, but do not require, attribution. An attribution usually includes the 18 | title, author, publisher, and ISBN. For example: “AWS System Administration by Mike 19 | Ryan and Federico Lucifredi (O’Reilly). Copyright 2018 by Mike Ryan and Federico 20 | Lucifredi 978-1-449-34257-9.” 21 | 22 | If you feel your use of code examples falls outside fair use or the permission given 23 | above, feel free to contact us at permissions@oreilly.com. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Cover of the Peccary Book](https://pbs.twimg.com/media/DjvG0sBX0AAjwOZ.jpg:large) 2 | 3 | # AWS System Administration 4 | 5 | This repository tracks the code used by significant examples in the O'REILLY title [AWS System Administration](http://bit.ly/peccary-book), co-authored by yours truly and the awesome [Mike Ryan](https://github.com/mikery) — and commonly known as the "Peccary Book". We provide this repository for your convenience, and we will strive to keep it current and up-to-date as AWS and our book evolve. 6 | -------------------------------------------------------------------------------- /chapter-10/update_route53.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import boto.route53 5 | from boto.utils import get_instance_metadata 6 | 7 | def do_startup(): 8 | """ This function is executed when the instance launches. The instance's 9 | IP address will be added to the master or slave DNS record. If the 10 | record does not exist it will be created. 11 | """ 12 | # Check if the master resource record exists 13 | if zone.get_cname(master_hostname) is None: 14 | print 'Creating master record: %s' % master_hostname 15 | status = zone.add_cname(master_hostname, instance_ip, ttl) 16 | return 17 | print "Master record exists. Assuming slave role" 18 | # Check if the slave resource record exists - if more than one result is 19 | # found by get_cname, an exception is raised. This means that more than 20 | # one record exists so we can ignore it. 21 | try: 22 | slave_rr_exists = (zone.get_cname(slave_hostname) != None) 23 | except boto.exception.TooManyRecordsException: 24 | slave_rr_exists = True 25 | 26 | if slave_rr_exists: 27 | print 'Slave record exists. Adding instance to pool: %s' \ 28 | % slave_hostname 29 | else: 30 | print 'Creating slave record: %s' % slave_hostname 31 | # Create or update the slave Weighted Resource Record Set 32 | status = zone.add_cname(slave_hostname, instance_ip, ttl, slave_identifier) 33 | 34 | 35 | def do_promote(): 36 | master_rr = zone.get_cname(master_hostname) 37 | print 'Updating master record: %s %s' % (master_hostname, instance_ip) 38 | zone.update_cname(master_hostname, instance_ip) 39 | # Remove this instance from the slave CNAME pool by deleting its WRRS 40 | print 'Removing slave CNAME: %s %s' % (slave_hostname, slave_identifier) 41 | zone.delete_cname(slave_hostname, slave_identifier) 42 | 43 | 44 | parser = argparse.ArgumentParser(description='Update Route 53 master/slave DNS records') 45 | parser.add_argument('action', choices=['startup', 'promote']) 46 | #parser.add_argument('--hosted-zone-id', required=True) 47 | parser.add_argument('--domain', required=True) 48 | parser.add_argument('--cluster-name', required=True) 49 | parser.add_argument('--test') 50 | 51 | 52 | args = parser.parse_args() 53 | 54 | metadata = get_instance_metadata() 55 | 56 | instance_ip = metadata['local-ipv4'] 57 | instance_id = metadata['instance-id'] 58 | 59 | ttl = 60 # seconds 60 | 61 | master_hostname = 'master-%s.%s' % (args.cluster_name, args.domain) 62 | slave_hostname = 'slave-%s.%s' % (args.cluster_name, args.domain) 63 | # Identifier used for slave Weighted Resource Record Set 64 | slave_identifier = ('slave-%s' % instance_id, 10) 65 | 66 | 67 | conn = boto.route53.connect_to_region('us-east-1') 68 | zone = conn.get_zone(args.domain) 69 | 70 | if args.action == 'startup': 71 | do_startup() 72 | elif args.action == 'promote': 73 | do_promote() -------------------------------------------------------------------------------- /chapter-11/scaling_metric.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Auto Scaling on Custom Metrics", 4 | "Resources":{ 5 | "CustomMetricLaunchConfig":{ 6 | "Type":"AWS::AutoScaling::LaunchConfiguration", 7 | "Properties":{ 8 | "ImageId":"ami-43a15f3e", 9 | "InstanceType":"m3.medium" 10 | } 11 | }, 12 | "CustomMetricScalingGroup":{ 13 | "Type":"AWS::AutoScaling::AutoScalingGroup", 14 | "Properties":{ 15 | "AvailabilityZones":[ 16 | "us-east-1a" 17 | ], 18 | "Cooldown":"300", 19 | "DesiredCapacity":"1", 20 | "LaunchConfigurationName":{ 21 | "Ref":"CustomMetricLaunchConfig" 22 | }, 23 | "MaxSize":"10", 24 | "MinSize":"1" 25 | } 26 | }, 27 | "ScaleUpPolicy":{ 28 | "Type":"AWS::AutoScaling::ScalingPolicy", 29 | "Properties":{ 30 | "AdjustmentType":"ChangeInCapacity", 31 | "AutoScalingGroupName":{ 32 | "Ref":"CustomMetricScalingGroup" 33 | }, 34 | "ScalingAdjustment":"1" 35 | } 36 | }, 37 | "ScaleDownPolicy":{ 38 | "Type":"AWS::AutoScaling::ScalingPolicy", 39 | "Properties":{ 40 | "AdjustmentType":"ChangeInCapacity", 41 | "AutoScalingGroupName":{ 42 | "Ref":"CustomMetricScalingGroup" 43 | }, 44 | "ScalingAdjustment":"-1" 45 | } 46 | }, 47 | "WaitingTasksAlarm":{ 48 | "Type":"AWS::CloudWatch::Alarm", 49 | "Properties":{ 50 | "AlarmActions":[ 51 | { 52 | "Ref":"ScaleUpPolicy" 53 | } 54 | ], 55 | "ComparisonOperator":"GreaterThanThreshold", 56 | "EvaluationPeriods":"1", 57 | "MetricName":"WaitingTasks", 58 | "Namespace":"MyAppMetrics", 59 | "OKActions":[ 60 | { 61 | "Ref":"ScaleDownPolicy" 62 | } 63 | ], 64 | "Period":"60", 65 | "Statistic":"Maximum", 66 | "Threshold":"10", 67 | "Unit":"Count" 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /chapter-2/delete-retired-amis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from boto.ec2 import connect_to_region 4 | 5 | ec2_conn = connect_to_region('us-east-1') 6 | 7 | print 'Deleting retired AMI images.\n' 8 | 9 | for image in ec2_conn.get_all_images(owners = 'self', filters = {'tag:environment': 'retired'}): 10 | print ' Deleting image %s and associated snapshot' % (image.id) 11 | image.deregister(delete_snapshot = True) -------------------------------------------------------------------------------- /chapter-2/example-stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"A simple stack that launches an instance.", 4 | "Resources":{ 5 | "Ec2Instance":{ 6 | "Type":"AWS::EC2::Instance", 7 | "Properties":{ 8 | "InstanceType":"t2.micro", 9 | "ImageId":"ami-43a15f3e" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /chapter-3/image-resizing.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from PIL import Image 3 | import shutil 4 | import sys 5 | from boto.s3.connection import S3Connection 6 | from boto.s3.key import Key 7 | 8 | IMAGE_SIZES = [ 9 | (250, 250), 10 | (125, 125) 11 | ] 12 | 13 | bucket_name = sys.argv[1] 14 | # Create a temporary directory to store local files 15 | tmpdir = tempfile.mkdtemp() 16 | conn = S3Connection() 17 | bucket = conn.get_bucket(bucket_name) 18 | for key in bucket.list(prefix='incoming/'): 19 | filename = key.key.strip('incoming/') 20 | print 'Resizing %s' % filename 21 | # Copy the file to a local temp file 22 | tmpfile = '%s/%s' % (tmpdir, filename) 23 | key.get_contents_to_filename(tmpfile) 24 | # Resize the image with PIL 25 | orig_image = Image.open(tmpfile) 26 | # Find the file extension and remove it from filename 27 | file_ext = filename.split('.')[-1] 28 | for resolution in IMAGE_SIZES: 29 | resized_name = '%s%sx%s.%s' % (filename.rstrip(file_ext), resolution[0], resolution[1], file_ext) 30 | print 'Creating %s' % resized_name 31 | resized_tmpfile = '%s/%s' % (tmpdir, resized_name) 32 | resized_image = orig_image.resize(resolution) 33 | resized_image.save(resized_tmpfile) 34 | # Copy the resized image to the S3 bucket 35 | resized_key = Key(bucket) 36 | resized_key.key = 'processed/%s' % resized_name 37 | resized_key.set_contents_from_filename(resized_tmpfile) 38 | # Delete the original file from the bucket 39 | key.delete() 40 | 41 | # Delete the temp dir 42 | shutil.rmtree(tmpdir) -------------------------------------------------------------------------------- /chapter-3/openvpn.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"OpenVPN EC2 Instance and Security Group", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"EC2 KeyPair name", 7 | "Type":"String", 8 | "MinLength":"1", 9 | "MaxLength":"255", 10 | "AllowedPattern":"[\\x20-\\x7E]*", 11 | "ConstraintDescription":"can contain only ASCII characters." 12 | }, 13 | "AllowedIPRange":{ 14 | "Description":"IP Range allowed to access OpenVPN via SSH and HTTP(S)", 15 | "Type":"String", 16 | "MinLength":"9", 17 | "MaxLength":"18", 18 | "Default":"0.0.0.0/0", 19 | "AllowedPattern":"(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/(\\d{1,2})", 20 | "ConstraintDescription":"Must be a valid IP CIDR range of the form x.x.x.x/x." 21 | }, 22 | "AMI":{ 23 | "Description":"OpenVPN AMI ID", 24 | "Type":"String" 25 | } 26 | }, 27 | "Resources":{ 28 | "OpenVPNInstance":{ 29 | "Type":"AWS::EC2::Instance", 30 | "Properties":{ 31 | "InstanceType":"t2.micro", 32 | "SecurityGroups":[ 33 | { 34 | "Ref":"OpenVPNSecurityGroup" 35 | } 36 | ], 37 | "KeyName":{ 38 | "Ref":"KeyName" 39 | }, 40 | "ImageId":{ 41 | "Ref":"AMI" 42 | }, 43 | "SourceDestCheck":"false" 44 | } 45 | }, 46 | "OpenVPNSecurityGroup":{ 47 | "Type":"AWS::EC2::SecurityGroup", 48 | "Properties":{ 49 | "GroupDescription":"Allow SSH, HTTPS and OpenVPN access", 50 | "SecurityGroupIngress":[ 51 | { 52 | "IpProtocol":"tcp", 53 | "FromPort":"22", 54 | "ToPort":"22", 55 | "CidrIp":{ 56 | "Ref":"AllowedIPRange" 57 | } 58 | }, 59 | { 60 | "IpProtocol":"tcp", 61 | "FromPort":"443", 62 | "ToPort":"443", 63 | "CidrIp":{ 64 | "Ref":"AllowedIPRange" 65 | } 66 | }, 67 | { 68 | "IpProtocol":"tcp", 69 | "FromPort":"943", 70 | "ToPort":"943", 71 | "CidrIp":{ 72 | "Ref":"AllowedIPRange" 73 | } 74 | }, 75 | { 76 | "IpProtocol":"udp", 77 | "FromPort":"1194", 78 | "ToPort":"1194", 79 | "CidrIp":{ 80 | "Ref":"AllowedIPRange" 81 | } 82 | } 83 | ] 84 | } 85 | } 86 | }, 87 | "Outputs":{ 88 | "InstanceId":{ 89 | "Description":"InstanceId of the OpenVPN EC2 instance", 90 | "Value":{ 91 | "Ref":"OpenVPNInstance" 92 | } 93 | }, 94 | "OpenVPNSecurityGroup":{ 95 | "Description":"ID of the OpenVPN Security Group", 96 | "Value":{ 97 | "Fn::GetAtt":[ 98 | "OpenVPNSecurityGroup", 99 | "GroupId" 100 | ] 101 | } 102 | }, 103 | "PublicIP":{ 104 | "Description":"Public IP address of the newly created EC2 instance", 105 | "Value":{ 106 | "Fn::GetAtt":[ 107 | "OpenVPNInstance", 108 | "PublicIp" 109 | ] 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /chapter-3/protected_instance_cloudformation.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Example EC2 instance behind an OpenVPN server", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"EC2 KeyPair name", 7 | "Type":"String", 8 | "MinLength":"1", 9 | "MaxLength":"255", 10 | "AllowedPattern":"[\\x20-\\x7E]*", 11 | "ConstraintDescription":"can contain only ASCII characters." 12 | }, 13 | "AMI":{ 14 | "Description":"AMI ID", 15 | "Type":"String" 16 | }, 17 | "OpenVPNSecurityGroup":{ 18 | "Description":"OpenVPN Security Group ID", 19 | "Type":"String" 20 | } 21 | }, 22 | "Resources":{ 23 | "Ec2Instance":{ 24 | "Type":"AWS::EC2::Instance", 25 | "Properties":{ 26 | "InstanceType":"t2.micro", 27 | "SecurityGroups":[ 28 | { 29 | "Ref":"InstanceSecurityGroup" 30 | } 31 | ], 32 | "KeyName":{ 33 | "Ref":"KeyName" 34 | }, 35 | "ImageId":{ 36 | "Ref":"AMI" 37 | } 38 | } 39 | }, 40 | "InstanceSecurityGroup":{ 41 | "Type":"AWS::EC2::SecurityGroup", 42 | "Properties":{ 43 | "GroupDescription":"Allows SSH access from the OpenVPN instance", 44 | "SecurityGroupIngress":[ 45 | { 46 | "IpProtocol":"tcp", 47 | "FromPort":"22", 48 | "ToPort":"22", 49 | "SourceSecurityGroupId":{ 50 | "Ref":"OpenVPNSecurityGroup" 51 | } 52 | } 53 | ] 54 | } 55 | } 56 | }, 57 | "Outputs":{ 58 | "PrivateIP":{ 59 | "Description":"Private IP address of the EC2 instance", 60 | "Value":{ 61 | "Fn::GetAtt":[ 62 | "Ec2Instance", 63 | "PrivateIp" 64 | ] 65 | } 66 | }, 67 | "PublicIP":{ 68 | "Description":"Public IP address of the EC2 instance", 69 | "Value":{ 70 | "Fn::GetAtt":[ 71 | "Ec2Instance", 72 | "PublicIp" 73 | ] 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /chapter-4/install_puppet.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | wget --quiet http://apt.puppet.com/puppetlabs-release-trusty.deb 4 | sudo dpkg -i puppetlabs-release-trusty.deb 5 | sudo apt update 6 | # Should not be installed, but just in case we caught you using an old instance... 7 | sudo apt remove --yes puppet puppet-common 8 | # Install latest version of puppet from PuppetLabs repo 9 | sudo apt install --yes puppet facter -t trusty 10 | #install the stdlib module 11 | sudo puppet module install puppetlabs-stdlib 12 | rm puppetlabs-release-trusty.deb -------------------------------------------------------------------------------- /chapter-4/packer_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables":{ 3 | "aws_access_key":"", 4 | "aws_secret_key":"" 5 | }, 6 | "provisioners":[ 7 | { 8 | "type":"shell", 9 | "script":"install_puppet.sh" 10 | }, 11 | { 12 | "type":"puppet-masterless", 13 | "manifest_file":"puppet/manifests/site.pp", 14 | "module_paths":[ 15 | "puppet/modules" 16 | ] 17 | } 18 | ], 19 | "builders":[ 20 | { 21 | "type":"amazon-ebs", 22 | "access_key":"", 23 | "secret_key":"", 24 | "region":"us-east-1", 25 | "source_ami":"ami-c80b0aa2", 26 | "instance_type":"t2.small", 27 | "ssh_username":"ubuntu", 28 | "ami_name":"my-packer-example-", 29 | "associate_public_ip_address":true 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /chapter-4/puppet_client_cloudformation.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Example Puppet client stack", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"EC2 KeyPair name", 7 | "Type":"String", 8 | "MinLength":"1", 9 | "MaxLength":"255", 10 | "AllowedPattern":"[\\x20-\\x7E]*", 11 | "ConstraintDescription":"can contain only ASCII characters." 12 | }, 13 | "AMI":{ 14 | "Description":"AMI ID", 15 | "Type":"String" 16 | }, 17 | "PuppetMasterDNS":{ 18 | "Description":"Private DNS name of the Puppet master instance", 19 | "Type":"String" 20 | }, 21 | "PuppetClientSecurityGroup":{ 22 | "Description":"Name of the Puppet client Security Group", 23 | "Type":"String" 24 | } 25 | }, 26 | "Resources":{ 27 | "CFNKeys":{ 28 | "Type":"AWS::IAM::AccessKey", 29 | "Properties":{ 30 | "UserName":{ 31 | "Ref":"CFNInitUser" 32 | } 33 | } 34 | }, 35 | "CFNInitUser":{ 36 | "Type":"AWS::IAM::User", 37 | "Properties":{ 38 | "Policies":[ 39 | { 40 | "PolicyName":"AccessForCFNInit", 41 | "PolicyDocument":{ 42 | "Statement":[ 43 | { 44 | "Action":"cloudformation:DescribeStackResource", 45 | "Resource":"*", 46 | "Effect":"Allow" 47 | } 48 | ] 49 | } 50 | } 51 | ] 52 | } 53 | }, 54 | "PuppetClientInstance":{ 55 | "Type":"AWS::EC2::Instance", 56 | "Properties":{ 57 | "UserData":{ 58 | "Fn::Base64":{ 59 | "Fn::Join":[ 60 | "", 61 | [ 62 | "#!/bin/bash\n", 63 | "/opt/aws/bin/cfn-init --region ", 64 | { 65 | "Ref":"AWS::Region" 66 | }, 67 | " -s ", 68 | { 69 | "Ref":"AWS::StackName" 70 | }, 71 | " -r PuppetClientInstance ", 72 | " --access-key ", 73 | { 74 | "Ref":"CFNKeys" 75 | }, 76 | " --secret-key ", 77 | { 78 | "Fn::GetAtt":[ 79 | "CFNKeys", 80 | "SecretAccessKey" 81 | ] 82 | } 83 | ] 84 | ] 85 | } 86 | }, 87 | "KeyName":{ 88 | "Ref":"KeyName" 89 | }, 90 | "SecurityGroups":[ 91 | { 92 | "Ref":"PuppetClientSecurityGroup" 93 | } 94 | ], 95 | "InstanceType":"t2.micro", 96 | "ImageId":{ 97 | "Ref":"AMI" 98 | } 99 | }, 100 | "Metadata":{ 101 | "AWS::CloudFormation::Init":{ 102 | "config":{ 103 | "files":{ 104 | "/etc/puppet/puppet.conf":{ 105 | "content":{ 106 | "Fn::Join":[ 107 | "", 108 | [ 109 | "[main]\n", 110 | " server=", 111 | { 112 | "Ref":"PuppetMasterDNS" 113 | }, 114 | "\n", 115 | " logdir=/var/log/puppet\n", 116 | " rundir=/var/run/puppet\n", 117 | " ssldir=$vardir/ssl\n", 118 | " pluginsync=true\n", 119 | "[agent]\n", 120 | " classfile=$vardir/classes.txt\n", 121 | " localconfig=$vardir/localconfig\n" 122 | ] 123 | ] 124 | }, 125 | "owner":"root", 126 | "group":"root", 127 | "mode":"000644" 128 | } 129 | }, 130 | "packages":{ 131 | "rubygems":{ 132 | "json":[ 133 | 134 | ] 135 | }, 136 | "yum":{ 137 | "gcc":[ 138 | 139 | ], 140 | "rubygems":[ 141 | 142 | ], 143 | "ruby-devel":[ 144 | 145 | ], 146 | "make":[ 147 | 148 | ], 149 | "puppet":[ 150 | 151 | ] 152 | } 153 | }, 154 | "services":{ 155 | "sysvinit":{ 156 | "puppet":{ 157 | "ensureRunning":"true", 158 | "enabled":"true" 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | }, 167 | "Outputs":{ 168 | "PuppetClientIP":{ 169 | "Description":"Public IP of the Puppet client instance", 170 | "Value":{ 171 | "Fn::GetAtt":[ 172 | "PuppetClientInstance", 173 | "PublicIp" 174 | ] 175 | } 176 | }, 177 | "PuppetClientPrivateDNS":{ 178 | "Description":"Private DNS of the Puppet client instance", 179 | "Value":{ 180 | "Fn::GetAtt":[ 181 | "PuppetMasterInstance", 182 | "PrivateDnsName" 183 | ] 184 | } 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /chapter-4/puppet_master_cloudformation.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Example Puppet master stack", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"EC2 KeyPair name", 7 | "Type":"String", 8 | "MinLength":"1", 9 | "MaxLength":"255", 10 | "AllowedPattern":"[\\x20-\\x7E]*", 11 | "ConstraintDescription":"can contain only ASCII characters." 12 | }, 13 | "AMI":{ 14 | "Description":"AMI ID", 15 | "Type":"String" 16 | } 17 | }, 18 | "Resources":{ 19 | "CFNKeys":{ 20 | "Type":"AWS::IAM::AccessKey", 21 | "Properties":{ 22 | "UserName":{ 23 | "Ref":"CFNInitUser" 24 | } 25 | } 26 | }, 27 | "CFNInitUser":{ 28 | "Type":"AWS::IAM::User", 29 | "Properties":{ 30 | "Policies":[ 31 | { 32 | "PolicyName":"AccessForCFNInit", 33 | "PolicyDocument":{ 34 | "Statement":[ 35 | { 36 | "Action":"cloudformation:DescribeStackResource", 37 | "Resource":"*", 38 | "Effect":"Allow" 39 | } 40 | ] 41 | } 42 | } 43 | ] 44 | } 45 | }, 46 | "PuppetClientSecurityGroup":{ 47 | "Type":"AWS::EC2::SecurityGroup", 48 | "Properties":{ 49 | "SecurityGroupIngress":[ 50 | { 51 | "ToPort":"22", 52 | "IpProtocol":"tcp", 53 | "CidrIp":"0.0.0.0/0", 54 | "FromPort":"22" 55 | } 56 | ], 57 | "GroupDescription":"Group for SSH access to Puppet clients" 58 | } 59 | }, 60 | "PuppetMasterSecurityGroup":{ 61 | "Type":"AWS::EC2::SecurityGroup", 62 | "Properties":{ 63 | "SecurityGroupIngress":[ 64 | { 65 | "ToPort":"8140", 66 | "IpProtocol":"tcp", 67 | "SourceSecurityGroupName":{ 68 | "Ref":"PuppetClientSecurityGroup" 69 | }, 70 | "FromPort":"8140" 71 | }, 72 | { 73 | "ToPort":"22", 74 | "IpProtocol":"tcp", 75 | "CidrIp":"0.0.0.0/0", 76 | "FromPort":"22" 77 | } 78 | ], 79 | "GroupDescription":"Group for Puppet client to master communication" 80 | } 81 | }, 82 | "PuppetMasterInstance":{ 83 | "Type":"AWS::EC2::Instance", 84 | "Properties":{ 85 | "UserData":{ 86 | "Fn::Base64":{ 87 | "Fn::Join":[ 88 | "", 89 | [ 90 | "#!/bin/bash\n", 91 | "/opt/aws/bin/cfn-init --region ", 92 | { 93 | "Ref":"AWS::Region" 94 | }, 95 | " -s ", 96 | { 97 | "Ref":"AWS::StackName" 98 | }, 99 | " -r PuppetMasterInstance ", 100 | " --access-key ", 101 | { 102 | "Ref":"CFNKeys" 103 | }, 104 | " --secret-key ", 105 | { 106 | "Fn::GetAtt":[ 107 | "CFNKeys", 108 | "SecretAccessKey" 109 | ] 110 | }, 111 | "\n" 112 | ] 113 | ] 114 | } 115 | }, 116 | "KeyName":{ 117 | "Ref":"KeyName" 118 | }, 119 | "SecurityGroups":[ 120 | { 121 | "Ref":"PuppetMasterSecurityGroup" 122 | } 123 | ], 124 | "InstanceType":"t2.micro", 125 | "ImageId":{ 126 | "Ref":"AMI" 127 | } 128 | }, 129 | "Metadata":{ 130 | "AWS::CloudFormation::Init":{ 131 | "config":{ 132 | "files":{ 133 | "/etc/puppet/autosign.conf":{ 134 | "content":"*.internal\n", 135 | "owner":"root", 136 | "group":"wheel", 137 | "mode":"100644" 138 | }, 139 | "/etc/puppet/manifests/site.pp":{ 140 | "content":"import \"nodes\"\n", 141 | "owner":"root", 142 | "group":"wheel", 143 | "mode":"100644" 144 | }, 145 | "/etc/puppet/manifests/nodes.pp":{ 146 | "content":{ 147 | "Fn::Join":[ 148 | "", 149 | [ 150 | "node basenode {\n", 151 | " include cfn\n", 152 | " package { 'nginx':\n", 153 | " ensure => installed\n", 154 | " }\n", 155 | " service { 'nginx':\n", 156 | " ensure => running,\n", 157 | " require=> Package['nginx']\n", 158 | " }\n", 159 | "}\n", 160 | "node /^.*internal$/ inherits basenode {\n", 161 | "}\n" 162 | ] 163 | ] 164 | }, 165 | "owner":"root", 166 | "group":"wheel", 167 | "mode":"100644" 168 | }, 169 | "/etc/puppet/modules/cfn/lib/facter/cfn.rb":{ 170 | "owner":"root", 171 | "source":"https://s3.amazonaws.com/cloudformationexamples/cfn-facter-plugin.rb", 172 | "group":"wheel", 173 | "mode":"100644" 174 | }, 175 | "/etc/yum.repos.d/epel.repo":{ 176 | "owner":"root", 177 | "source":"https://s3.amazonaws.com/cloudformationexamples/enable-epel-on-amazon-linux-ami", 178 | "group":"root", 179 | "mode":"000644" 180 | }, 181 | "/etc/puppet/fileserver.conf":{ 182 | "content":"[modules]\n allow *.internal\n", 183 | "owner":"root", 184 | "group":"wheel", 185 | "mode":"100644" 186 | }, 187 | "/etc/puppet/puppet.conf":{ 188 | "content":{ 189 | "Fn::Join":[ 190 | "", 191 | [ 192 | "[main]\n", 193 | " logdir=/var/log/puppet\n", 194 | " rundir=/var/run/puppet\n", 195 | " ssldir=$vardir/ssl\n", 196 | " pluginsync=true\n", 197 | "[agent]\n", 198 | " classfile=$vardir/classes.txt\n", 199 | " localconfig=$vardir/localconfig\n" 200 | ] 201 | ] 202 | }, 203 | "owner":"root", 204 | "group":"root", 205 | "mode":"000644" 206 | }, 207 | "/etc/puppet/modules/cfn/manifests/init.pp":{ 208 | "content":"class cfn {}", 209 | "owner":"root", 210 | "group":"wheel", 211 | "mode":"100644" 212 | } 213 | }, 214 | "packages":{ 215 | "rubygems":{ 216 | "json":[ 217 | 218 | ] 219 | }, 220 | "yum":{ 221 | "gcc":[ 222 | 223 | ], 224 | "rubygems":[ 225 | 226 | ], 227 | "ruby-devel":[ 228 | 229 | ], 230 | "make":[ 231 | 232 | ], 233 | "puppet-server":[ 234 | 235 | ], 236 | "puppet":[ 237 | 238 | ] 239 | } 240 | }, 241 | "services":{ 242 | "sysvinit":{ 243 | "puppetmaster":{ 244 | "ensureRunning":"true", 245 | "enabled":"true" 246 | } 247 | } 248 | } 249 | } 250 | } 251 | } 252 | } 253 | }, 254 | "Outputs":{ 255 | "PuppetMasterPrivateDNS":{ 256 | "Description":"Private DNS Name of PuppetMaster", 257 | "Value":{ 258 | "Fn::GetAtt":[ 259 | "PuppetMasterInstance", 260 | "PrivateDnsName" 261 | ] 262 | } 263 | }, 264 | "PuppetMasterPublicDNS":{ 265 | "Description":"Public DNS Name of PuppetMaster", 266 | "Value":{ 267 | "Fn::GetAtt":[ 268 | "PuppetMasterInstance", 269 | "PublicDnsName" 270 | ] 271 | } 272 | }, 273 | "PuppetClientSecurityGroup":{ 274 | "Description":"Name of the Puppet client Security Group", 275 | "Value":{ 276 | "Ref":"PuppetClientSecurityGroup" 277 | } 278 | } 279 | } 280 | } -------------------------------------------------------------------------------- /chapter-4/puppet_masterless.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Example Puppet masterless stack", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"EC2 KeyPair name", 7 | "Type":"String", 8 | "MinLength":"1", 9 | "MaxLength":"255", 10 | "AllowedPattern":"[\\x20-\\x7E]*", 11 | "ConstraintDescription":"can contain only ASCII characters." 12 | }, 13 | "AMI":{ 14 | "Description":"AMI ID", 15 | "Type":"String" 16 | } 17 | }, 18 | "Resources":{ 19 | "CFNKeys":{ 20 | "Type":"AWS::IAM::AccessKey", 21 | "Properties":{ 22 | "UserName":{ 23 | "Ref":"CFNInitUser" 24 | } 25 | } 26 | }, 27 | "CFNInitUser":{ 28 | "Type":"AWS::IAM::User", 29 | "Properties":{ 30 | "Policies":[ 31 | { 32 | "PolicyName":"AccessForCFNInit", 33 | "PolicyDocument":{ 34 | "Statement":[ 35 | { 36 | "Action":"cloudformation:DescribeStackResource", 37 | "Resource":"*", 38 | "Effect":"Allow" 39 | } 40 | ] 41 | } 42 | } 43 | ] 44 | } 45 | }, 46 | "NginxInstance":{ 47 | "Type":"AWS::EC2::Instance", 48 | "Metadata":{ 49 | "AWS::CloudFormation::Init":{ 50 | "config":{ 51 | "packages":{ 52 | "yum":{ 53 | "puppet":[ 54 | 55 | ], 56 | "ruby-devel":[ 57 | 58 | ], 59 | "gcc":[ 60 | 61 | ], 62 | "make":[ 63 | 64 | ], 65 | "rubygems":[ 66 | 67 | ] 68 | }, 69 | "rubygems":{ 70 | "json":[ 71 | 72 | ] 73 | } 74 | }, 75 | "files":{ 76 | "/etc/yum.repos.d/epel.repo":{ 77 | "source":"https://s3.amazonaws.com/cloudformationexamples/enable-epel-on-amazon-linux-ami", 78 | "mode":"000644", 79 | "owner":"root", 80 | "group":"root" 81 | }, 82 | "/etc/puppet/autosign.conf":{ 83 | "content":"*.internal\n", 84 | "mode":"100644", 85 | "owner":"root", 86 | "group":"wheel" 87 | }, 88 | "/etc/puppet/puppet.conf":{ 89 | "content":{ 90 | "Fn::Join":[ 91 | "", 92 | [ 93 | "[main]\n", 94 | " logdir=/var/log/puppet\n", 95 | " rundir=/var/run/puppet\n", 96 | " ssldir=$vardir/ssl\n", 97 | " pluginsync=true\n", 98 | "[agent]\n", 99 | " classfile=$vardir/classes.txt\n", 100 | " localconfig=$vardir/localconfig\n" 101 | ] 102 | ] 103 | }, 104 | "mode":"000644", 105 | "owner":"root", 106 | "group":"root" 107 | }, 108 | "/etc/puppet/manifests/site.pp":{ 109 | "content":{ 110 | "Fn::Join":[ 111 | "", 112 | [ 113 | "node basenode {\n", 114 | " package { 'nginx':\n", 115 | " ensure => present\n", 116 | " }\n\n", 117 | " service { 'nginx':\n", 118 | " ensure => running,\n", 119 | " require=> Package['nginx']\n", 120 | " }\n", 121 | "}\n", 122 | "node /^.*internal$/ inherits basenode {\n", 123 | "}\n" 124 | ] 125 | ] 126 | } 127 | }, 128 | "mode":"100644", 129 | "owner":"root", 130 | "group":"wheel" 131 | } 132 | } 133 | } 134 | }, 135 | "Properties":{ 136 | "InstanceType":"t2.micro", 137 | "SecurityGroups":[ 138 | { 139 | "Ref":"NginxGroup" 140 | } 141 | ], 142 | "KeyName":{ 143 | "Ref":"KeyName" 144 | }, 145 | "ImageId":{ 146 | "Ref":"AMI" 147 | }, 148 | "UserData":{ 149 | "Fn::Base64":{ 150 | "Fn::Join":[ 151 | "", 152 | [ 153 | "#!/bin/bash\n", 154 | "/opt/aws/bin/cfn-init --region ", 155 | { 156 | "Ref":"AWS::Region" 157 | }, 158 | " -s ", 159 | { 160 | "Ref":"AWS::StackName" 161 | }, 162 | " -r NginxInstance ", 163 | " --access-key ", 164 | { 165 | "Ref":"CFNKeys" 166 | }, 167 | " --secret-key ", 168 | { 169 | "Fn::GetAtt":[ 170 | "CFNKeys", 171 | "SecretAccessKey" 172 | ] 173 | }, 174 | "\n", 175 | "/usr/bin/puppet apply /etc/puppet/manifests/site.pp", 176 | "\n" 177 | ] 178 | ] 179 | } 180 | } 181 | } 182 | }, 183 | "NginxGroup":{ 184 | "Type":"AWS::EC2::SecurityGroup", 185 | "Properties":{ 186 | "SecurityGroupIngress":[ 187 | { 188 | "ToPort":"22", 189 | "IpProtocol":"tcp", 190 | "CidrIp":"0.0.0.0/0", 191 | "FromPort":"22" 192 | } 193 | ], 194 | "GroupDescription":"Security Group for managed Nginx" 195 | } 196 | } 197 | }, 198 | "Outputs":{ 199 | "NginxDNSName":{ 200 | "Value":{ 201 | "Fn::GetAtt":[ 202 | "NginxInstance", 203 | "PublicDnsName" 204 | ] 205 | }, 206 | "Description":"DNS Name of Nginx managed instance" 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /chapter-4/puppet_stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Example Puppet master and client stack (manual install)", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"EC2 KeyPair name", 7 | "Type":"String", 8 | "MinLength":"1", 9 | "MaxLength":"255", 10 | "AllowedPattern":"[\\x20-\\x7E]*", 11 | "ConstraintDescription":"can contain only ASCII characters." 12 | }, 13 | "AMI":{ 14 | "Description":"AMI ID", 15 | "Type":"String" 16 | } 17 | }, 18 | "Resources":{ 19 | "PuppetClientGroup":{ 20 | "Type":"AWS::EC2::SecurityGroup", 21 | "Properties":{ 22 | "SecurityGroupIngress":[ 23 | { 24 | "ToPort":"22", 25 | "IpProtocol":"tcp", 26 | "CidrIp":"0.0.0.0/0", 27 | "FromPort":"22" 28 | } 29 | ], 30 | "GroupDescription":"Group for Puppet clients" 31 | } 32 | }, 33 | "PuppetMasterGroup":{ 34 | "Type":"AWS::EC2::SecurityGroup", 35 | "Properties":{ 36 | "SecurityGroupIngress":[ 37 | { 38 | "ToPort":"8140", 39 | "IpProtocol":"tcp", 40 | "SourceSecurityGroupName":{ 41 | "Ref":"PuppetClientGroup" 42 | }, 43 | "FromPort":"8140" 44 | }, 45 | { 46 | "ToPort":"22", 47 | "IpProtocol":"tcp", 48 | "CidrIp":"0.0.0.0/0", 49 | "FromPort":"22" 50 | } 51 | ], 52 | "GroupDescription":"Group for Puppet master" 53 | } 54 | }, 55 | "PuppetMasterInstance":{ 56 | "Type":"AWS::EC2::Instance", 57 | "Properties":{ 58 | "ImageId":{ 59 | "Ref":"AMI" 60 | }, 61 | "KeyName":{ 62 | "Ref":"KeyName" 63 | }, 64 | "SecurityGroups":[ 65 | { 66 | "Ref":"PuppetMasterGroup" 67 | } 68 | ], 69 | "InstanceType":"t2.micro" 70 | } 71 | }, 72 | "PuppetClientInstance":{ 73 | "Type":"AWS::EC2::Instance", 74 | "Properties":{ 75 | "ImageId":{ 76 | "Ref":"AMI" 77 | }, 78 | "KeyName":{ 79 | "Ref":"KeyName" 80 | }, 81 | "SecurityGroups":[ 82 | { 83 | "Ref":"PuppetClientGroup" 84 | } 85 | ], 86 | "InstanceType":"t2.micro", 87 | "UserData":{ 88 | "Fn::Base64":{ 89 | "Fn::GetAtt":[ 90 | "PuppetMasterInstance", 91 | "PrivateDnsName" 92 | ] 93 | } 94 | } 95 | } 96 | } 97 | }, 98 | "Outputs":{ 99 | "PuppetMasterIP":{ 100 | "Description":"Public IP of the Puppet master instance", 101 | "Value":{ 102 | "Fn::GetAtt":[ 103 | "PuppetMasterInstance", 104 | "PublicIp" 105 | ] 106 | } 107 | }, 108 | "PuppetClientIP":{ 109 | "Description":"Public IP of the Puppet client instance", 110 | "Value":{ 111 | "Fn::GetAtt":[ 112 | "PuppetClientInstance", 113 | "PublicIp" 114 | ] 115 | } 116 | }, 117 | "PuppetMasterPrivateDNS":{ 118 | "Description":"Private DNS of the Puppet master instance", 119 | "Value":{ 120 | "Fn::GetAtt":[ 121 | "PuppetMasterInstance", 122 | "PrivateDnsName" 123 | ] 124 | } 125 | }, 126 | "PuppetClientPrivateDNS":{ 127 | "Description":"Private DNS of the Puppet client instance", 128 | "Value":{ 129 | "Fn::GetAtt":[ 130 | "PuppetClientInstance", 131 | "PrivateDnsName" 132 | ] 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /chapter-4/site.pp: -------------------------------------------------------------------------------- 1 | require stdlib 2 | 3 | node default { 4 | $userdata = parsejson($ec2_userdata) 5 | 6 | $role = $userdata['role'] 7 | 8 | case $role { 9 | "web": { 10 | require my_web_module 11 | } 12 | "db": { 13 | require my_database_module 14 | } 15 | default: { fail("Unrecognised role: $role") } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /chapter-5/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | default_app_config = 'myblog.apps.MyBlogConfig' 4 | 5 | # This will make sure the app is always imported when 6 | # Django starts so that shared_task will use this app. 7 | from .celery import app as celery_app 8 | 9 | __all__ = ('celery_app') -------------------------------------------------------------------------------- /chapter-5/__init__.py.erb: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | default_app_config = 'myblog.apps.MyBlogConfig' 4 | 5 | # This will make sure the app is always imported when 6 | # Django starts so that shared_task will use this app. 7 | from .celery import app as celery_app 8 | 9 | __all__ = ('celery_app') -------------------------------------------------------------------------------- /chapter-5/celery.py: -------------------------------------------------------------------------------- 1 | class myblog::celery { 2 | Class["myblog::celery"] -> Class["myblog"] 3 | 4 | supervisor::service { "myblog_celery": 5 | ensure => present, 6 | enable => true, 7 | command => "/usr/bin/python ${myblog::app_path}/manage.py celery -A myblog worker", 8 | user => "mezzanine", 9 | group => "mezzanine" 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /chapter-5/celery.py.erb: -------------------------------------------------------------------------------- 1 | class myblog::celery { 2 | Class["myblog::celery"] -> Class["myblog"] 3 | 4 | supervisor::service { "myblog_celery": 5 | ensure => present, 6 | enable => true, 7 | command => "/usr/bin/python ${myblog::app_path}/manage.py celery -A myblog worker", 8 | user => "mezzanine", 9 | group => "mezzanine" 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /chapter-5/create_project.pp: -------------------------------------------------------------------------------- 1 | class myblog::create_project { 2 | 3 | # Create the Mezzanine project 4 | exec { "init-mezzanine-project": 5 | command => "/usr/local/bin/mezzanine-project $myblog::app_path", 6 | user => "mezzanine", 7 | creates => "$myblog::app_path/__init__.py" 8 | } 9 | 10 | # Create the local_settings.py file 11 | file { "$myblog::app_path/myblog/local_settings.py": 12 | ensure => present, 13 | content => template("myblog/local_settings.py.erb"), 14 | owner => "mezzanine", 15 | group => "mezzanine", 16 | require => Exec["init-mezzanine-project"], 17 | notify => Exec["init-mezzanine-db"] 18 | } 19 | 20 | # Create the database 21 | exec { "init-mezzanine-db": 22 | command => "/usr/bin/python manage.py createdb --noinput", 23 | user => "mezzanine", 24 | cwd => "$myblog::app_path", 25 | refreshonly => true 26 | } -------------------------------------------------------------------------------- /chapter-5/init.pp: -------------------------------------------------------------------------------- 1 | class myblog ( $db_endpoint, $db_user, $db_password ) { 2 | 3 | $app_path = "/srv/mezzanine" 4 | 5 | class {"supervisor": } 6 | 7 | require myblog::requirements 8 | } -------------------------------------------------------------------------------- /chapter-5/local_settings.new: -------------------------------------------------------------------------------- 1 | ALLOWED_HOSTS = "*" 2 | DEBUG = True 3 | DATABASES = { 4 | "default": { 5 | "ENGINE": "django.db.backends.mysql", 6 | "NAME": "myblog", 7 | "USER": "awsuser", 8 | "PASSWORD": "foobarbaz", 9 | "HOST": "myblog.cvqj2dqsvoab.us-east-1.rds.amazonaws.com", 10 | "PORT": "3306" 11 | } 12 | } -------------------------------------------------------------------------------- /chapter-5/local_settings.py.erb: -------------------------------------------------------------------------------- 1 | ALLOWED_HOSTS = "*" 2 | 3 | DEBUG = True 4 | 5 | DATABASES = { 6 | "default": { 7 | "ENGINE": "django.db.backends.mysql", 8 | "NAME": "mydb", 9 | "USER": "<%= @db_user %>", 10 | "PASSWORD": "<%= @db_password %>", 11 | "HOST": "<%= @db_endpoint %>", 12 | "PORT": "3306" 13 | } 14 | } -------------------------------------------------------------------------------- /chapter-5/myblog.conf: -------------------------------------------------------------------------------- 1 | upstream myblog_app { 2 | 3 | server localhost:8000; 4 | 5 | } 6 | 7 | server { 8 | listen *:80 default; 9 | 10 | server_name blog.example.com; 11 | access_log /var/log/nginx/blog.example.com.access.log; 12 | location / { 13 | proxy_pass http://myblog_app; 14 | proxy_read_timeout 90; 15 | proxy_set_header Host $http_host; 16 | } 17 | } -------------------------------------------------------------------------------- /chapter-5/myblog.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Mezzanine-powered blog with RDS, served with Nginx.", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"Name of an existing EC2 KeyPair to enable SSH access", 7 | "Type":"String" 8 | }, 9 | "WebAMI":{ 10 | "Type":"String" 11 | }, 12 | "CeleryAMI":{ 13 | "Type":"String" 14 | }, 15 | "KeyPair":{ 16 | "Type":"String" 17 | }, 18 | "DBUser":{ 19 | "Type":"String" 20 | }, 21 | "DBPassword":{ 22 | "Type":"String", 23 | "NoEcho":"TRUE" 24 | } 25 | }, 26 | "Resources":{ 27 | "CacheCluster":{ 28 | "Type":"AWS::ElastiCache::CacheCluster", 29 | "Properties":{ 30 | "CacheNodeType":"cache.r3.large", 31 | "CacheSecurityGroupNames":[ 32 | "CacheSecurityGroup" 33 | ], 34 | "Engine":"memcached", 35 | "NumCacheNodes":"1" 36 | } 37 | }, 38 | "CacheSecurityGroup":{ 39 | "Type":"AWS::ElastiCache::SecurityGroup", 40 | "Properties":{ 41 | "Description":"Allow access from Web instances" 42 | } 43 | }, 44 | "CacheSecurityGroupIngress":{ 45 | "Type":"AWS::ElastiCache::SecurityGroupIngress", 46 | "Properties":{ 47 | "CacheSecurityGroupName":{ 48 | "Ref":"CacheSecurityGroup" 49 | }, 50 | "EC2SecurityGroupName":{ 51 | "Ref":"WebSecurityGroup" 52 | } 53 | } 54 | }, 55 | "BlogDB":{ 56 | "Type":"AWS::RDS::DBInstance", 57 | "Properties":{ 58 | "DBSecurityGroups":[ 59 | { 60 | "Ref":"DBSecurityGroup" 61 | } 62 | ], 63 | "DBName":"myblog", 64 | "AllocatedStorage":5, 65 | "DBInstanceClass":"t2.micro", 66 | "Engine":"MySQL", 67 | "EngineVersion":"5.5", 68 | "MasterUsername":{ 69 | "Ref":"DBUser" 70 | }, 71 | "MasterUserPassword":{ 72 | "Ref":"DBPassword" 73 | } 74 | }, 75 | "DeletionPolicy":"Snapshot" 76 | }, 77 | "DBSecurityGroup":{ 78 | "Type":"AWS::EC2::SecurityGroup", 79 | "Properties":{ 80 | "GroupDescription":"Allow inbound MySQL access from web instances", 81 | "SecurityGroupIngress":[ 82 | { 83 | "IpProtocol":"tcp", 84 | "FromPort":"3306", 85 | "ToPort":"3306", 86 | "SourceSecurityGroupName":{ 87 | "Ref":"WebSecurityGroup" 88 | } 89 | }, 90 | { 91 | "IpProtocol":"tcp", 92 | "FromPort":"3306", 93 | "ToPort":"3306", 94 | "SourceSecurityGroupName":{ 95 | "Ref":"CelerySecurityGroup" 96 | } 97 | } 98 | ] 99 | } 100 | }, 101 | "CeleryQueue":{ 102 | "Type":"AWS::SQS::Queue" 103 | }, 104 | "MyBlogRole":{ 105 | "Type":"AWS::IAM::Role", 106 | "Properties":{ 107 | "AssumeRolePolicyDocument":{ 108 | "Statement":[ 109 | { 110 | "Effect":"Allow", 111 | "Principal":{ 112 | "Service":[ 113 | "ec2.amazonaws.com" 114 | ] 115 | }, 116 | "Action":[ 117 | "sts:AssumeRole" 118 | ] 119 | } 120 | ] 121 | }, 122 | "Path":"/" 123 | } 124 | }, 125 | "MyBlogRolePolicies":{ 126 | "Type":"AWS::IAM::Policy", 127 | "Properties":{ 128 | "PolicyName":"MyBlogRole", 129 | "PolicyDocument":{ 130 | "Statement":[ 131 | { 132 | "Effect":"Allow", 133 | "Action":[ 134 | "sqs:*" 135 | ], 136 | "Resource":{ 137 | "Ref":"CeleryQueue" 138 | } 139 | } 140 | ] 141 | }, 142 | "Roles":[ 143 | { 144 | "Ref":"MyBlogRole" 145 | } 146 | ] 147 | } 148 | }, 149 | "MyBlogInstanceProfile":{ 150 | "Type":"AWS::IAM::InstanceProfile", 151 | "Properties":{ 152 | "Path":"/", 153 | "Roles":[ 154 | { 155 | "Ref":"MyBlogRole" 156 | } 157 | ] 158 | } 159 | }, 160 | "CeleryInstance":{ 161 | "Type":"AWS::EC2::Instance", 162 | "Properties":{ 163 | "SecurityGroups":[ 164 | { 165 | "Ref":"CelerySecurityGroup" 166 | } 167 | ], 168 | "KeyName":"my-ssh-keypair", 169 | "ImageId":{ 170 | "Ref":"CeleryAMI" 171 | }, 172 | "IamInstanceProfile":{ 173 | "Ref":"MyBlogInstanceProfile" 174 | }, 175 | "UserData":{ 176 | "Fn::Base64":{ 177 | "Fn::Join":[ 178 | "", 179 | [ 180 | "{\"role\": \"celery\",", 181 | " \"db_endpoint\": \"", 182 | { 183 | "Fn::GetAtt":[ 184 | "BlogDB", 185 | "Endpoint.Address" 186 | ] 187 | }, 188 | "\",", 189 | " \"db_user\": \"", 190 | { 191 | "Ref":"DBUser" 192 | }, 193 | "\",", 194 | " \"db_password\": \"", 195 | { 196 | "Ref":"DBPassword" 197 | }, 198 | "\",", 199 | " \"cache_endpoint\": \"", 200 | { 201 | "Fn::GetAtt":[ 202 | "CacheCluster", 203 | "ConfigurationEndpoint.Address" 204 | ] 205 | }, 206 | "\"}" 207 | ] 208 | ] 209 | } 210 | } 211 | } 212 | }, 213 | "CelerySecurityGroup":{ 214 | "Type":"AWS::EC2::SecurityGroup", 215 | "Properties":{ 216 | "GroupDescription":"Allow SSH from anywhere", 217 | "SecurityGroupIngress":[ 218 | { 219 | "IpProtocol":"tcp", 220 | "FromPort":"22", 221 | "ToPort":"22", 222 | "CidrIp":"0.0.0.0/0" 223 | } 224 | ] 225 | } 226 | }, 227 | "CelerySecurityGroupIngress":{ 228 | "Type":"AWS::ElastiCache::SecurityGroupIngress", 229 | "Properties":{ 230 | "CacheSecurityGroupName":{ 231 | "Ref":"CacheSecurityGroup" 232 | }, 233 | "EC2SecurityGroupName":{ 234 | "Ref":"CelerySecurityGroup" 235 | } 236 | } 237 | }, 238 | "WebInstance":{ 239 | "Type":"AWS::EC2::Instance", 240 | "Properties":{ 241 | "SecurityGroups":[ 242 | { 243 | "Ref":"WebSecurityGroup" 244 | } 245 | ], 246 | "KeyName":{ 247 | "Ref":"KeyPair" }, 248 | "ImageId":{ 249 | "Ref":"WebAMI" 250 | }, 251 | "IamInstanceProfile":{ 252 | "Ref":"MyBlogInstanceProfile" 253 | }, 254 | "UserData":{ 255 | "Fn::Base64":{ 256 | "Fn::Join":[ 257 | "", 258 | [ 259 | "{\"db_endpoint\": \"", 260 | { 261 | "Fn::GetAtt":[ 262 | "BlogDB", 263 | "Endpoint.Address" 264 | ] 265 | }, 266 | "\",", 267 | " \"db_user\": \"", 268 | { 269 | "Ref":"DBUser" 270 | }, 271 | "\",", 272 | " \"db_password\": \"", 273 | { 274 | "Ref":"DBPassword" 275 | }, 276 | "\",", 277 | " \"cache_endpoint\": \"", 278 | { 279 | "Fn::GetAtt":[ 280 | "CacheCluster", 281 | "ConfigurationEndpoint.Address" 282 | ] 283 | }, 284 | "\" }" 285 | ] 286 | ] 287 | } 288 | } 289 | } 290 | }, 291 | "WebSecurityGroup":{ 292 | "Type":"AWS::EC2::SecurityGroup", 293 | "Properties":{ 294 | "GroupDescription":"Allow SSH and HTTP from anywhere", 295 | "SecurityGroupIngress":[ 296 | { 297 | "IpProtocol":"tcp", 298 | "FromPort":"22", 299 | "ToPort":"22", 300 | "CidrIp":"0.0.0.0/0" 301 | }, 302 | { 303 | "IpProtocol":"tcp", 304 | "FromPort":"80", 305 | "ToPort":"80", 306 | "CidrIp":"0.0.0.0/0" 307 | } 308 | ] 309 | } 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /chapter-5/myblog_web.conf: -------------------------------------------------------------------------------- 1 | [program:myblog_app] 2 | command=/usr/bin/python /srv/mezzanine/manage.py runserver 3 | autostart=true 4 | autorestart=unexpected 5 | stopwaitsecs=10 6 | stopasgroup=true 7 | killasgroup=true 8 | user=mezzanine -------------------------------------------------------------------------------- /chapter-5/mynginx.pp: -------------------------------------------------------------------------------- 1 | class myblog::mynginx { 2 | 3 | class { "nginx": } 4 | 5 | nginx::resource::upstream { "myblog_app": 6 | ensure => present, 7 | members => [ 8 | 'localhost:8000', 9 | ] 10 | } 11 | 12 | nginx::resource::vhost { "blog.example.com": 13 | ensure => enable, 14 | listen_options => "default", 15 | proxy => "http://myblog_app" 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /chapter-5/requirements.pp: -------------------------------------------------------------------------------- 1 | class myblog::requirements { 2 | 3 | $packages = ["python-dev", "python-pip", "libtiff5-dev", "libjpeg8-dev", "zlib1g-dev", "libfreetype6-dev","python-mysqldb", "mysql-client-5.6", 4 | "libmysqlclient-dev"] 5 | 6 | package { $packages: 7 | ensure => installed 8 | } 9 | 10 | $pip_packages = ["Mezzanine"] 11 | 12 | package { $pip_packages: 13 | ensure => installed, 14 | provider => pip, 15 | require => Package[$packages] 16 | } 17 | 18 | user { "mezzanine": 19 | ensure => present 20 | } 21 | 22 | file { "$myblog::app_path": 23 | ensure => "directory", 24 | owner => "mezzanine", 25 | group => "mezzanine" 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /chapter-5/site.pp: -------------------------------------------------------------------------------- 1 | require stdlib 2 | 3 | node default { 4 | 5 | $userdata = parsejson($ec2_userdata) 6 | 7 | # Set variables from userdata 8 | $role = $userdata['role'] 9 | $db_endpoint = $userdata['db_endpoint'] 10 | $db_user = $userdata['db_user'] 11 | $db_password = $userdata['db_password'] 12 | 13 | case $role { 14 | "web": { $role_class = "myblog::web" } 15 | "celery": { $role_class = "myblog::celery" } 16 | default: { fail("Unrecognized role: $role") } 17 | } 18 | 19 | # Main myblog class, takes all params 20 | class { "myblog": 21 | db_endpoint => $db_endpoint, 22 | db_user => $db_user, 23 | db_password => $db_password 24 | } 25 | # Role-specific class, e.g. myblog::web 26 | class { $role_class: } 27 | 28 | } -------------------------------------------------------------------------------- /chapter-5/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | import os 3 | import urllib 4 | from django.dispatch import receiver 5 | from django.db.models.signals import post_save 6 | from mezzanine.generic.models import ThreadedComment 7 | 8 | AWS_KEY = os.getenv('AWS_ACCESS_KEY_ID') 9 | AWS_SECRET = os.getenv('AWS_SECRET_ACCESS_KEY') 10 | if not (AWS_KEY and AWS_SECRET): 11 | print "AWS environment variables are not set\n" 12 | exit(1) 13 | 14 | app = Celery('tasks', broker = 'sqs://@') 15 | 16 | def is_comment_spam(comment): 17 | # This check is just an example! 18 | if "spam" in comment.comment: 19 | return True 20 | 21 | @app.task 22 | def process_comment_async(comment_id): 23 | print "Processing comment" 24 | comment = ThreadedComment.objects.get(pk=comment_id) 25 | if is_comment_spam(comment): 26 | # The comment is spam, so hide it 27 | ThreadedComment.objects.filter(id=comment_id).update(is_public=False) 28 | 29 | @receiver(post_save, sender=ThreadedComment) 30 | def process_comment(sender, instance, **kwargs): 31 | process_comment_async.delay(instance.id) 32 | -------------------------------------------------------------------------------- /chapter-5/tasks.py.erb: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | import os 3 | import urllib 4 | from django.dispatch import receiver 5 | from django.db.models.signals import post_save 6 | from mezzanine.generic.models import ThreadedComment 7 | 8 | AWS_KEY = os.getenv('AWS_ACCESS_KEY_ID') 9 | AWS_SECRET = os.getenv('AWS_SECRET_ACCESS_KEY') 10 | if not (AWS_KEY and AWS_SECRET): 11 | print "AWS environment variables are not set\n" 12 | exit(1) 13 | 14 | app = Celery('tasks', broker = 'sqs://{0}:{1}@'.format(urllib.quote(AWS_KEY, \ 15 | safe=''), urllib.quote(AWS_SECRET, safe=''))) 16 | 17 | def is_comment_spam(comment): 18 | # This check is just an example! 19 | if "spam" in comment.comment: 20 | return True 21 | 22 | @app.task 23 | def process_comment_async(comment_id): 24 | print "Processing comment" 25 | comment = ThreadedComment.objects.get(pk=comment_id) 26 | if is_comment_spam(comment): 27 | # The comment is spam, so hide it 28 | ThreadedComment.objects.filter(id=comment_id).update(is_public=False) 29 | 30 | @receiver(post_save, sender=ThreadedComment) 31 | def process_comment(sender, instance, **kwargs): 32 | process_comment_async.delay(instance.id) -------------------------------------------------------------------------------- /chapter-5/web.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables":{ 3 | "aws_access_key":"", 4 | "aws_secret_key":"" 5 | }, 6 | "provisioners":[ 7 | { 8 | "type":"shell", 9 | "script":"install_puppet.sh" 10 | }, 11 | { 12 | "type":"puppet-masterless", 13 | "manifest_file":"puppet/manifests/site.pp", 14 | "module_paths":[ 15 | "puppet/modules" 16 | ] 17 | } 18 | ], 19 | "builders":[ 20 | { 21 | "type":"amazon-ebs", 22 | "access_key":"", 23 | "secret_key":"", 24 | "region":"us-east-1", 25 | "source_ami":"ami-43a15f3e", 26 | "instance_type":"t1.micro", 27 | "ssh_username":"ubuntu", 28 | "associate_public_ip_address":true, 29 | "ami_name":"myblog-web-", 30 | "user_data":"{\"role\": \"web\"}" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /chapter-5/web.pp: -------------------------------------------------------------------------------- 1 | class myblog::web { 2 | Class["myblog::web"] -> Class["myblog"] 3 | 4 | require myblog::mynginx 5 | 6 | supervisor::service { "myblog_app": 7 | ensure => present, 8 | enable => true, 9 | command => "/usr/bin/python ${myblog::app_path}/manage.py runserver", 10 | stopasgroup => true, 11 | killasgroup => true, 12 | user => "mezzanine", 13 | group => "mezzanine" 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /chapter-6/autoscaling.json: -------------------------------------------------------------------------------- 1 | "MyLaunchConfig" : { 2 | "Type" : "AWS::AutoScaling::LaunchConfiguration", 3 | "Properties" : { 4 | "ImageId" : "ami-XXXXXXXX", 5 | "SecurityGroups" : [ { "Ref" : "MySecurityGroup" } ], 6 | "InstanceType" : "t2.micro" 7 | } 8 | }, 9 | "MyASGroup" : { 10 | "Type" : "AWS::AutoScaling::AutoScalingGroup", 11 | "Properties" : { 12 | "AvailabilityZones" : ["us-east-1a", "us-east-1b", "us-east-1c", "useast-1d", 13 | "us-east-1e"], 14 | "LaunchConfigurationName" : { "Ref" : "MyLaunchConfig" }, 15 | "MinSize" : "1", 16 | "MaxSize" : "1", 17 | "DesiredCapacity" : "1" 18 | } 19 | } -------------------------------------------------------------------------------- /chapter-6/myblog.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion":"2010-09-09", 3 | "Description":"Mezzanine-powered blog with RDS, served with Nginx.", 4 | "Parameters":{ 5 | "KeyName":{ 6 | "Description":"Name of an existing EC2 KeyPair to enable SSH access to the instance", 7 | "Type":"String" 8 | }, 9 | "WebAMI":{ 10 | "Type":"String" 11 | }, 12 | "CeleryAMI":{ 13 | "Type":"String" 14 | }, 15 | "KeyPair":{ 16 | "Type":"String" 17 | }, 18 | "DBUser":{ 19 | "Type":"String" 20 | }, 21 | "DBPassword":{ 22 | "Type":"String", 23 | "NoEcho":"TRUE" 24 | } 25 | }, 26 | "Resources":{ 27 | "CacheCluster":{ 28 | "Type":"AWS::ElastiCache::CacheCluster", 29 | "Properties":{ 30 | "CacheNodeType":"cache.r3.large", 31 | "CacheSecurityGroupNames":[ 32 | "CacheSecurityGroup" 33 | ], 34 | "Engine":"memcached", 35 | "NumCacheNodes":"1" 36 | } 37 | }, 38 | "CacheSecurityGroup":{ 39 | "Type":"AWS::ElastiCache::SecurityGroup", 40 | "Properties":{ 41 | "Description":"Allow access from Web instances" 42 | } 43 | }, 44 | "CacheSecurityGroupIngress":{ 45 | "Type":"AWS::ElastiCache::SecurityGroupIngress", 46 | "Properties":{ 47 | "CacheSecurityGroupName":{ 48 | "Ref":"CacheSecurityGroup" 49 | }, 50 | "EC2SecurityGroupName":{ 51 | "Ref":"WebSecurityGroup" 52 | } 53 | } 54 | }, 55 | "BlogDB":{ 56 | "Type":"AWS::RDS::DBInstance", 57 | "Properties":{ 58 | "DBSecurityGroups":[ 59 | { 60 | "Ref":"DBSecurityGroup" 61 | } 62 | ], 63 | "DBName":"myblog", 64 | "AllocatedStorage":5, 65 | "DBInstanceClass":"t2.micro", 66 | "Engine":"MySQL", 67 | "EngineVersion":"5.5", 68 | "MasterUsername":{ 69 | "Ref":"DBUser" 70 | }, 71 | "MasterUserPassword":{ 72 | "Ref":"DBPassword" 73 | } 74 | }, 75 | "DeletionPolicy":"Snapshot" 76 | }, 77 | "DBSecurityGroup":{ 78 | "Type":"AWS::EC2::SecurityGroup", 79 | "Properties":{ 80 | "GroupDescription":"Allow inbound MySQL access from web instances", 81 | "SecurityGroupIngress":[ 82 | { 83 | "IpProtocol":"tcp", 84 | "FromPort":"3306", 85 | "ToPort":"3306", 86 | "SourceSecurityGroupName":{ 87 | "Ref":"WebSecurityGroup" 88 | } 89 | }, 90 | { 91 | "IpProtocol":"tcp", 92 | "FromPort":"3306", 93 | "ToPort":"3306", 94 | "SourceSecurityGroupName":{ 95 | "Ref":"CelerySecurityGroup" 96 | } 97 | } 98 | ] 99 | }, 100 | "CeleryQueue":{ 101 | "Type":"AWS::SQS::Queue" 102 | }, 103 | "MyBlogRole":{ 104 | "Type":"AWS::IAM::Role", 105 | "Properties":{ 106 | "AssumeRolePolicyDocument":{ 107 | "Statement":[ 108 | { 109 | "Effect":"Allow", 110 | "Principal":{ 111 | "Service":[ 112 | "ec2.amazonaws.com" 113 | ] 114 | }, 115 | "Action":[ 116 | "sts:AssumeRole" 117 | ] 118 | } 119 | ] 120 | }, 121 | "Path":"/" 122 | } 123 | }, 124 | "MyBlogRolePolicies":{ 125 | "Type":"AWS::IAM::Policy", 126 | "Properties":{ 127 | "PolicyName":"MyBlogRole", 128 | "PolicyDocument":{ 129 | "Statement":[ 130 | { 131 | "Effect":"Allow", 132 | "Action":[ 133 | "sqs:*" 134 | ], 135 | "Resource":{ 136 | "Ref":"CeleryQueue" 137 | } 138 | } 139 | ] 140 | }, 141 | "Roles":[ 142 | { 143 | "Ref":"MyBlogRole" 144 | } 145 | ] 146 | } 147 | }, 148 | "MyBlogInstanceProfile":{ 149 | "Type":"AWS::IAM::InstanceProfile", 150 | "Properties":{ 151 | "Path":"/", 152 | "Roles":[ 153 | { 154 | "Ref":"MyBlogRole" 155 | } 156 | ] 157 | } 158 | }, 159 | "CeleryLaunchConfig":{ 160 | "Type":"AWS::AutoScaling::LaunchConfiguration", 161 | "Properties":{ 162 | "ImageId":{ 163 | "Ref":"CeleryAMI" 164 | }, 165 | "SecurityGroups":[ 166 | { 167 | "Ref":"CelerySecurityGroup" 168 | } 169 | ] 170 | } 171 | }, 172 | "CeleryGroup":{ 173 | "Type":"AWS::AutoScaling::AutoScalingGroup", 174 | "Properties":{ 175 | "AvailabilityZones":{ 176 | "Fn::GetAZs":"" 177 | }, 178 | "LaunchConfigurationName":{ 179 | "Ref":"CeleryLaunchConfig" 180 | }, 181 | "MinSize":"1", 182 | "MaxSize":"2", 183 | "DesiredCapacity":"1" 184 | } 185 | }, 186 | "WebLaunchConfig":{ 187 | "Type":"AWS::AutoScaling::LaunchConfiguration", 188 | "Properties":{ 189 | "ImageId":{ 190 | "Ref":"WebAMI" 191 | }, 192 | "SecurityGroups":[ 193 | { 194 | "Ref":"WebSecurityGroup" 195 | } 196 | ] 197 | } 198 | }, 199 | "WebGroup":{ 200 | "Type":"AWS::AutoScaling::AutoScalingGroup", 201 | "Properties":{ 202 | "AvailabilityZones":{ 203 | "Fn::GetAZs":"" 204 | }, 205 | "LaunchConfigurationName":{ 206 | "Ref":"WebLaunchConfig" 207 | }, 208 | "MinSize":"1", 209 | "MaxSize":"2", 210 | "DesiredCapacity":"1" 211 | } 212 | } 213 | } 214 | }, 215 | "CeleryScaleUpPolicy":{ 216 | "Type":"AWS::AutoScaling::ScalingPolicy", 217 | "Properties":{ 218 | "AdjustmentType":"ChangeInCapacity", 219 | "AutoScalingGroupName":{ 220 | "Ref":"CeleryGroup" 221 | }, 222 | "Cooldown":"1", 223 | "ScalingAdjustment":"1" 224 | } 225 | }, 226 | "CeleryScaleDownPolicy":{ 227 | "Type":"AWS::AutoScaling::ScalingPolicy", 228 | "Properties":{ 229 | "AdjustmentType":"ChangeInCapacity", 230 | "AutoScalingGroupName":{ 231 | "Ref":"CeleryGroup" 232 | }, 233 | "Cooldown":"1", 234 | "ScalingAdjustment":"-1" 235 | } 236 | }, 237 | "CelerySQSAlarmHigh":{ 238 | "Type":"AWS::CloudWatch::Alarm", 239 | "Properties":{ 240 | "EvaluationPeriods":"1", 241 | "Statistic":"Sum", 242 | "Threshold":"100", 243 | "AlarmDescription":"Triggered when SQS queue length >100", 244 | "Period":"60", 245 | "AlarmActions":[ 246 | { 247 | "Ref":"CeleryScaleUpPolicy" 248 | } 249 | ], 250 | "Namespace":"AWS/SQS", 251 | "Dimensions":[ 252 | { 253 | "Name":"QueueName", 254 | "Value":{ 255 | "GetAtt":[ 256 | "CeleryQueue", 257 | "QueueName" 258 | ] 259 | } 260 | } 261 | ], 262 | "ComparisonOperator":"GreaterThanThreshold", 263 | "MetricName":"ApproximateNumberOfMessagesVisible" 264 | } 265 | }, 266 | "CelerySQSAlarmLow":{ 267 | "Type":"AWS::CloudWatch::Alarm", 268 | "Properties":{ 269 | "EvaluationPeriods":"1", 270 | "Statistic":"Sum", 271 | "Threshold":"20", 272 | "AlarmDescription":"Triggered when SQS queue length <20", 273 | "Period":"60", 274 | "AlarmActions":[ 275 | { 276 | "Ref":"CeleryScaleDownPolicy" 277 | } 278 | ], 279 | "Namespace":"AWS/SQS", 280 | "Dimensions":[ 281 | { 282 | "Name":"QueueName", 283 | "Value":{ 284 | "GetAtt":[ 285 | "CeleryQueue", 286 | "QueueName" 287 | ] 288 | } 289 | } 290 | ], 291 | "ComparisonOperator":"LessThanThreshold", 292 | "MetricName":"ApproximateNumberOfMessagesVisible" 293 | } 294 | }, 295 | "WebELB":{ 296 | "Type":"AWS::ElasticLoadBalancing::LoadBalancer", 297 | "Properties":{ 298 | "AvailabilityZones":{ 299 | "Fn::GetAZs":"" 300 | }, 301 | "Listeners":[ 302 | { 303 | "LoadBalancerPort":"80", 304 | "InstancePort":"80", 305 | "Protocol":"HTTP" 306 | } 307 | ], 308 | "HealthCheck":{ 309 | "Target":{ 310 | "Fn::Join":[ 311 | "", 312 | [ 313 | "HTTP:80/" 314 | ] 315 | ] 316 | }, 317 | "HealthyThreshold":"3", 318 | "UnhealthyThreshold":"5", 319 | "Interval":"30", 320 | "Timeout":"5" 321 | } 322 | } 323 | }, 324 | "WebGroup":{ 325 | "Type":"AWS::AutoScaling::AutoScalingGroup", 326 | "Properties":{ 327 | "AvailabilityZones":{ 328 | "Fn::GetAZs":"" 329 | }, 330 | "LaunchConfigurationName":{ 331 | "Ref":"WebLaunchConfig" 332 | }, 333 | "MinSize":"1", 334 | "MaxSize":"2", 335 | "DesiredCapacity":"2", 336 | "LoadBalancerNames":[ 337 | { 338 | "Ref":"WebELB" 339 | } 340 | ] 341 | } 342 | } 343 | } -------------------------------------------------------------------------------- /chapter-6/notification.json: -------------------------------------------------------------------------------- 1 | "ScalingSNSTopic" : { 2 | "Type" : "AWS::SNS::Topic", 3 | "Properties" : { 4 | "Subscription" : [ { 5 | "Endpoint" : "notifications@example.com", 6 | "Protocol" : "email" 7 | } ] 8 | } 9 | } 10 | "CeleryGroup" : { 11 | "Type" : "AWS::AutoScaling::AutoScalingGroup", 12 | "Properties" : { 13 | "AvailabilityZones" : { "Fn::GetAZs" : ""}, 14 | "LaunchConfigurationName" : { "Ref" : "CeleryLaunchConfig" }, 15 | "MinSize" : "1", 16 | "MaxSize" : "2", 17 | "DesiredCapacity" : "1", 18 | "NotificationConfiguration" : { 19 | "TopicARN" : { "Ref" : "ScalingSNSTopic" }, 20 | "NotificationTypes" : [ 21 | "autoscaling:EC2_INSTANCE_LAUNCH", 22 | "autoscaling:EC2_INSTANCE_LAUNCH_ERROR", 23 | "autoscaling:EC2_INSTANCE_TERMINATE", 24 | "autoscaling:EC2_INSTANCE_TERMINATE_ERROR" 25 | ] 26 | } 27 | } 28 | }, -------------------------------------------------------------------------------- /chapter-7/fabfile.py.1: -------------------------------------------------------------------------------- 1 | from fabric.api import env 2 | 3 | def production(): 4 | env.roledefs = { 5 | 'web': ['www1-prod', 'www2-prod', 'www3-prod'], 6 | 'db': ['db1-prod', 'db2-prod'] 7 | } 8 | 9 | def staging(): 10 | env.roledefs = { 11 | 'web': ['www1-staging', 'www2-staging'], 12 | 'db': ['db1-staging'] 13 | } 14 | 15 | def deploy(): 16 | run('deploy.py') -------------------------------------------------------------------------------- /chapter-7/fabfile.py.2: -------------------------------------------------------------------------------- 1 | from fabric.api import run, roles, sudo, env 2 | from fabric_ec2 import EC2TagManager 3 | 4 | def configure_roles(environment, region): 5 | """ Set up the Fabric env.roledefs, using the correct roles for the given environment 6 | """ 7 | tags = EC2TagManager(common_tags={'environment': environment}, regions=[region]) 8 | 9 | roles = {} 10 | for role in ['web', 'db']: 11 | roles[role] = tags.get_instances(role=role) 12 | 13 | return roles 14 | 15 | # select staging or production environment to filter roles accordingly, 16 | env.roledefs = configure_roles(env.environment, env.region) 17 | 18 | @roles('web') 19 | def restart_web(): 20 | sudo('/etc/init.d/nginx restart') 21 | 22 | @roles('db') 23 | def restart_db(): 24 | sudo('/etc/init.d/postgresql restart') 25 | 26 | def hostname(): 27 | run('hostname') -------------------------------------------------------------------------------- /chapter-7/fabfile.py.3: -------------------------------------------------------------------------------- 1 | from fabric.api import * 2 | from fabric_aws import * 3 | 4 | @ec2('us-east-1', instance_ids=['i-02f7acf3eafb0b4af','i-06eab5b7e64f5af4c','i-06eab5b7e64f5af4c']) 5 | @task 6 | def uptime_instance_ids(): 7 | run('uptime') 8 | 9 | @ec2('us-east-1', filters={'instance_type':'t2.micro'}) 10 | @task 11 | def hostname_instance_type(): 12 | run('hostname') -------------------------------------------------------------------------------- /chapter-8/roles.pp: -------------------------------------------------------------------------------- 1 | node default { 2 | 3 | require stdlib 4 | 5 | $userdata = loadjson('/tmp/role.json') 6 | $role = $userdata['role'] 7 | 8 | case $role { 9 | 'web': { 10 | include role::www::dev 11 | 12 | } 13 | 'db': { 14 | include role::db::dev 15 | } 16 | default: { fail("Unrecognized role: ${role}") } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /chapter-8/tags.py: -------------------------------------------------------------------------------- 1 | from boto.utils import get_instance_metadata 2 | from boto.ec2 import connect_to_region 3 | 4 | metadata = get_instance_metadata() 5 | my_instance_id = metadata['instance-id'] 6 | 7 | conn = connect_to_region('us-east-1') 8 | for reservations in conn.get_all_instances(filters={'instance-id': my_instance_id}): 9 | # There will be only one instance in the results 10 | for instance in reservations.instances: 11 | for tag in instance.tags: 12 | # Iterate through the tags, printing the keys and values 13 | print "Key \'%s\' has value \'%s\'" % (tag, instance.tags[tag]) -------------------------------------------------------------------------------- /chapter-8/tags.rb: -------------------------------------------------------------------------------- 1 | require 'facter' 2 | require 'json' 3 | 4 | if Facter.value("ec2_instance_id") != nil 5 | instance_id = Facter.value("ec2_instance_id") 6 | region = Facter.value("ec2_placement_availability_zone")[0..-2] 7 | 8 | cmd = <