From b67e15f0f3068a9540cea7d79443e5eec7493a5b Mon Sep 17 00:00:00 2001 From: Tobias McNulty Date: Mon, 2 Oct 2017 22:02:11 -0400 Subject: [PATCH] add auto scaling and ELB support to Dokku stack --- README.rst | 25 ++++++----------- stack/dokku.py | 63 +++++++++++++++++++++--------------------- stack/load_balancer.py | 28 +++++++++++-------- stack/vpc.py | 59 +++++++++++++++++++-------------------- 4 files changed, 86 insertions(+), 89 deletions(-) diff --git a/README.rst b/README.rst index 06c09ae..46dee18 100644 --- a/README.rst +++ b/README.rst @@ -325,11 +325,18 @@ The CloudFormation stack creation should not finish until Dokku is fully install `_ is used in the template to signal CloudFormation once the installation is complete. +Load Balancer +~~~~~~~~~~~~~ + +The Dokku stack includes a load balancer and a free SSL certificate via AWS Certificate Manager, so +HTTPS needs to be configured on the Dokku instance only if you wish to encrypt traffic between the +load balancer and the Dokku instance. + DNS ~~~ -After the stack is created, you'll want to inspect the Outputs for the PublicIP of the instance and -create a DNS ``A`` record (possibly including a wildcard record, if you're using vhost-based apps) +After the stack is created, you'll want to inspect the Outputs for the `LoadBalancerDNSName` and +create a DNS ``CNAME`` record (possibly including a wildcard record, if you're using vhost-based apps) for your chosen domain. For help creating a DNS record, please refer to the `Dokku DNS documentation @@ -370,20 +377,6 @@ http://python-sample.your.domain/ For additional help deploying to your new instance, please refer to the `Dokku documentation `_. -Let's Encrypt -~~~~~~~~~~~~~ - -The Dokku stack does not create a load balancer and hence does not include a free SSL certificate -via Amazon Certificate Manager, so let's create one with the Let's Encrypt plugin, and add a cron -job to automatically renew the cert as needed:: - - ssh ubuntu@ sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git - ssh dokku@ config:set --no-restart python-sample DOKKU_LETSENCRYPT_EMAIL=your@email.tld - ssh dokku@ letsencrypt python-sample - ssh dokku@ letsencrypt:cron-job --add python-sample - -The Python sample app should now be accessible over HTTPS at https://python-sample.your.domain/ - Contributing ------------ diff --git a/stack/dokku.py b/stack/dokku.py index 53e9597..0c2bcad 100644 --- a/stack/dokku.py +++ b/stack/dokku.py @@ -1,16 +1,18 @@ +import troposphere.autoscaling as autoscaling import troposphere.cloudformation as cloudformation import troposphere.ec2 as ec2 import troposphere.iam as iam -from troposphere import Base64, FindInMap, Join, Output, Parameter, Ref, Tags +from troposphere import AWS_STACK_NAME, Base64, FindInMap, Join, Parameter, Ref from troposphere.policies import CreationPolicy, ResourceSignal from .assets import assets_management_policy from .common import container_instance_type from .domain import domain_name from .environment import environment_variables +from .load_balancer import load_balancer from .logs import logging_policy from .template import template -from .vpc import container_a_subnet, vpc +from .vpc import container_a_subnet, container_b_subnet, vpc key_name = template.add_parameter( Parameter( @@ -150,20 +152,15 @@ ] )) -# Elastic IP for EC2 instance -eip = template.add_resource(ec2.EIP("Eip")) - - # The Dokku EC2 instance -ec2_instance_name = 'Ec2Instance' -ec2_instance = template.add_resource(ec2.Instance( - ec2_instance_name, +launch_configuration_name = 'LaunchConfiguration' +launch_configuration = template.add_resource(autoscaling.LaunchConfiguration( + launch_configuration_name, ImageId=FindInMap("RegionMap", Ref("AWS::Region"), "AMI"), InstanceType=container_instance_type, KeyName=Ref(key_name), - SecurityGroupIds=[Ref(security_group)], + SecurityGroups=[Ref(security_group)], IamInstanceProfile=Ref(instance_profile), - SubnetId=Ref(container_a_subnet), BlockDeviceMappings=[ ec2.BlockDeviceMapping( DeviceName="/dev/sda1", @@ -190,10 +187,10 @@ 'update-rc.d cfn-hup defaults\n', # call our "on_first_boot" configset (defined below): 'cfn-init --stack="', Ref('AWS::StackName'), '" --region=', Ref('AWS::Region'), - ' -r %s -c on_first_boot\n' % ec2_instance_name, + ' -r %s -c on_first_boot\n' % launch_configuration_name, # send the exit code from cfn-init to our CreationPolicy: 'cfn-signal -e $? --stack="', Ref('AWS::StackName'), '" --region=', Ref('AWS::Region'), - ' --resource %s\n' % ec2_instance_name, + ' --resource %s\n' % launch_configuration_name, ])), Metadata=cloudformation.Metadata( cloudformation.Init( @@ -264,10 +261,10 @@ # trigger the on_metadata_update configset on any changes to Ec2Instance metadata '[cfn-auto-reloader-hook]\n', 'triggers=post.update\n', - 'path=Resources.%s.Metadata\n' % ec2_instance_name, + 'path=Resources.%s.Metadata\n' % launch_configuration_name, 'action=/usr/local/bin/cfn-init', ' --stack=', Ref('AWS::StackName'), - ' --resource=%s' % ec2_instance_name, + ' --resource=%s' % launch_configuration_name, ' --configsets=on_metadata_update', ' --region=', Ref('AWS::Region'), '\n', 'runas=root\n', @@ -280,22 +277,24 @@ ), ), ), - Tags=Tags( - Name=Ref("AWS::StackName"), - ), -)) - -# Associate the Elastic IP separately, so it doesn't change when the instance changes. -eip_assoc = template.add_resource(ec2.EIPAssociation( - "EipAssociation", - InstanceId=Ref(ec2_instance), - EIP=Ref(eip), )) -template.add_output([ - Output( - "PublicIP", - Description="Public IP address of Elastic IP associated with the Dokku instance", - Value=Ref(eip), - ), -]) +autoscaling_group = autoscaling.AutoScalingGroup( + "AutoScalingGroup", + template=template, + VPCZoneIdentifier=[Ref(container_a_subnet), Ref(container_b_subnet)], + MinSize=1, + MaxSize=1, + DesiredCapacity=1, + LaunchConfigurationName=Ref(launch_configuration), + LoadBalancerNames=[Ref(load_balancer)], + HealthCheckType="EC2", + HealthCheckGracePeriod=300, + Tags=[ + { + "Key": "Name", + "Value": Join("-", [Ref(AWS_STACK_NAME), "dokku_instance"]), + "PropagateAtLaunch": True, + } + ], +) diff --git a/stack/load_balancer.py b/stack/load_balancer.py index 049510d..be4d357 100644 --- a/stack/load_balancer.py +++ b/stack/load_balancer.py @@ -30,6 +30,9 @@ group="Load Balancer", label="Web Worker Port", )) +elif os.environ.get('USE_DOKKU') == 'on': + # TODO: optionally support both port 80 and port 443 + web_worker_port = 80 else: # default to port 80 for EC2 and Elastic Beanstalk options web_worker_port = Ref(template.add_parameter( @@ -43,17 +46,20 @@ label="Web Worker Port", )) -web_worker_protocol = Ref(template.add_parameter( - Parameter( - "WebWorkerProtocol", - Description="Web worker instance protocol", - Type="String", - Default="HTTP", - AllowedValues=["HTTP", "HTTPS"], - ), - group="Load Balancer", - label="Web Worker Protocol", -)) +if os.environ.get('USE_DOKKU') == 'on': + web_worker_protocol = 'HTTP' +else: + web_worker_protocol = Ref(template.add_parameter( + Parameter( + "WebWorkerProtocol", + Description="Web worker instance protocol", + Type="String", + Default="HTTP", + AllowedValues=["HTTP", "HTTPS"], + ), + group="Load Balancer", + label="Web Worker Protocol", + )) tcp_health_check_condition = "TcpHealthCheck" template.add_condition( diff --git a/stack/vpc.py b/stack/vpc.py index e9df101..34375e5 100644 --- a/stack/vpc.py +++ b/stack/vpc.py @@ -117,39 +117,38 @@ ) -if not USE_DOKKU: - # Holds load balancer - loadbalancer_a_subnet_cidr = "10.0.2.0/24" - loadbalancer_a_subnet = Subnet( - "LoadbalancerASubnet", - template=template, - VpcId=Ref(vpc), - CidrBlock=loadbalancer_a_subnet_cidr, - AvailabilityZone=Ref(primary_az), - ) +# Holds load balancer +loadbalancer_a_subnet_cidr = "10.0.2.0/24" +loadbalancer_a_subnet = Subnet( + "LoadbalancerASubnet", + template=template, + VpcId=Ref(vpc), + CidrBlock=loadbalancer_a_subnet_cidr, + AvailabilityZone=Ref(primary_az), +) - SubnetRouteTableAssociation( - "LoadbalancerASubnetRouteTableAssociation", - template=template, - RouteTableId=Ref(public_route_table), - SubnetId=Ref(loadbalancer_a_subnet), - ) +SubnetRouteTableAssociation( + "LoadbalancerASubnetRouteTableAssociation", + template=template, + RouteTableId=Ref(public_route_table), + SubnetId=Ref(loadbalancer_a_subnet), +) - loadbalancer_b_subnet_cidr = "10.0.3.0/24" - loadbalancer_b_subnet = Subnet( - "LoadbalancerBSubnet", - template=template, - VpcId=Ref(vpc), - CidrBlock=loadbalancer_b_subnet_cidr, - AvailabilityZone=Ref(secondary_az), - ) +loadbalancer_b_subnet_cidr = "10.0.3.0/24" +loadbalancer_b_subnet = Subnet( + "LoadbalancerBSubnet", + template=template, + VpcId=Ref(vpc), + CidrBlock=loadbalancer_b_subnet_cidr, + AvailabilityZone=Ref(secondary_az), +) - SubnetRouteTableAssociation( - "LoadbalancerBSubnetRouteTableAssociation", - template=template, - RouteTableId=Ref(public_route_table), - SubnetId=Ref(loadbalancer_b_subnet), - ) +SubnetRouteTableAssociation( + "LoadbalancerBSubnetRouteTableAssociation", + template=template, + RouteTableId=Ref(public_route_table), + SubnetId=Ref(loadbalancer_b_subnet), +) if USE_NAT_GATEWAY: