Skip to content

Instantly share code, notes, and snippets.

@JosephMaxwell
Created August 12, 2016 12:34
Show Gist options
  • Save JosephMaxwell/b1ae9015f8b40638586d6d2b37d1269b to your computer and use it in GitHub Desktop.
Save JosephMaxwell/b1ae9015f8b40638586d6d2b37d1269b to your computer and use it in GitHub Desktop.
CloudFormation Template for Jenkins on EC2
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Launches a Jenkins server.",
"Parameters" : {
"InstanceType" : {
"Description" : "EC2 instance type",
"Type" : "String",
"Default" : "t2.small",
"AllowedValues" : [ "t1.micro","t2.small","m1.small","m1.medium","m1.large","m1.xlarge","m2.xlarge","m2.2xlarge","m2.4xlarge","m3.xlarge","m3.2xlarge","c1.medium","c1.xlarge","cc1.4xlarge","cc2.8xlarge","cg1.4xlarge"],
"ConstraintDescription" : "must be a valid EC2 instance type."
},
"SshKey" : {
"Description" : "Name of an existing EC2 keypair to enable SSH access to the instances",
"Default": "SwiftOtter",
"Type" : "AWS::EC2::KeyPair::KeyName"
},
"DnsPrefix" : {
"Description" : "Prefix for Jenkins' DNS record (<prefix>.<zone>)",
"Type": "String",
"Default": "builds"
},
"DnsZone" : {
"Description" : "Route53-hosted zone to use for the DNS record (<prefix>.<zone>)",
"Type": "String",
"Default": "swiftotter.com"
},
"S3Bucket" : {
"Description" : "Existing S3 bucket to use for Jenkins backups and restores",
"Type" : "String",
"Default": "swiftotter-jenkins"
},
"S3Prefix" : {
"Description" : "[Optional] Key prefix to use for Jenkins backups",
"Type" : "String",
"Default": ""
},
"Subnets" : {
"Description" : "List of VPC subnet IDs for the cluster",
"Type" : "List<AWS::EC2::Subnet::Id>"
},
"VpcId" : {
"Description" : "VPC associated with the provided subnets",
"Type" : "AWS::EC2::VPC::Id"
},
"AdminSecurityGroup" : {
"Description" : "Existing security group that should be granted administrative access to Jenkins (e.g., 'sg-123456')",
"Default": "Primary",
"Type": "AWS::EC2::SecurityGroup::Id"
}
},
"Mappings" : {
"RegionMap" : {
"us-east-1" : {
"AMI" : "ami-6869aa05"
},
"us-west-1" : {
"AMI" : "ami-7172b611"
},
"us-west-2" : {
"AMI" : "ami-31490d51"
},
"eu-west-1" : {
"AMI" : "ami-f9dd458a"
}
}
},
"Resources" : {
"CloudFormationLogs": {
"Type": "AWS::Logs::LogGroup",
"Properties": {
"RetentionInDays": 7
}
},
"SwiftOtterJenkins" : {
"Type" : "AWS::IAM::User",
"Properties" : {
"Policies" : [{
"PolicyName" : "S3Access",
"PolicyDocument" : {
"Statement": [{
"Effect" : "Allow",
"Action" : "s3:*",
"Resource" : { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref" : "S3Bucket"} , "/*"]]}
}]
}
},
{
"PolicyName" : "IAMAccess",
"PolicyDocument" : {
"Statement" : [{
"Effect" : "Allow",
"NotAction" : "iam:*",
"Resource" : "*"
}]
}
},
{
"PolicyName" : "EC2Access",
"PolicyDocument" : {
"Statement" : [{
"Effect" : "Allow",
"Action" : "ec2:*",
"Resource" : "*"
}]
}
}]
}
},
"BuildRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version" : "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": [ "ec2.amazonaws.com" ]
},
"Action": [ "sts:AssumeRole" ]
}]
},
"Policies": [ {
"PolicyName": "root",
"PolicyDocument": {
"Version" : "2012-10-17",
"Statement": [ {
"Effect": "Allow",
"Action": "*",
"Resource": "*"
} ]
}
} ]
}
},
"RolePolicies": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "root",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "ec2:*",
"Effect": "Allow",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "elasticloadbalancing:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "cloudwatch:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "autoscaling:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
},
{
"Effect":"Allow",
"Action": ["iam:PassRole", "iam:ListInstanceProfiles", "ec2:*"],
"Resource": "*"
}
]
},
"Roles": [ { "Ref": "BuildRole" } ]
}
},
"BuildInstanceProfile": {
"Type": "AWS::IAM::InstanceProfile",
"Properties" : {
"Path": "/",
"Roles": [ { "Ref": "BuildRole" }]
}
},
"HostKeys" : {
"Type" : "AWS::IAM::AccessKey",
"Properties" : {
"UserName" : { "Ref" : "SwiftOtterJenkins" }
}
},
"ServerGroup" : {
"Type" : "AWS::AutoScaling::AutoScalingGroup",
"Properties" : {
"AvailabilityZones" : { "Fn::GetAZs" : "" },
"LaunchConfigurationName" : { "Ref" : "LaunchConfig" },
"MinSize" : "1",
"MaxSize" : "1",
"DesiredCapacity" : "1",
"LoadBalancerNames" : [ { "Ref" : "ElasticLoadBalancer" } ]
}
},
"LaunchConfig" : {
"Type" : "AWS::AutoScaling::LaunchConfiguration",
"Metadata" : {
"AWS::CloudFormation::Init" : {
"configSets" : {
"install" : [ "installConfig", "installApp", "installLogs" ]
},
"installConfig" : {
"files" : {
"/etc/cfn/cfn-hup.conf" : {
"content" : { "Fn::Join" : ["", [
"[main]\n",
"stack=", { "Ref" : "AWS::StackId" }, "\n",
"region=", { "Ref" : "AWS::Region" }, "\n"
]]},
"mode" : "000400",
"owner" : "root",
"group" : "root"
},
"/etc/cfn/hooks.d/cfn-auto-reloader.conf" : {
"content": { "Fn::Join" : ["", [
"[cfn-auto-reloader-hook]\n",
"triggers=post.update\n",
"path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init\n",
"action=/opt/aws/bin/cfn-init -v ",
" --stack ", { "Ref" : "AWS::StackName" },
" --resource WebServerInstance ",
" --configsets install_all ",
" --region ", { "Ref" : "AWS::Region" }, "\n",
"runas=root\n"
]]}
}
},
"services" : {
"sysvinit" : {
"cfn-hup" : { "enabled" : "true", "ensureRunning" : "true",
"files" : ["/etc/cfn/cfn-hup.conf", "/etc/cfn/hooks.d/cfn-auto-reloader.conf"]}
}
}
},
"installLogs": {
"packages": {
"yum": {
"awslogs": []
}
},
"commands" : {
"01_create_state_directory" : {
"command" : "mkdir -p /var/awslogs/state"
}
},
"services" : {
"sysvinit" : {
"awslogs" : { "enabled" : "true", "ensureRunning" : "true",
"files" : [ "/etc/awslogs/awslogs.conf" ] }
}
},
"files" : {
"/etc/awslogs/awslogs.conf": {
"content": { "Fn::Join": [ "", [
"[general]\n",
"state_file= /var/awslogs/state/agent-state\n",
"[/var/log/cloud-init.log]\n",
"file = /var/log/cloud-init.log\n",
"log_group_name = ", { "Ref": "CloudFormationLogs" }, "\n",
"log_stream_name = {instance_id}/cloud-init.log\n",
"datetime_format = \n",
"[/var/log/cloud-init-output.log]\n",
"file = /var/log/cloud-init-output.log\n",
"log_group_name = ", { "Ref": "CloudFormationLogs" }, "\n",
"log_stream_name = {instance_id}/cloud-init-output.log\n",
"datetime_format = \n",
"[/var/log/cfn-init.log]\n",
"file = /var/log/cfn-init.log\n",
"log_group_name = ", { "Ref": "CloudFormationLogs" }, "\n",
"log_stream_name = {instance_id}/cfn-init.log\n",
"datetime_format = \n",
"[/var/log/cfn-hup.log]\n",
"file = /var/log/cfn-hup.log\n",
"log_group_name = ", { "Ref": "CloudFormationLogs" }, "\n",
"log_stream_name = {instance_id}/cfn-hup.log\n",
"datetime_format = \n",
"[/var/log/cfn-wire.log]\n",
"file = /var/log/cfn-wire.log\n",
"log_group_name = ", { "Ref": "CloudFormationLogs" }, "\n",
"log_stream_name = {instance_id}/cfn-wire.log\n",
"datetime_format = \n",
"[/var/log/httpd]\n",
"file = /var/log/httpd/*\n",
"log_group_name = ", { "Ref": "CloudFormationLogs" }, "\n",
"log_stream_name = {instance_id}/httpd\n",
"datetime_format = %d/%b/%Y:%H:%M:%S\n"
] ] },
"mode": "000444",
"owner": "root",
"group": "root"
},
"/etc/awslogs/awscli.conf": {
"content": { "Fn::Join": [ "", [
"[plugins]\n",
"cwlogs = cwlogs\n",
"[default]\n",
"region = ", { "Ref" : "AWS::Region" }, "\n"
] ] },
"mode": "000444",
"owner": "root",
"group": "root"
}
}
},
"installApp": {
"packages" : {
"python" : {
"awscli":[]
},
"yum" : {
"git-all":[]
}
},
"files" : {
"/etc/aws.conf" : {
"content" : { "Fn::Join" : ["\n", [
"[default]",
"aws_access_key_id={{access_key}}",
"aws_secret_access_key={{secret_key}}"
]]},
"context" : {
"access_key" : { "Ref" : "HostKeys" },
"secret_key" : { "Fn::GetAtt" : ["HostKeys", "SecretAccessKey"]}
},
"mode" : "000700",
"owner" : "root",
"group" : "root"
},
"/usr/local/bin/jenkins-restore" : {
"content" : { "Fn::Join" : ["\n", [
"#!/bin/bash -e",
"",
"USAGE=\"Usage: $0 S3_TARGET JENKINS_HOME\\n",
"\\n",
"Example:\\n",
"$0 s3://mybucket/jenkins/jenkins-201405011901.tar.gz /var/lib/jenkins\\n",
"\\n",
"If S3_TARGET is a directory, restore from the newest file. Make sure to include the trailing slash:\\n",
"$0 s3://mybucket/jenkins/ /var/lib/jenkins\"",
"",
"S3_TARGET=$1",
"JENKINS_HOME=$2",
"if [[ -z \"`echo $S3_TARGET|grep '^s3://'`\" ]]; then",
" echo -e $USAGE",
" exit 1",
"fi",
"",
"if [[ \"$S3_TARGET\" == */ ]]; then",
" S3_TARGET=$S3_TARGET`aws s3 ls $S3_TARGET|tail -1|awk '{print $NF}'`",
"fi",
"",
"LOCAL_BACKUP=/tmp/`basename $S3_TARGET`",
"aws s3 cp $S3_TARGET $LOCAL_BACKUP",
"",
"rm -rf $JENKINS_HOME",
"#if [[ -d \"$JENKINS_HOME\" ]]; then",
"# read -p \"Delete existing $JENKINS_HOME? (y/n) \" -n 1 -r",
"# echo",
"# if [[ $REPLY =~ ^[Yy]$ ]]; then",
"# rm -rf $JENKINS_HOME",
"# else",
"# echo \"Bailing out\"",
"# exit 1",
"# fi",
"#fi",
"",
"mkdir -p $JENKINS_HOME",
"tar zxf $LOCAL_BACKUP -C $JENKINS_HOME",
"rm -f $LOCAL_BACKUP"
]]},
"mode" : "000755",
"owner" : "root",
"group" : "root"
},
"/usr/local/bin/jenkins-backup" : {
"content" : { "Fn::Join" : ["\n", [
"#!/bin/bash -e",
"",
"USAGE=\"Usage: $0 JENKINS_HOME S3_TARGET\\n",
"\\n",
"Examples:\\n",
"$0 /var/lib/jenkins s3://mybucket/jenkins/jenkins-201405011901.tar.gz\"",
"",
"JENKINS_HOME=$1",
"S3_TARGET=$2",
"if [[ -z \"`echo $S3_TARGET|grep '^s3://'`\" || ! -d \"$JENKINS_HOME\" ]]; then",
" echo -e $USAGE",
" exit 1",
"fi",
"",
"LOCAL_BACKUP=/tmp/`basename $S3_TARGET`",
"",
"tar -C $JENKINS_HOME -zcf $LOCAL_BACKUP .\\",
" --exclude \"config-history/\" \\",
" --exclude \"config-history/*\" \\",
" --exclude \"jobs/*/workspace*\" \\",
" --exclude \"jobs/*/builds/*/archive\" \\",
" --exclude \"plugins/*/*\" \\",
" --exclude \"plugins/*.bak\" \\",
" --exclude \"war\" \\",
" --exclude \"cache\"",
"",
"aws s3 cp $LOCAL_BACKUP $S3_TARGET",
"rm -f $LOCAL_BACKUP"
]]},
"mode" : "000755",
"owner" : "root",
"group" : "root"
},
"/etc/cron.daily/jenkins" : {
"content" : { "Fn::Join" : ["\n", [
"#!/bin/bash\n",
"AWS_CONFIG_FILE=/etc/aws.conf\n",
"PATH=/bin:/usr/bin::/usr/local/bin\n",
"source /usr/local/bin/jenkins-backup /var/lib/jenkins s3://{{s3_bucket}}/{{s3_prefix}}jenkins-`date +\\%Y\\%m\\%d\\%H\\%M.tar.gz` >> /var/log/jenkins-backup.log 2>&1\n",
"echo \"ec2-terminate-instances $(curl -s http://169.254.169.254/latest/meta-data/instance-id)\" | at now + 15 min"
]]},
"context" : {
"s3_bucket" : { "Ref" : "S3Bucket"},
"s3_prefix" : { "Ref" : "S3Prefix"}
},
"mode" : "000755",
"owner" : "root",
"group" : "root"
}
}
}
}
},
"Properties" : {
"KeyName" : { "Ref" : "SshKey" },
"IamInstanceProfile": { "Ref" : "BuildInstanceProfile" },
"ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI"] },
"SecurityGroups" : [ { "Ref" : "ServerSecurityGroup" }, { "Ref": "AdminSecurityGroup" } ],
"InstanceType" : { "Ref" : "InstanceType" },
"UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
"#!/bin/bash -xe\n",
"# Helper function\n",
"function error_exit\n",
"{\n",
" cfn-signal -e 1 -r \"$1\" '", { "Ref" : "WaitHandle" }, "'\n",
" exit 1\n",
"}\n",
"/opt/aws/bin/cfn-init --stack ", { "Ref" : "AWS::StackName" },
" --resource LaunchConfig",
" --configsets install",
" --access-key ", { "Ref" : "HostKeys" },
" --secret-key ", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]},
" --region ", { "Ref" : "AWS::Region" }, " || error_exit 'Failed to run cfn-init'\n",
"# Post-cfn work\n",
"sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins.io/redhat-stable/jenkins.repo\n",
"sudo rpm --import http://pkg.jenkins.io/redhat-stable/jenkins.io.key\n",
"yum install -y jenkins\n",
"# Handle case where cron doesn't detect the new /etc/cron.d file\n",
"#service cron restart\n",
"# Attempt to restore from backup\n",
"export AWS_CONFIG_FILE=/etc/aws.conf\n",
"sudo /usr/local/bin/jenkins-restore s3://",{ "Ref": "S3Bucket" },"/",{ "Ref": "S3Prefix" }," /var/lib/jenkins || true # ignore errors\n",
"sudo /etc/init.d/jenkins start\n",
"sudo chkconfig jenkins on\n",
"# Start Jenkins\n",
"# All is well, signal success\n",
"cfn-signal -e 0 -r \"Stack setup complete\" '", { "Ref" : "WaitHandle" }, "'\n",
"#EOF"
]]}}
}
},
"LbSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Jenkins LBs",
"VpcId" : { "Ref" : "VpcId" },
"SecurityGroupIngress" :
[ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "CidrIp" : "0.0.0.0/0" }]
}
},
"ServerSecurityGroup" : {
"Type" : "AWS::EC2::SecurityGroup",
"Properties" : {
"GroupDescription" : "Jenkins servers",
"VpcId" : { "Ref" : "VpcId" },
"SecurityGroupIngress" :
[ { "IpProtocol" : "tcp", "FromPort" : "8080", "ToPort" : "8080", "CidrIp" : "0.0.0.0/0" }]
}
},
"ElasticLoadBalancer" : {
"Type" : "AWS::ElasticLoadBalancing::LoadBalancer",
"Properties" : {
"SecurityGroups": [{ "Ref": "LbSecurityGroup" }, { "Ref": "AdminSecurityGroup" }],
"Subnets": { "Ref": "Subnets" },
"Listeners" : [ {
"LoadBalancerPort" : "80",
"InstancePort" : "8080",
"Protocol" : "HTTP"
} ],
"HealthCheck" : {
"Target" : "TCP:8080",
"HealthyThreshold" : "3",
"UnhealthyThreshold" : "5",
"Interval" : "30",
"Timeout" : "5"
}
}
},
"DnsRecord" : {
"Type" : "AWS::Route53::RecordSet",
"Properties" : {
"HostedZoneName" : { "Fn::Join" : [ "", [{"Ref" : "DnsZone"}, "." ]]},
"Name" : { "Fn::Join" : [ "", [{"Ref" : "DnsPrefix"}, ".", {"Ref" : "DnsZone"}, "."]]},
"Type" : "CNAME",
"TTL" : "900",
"ResourceRecords" : [ { "Fn::GetAtt" : [ "ElasticLoadBalancer", "DNSName" ] } ]
}
},
"WaitHandle" : {
"Type" : "AWS::CloudFormation::WaitConditionHandle"
}
},
"Outputs" : {
"DnsAddress" : {
"Description" : "Jenkins URL",
"Value" : { "Fn::Join" : ["", [
"http://", { "Ref" : "DnsRecord" }
]]}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment