├── LICENSE ├── README.md └── zookeeper.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Original work copyright (c) 2014 The Factory 4 | Modified work copyright (c) 2015 Michael Babineau 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CloudFormation template for an [Exhibitor](https://github.com/Netflix/exhibitor)-managed [ZooKeeper](http://zookeeper.apache.org/) cluster. 2 | 3 | ## Overview 4 | 5 | This template bootstraps a ZooKeeper cluster. The ZK nodes are managed by Exhibitor with S3 for backups and automatic node discovery. 6 | 7 | ZooKeeper and Exhibitor are run via a Docker container. You may use the default ([mbabineau/zookeeper-exhibitor](https://github.com/mbabineau/docker-zk-exhibitor)) or provide your own image. 8 | 9 | The servers are part of an auto-scaling group. Incrementing, decrementing, or otherwise modifying the server list should be handled gracefully by ZooKeeper (thanks to Exhibitor). 10 | 11 | The template creates a security group for ZK clients, the id for which is exposed as an output (`ClientSecurityGroup`). 12 | 13 | The template also creates an internal-facing ELB for clients to interact with Exhibitor via a static endpoint. This is especially useful for node discovery, so Exhibitor's `/cluster/list` API is exposed as an output as well (`ExhibitorDiscoveryUrl`). 14 | 15 | Note that this template must be used with Amazon VPC. New AWS accounts automatically use VPC, but if you have an old account and are still using EC2-Classic, you'll need to modify this template or make the switch. 16 | 17 | ## Usage 18 | 19 | ### 1. Clone the repository 20 | ```bash 21 | git clone https://github.com/mbabineau/cloudformation-zookeeper.git 22 | ``` 23 | 24 | ### 2. Create an Admin security group 25 | This is a VPC security group containing access rules for cluster administration, and should be locked down to your IP range, a bastion host, or similar. This security group will be associated with the ZooKeeper servers. 26 | 27 | Inbound rules are at your discretion, but you may want to include access to: 28 | * `22 [tcp]` - SSH port 29 | * `2181 [tcp]` - ZooKeeper client port 30 | * `8181 [tcp]` - Exhibitor HTTP port (for both web UI and REST API) 31 | 32 | ### 3. Launch the stack 33 | Launch the stack via the AWS console, a script, or [aws-cli](https://github.com/aws/aws-cli). 34 | 35 | See `zookeeper.json` for the full list of parameters, descriptions, and default values. 36 | 37 | Example using `aws-cli`: 38 | ```bash 39 | aws cloudformation create-stack \ 40 | --template-body file://zookeeper.json \ 41 | --stack-name \ 42 | --capabilities CAPABILITY_IAM \ 43 | --parameters \ 44 | ParameterKey=KeyName,ParameterValue= \ 45 | ParameterKey=ExhibitorS3Bucket,ParameterValue= \ 46 | ParameterKey=ExhibitorS3Region,ParameterValue= \ 47 | ParameterKey=ExhibitorS3Prefix,ParameterValue= \ 48 | ParameterKey=VpcId,ParameterValue= \ 49 | ParameterKey=Subnets,ParameterValue='\,' \ 50 | ParameterKey=AdminSecurityGroup,ParameterValue= 51 | ``` 52 | 53 | ### 4. Watch the cluster converge 54 | Once the stack has been provisioned, visit `http://:8181/exhibitor/v1/ui/index.html` on one of the nodes. You will need to do this from a location granted access by the specified `AdminSecurityGroup`. 55 | 56 | _Note the Docker image may take several minutes to retrieve. This can be improved with the use of a private Docker registry._ 57 | 58 | You should see Exhibitor's management UI with a list of ZK nodes in this cluster. Exhibitor adds each node to the cluster via a rolling restart, so you may see nodes getting added and restarting during the first few minutes they're up. 59 | -------------------------------------------------------------------------------- /zookeeper.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "AWSTemplateFormatVersion" : "2010-09-09", 4 | 5 | "Description" : "Launches an Exhibitor-managed ZooKeeper cluster", 6 | 7 | "Parameters" : { 8 | "InstanceType" : { 9 | "Description" : "EC2 instance type", 10 | "Type" : "String", 11 | "Default" : "t2.micro", 12 | "AllowedValues" : [ 13 | "t2.micro", "t2.small", "t2.medium", 14 | "m3.medium", "m3.large", "m3.xlarge", "m3.2xlarge", 15 | "c3.large", "c3.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", 16 | "c4.large", "c4.xlarge", "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", 17 | "r3.large", "r3.xlarge", "r3.2xlarge", "r3.4xlarge", "r3.8xlarge", 18 | "i2.xlarge", "i2.2xlarge", "i2.4xlarge", "i2.8xlarge", 19 | "hs1.8xlarge", "g2.2xlarge" 20 | ], 21 | "ConstraintDescription" : "must be a valid, HVM-compatible EC2 instance type." 22 | }, 23 | "KeyName" : { 24 | "Description" : "Existing EC2 KeyPair to be associated with all cluster instances for SSH access. For default AMIs, log in as the 'ubuntu' user.", 25 | "Type" : "AWS::EC2::KeyPair::KeyName" 26 | }, 27 | "ClusterSize" : { 28 | "Description" : "Number of nodes to launch", 29 | "Type" : "Number", 30 | "Default": 3 31 | }, 32 | "DockerImage" : { 33 | "Description" : "Path of the ZK+Exhibitor Docker image (format: '[[:]/]:')", 34 | "Type" : "String", 35 | "Default" : "mbabineau/zookeeper-exhibitor:3.4.6_1.5.4" 36 | }, 37 | "ExhibitorS3Bucket" : { 38 | "Description" : "Bucket for Exhibitor backups of ZK configs", 39 | "Type" : "String" 40 | }, 41 | "ExhibitorS3Region" : { 42 | "Description" : "Region for Exhibitor backups of ZK configs", 43 | "Type" : "String" 44 | }, 45 | "ExhibitorS3Prefix" : { 46 | "Description" : "Key prefix for S3 backups. Should be unique per S3 bucket per cluster", 47 | "Type" : "String" 48 | }, 49 | "Subnets" : { 50 | "Description" : "List of VPC subnet IDs for the cluster. Note: must match up with the passed AvailabilityZones.", 51 | "Type" : "List" 52 | }, 53 | "VpcId" : { 54 | "Description" : "VPC associated with the provided subnets", 55 | "Type" : "AWS::EC2::VPC::Id" 56 | }, 57 | "AdminSecurityGroup" : { 58 | "Description" : "Existing security group that should be granted administrative access to ZooKeeper (e.g., 'sg-123456')", 59 | "Type" : "AWS::EC2::SecurityGroup::Id" 60 | }, 61 | "AvailabilityZones": { 62 | "Description" : "(Optional) If passed, only launch nodes in these AZs (e.g., 'us-east-1a,us-east-1b'). Note: these must match up with the passed Subnets.", 63 | "Type" : "CommaDelimitedList", 64 | "Default" : "" 65 | } 66 | }, 67 | 68 | "Mappings" : { 69 | "RegionMap" : { 70 | "us-east-1" : { 71 | "AMI" : "ami-87705d90" 72 | }, 73 | "us-west-1" : { 74 | "AMI" : "ami-1b1e4b7b" 75 | }, 76 | "us-west-2" : { 77 | "AMI" : "ami-70b71410" 78 | }, 79 | "eu-west-1" : { 80 | "AMI" : "ami-fb91c788" 81 | }, 82 | "ap-southeast-1" : { 83 | "AMI" : "ami-c7e447a4" 84 | }, 85 | "ap-southeast-2" : { 86 | "AMI" : "ami-ac3a05cf" 87 | }, 88 | "ap-northeast-1" : { 89 | "AMI" : "ami-6dc0690c" 90 | }, 91 | "sa-east-1" : { 92 | "AMI" : "ami-45ff6029" 93 | } 94 | } 95 | }, 96 | 97 | "Conditions" : { 98 | "UseAllAvailabilityZones" : {"Fn::Equals" : [{ "Fn::Join" : ["", {"Ref" : "AvailabilityZones"} ]}, ""]} 99 | }, 100 | 101 | "Resources" : { 102 | "IAMUser" : { 103 | "Type" : "AWS::IAM::User", 104 | "Properties" : { 105 | "Policies" : [{ 106 | "PolicyName" : "S3Access", 107 | "PolicyDocument" : { 108 | "Statement": [{ 109 | "Effect" : "Allow", 110 | "Action" : "s3:*", 111 | "Resource" : { "Fn::Join" : ["", ["arn:aws:s3:::", {"Ref" : "ExhibitorS3Bucket"} , "/*"]]} 112 | }] 113 | } 114 | }, 115 | { 116 | "PolicyName" : "IAMAccess", 117 | "PolicyDocument" : { 118 | "Statement" : [{ 119 | "Effect" : "Allow", 120 | "NotAction" : "iam:*", 121 | "Resource" : "*" 122 | }] 123 | } 124 | }] 125 | } 126 | }, 127 | 128 | "HostKeys" : { 129 | "Type" : "AWS::IAM::AccessKey", 130 | "Properties" : { 131 | "UserName" : {"Ref": "IAMUser"} 132 | } 133 | }, 134 | 135 | "ServerGroup" : { 136 | "Type" : "AWS::AutoScaling::AutoScalingGroup", 137 | "Properties" : { 138 | "AvailabilityZones" : { 139 | "Fn::If" : [ 140 | "UseAllAvailabilityZones", 141 | { "Fn::GetAZs": "AWS::Region" }, 142 | { "Ref" : "AvailabilityZones" } 143 | ] 144 | }, 145 | "LaunchConfigurationName" : { "Ref" : "LaunchConfig" }, 146 | "MinSize" : "1", 147 | "MaxSize" : "9", 148 | "DesiredCapacity" : { "Ref" : "ClusterSize" }, 149 | "LoadBalancerNames" : [ { "Ref" : "ElasticLoadBalancer" } ], 150 | "VPCZoneIdentifier" : { "Ref" : "Subnets" }, 151 | "Tags" : [ 152 | { 153 | "Key" : "role", 154 | "Value" : "zookeeper", 155 | "PropagateAtLaunch" : "true" 156 | } 157 | ] 158 | } 159 | }, 160 | 161 | "LaunchConfig" : { 162 | "Type" : "AWS::AutoScaling::LaunchConfiguration", 163 | "Properties" : { 164 | "KeyName" : { "Ref" : "KeyName" }, 165 | "ImageId" : { "Fn::FindInMap" : [ "RegionMap", { "Ref" : "AWS::Region" }, "AMI"] }, 166 | "SecurityGroups" : [ { "Ref" : "ServerSecurityGroup" }, { "Ref" : "AdminSecurityGroup" } ], 167 | "AssociatePublicIpAddress": "true", 168 | "InstanceType" : { "Ref" : "InstanceType" }, 169 | "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [ 170 | "#!/bin/bash -ex\n", 171 | 172 | "# Helper function\n", 173 | "function error_exit\n", 174 | "{\n", 175 | " cfn-signal -e 1 -r \"$1\" '", { "Ref" : "WaitHandle" }, "'\n", 176 | " exit 1\n", 177 | "}\n", 178 | 179 | "# Set up and start the Exhibitor+ZooKeeper service\n", 180 | "docker pull ", { "Ref": "DockerImage" }, "\n", 181 | "runit-service create zk docker run ", 182 | " -p 8181:8181 -p 2181:2181 -p 2888:2888 -p 3888:3888", 183 | " -e 'S3_BUCKET=", { "Ref" : "ExhibitorS3Bucket" }, "'", 184 | " -e 'S3_PREFIX=", { "Ref" : "ExhibitorS3Prefix" }, "'", 185 | " -e 'AWS_ACCESS_KEY_ID=", { "Ref" : "HostKeys" }, "'", 186 | " -e 'AWS_SECRET_ACCESS_KEY=", {"Fn::GetAtt": ["HostKeys", "SecretAccessKey"]}, "'", 187 | " -e 'AWS_REGION=", { "Ref" : "ExhibitorS3Region" }, "'", 188 | " -e \"HOSTNAME=`ec2metadata --public-hostname`\"", 189 | " ", { "Ref": "DockerImage" }, "\n", 190 | "runit-service enable zk\n", 191 | 192 | "# All is well so signal success\n", 193 | "cfn-signal -e 0 -r \"Stack setup complete\" '", { "Ref" : "WaitHandle" }, "'\n", 194 | 195 | "#EOF" 196 | ]]}} 197 | } 198 | }, 199 | 200 | "ClientSecurityGroup" : { 201 | "Type" : "AWS::EC2::SecurityGroup", 202 | "Properties" : { 203 | "GroupDescription" : "For ZooKeeper clients. Grants access to the associated ZooKeeper cluster.", 204 | "VpcId" : { "Ref" : "VpcId" } 205 | } 206 | }, 207 | 208 | "ServerSecurityGroup" : { 209 | "Type" : "AWS::EC2::SecurityGroup", 210 | "Properties" : { 211 | "GroupDescription" : "Enable SSH and Exhibitor access", 212 | "VpcId" : { "Ref" : "VpcId" }, 213 | "SecurityGroupIngress" : 214 | [ { "IpProtocol" : "tcp", "FromPort" : "8181", "ToPort" : "8181", "SourceSecurityGroupId" : { "Ref" : "LbSecurityGroup"} }, 215 | { "IpProtocol" : "tcp", "FromPort" : "2181", "ToPort" : "2181", "SourceSecurityGroupId" : { "Ref" : "ClientSecurityGroup"} }, 216 | { "IpProtocol" : "tcp", "FromPort" : "2888", "ToPort" : "2888", "SourceSecurityGroupId" : { "Ref" : "ClientSecurityGroup"} }, 217 | { "IpProtocol" : "tcp", "FromPort" : "3888", "ToPort" : "3888", "SourceSecurityGroupId" : { "Ref" : "ClientSecurityGroup"} } ] 218 | } 219 | }, 220 | 221 | "SecurityGroupIngress": { 222 | "Type": "AWS::EC2::SecurityGroupIngress", 223 | "Properties": { 224 | "GroupId": { "Ref": "ServerSecurityGroup" }, 225 | "IpProtocol": "-1", 226 | "FromPort": "0", 227 | "ToPort": "65535", 228 | "SourceSecurityGroupId": { "Ref": "ServerSecurityGroup" } 229 | } 230 | }, 231 | 232 | "LbSecurityGroup" : { 233 | "Type" : "AWS::EC2::SecurityGroup", 234 | "Properties" : { 235 | "GroupDescription" : "Enable Exhibitor access", 236 | "VpcId" : { "Ref" : "VpcId" }, 237 | "SecurityGroupIngress" : 238 | [ { "IpProtocol" : "tcp", "FromPort" : "80", "ToPort" : "80", "SourceSecurityGroupId" : { "Ref" : "ClientSecurityGroup"} } ] 239 | } 240 | }, 241 | 242 | "ElasticLoadBalancer" : { 243 | "Type" : "AWS::ElasticLoadBalancing::LoadBalancer", 244 | "Properties" : { 245 | "CrossZone": "true", 246 | "Scheme": "internal", 247 | "SecurityGroups": [{ "Ref": "LbSecurityGroup" }, { "Ref": "AdminSecurityGroup" }], 248 | "Subnets": { "Ref": "Subnets" }, 249 | "Listeners" : [ { 250 | "LoadBalancerPort" : "80", 251 | "InstancePort" : "8181", 252 | "Protocol" : "HTTP" 253 | } ], 254 | "HealthCheck" : { 255 | "Target" : "HTTP:8181/exhibitor/v1/cluster/state", 256 | "HealthyThreshold" : "3", 257 | "UnhealthyThreshold" : "5", 258 | "Interval" : "30", 259 | "Timeout" : "5" 260 | } 261 | } 262 | }, 263 | 264 | "WaitHandle" : { 265 | "Type" : "AWS::CloudFormation::WaitConditionHandle" 266 | } 267 | }, 268 | 269 | "Outputs" : { 270 | "ExhibitorDiscoveryUrl" : { 271 | "Value" : { "Fn::Join" : ["", [ 272 | "http://", { "Fn::GetAtt" : [ "ElasticLoadBalancer", "DNSName" ]}, "/exhibitor/v1/cluster/list" 273 | ]]} 274 | }, 275 | "ClientSecurityGroup" : { 276 | "Value" : { "Ref" : "ClientSecurityGroup" } 277 | } 278 | } 279 | } 280 | --------------------------------------------------------------------------------