AWS CloudFormation::Init is great for integrating basic configuration management (files, services and packages) into your CloudFormation based instances or launch configurations. CodeDeploy is interesting in deploying your code to multiple AWS instances. There are lots of documentation of getting AWS::CloudFormation::Init or CodeDeploy working with Amazon Linux, but nothing on CentOS 7; so here are some snippets to get you started.
Written on: 2017-01-29
Last updated: 2016-10-09
Distributions: AWS CentOS_7 RHEL_7
This is not a tutorial (read the CloudFormation::Init documentation first); it's just how you can modify the examples given in the documentation to work with CentOS 7.
This is with the official CentOS7 AMI. You will have to replace mentions of 'SampleServer' below with the name of your resource. The example cfn configuration checks for changes to your template every 5 minutes.
AWSTemplateFormatVersion: 2010-09-09 Description: Sample template of cfn-init on CentOS 7, add your IP/rules to the security group Parameters: VPC: Description: VPC ID Type: String Subnet: Description: Subnet ID Type: String AMI: Description: AMI to use for the web server Type: String Default: ami-fedafc9d # CentOS 7 HVM InstanceType: Description: Instance size to use for the web server Type: String Default: t2.micro KeyPairName: Description: SSH keypair to use Type: String ServerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Server security group VpcId: !Ref "VPC" SecurityGroupIngress: - IpProtocol: -1 CidrIp: 1.2.3.4/32 # If you change the name of this Resource, modify the cfn-auto-reloader.conf # and UserData below with the updated name Server: Type: AWS::EC2::Instance Metadata: AWS::CloudFormation::Init: configSets: default: - install_base install_base: packages: yum: # Example packages: bash-completion: [] telnet: [] wget: [] files: # These files are needed for CloudFormation::Init to work /etc/cfn/cfn-hup.conf: content: !Sub | [main] stack=${AWS::StackId} region=${AWS::Region} interval=1 /etc/cfn/hooks.d/cfn-auto-reloader.conf: content: !Sub | [cfn-auto-reloader-hook] triggers=post.update path=Resources.Server.Metadata.AWS::CloudFormation::Init action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackId} --region ${AWS::Region} --resource Server runas=root services: sysvinit: cfn-hup: enabled: true ensureRunning: true Properties: ImageId: !Ref AMI InstanceType: !Ref InstanceType InstanceInitiatedShutdownBehavior: stop KeyName: !Ref KeyPairName BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: VolumeSize: 8 VolumeType: gp2 SecurityGroupIds: - !Ref ServerSecurityGroup SubnetId: !Ref Subnet Tags: - Key: Name Value: sample-centos7-server UserData: Fn::Base64: !Sub | #!/bin/bash yum install -y epel-release yum install -y awscli /usr/bin/easy_install --script-dir /opt/aws/bin https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz cp -v /usr/lib/python2*/site-packages/aws_cfn_bootstrap*/init/redhat/cfn-hup /etc/init.d chmod +x /etc/init.d/cfn-hup /opt/aws/bin/cfn-init --stack ${AWS::StackId} --resource Server --region ${AWS::Region} /opt/aws/bin/cfn-signal -e 0 --stack ${AWS::StackName} --resource Server --region ${AWS::Region}
JSON:
{ ... "Mappings": { "AWSRegion2AMI" : { "ap-southeast-2": { "AMI" : "ami-fedafc9d" }, "us-east-1": { "AMI" : "ami-6d1c2007" }, "us-west-1": { "AMI" : "ami-af4333cf" }, "us-west-2": { "AMI" : "ami-d2c924b2" }, "eu-west-1": { "AMI" : "ami-7abd0209" }, "eu-central-1": { "AMI" : "ami-9bf712f4" }, "ap-south-1": { "AMI" : "ami-95cda6fa" }, "ap-southeast-1": { "AMI" : "ami-f068a193" }, "ap-northeast-1": { "AMI" : "ami-eec1c380" }, "ap-northeast-2": { "AMI" : "ami-c74789a9" }, "sa-east-1": { "AMI" : "ami-26b93b4a" } } }, "SampleServer": { "Type": "AWS::EC2::Instance", "Metadata": { "Comment": "Only the cfn_base section below is needed for cfn-init to work. Don't forget to change SampleServer with your actual resource name", "AWS::CloudFormation::Init": { "configSets": { "default": [ "cfn_base", "example_config" ] }, "cfn_base": { "files": { "/etc/cfn/cfn-hup.conf": { "content": {"Fn::Join":["\n", [ "[main]", "stack={{stackid}}", "region={{region}}", "interval=5", "", "" ]]}, "context": { "stackid": {"Ref":"AWS::StackId"}, "region": {"Ref":"AWS::Region"} } }, "/etc/cfn/hooks.d/cfn-auto-reloader.conf": { "content": {"Fn::Join":["\n", [ "[cfn-auto-reloader-hook]", "triggers=post.update", "path=Resources.SampleServer.Metadata.AWS::CloudFormation::Init", "action=/opt/aws/bin/cfn-init -v --stack {{stackid}} --region {{region}} --resource SampleServer", "runas=root", "" ]]}, "context": { "stackid": {"Ref":"AWS::StackId"}, "region": {"Ref":"AWS::Region"} } } }, "services": { "sysvinit": { "cfn-hup": { "enabled": "true", "ensureRunning" : "true" } } } }, "example_config": { "packages":{ "yum": { "telnet": [], "bash-completion": [], "unzip": [], "wget": [] } }, "files":{ "/etc/yum.repos.d/jenkins.repo": { "source": "http://pkg.jenkins-ci.org/redhat-stable/jenkins.repo" }, "/var/tmp/jenkins-ci.org.key": { "source": "https://jenkins-ci.org/redhat/jenkins-ci.org.key" } }, "commands": { "sample_rpm_import_command": { "command": "rpm --import /var/tmp/jenkins-ci.org.key", "test": "rpm -q gpg-pubkey-d50582e6-4a3feef6 | grep \"not installed\"" } } } } }, "Properties": { "ImageId": {"Fn::FindInMap":["AWSRegion2AMI",{"Ref":"AWS::Region"},"AMI"]}, "Fill this here with more stuff, like InstanceType, KeyName, SecurityGroupIds and SubnetId": "", "UserData": {"Fn::Base64": {"Fn::Join":["", [ "#!/bin/bash\n", "yum install -y epel-release\n", "yum install -y awscli\n", "/usr/bin/easy_install --script-dir /opt/aws/bin https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", "cp -v /usr/lib/python2*/site-packages/aws_cfn_bootstrap*/init/redhat/cfn-hup /etc/init.d \n", "chmod +x /etc/init.d/cfn-hup \n", "/opt/aws/bin/cfn-init --stack ", {"Ref":"AWS::StackId"}, " --resource SampleServer --region ", {"Ref":"AWS::Region"}, "\n", "/opt/aws/bin/cfn-signal -e 0 --stack ", {"Ref":"AWS::StackName"}, " --resource SampleServer ", " --region ", {"Ref":"AWS::Region"}, "\n" ]]}} } } }
Example snippet of an AutoScaling LaunchConfiguration that uses CloudFormation::Init to install the CodeDeploy agent on CentOS 7, replace 'SampleLaunchConfiguration' and 'SampleAutoScalingGroup' with your actual resource name:
{ "Mappings": { "AWSRegion2AMI" : { "ap-southeast-2": { "AMI" : "ami-fedafc9d" }, "us-east-1": { "AMI" : "ami-6d1c2007" }, "us-west-1": { "AMI" : "ami-af4333cf" }, "us-west-2": { "AMI" : "ami-d2c924b2" }, "eu-west-1": { "AMI" : "ami-7abd0209" }, "eu-central-1": { "AMI" : "ami-9bf712f4" }, "ap-south-1": { "AMI" : "ami-95cda6fa" }, "ap-southeast-1": { "AMI" : "ami-f068a193" }, "ap-northeast-1": { "AMI" : "ami-eec1c380" }, "ap-northeast-2": { "AMI" : "ami-c74789a9" }, "sa-east-1": { "AMI" : "ami-26b93b4a" } } }, "SampleLaunchConfiguration": { "Type": "AWS::AutoScaling::LaunchConfiguration", "Metadata": { "AWS::CloudFormation::Init" : { "configSets": { "default": [ "install_base" ] }, "install_base": { "files": { "/etc/cfn/cfn-hup.conf": { "content": {"Fn::Join":["\n", [ "[main]", "stack={{stackid}}", "region={{region}}", "interval=5", "" ]]}, "context": { "stackid": {"Ref":"AWS::StackId"}, "region": {"Ref":"AWS::Region"} } }, "/etc/cfn/hooks.d/cfn-auto-reloader.conf": { "content": {"Fn::Join":["\n", [ "[cfn-auto-reloader-hook]", "triggers=post.update", "path=Resources.{{resource}}.Metadata.AWS::CloudFormation::Init", "action=/opt/aws/bin/cfn-init -v --stack {{stackid}} --region {{region}} --resource {{resource}}", "runas=root", "", "" ]]}, "context": { "resource": "SampleLaunchConfiguration", "stackid": {"Ref":"AWS::StackId"}, "region": {"Ref":"AWS::Region"} } } }, "services": { "sysvinit": { "cfn-hup": { "enabled": "true", "ensureRunning" : "true" } } } } } }, "Properties": { "ImageId": {"Fn::FindInMap":["AWSRegion2AMI",{"Ref":"AWS::Region"},"AMI"]}, "... Fill in InstanceType, SecurityGroups, InstanceProfiles,etc here ...": "", "UserData": {"Fn::Base64": {"Fn::Join":["", [ "#!/bin/bash\n", "yum install -y epel-release\n", "yum install -y awscli\n", "/usr/bin/easy_install --script-dir /opt/aws/bin https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz\n", "cp -v /usr/lib/python2*/site-packages/aws_cfn_bootstrap*/init/redhat/cfn-hup /etc/init.d \n", "chmod +x /etc/init.d/cfn-hup \n", "/opt/aws/bin/cfn-init --stack ", {"Ref":"AWS::StackId"}, " --resource SampleLaunchConfiguration --region ", {"Ref":"AWS::Region"}, " || error_exit 'Failed to run cfn-init.'\n", "/etc/init.d/cfn-hup start \n", "function handleError() {\n", " /opt/aws/bin/cfn-signal -e 1 --stack ", {"Ref":"AWS::StackName"}, " --reason \"$1\" --resource SampleAutoScalingGroup", " --region ", {"Ref":"AWS::Region"}, "\n", " exit 0 \n", "}\n", "cd /tmp/ \n", "aws s3 cp 's3://aws-codedeploy-", {"Ref":"AWS::Region"}, "/latest/codedeploy-agent.noarch.rpm' . || handleError 'Failed to download AWS CodeDeploy Agent.'\n", "yum install -y ruby\n", "yum -y install codedeploy-agent.noarch.rpm || handleError 'Failed to yum install codedeploy-agent.noarch.rpm' \n", "/opt/aws/bin/cfn-signal -e 0 --stack ", {"Ref":"AWS::StackName"}, " --resource SampleAutoScalingGroup ", " --region ", {"Ref":"AWS::Region"}, "\n", "echo Finished UserData script \n" ]]}}, "BlockDeviceMappings": [ { "DeviceName" : "/dev/sda1", "Ebs": { "VolumeSize": "15", "VolumeType": "gp2" } } ] } } }
If you are using CodeDeploy with AutoScaling, and if you have issues with AutoScaling getting an ABANDONED error code during autoscaling, it's because the CodeDeploy did not finish successfully. In addition to/instead of the local /var/log/aws/codedeploy/* logs, you can go to the CodeDeploy section in the management console, it should give you details on what went wrong.
Unfortunately, once it fails, it's very difficult to stop it from going into a loop of retrying the deployment; and it won't even let you change the deployment revision while it's in the loop. If you want to force a success to prevent a loop, during an instance launch, you can do a:
aws autoscaling describe-lifecycle-hooks --auto-scaling-group-name YourAutoScalingGroupName
to find out the dynamic generated launch deployment hook, and then use that to force a success (continue) on the instance that is creating the trouble:
aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id i-12345abcde --lifecycle-hook-name <output-from-previous-command> --auto-scaling-group-name YourAutoScalingGroupName
Some other caveats of CodeDeploy you need to know about which may not be clear from the documentation: not only does CodeDeploy refuse to overwrite any files it doesn't know about, but it would actually return a deployment error. This is pretty painful, and as of writing there is no config to override this behaviour. A workaround is having something like this in your appspec.yml file:
version: 0.0 os: linux files: - source: / destination: /var/codedeploy hooks: BeforeInstall: - location: codedeploy/BeforeInstall.sh timeout: 90 runas: root AfterInstall: - location: codedeploy/AfterInstall.sh timeout: 360 runas: root
which makes CodeDeploy write to it's own dedicated directory, which is emptied out in the BeforeInstall script to prevent any chance of failure:
# codedeploy/BeforeInstall.sh set -u CODEDEPLOY_ROOT="/var/codedeploy" rm -rf $CODEDEPLOY_ROOT mkdir -p $CODEDEPLOY_ROOT
with the actual deployment to where you want it doen in the AfterInstall bit:
# codedeploy/AfterInstall.sh set -u CODEDEPLOY_ROOT="/var/codedeploy" WEB_ROOT="/var/www/html" CODEDEPLOY_DIR="codedeploy" # Optional Sanity check #if [ ! -f "$CODEDEPLOY_ROOT/index.php" ]; then # echo "index.php does not exist in $CODEDEPLOY_ROOT, aborting" # exit 1 #fi rsync --delete --delete-excluded --recursive --checksum --exclude /$CODEDEPLOY_DIR/ --exclude .git $CODEDEPLOY_ROOT/ $WEB_ROOT/ #...