Skip to content

Commit d6e468b

Browse files
authored
Merge branch 'main' into main
2 parents 042ff2c + 8ed0570 commit d6e468b

30 files changed

+975
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*.swp
2+
package-lock.json
3+
__pycache__
4+
.pytest_cache
5+
.venv
6+
*.egg-info
7+
8+
# CDK asset staging directory
9+
.cdk.staging
10+
cdk.out
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
# Welcome to your CDK Python project!
3+
4+
This is a CDK example showcasing ECS Service Connect. ECS Service Connect was released in 2022 and provides customers a way to build seamless communication between microservices. This example showcases a simple frontend container that can be accessed via an ALB URL. When the endpoint is hit it will call the backend container to retrieve data @ data.scapp.local:5001.
5+
6+
7+
To manually create a virtualenv on MacOS and Linux:
8+
9+
```
10+
$ python3 -m venv .venv
11+
```
12+
13+
After the init process completes and the virtualenv is created, you can use the following
14+
step to activate your virtualenv.
15+
16+
```
17+
$ source .venv/bin/activate
18+
```
19+
20+
If you are a Windows platform, you would activate the virtualenv like this:
21+
22+
```
23+
% .venv\Scripts\activate.bat
24+
```
25+
26+
Once the virtualenv is activated, you can install the required dependencies.
27+
28+
```
29+
$ pip install -r requirements.txt
30+
```
31+
32+
At this point you can now synthesize the CloudFormation template for this code.
33+
34+
```
35+
$ cdk synth
36+
```
37+
38+
To add additional dependencies, for example other CDK libraries, just add
39+
them to your `setup.py` file and rerun the `pip install -r requirements.txt`
40+
command.
41+
42+
## Useful commands
43+
44+
* `cdk ls` list all stacks in the app
45+
* `cdk synth` emits the synthesized CloudFormation template
46+
* `cdk deploy` deploy this stack to your default AWS account/region
47+
* `cdk diff` compare deployed stack with current state
48+
* `cdk docs` open CDK documentation
49+
50+
51+
To deploy this stack run `cdk deploy --all`
52+
53+
You can then see how the two containers by running `curl <ALB_ENDPOINT>/get-data`, which will return an array from the backend service at its local domain.
54+
55+
Enjoy!

python/ecs-serviceconnect/app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/usr/bin/env python3
2+
import aws_cdk as cdk
3+
from cdk_examples_service_connect.cdk_examples_service_connect_stack import CdkExamplesServiceConnectStack
4+
5+
6+
app = cdk.App()
7+
CdkExamplesServiceConnectStack(app, "CdkExamplesServiceConnectStack")
8+
9+
app.synth()

python/ecs-serviceconnect/cdk.json

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{
2+
"app": "python3 app.py",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"requirements*.txt",
11+
"source.bat",
12+
"**/__init__.py",
13+
"**/__pycache__",
14+
"tests"
15+
]
16+
},
17+
"context": {
18+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
19+
"@aws-cdk/core:checkSecretUsage": true,
20+
"@aws-cdk/core:target-partitions": [
21+
"aws",
22+
"aws-cn"
23+
],
24+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27+
"@aws-cdk/aws-iam:minimizePolicies": true,
28+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
31+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
32+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
33+
"@aws-cdk/core:enablePartitionLiterals": true,
34+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
35+
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
36+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
37+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
38+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
39+
"@aws-cdk/aws-route53-patters:useCertificate": true,
40+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
41+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
42+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
43+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
44+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
45+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
46+
"@aws-cdk/aws-redshift:columnId": true,
47+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
48+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
49+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
50+
"@aws-cdk/aws-kms:aliasNameRef": true,
51+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
52+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
53+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
54+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
55+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
56+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
57+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
58+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
59+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
60+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
61+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
62+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
63+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
64+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
65+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
66+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
67+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
68+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false
69+
}
70+
}

python/ecs-serviceconnect/cdk_examples_service_connect/__init__.py

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from aws_cdk import (
2+
Stack,
3+
aws_ec2 as ec2,
4+
)
5+
from constructs import Construct
6+
from ecs.ecs_stack import EcsStack
7+
from ecr.ecr_stack import EcrStack
8+
class CdkExamplesServiceConnectStack(Stack):
9+
10+
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
11+
super().__init__(scope, construct_id, **kwargs)
12+
# Creating a shared VPC with public subnets and private subnets with NAT Gateways
13+
vpc = ec2.Vpc(self, "ServiceConnectVPC",
14+
ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"),
15+
create_internet_gateway=True,
16+
max_azs=2,
17+
nat_gateways=2,
18+
enable_dns_hostnames=True,
19+
enable_dns_support=True,
20+
vpc_name="App-Mesh-VPC",
21+
subnet_configuration=[
22+
ec2.SubnetConfiguration(
23+
subnet_type=ec2.SubnetType.PUBLIC,
24+
name="Public",
25+
cidr_mask=24
26+
),
27+
ec2.SubnetConfiguration(
28+
subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS,
29+
name="Private",
30+
cidr_mask=24
31+
)
32+
]
33+
)
34+
AWSRegion=Stack.of(self).region
35+
AWSStackId=Stack.of(self).stack_id
36+
ecr_stack = EcrStack(self, "EcrStack")
37+
ecs_stack = EcsStack(self, "EcsStack", vpc=vpc, frontend_repository=ecr_stack.frontend_docker_asset, backend_data_repository=ecr_stack.backend_data_docker_asset)

python/ecs-serviceconnect/ecr/__init__.py

Whitespace-only changes.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from constructs import Construct
2+
from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform
3+
import cdk_ecr_deployment as ecrdeploy
4+
5+
from aws_cdk import (
6+
NestedStack,
7+
aws_ecr as ecr,
8+
Aws
9+
)
10+
class EcrStack(NestedStack):
11+
12+
def __init__(self, scope: Construct, id: str, **kwargs, ) -> None:
13+
super().__init__(scope, id, **kwargs )
14+
# Creates two ecr repositories that will host the docker images for the color teller gateway app and color teller app
15+
FrontendRepository = ecr.Repository(self, "FrontendRepository", repository_name="frontend")
16+
BackendDataRepository = ecr.Repository(self, "BackendDataRepository", repository_name="backend_data")
17+
18+
# The docker images were built on a M1 Macbook Pro, you may have to rebuild your images
19+
frontendAsset = DockerImageAsset(self, "frontendAsset",
20+
directory="./services/frontend",
21+
build_args={
22+
"SERVICE_B_URL_BUILD_ARG": "data.scapp.local" # This argument will be passed to the dockerfile and is the URL that the frontend app will use to call the backend
23+
},
24+
25+
platform=Platform.LINUX_AMD64
26+
)
27+
dataAsset = DockerImageAsset(self, "dataAsset",
28+
directory="./services/data",
29+
30+
)
31+
# Deploying images to ECR
32+
ecrdeploy.ECRDeployment(self, "DeployFrontendImage",
33+
src=ecrdeploy.DockerImageName(frontendAsset.image_uri),
34+
dest=ecrdeploy.DockerImageName(f"{Aws.ACCOUNT_ID}.dkr.ecr.{Aws.REGION}.amazonaws.com/frontend:latest")
35+
)
36+
37+
38+
39+
ecrdeploy.ECRDeployment(self, "DeployBackendImage",
40+
src=ecrdeploy.DockerImageName(dataAsset.image_uri),
41+
dest=ecrdeploy.DockerImageName(f"{Aws.ACCOUNT_ID}.dkr.ecr.{Aws.REGION}.amazonaws.com/backend_data:latest")
42+
)
43+
44+
# Exporting values to be used in other stacks
45+
self.frontend_docker_asset = frontendAsset
46+
self.backend_data_docker_asset = dataAsset

python/ecs-serviceconnect/ecs/__init__.py

Whitespace-only changes.
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
from aws_cdk import (
2+
NestedStack,
3+
aws_ec2 as ec2,
4+
aws_servicediscovery as servicediscovery,
5+
aws_ecs as ecs,
6+
Duration,
7+
aws_logs as logs,
8+
aws_iam as iam,
9+
aws_iam as iam,
10+
aws_ecr_assets as ecr_assets,
11+
aws_elasticloadbalancingv2 as elbv2,
12+
RemovalPolicy,
13+
CfnOutput
14+
15+
)
16+
from constructs import Construct
17+
18+
class EcsStack(NestedStack):
19+
20+
def __init__(self, scope: Construct, construct_id: str, vpc: ec2.Vpc, frontend_repository: ecr_assets.DockerImageAsset, backend_data_repository: ecr_assets.DockerImageAsset, **kwargs) -> None:
21+
super().__init__(scope, construct_id, **kwargs)
22+
# Creating the ECS Cluster and the cloud map namespace
23+
ecs_cluster = ecs.Cluster(self, "ECSCluster",
24+
vpc=vpc,
25+
cluster_name="App-Service-Connect-Cluster",
26+
container_insights=True)
27+
default_cloud_map_namespace=ecs_cluster.add_default_cloud_map_namespace(name="scapp.local", use_for_service_connect=True, type=servicediscovery.NamespaceType.DNS_PRIVATE)
28+
# Creating the Cloudwatch log group where ECS Logs will be stored
29+
ECSServiceLogGroup = logs.LogGroup(self, "ECSServiceLogGroup",
30+
log_group_name=f"{ecs_cluster.cluster_name}-service",
31+
removal_policy=RemovalPolicy.DESTROY,
32+
retention=logs.RetentionDays.FIVE_DAYS,
33+
)
34+
# Creating the task and execution IAM roles that the containers will assume to read and write to cloudwatch, Task Execution
35+
# Role will read from ECR
36+
ECSTaskIamRole = iam.Role(self, "ECSTaskIamRole",
37+
assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
38+
managed_policies=[
39+
iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchFullAccess"),
40+
],
41+
)
42+
TaskExecutionRole = iam.Role(self, "TaskexecutionRole",
43+
assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
44+
managed_policies=[
45+
iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly"),
46+
iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchLogsFullAccess"),
47+
],
48+
)
49+
# ECS Security group, this will allow access from the Load Balancer and allow LAN access so that the
50+
# ECS containers can talk to eachother on port 5001 (which is the port that the backend uses)
51+
ECSSecurityGroup = ec2.SecurityGroup(self, "ECSSecurityGroup",
52+
vpc=vpc,
53+
description="ECS Security Group",
54+
allow_all_outbound=True,
55+
)
56+
ECSSecurityGroup.add_ingress_rule(ec2.Peer.ipv4(vpc.vpc_cidr_block), ec2.Port.tcp(5001), description="All traffic within VPC",)
57+
# Task definitions for the frontend and backend
58+
frontend_definition = ecs.FargateTaskDefinition(
59+
self, f"FrontendTaskDefinition",
60+
family="frontend",
61+
cpu=256,
62+
memory_limit_mib=512,
63+
task_role=TaskExecutionRole,
64+
execution_role=ECSTaskIamRole
65+
)
66+
backend_definition = ecs.FargateTaskDefinition(
67+
self, f"BackendTaskDefinition",
68+
family="backend",
69+
cpu=256,
70+
memory_limit_mib=512,
71+
task_role=TaskExecutionRole,
72+
execution_role=ECSTaskIamRole
73+
)
74+
75+
# Containers for each application, when the frontend is hit on /get-data it makes a call to the backend endpoint /data
76+
frontend_container = frontend_definition.add_container("FrontendContainer",
77+
container_name="frontend-app",
78+
image=ecs.ContainerImage.from_docker_image_asset(frontend_repository),
79+
port_mappings=[
80+
ecs.PortMapping(
81+
container_port=5000, # Flask app is running on 5001
82+
host_port=5000,
83+
name="frontend" # Name of the port mapping
84+
)
85+
],
86+
logging=ecs.LogDriver.aws_logs(stream_prefix="ecs-logs"))
87+
backend_container = backend_definition.add_container("BackendContainer",
88+
image=ecs.ContainerImage.from_docker_image_asset(backend_data_repository),
89+
port_mappings=[
90+
ecs.PortMapping(
91+
container_port=5001, # Flask app is running on 5001
92+
host_port=5001,
93+
name="data" # Name of the port mapping
94+
95+
)
96+
],
97+
container_name="backend",
98+
logging=ecs.LogDriver.aws_logs(stream_prefix="ecs-logs"))
99+
# Creating the service definitions and port mappings
100+
frontend_service = ecs.FargateService(self, "FrontendService",
101+
cluster=ecs_cluster,
102+
task_definition=frontend_definition,
103+
desired_count=1,
104+
max_healthy_percent=200,
105+
min_healthy_percent=100,
106+
vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
107+
security_groups=[ECSSecurityGroup],
108+
service_connect_configuration=ecs.ServiceConnectProps(
109+
namespace=default_cloud_map_namespace.namespace_name,
110+
services=[ecs.ServiceConnectService(
111+
port_mapping_name="frontend", # Logical name for the service
112+
port=5000, # Container port
113+
)]),
114+
service_name="frontend-service")
115+
backend_service = ecs.FargateService(self, "BackendService",
116+
cluster=ecs_cluster,
117+
task_definition=backend_definition,
118+
desired_count=1,
119+
max_healthy_percent=200,
120+
min_healthy_percent=100,
121+
vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS),
122+
security_groups=[ECSSecurityGroup],
123+
service_connect_configuration=ecs.ServiceConnectProps(
124+
namespace=default_cloud_map_namespace.namespace_name,
125+
services=[ecs.ServiceConnectService(
126+
port_mapping_name="data", # Logical name for the service
127+
port=5001, # Container port
128+
)]),
129+
service_name="backend-service")
130+
# Creating a public load balancer that will listen on port 80 and forward requests to the frontend ecs container,
131+
# healthchecks are established on port 5000
132+
public_lb_sg = ec2.SecurityGroup(self, "PublicLBSG", vpc=vpc, description="Public LB SG", allow_all_outbound=True)
133+
target_group = elbv2.ApplicationTargetGroup(
134+
self, "TargetGroup",
135+
target_group_name="ecs-target-group",
136+
vpc=vpc,
137+
port=80,
138+
targets=[frontend_service],
139+
target_type=elbv2.TargetType.IP,
140+
protocol=elbv2.ApplicationProtocol.HTTP,
141+
health_check=elbv2.HealthCheck(
142+
path="/",
143+
port="5000",
144+
interval=Duration.seconds(6),
145+
timeout=Duration.seconds(5),
146+
healthy_threshold_count=2,
147+
unhealthy_threshold_count=2,
148+
),
149+
)
150+
target_group.set_attribute(key="deregistration_delay.timeout_seconds",
151+
value="120")
152+
public_lb_sg.add_ingress_rule(peer=ec2.Peer.any_ipv4(), connection=ec2.Port.tcp(80), description="Allow HTTP traffic")
153+
public_lb = elbv2.ApplicationLoadBalancer(self, "FrontendLB", vpc=vpc, internet_facing=True, security_group=public_lb_sg, vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC))
154+
public_lb.set_attribute(key="idle_timeout.timeout_seconds", value="30")
155+
listener = public_lb.add_listener("Listener", port=80, default_action=elbv2.ListenerAction.forward(target_groups=[target_group]))
156+
lb_rule = elbv2.ApplicationListenerRule(
157+
self, "ListenerRule",
158+
listener=listener,
159+
priority=1,
160+
action=elbv2.ListenerAction.forward(target_groups=[target_group]),
161+
conditions=[elbv2.ListenerCondition.path_patterns(["*"])],
162+
)
163+
CfnOutput(self, "Load Balancer URL", value=f"http://{public_lb.load_balancer_dns_name}")

0 commit comments

Comments
 (0)