Skip to content

Commit

Permalink
add auto scaling and ELB support to Dokku stack
Browse files Browse the repository at this point in the history
  • Loading branch information
tobiasmcnulty committed Oct 13, 2017
1 parent efaaf5f commit b67e15f
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 89 deletions.
25 changes: 9 additions & 16 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,18 @@ The CloudFormation stack creation should not finish until Dokku is fully install
<http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-signal.html>`_ 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
Expand Down Expand Up @@ -370,20 +377,6 @@ http://python-sample.your.domain/
For additional help deploying to your new instance, please refer to the `Dokku documentation
<http://dokku.viewdocs.io/dokku/deployment/application-deployment/>`_.

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@<your domain or IP> sudo dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
ssh dokku@<your domain or IP> config:set --no-restart python-sample [email protected]
ssh dokku@<your domain or IP> letsencrypt python-sample
ssh dokku@<your domain or IP> letsencrypt:cron-job --add python-sample

The Python sample app should now be accessible over HTTPS at https://python-sample.your.domain/

Contributing
------------

Expand Down
63 changes: 31 additions & 32 deletions stack/dokku.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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(
Expand Down Expand Up @@ -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',
Expand All @@ -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,
}
],
)
28 changes: 17 additions & 11 deletions stack/load_balancer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down
59 changes: 29 additions & 30 deletions stack/vpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit b67e15f

Please sign in to comment.