cd ~/environment/ecsdemo-crystal/cdk
cdk synth
cdk diff
cdk deploy --require-approval never
As we mentioned in the platform build, we are defining our deployment configuration via code. Let’s look through the code to better understand how cdk is deploying.
Because we built the platform in its own stack, there are certain environmental values that we will need to reuse amongst all services being deployed. In this custom construct, we are importing the VPC, ECS Cluster, and Cloud Map namespace from the base platform stack. By wrapping these into a custom construct, we are isolating the platform imports from our service deployment logic.
class BasePlatform(core.Construct):
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
self.vpc = aws_ec2.Vpc.from_lookup(
self, "ECSWorkshopVPC",
vpc_name='ecsworkshop-base/BaseVPC'
)
# Importing the service discovery namespace from the base platform stack
self.sd_namespace = aws_servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes(
self, "SDNamespace",
namespace_name=core.Fn.import_value('NSNAME'),
namespace_arn=core.Fn.import_value('NSARN'),
namespace_id=core.Fn.import_value('NSID')
)
# Importing the ECS cluster from the base platform stack
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
)
# Importing the security group that allows frontend to communicate with backend services
self.services_sec_grp = aws_ec2.SecurityGroup.from_security_group_id(
self, "ServicesSecGrp",
security_group_id=core.Fn.import_value('ServicesSecGrp')
)
For the backend service, we simply want to run a container from a docker image, but still need to figure out how to deploy it and get it behind a scheduler. To do this on our own, we would need to build a task definition, ECS service, and figure out how to get it behind CloudMap for service discovery. To build these components on our own would equate to hundreds of lines of CloudFormation, whereas with the higher level constructs that the cdk provides, we are able to build everything with 30 lines of code.
class CrystalService(core.Stack):
def __init__(self, scope: core.Stack, id: str, **kwargs):
super().__init__(scope, id, **kwargs)
# Importing our shared values from the base stack construct
self.base_platform = BasePlatform(self, self.stack_name)
# The task definition is where we store details about the task that will be scheduled by the service
self.fargate_task_def = aws_ecs.TaskDefinition(
self, "TaskDef",
compatibility=aws_ecs.Compatibility.EC2_AND_FARGATE,
cpu='256',
memory_mib='512',
)
# The container definition defines the container(s) to be run when the task is instantiated
self.container = self.fargate_task_def.add_container(
"CrystalServiceContainerDef",
image=aws_ecs.ContainerImage.from_registry("brentley/ecsdemo-crystal"),
memory_reservation_mib=512,
logging=aws_ecs.LogDriver.aws_logs(
stream_prefix='ecsworkshop-crystal'
)
)
# Serve this container on port 3000
self.container.add_port_mappings(
aws_ecs.PortMapping(
container_port=3000
)
)
# Build the service definition to schedule the container in the shared cluster
self.fargate_service = aws_ecs.FargateService(
self, "CrystalFargateService",
task_definition=self.fargate_task_def,
cluster=self.base_platform.ecs_cluster,
security_group=self.base_platform.services_sec_grp,
desired_count=1,
cloud_map_options=aws_ecs.CloudMapOptions(
cloud_map_namespace=self.base_platform.sd_namespace,
name='ecsdemo-crystal'
)
)
Let’s bring up the Crystal Backend API!
cd ~/environment/ecsdemo-crystal
envsubst < ecs-params.yml.template >ecs-params.yml
ecs-cli compose --project-name ecsdemo-crystal service up \
--create-log-groups \
--private-dns-namespace service \
--enable-service-discovery \
--cluster-config container-demo \
--vpc $vpc
Here, we change directories into our crystal application code directory.
The envsubst
command templates our ecs-params.yml
file with our current values.
We then launch our crystal service on our ECS cluster (with a default launchtype
of Fargate)
Note: ecs-cli will take care of building our private dns namespace for service discovery, and log group in cloudwatch logs.
ecs-cli compose --project-name ecsdemo-crystal service ps \
--cluster-config container-demo
task_id=$(ecs-cli compose --project-name ecsdemo-crystal service ps --cluster-config container-demo | awk -F \/ 'FNR == 2 {print $1}')
We should have one task registered.
alb_url=$(aws cloudformation describe-stacks --stack-name container-demo-alb --query 'Stacks[0].Outputs[?OutputKey==`ExternalUrl`].OutputValue' --output text)
echo "Open $alb_url in your browser"
This command looks up the URL for our ingress ALB, and outputs it. You should be able to click to open, or copy-paste into your browser.
# Referencing task id from above ps command
ecs-cli logs --task-id $task_id \
--follow --cluster-config container-demo
To view logs, find the task id from the earlier ps
command, and use it in this
command. You can follow a task’s logs also.
ecs-cli compose --project-name ecsdemo-crystal service scale 3 \
--cluster-config container-demo
ecs-cli compose --project-name ecsdemo-crystal service ps \
--cluster-config container-demo
We can see that our containers have now been evenly distributed across all 3 of our availability zones.