From 80cada5971fb71756c05bcf634934114c6dea809 Mon Sep 17 00:00:00 2001 From: Adam Keller Date: Tue, 25 Feb 2020 02:02:08 +0000 Subject: [PATCH 1/6] cdk ecsworkshop --- cdk/app.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ cdk/cdk.json | 3 ++ cdk/requirements.txt | 1 + 3 files changed, 80 insertions(+) create mode 100644 cdk/app.py create mode 100644 cdk/cdk.json create mode 100644 cdk/requirements.txt diff --git a/cdk/app.py b/cdk/app.py new file mode 100644 index 00000000..7e93f2be --- /dev/null +++ b/cdk/app.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# cdk: 1.25.0 +from aws_cdk import ( + aws_ec2, + aws_ecs, + aws_ecs_patterns, + aws_servicediscovery, + core, +) + +from os import getenv + + +class FrontendService(core.Stack): + + def __init__(self, scope: core.Stack, id: str, **kwargs): + super().__init__(scope, id, **kwargs) + + # The base platform stack is where the VPC was created, so all we need is the name to do a lookup and import it into this stack for use + self.vpc = aws_ec2.Vpc.from_lookup( + self, "ECSWorkshopVPC", + vpc_name='ecsworkshop-base/BaseVPC' + ) + + self.sd_namespace = aws_servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes( + self, "SDNamespace", + namespace_name='service' + ) + + #self.fargate_load_balanced_service = aws_ecs_patterns.ApplicationLoadBalancedFargateService( + # self, "FrontendFargateLBService", + # vpc=self.vpc, + # image=aws_ecs.ContainerImage.from_registry("brentley/ecsdemo-frontend"), + # container_port=3000, + # cpu=256, + # memory_limit_mib=512, + # enable_logging=True, + # desired_count=1, + # public_load_balancer=True, + # environment={ + # "CRYSTAL_URL": "http://ecsdemo-crystal.service:3000/crystal", + # "NODEJS_URL": "http://ecsdemo-nodejs.service:3000" + # }, + # cloud_map_options= + #) +# +# + ## There has to be a better way, but for now this is what we got. + ## Allow inbound 3000 from Frontend Service to Backend + #self.sec_grp_ingress_backend_to_frontend_3000 = aws_ec2.CfnSecurityGroupIngress( + # self, "InboundBackendSecGrp3000", + # ip_protocol='TCP', + # source_security_group_id=self.fargate_load_balanced_service.service.connections.security_groups[0].security_group_id, + # from_port=3000, + # to_port=3000, + # group_id=self.services_3000_sec_group.security_group_id + #) +# + ## There has to be a better way, but for now this is what we got. + ## Allow inbound 3000 Backend to Frontend Service + #self.sec_grp_ingress_frontend_to_backend_3000 = aws_ec2.CfnSecurityGroupIngress( + # self, "InboundFrontendtoBackendSecGrp3000", + # ip_protocol='TCP', + # source_security_group_id=self.services_3000_sec_group.security_group_id, + # from_port=3000, + # to_port=3000, + # group_id=self.fargate_load_balanced_service.service.connections.security_groups[0].security_group_id, + #) + + +_env = core.Environment(account=getenv('AWS_ACCOUNT_ID'), region=getenv('AWS_DEFAULT_REGION')) +stack_name = "ecsworkshop-frontend" +app = core.App() +FrontendService(app, stack_name, env=_env) +app.synth() diff --git a/cdk/cdk.json b/cdk/cdk.json new file mode 100644 index 00000000..787a71dd --- /dev/null +++ b/cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "python3 app.py" +} diff --git a/cdk/requirements.txt b/cdk/requirements.txt new file mode 100644 index 00000000..4e8c6101 --- /dev/null +++ b/cdk/requirements.txt @@ -0,0 +1 @@ +aws_cdk.aws_ecs_patterns From de94abaa0179fa8d7f589810fb12313a479ed6b1 Mon Sep 17 00:00:00 2001 From: Adam Keller Date: Tue, 25 Feb 2020 04:16:52 +0000 Subject: [PATCH 2/6] Moved to construct for base platform --- .gitignore | 2 ++ cdk/app.py | 85 +++++++++++++++++++++++++++--------------------------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index e43b0f98..23079ad6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +cdk.context.* +cdk.out diff --git a/cdk/app.py b/cdk/app.py index 7e93f2be..9fc9764a 100644 --- a/cdk/app.py +++ b/cdk/app.py @@ -12,9 +12,9 @@ from os import getenv -class FrontendService(core.Stack): +class BasePlatform(core.Construct): - def __init__(self, scope: core.Stack, id: str, **kwargs): + def __init__(self, scope: core.Construct, id: str, **kwargs): super().__init__(scope, id, **kwargs) # The base platform stack is where the VPC was created, so all we need is the name to do a lookup and import it into this stack for use @@ -25,52 +25,51 @@ def __init__(self, scope: core.Stack, id: str, **kwargs): self.sd_namespace = aws_servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes( self, "SDNamespace", - namespace_name='service' + namespace_name=core.Fn.import_value('NSNAME'), + namespace_arn=core.Fn.import_value('NSARN'), + namespace_id=core.Fn.import_value('NSID') + ) + + self.ecs_cluster = aws_ecs.Cluster.from_cluster_attributes( + self, "ECSCluster", + cluster_name=core.Fn.import_value('ECSClusterName'), + security_groups=[], + vpc=self.vpc, + default_cloud_map_namespace=self.sd_namespace + ) + + +class FrontendService(core.Stack): + + def __init__(self, scope: core.Stack, id: str, **kwargs): + super().__init__(scope, id, **kwargs) + + self.base_platform = BasePlatform(self, self.stack_name) + + self.fargate_task_image = aws_ecs_patterns.ApplicationLoadBalancedTaskImageOptions( + image=aws_ecs.ContainerImage.from_registry("brentley/ecsdemo-frontend"), + container_port=3000, + environment={ + "CRYSTAL_URL": "http://ecsdemo-crystal.service:3000/crystal", + "NODEJS_URL": "http://ecsdemo-nodejs.service:3000" + }, ) - #self.fargate_load_balanced_service = aws_ecs_patterns.ApplicationLoadBalancedFargateService( - # self, "FrontendFargateLBService", - # vpc=self.vpc, - # image=aws_ecs.ContainerImage.from_registry("brentley/ecsdemo-frontend"), - # container_port=3000, - # cpu=256, - # memory_limit_mib=512, - # enable_logging=True, - # desired_count=1, - # public_load_balancer=True, - # environment={ - # "CRYSTAL_URL": "http://ecsdemo-crystal.service:3000/crystal", - # "NODEJS_URL": "http://ecsdemo-nodejs.service:3000" - # }, - # cloud_map_options= - #) -# -# - ## There has to be a better way, but for now this is what we got. - ## Allow inbound 3000 from Frontend Service to Backend - #self.sec_grp_ingress_backend_to_frontend_3000 = aws_ec2.CfnSecurityGroupIngress( - # self, "InboundBackendSecGrp3000", - # ip_protocol='TCP', - # source_security_group_id=self.fargate_load_balanced_service.service.connections.security_groups[0].security_group_id, - # from_port=3000, - # to_port=3000, - # group_id=self.services_3000_sec_group.security_group_id - #) -# - ## There has to be a better way, but for now this is what we got. - ## Allow inbound 3000 Backend to Frontend Service - #self.sec_grp_ingress_frontend_to_backend_3000 = aws_ec2.CfnSecurityGroupIngress( - # self, "InboundFrontendtoBackendSecGrp3000", - # ip_protocol='TCP', - # source_security_group_id=self.services_3000_sec_group.security_group_id, - # from_port=3000, - # to_port=3000, - # group_id=self.fargate_load_balanced_service.service.connections.security_groups[0].security_group_id, - #) + self.fargate_load_balanced_service = aws_ecs_patterns.ApplicationLoadBalancedFargateService( + self, "FrontendFargateLBService", + cluster=self.base_platform.ecs_cluster, + cpu=256, + memory_limit_mib=512, + desired_count=1, + public_load_balancer=True, + cloud_map_options=self.base_platform.sd_namespace, + task_image_options=self.fargate_task_image + ) _env = core.Environment(account=getenv('AWS_ACCOUNT_ID'), region=getenv('AWS_DEFAULT_REGION')) -stack_name = "ecsworkshop-frontend" +environment = "ecsworkshop" +stack_name = "{}-frontend".format(environment) app = core.App() FrontendService(app, stack_name, env=_env) app.synth() From 8748f1e20df9be2b304b925b69bbe180fb93d659 Mon Sep 17 00:00:00 2001 From: Adam Keller Date: Tue, 25 Feb 2020 04:19:30 +0000 Subject: [PATCH 3/6] requirements.txt update --- cdk/requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cdk/requirements.txt b/cdk/requirements.txt index 4e8c6101..ad8b8174 100644 --- a/cdk/requirements.txt +++ b/cdk/requirements.txt @@ -1 +1,6 @@ aws_cdk.aws_ecs_patterns +aws_cdk.aws_ec2 +aws_cdk.aws_ecs +aws_cdk.aws_ecs_patterns +aws_cdk.aws_servicediscovery +aws_cdk.core From f169acb263ce94e66da91bc4ac11871d08bf22f1 Mon Sep 17 00:00:00 2001 From: Adam Keller Date: Tue, 25 Feb 2020 17:25:31 +0000 Subject: [PATCH 4/6] Cleaning up env name for cdk --- cdk/app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cdk/app.py b/cdk/app.py index 9fc9764a..cc30b8ef 100644 --- a/cdk/app.py +++ b/cdk/app.py @@ -12,15 +12,17 @@ from os import getenv +# Creating a construct that will populate the required objects created in the platform repo such as vpc, ecs cluster, and service discovery namespace class BasePlatform(core.Construct): def __init__(self, scope: core.Construct, id: str, **kwargs): super().__init__(scope, id, **kwargs) + self.environment_name = 'ecsworkshop' # The base platform stack is where the VPC was created, so all we need is the name to do a lookup and import it into this stack for use self.vpc = aws_ec2.Vpc.from_lookup( - self, "ECSWorkshopVPC", - vpc_name='ecsworkshop-base/BaseVPC' + self, "VPC", + vpc_name='{}-base/BaseVPC'.format(self.environment_name) ) self.sd_namespace = aws_servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes( From 40563eb13850f4449a53b171a41623a73240f80d Mon Sep 17 00:00:00 2001 From: Adam Keller Date: Wed, 26 Feb 2020 01:19:00 +0000 Subject: [PATCH 5/6] Adding shared sec grp functionality --- cdk/app.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cdk/app.py b/cdk/app.py index cc30b8ef..84489205 100644 --- a/cdk/app.py +++ b/cdk/app.py @@ -40,6 +40,11 @@ def __init__(self, scope: core.Construct, id: str, **kwargs): default_cloud_map_namespace=self.sd_namespace ) + self.services_sec_grp = aws_ec2.SecurityGroup.from_security_group_id( + self, "ServicesSecGrp", + security_group_id=core.Fn.import_value('ServicesSecGrp') + ) + class FrontendService(core.Stack): @@ -67,6 +72,11 @@ def __init__(self, scope: core.Stack, id: str, **kwargs): cloud_map_options=self.base_platform.sd_namespace, task_image_options=self.fargate_task_image ) + + self.fargate_load_balanced_service.service.connections.allow_to( + self.base_platform.services_sec_grp, + port_range=aws_ec2.Port(protocol=aws_ec2.Protocol.TCP, string_representation="frontendtobackend", from_port=3000, to_port=3000) + ) _env = core.Environment(account=getenv('AWS_ACCOUNT_ID'), region=getenv('AWS_DEFAULT_REGION')) From 9182ac4d28b916253f594023bf2dc71a079a926b Mon Sep 17 00:00:00 2001 From: Adam Keller Date: Fri, 28 Feb 2020 17:50:09 +0000 Subject: [PATCH 6/6] Separate dockerfile/startup for cdk deployments --- Dockerfile.cdk | 24 +++++++++++++ cdk/app.py | 13 +++++-- startup-cdk.sh | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.cdk create mode 100755 startup-cdk.sh diff --git a/Dockerfile.cdk b/Dockerfile.cdk new file mode 100644 index 00000000..e184438e --- /dev/null +++ b/Dockerfile.cdk @@ -0,0 +1,24 @@ +FROM ruby:2.5-slim + +COPY Gemfile Gemfile.lock /usr/src/app/ +WORKDIR /usr/src/app + +RUN apt-get update && apt-get -y install iproute2 curl jq libgmp3-dev ruby-dev build-essential sqlite libsqlite3-dev python3 python3-pip && \ + bundle install && \ + pip3 install awscli && \ + apt-get autoremove -y --purge && \ + apt-get remove -y --auto-remove --purge ruby-dev libgmp3-dev build-essential libsqlite3-dev && \ + apt-get clean && \ + rm -rvf /root/* /root/.gem* /var/cache/* + +COPY . /usr/src/app +RUN chmod +x /usr/src/app/startup-cdk.sh + +# helpful when trying to update gems -> bundle update, remove the Gemfile.lock, start ruby +# RUN bundle update +# RUN rm -vf /usr/src/app/Gemfile.lock + +HEALTHCHECK --interval=10s --timeout=3s \ + CMD curl -f -s http://localhost:3000/health/ || exit 1 +EXPOSE 3000 +ENTRYPOINT ["bash","/usr/src/app/startup-cdk.sh"] diff --git a/cdk/app.py b/cdk/app.py index 84489205..5782dece 100644 --- a/cdk/app.py +++ b/cdk/app.py @@ -6,6 +6,7 @@ aws_ecs, aws_ecs_patterns, aws_servicediscovery, + aws_iam, core, ) @@ -54,11 +55,12 @@ def __init__(self, scope: core.Stack, id: str, **kwargs): self.base_platform = BasePlatform(self, self.stack_name) self.fargate_task_image = aws_ecs_patterns.ApplicationLoadBalancedTaskImageOptions( - image=aws_ecs.ContainerImage.from_registry("brentley/ecsdemo-frontend"), + image=aws_ecs.ContainerImage.from_registry("adam9098/ecsdemo-frontend"), container_port=3000, environment={ "CRYSTAL_URL": "http://ecsdemo-crystal.service:3000/crystal", - "NODEJS_URL": "http://ecsdemo-nodejs.service:3000" + "NODEJS_URL": "http://ecsdemo-nodejs.service:3000", + "REGION": getenv('AWS_DEFAULT_REGION') }, ) @@ -73,6 +75,13 @@ def __init__(self, scope: core.Stack, id: str, **kwargs): task_image_options=self.fargate_task_image ) + self.fargate_load_balanced_service.task_definition.add_to_task_role_policy( + aws_iam.PolicyStatement( + actions=['ec2:DescribeSubnets'], + resources=['*'] + ) + ) + self.fargate_load_balanced_service.service.connections.allow_to( self.base_platform.services_sec_grp, port_range=aws_ec2.Port(protocol=aws_ec2.Protocol.TCP, string_representation="frontendtobackend", from_port=3000, to_port=3000) diff --git a/startup-cdk.sh b/startup-cdk.sh new file mode 100755 index 00000000..b7e025b2 --- /dev/null +++ b/startup-cdk.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +set -x + +IP=$(ip route show |grep -o src.* |cut -f2 -d" ") +# kubernetes sets routes differently -- so we will discover our IP differently +if [[ ${IP} == "" ]]; then + IP=$(hostname -i) +fi + +SUBNET=$(echo ${IP} | cut -f1 -d.) +NETWORK=$(echo ${IP} | cut -f3 -d.) + +case "${SUBNET}" in + 10) + orchestrator=ecs + ;; + 192) + orchestrator=kubernetes + ;; + *) + orchestrator=unknown + ;; +esac + +if [[ "${orchestrator}" == 'ecs' ]]; then + case "${NETWORK}" in + 100) + zone=a + color=Crimson + ;; + 101) + zone=b + color=CornflowerBlue + ;; + 102) + zone=c + color=LightGreen + ;; + *) + zone=unknown + color=Yellow + ;; + esac +fi + +if [[ "${orchestrator}" == 'kubernetes' ]]; then + if ((0<=${NETWORK} && ${NETWORK}<32)) + then + zone=a + elif ((32<=${NETWORK} && ${NETWORK}<64)) + then + zone=b + elif ((64<=${NETWORK} && ${NETWORK}<96)) + then + zone=c + elif ((96<=${NETWORK} && ${NETWORK}<128)) + then + zone=a + elif ((128<=${NETWORK} && ${NETWORK}<160)) + then + zone=b + elif ((160<=${NETWORK})) + then + zone=c + else + zone=unknown + fi +fi + +if [[ ${orchestrator} == 'unknown' ]]; then + zone=$(curl -m2 -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.availabilityZone' | grep -o .$) +fi + +# Am I on ec2 instances? +if [[ ${zone} == "unknown" ]]; then + zone=$(curl -m2 -s http://169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.availabilityZone' | grep -o .$) +fi + +# Still no luck? Perhaps we're running fargate! +if [[ -z ${zone} ]]; then + export AWS_DEFAULT_REGION=$REGION + ip_addr=$(curl -m2 -s ${ECS_CONTAINER_METADATA_URI} | jq '.Networks[].IPv4Addresses[]') + declare -a subnets=( $(aws ec2 describe-subnets | jq .Subnets[].CidrBlock| sed ':a;N;$!ba;s/\n/ /g') ) + for sub in "${subnets[@]}"; do + if $(ruby -e "puts(IPAddr.new($sub.to_s).include? $ip_addr.to_s)") == 'true'; then + zone=$(aws ec2 describe-subnets | jq -r ".Subnets[] | select(.CidrBlock==$sub) | .AvailabilityZone" | grep -o .$) + fi + done +fi + +export CODE_HASH="$(cat code_hash.txt)" +export AZ="${IP} in AZ-${zone}" + +# exec bundle exec thin start +RAILS_ENV=production rake assets:precompile +exec rails s -e production -b 0.0.0.0