From ea3d88fc94e515a380a2e6d8786f43b24d448970 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Mon, 15 Jul 2024 15:43:32 -0500 Subject: [PATCH 01/24] Adding appmesh with ECS example to python examples repository --- python/appmesh-ecs/.gitignore | 10 + python/appmesh-ecs/README.md | 70 +++ python/appmesh-ecs/app.py | 30 ++ python/appmesh-ecs/appmesh_diagram.png | Bin 0 -> 82832 bytes python/appmesh-ecs/cdk.json | 70 +++ python/appmesh-ecs/colorapp/__init__.py | 0 .../appmesh-ecs/colorapp/appmesh_colorapp.py | 280 +++++++++++ .../colorapp/ecs_colorapp_stack.py | 469 ++++++++++++++++++ .../container_images/colorteller/.gitignore | 1 + .../container_images/colorteller/Dockerfile | 37 ++ .../container_images/colorteller/deploy.sh | 48 ++ .../container_images/colorteller/go.mod | 15 + .../container_images/colorteller/go.sum | 73 +++ .../container_images/colorteller/main.go | 61 +++ .../container_images/gateway/.gitignore | 1 + .../container_images/gateway/Dockerfile | 37 ++ .../container_images/gateway/deploy.sh | 50 ++ .../container_images/gateway/go.mod | 16 + .../container_images/gateway/go.sum | 73 +++ .../container_images/gateway/main.go | 228 +++++++++ .../core_infrastructure/appmesh/__init__.py | 0 .../appmesh/appmesh_stack.py | 24 + .../core_infrastructure/ecr/__init__.py | 0 .../core_infrastructure/ecr/ecr_stack.py | 51 ++ .../core_infrastructure/ecs/__init__.py | 0 .../core_infrastructure/ecs/ecs_stack.py | 162 ++++++ .../core_infrastructure/vpc/__init__.py | 0 .../core_infrastructure/vpc/vpc_stack.py | 177 +++++++ python/appmesh-ecs/deploy_images.sh | 32 ++ python/appmesh-ecs/requirements-dev.txt | 1 + python/appmesh-ecs/requirements.txt | 2 + python/appmesh-ecs/source.bat | 13 + .../appmesh-ecs/task_definitions/__init__.py | 0 .../color_app_task_definition_stack.py | 388 +++++++++++++++ 34 files changed, 2419 insertions(+) create mode 100644 python/appmesh-ecs/.gitignore create mode 100644 python/appmesh-ecs/README.md create mode 100644 python/appmesh-ecs/app.py create mode 100644 python/appmesh-ecs/appmesh_diagram.png create mode 100644 python/appmesh-ecs/cdk.json create mode 100644 python/appmesh-ecs/colorapp/__init__.py create mode 100644 python/appmesh-ecs/colorapp/appmesh_colorapp.py create mode 100644 python/appmesh-ecs/colorapp/ecs_colorapp_stack.py create mode 100644 python/appmesh-ecs/container_images/colorteller/.gitignore create mode 100644 python/appmesh-ecs/container_images/colorteller/Dockerfile create mode 100755 python/appmesh-ecs/container_images/colorteller/deploy.sh create mode 100644 python/appmesh-ecs/container_images/colorteller/go.mod create mode 100644 python/appmesh-ecs/container_images/colorteller/go.sum create mode 100644 python/appmesh-ecs/container_images/colorteller/main.go create mode 100644 python/appmesh-ecs/container_images/gateway/.gitignore create mode 100644 python/appmesh-ecs/container_images/gateway/Dockerfile create mode 100755 python/appmesh-ecs/container_images/gateway/deploy.sh create mode 100644 python/appmesh-ecs/container_images/gateway/go.mod create mode 100644 python/appmesh-ecs/container_images/gateway/go.sum create mode 100644 python/appmesh-ecs/container_images/gateway/main.go create mode 100644 python/appmesh-ecs/core_infrastructure/appmesh/__init__.py create mode 100644 python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py create mode 100644 python/appmesh-ecs/core_infrastructure/ecr/__init__.py create mode 100644 python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py create mode 100644 python/appmesh-ecs/core_infrastructure/ecs/__init__.py create mode 100644 python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py create mode 100644 python/appmesh-ecs/core_infrastructure/vpc/__init__.py create mode 100644 python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py create mode 100755 python/appmesh-ecs/deploy_images.sh create mode 100644 python/appmesh-ecs/requirements-dev.txt create mode 100644 python/appmesh-ecs/requirements.txt create mode 100644 python/appmesh-ecs/source.bat create mode 100644 python/appmesh-ecs/task_definitions/__init__.py create mode 100644 python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py diff --git a/python/appmesh-ecs/.gitignore b/python/appmesh-ecs/.gitignore new file mode 100644 index 0000000000..37833f8beb --- /dev/null +++ b/python/appmesh-ecs/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/python/appmesh-ecs/README.md b/python/appmesh-ecs/README.md new file mode 100644 index 0000000000..fb0f152262 --- /dev/null +++ b/python/appmesh-ecs/README.md @@ -0,0 +1,70 @@ + +# App Mesh + ECS Fargate + +This stack will create core infrastructure resources and then create ECS services and tasks along with +the appropriate app mesh resources. You can view the traces of the application in AWS X-Ray. + +![Detailed diagram](appmesh_diagram.png) + + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +This project is set up like a standard Python project. The initialization +process also creates a virtualenv within this project, stored under the `.venv` +directory. To create the virtualenv it assumes that there is a `python3` +(or `python` for Windows) executable in your path with access to the `venv` +package. If for any reason the automatic creation of the virtualenv fails, +you can create the virtualenv manually. + +To manually create a virtualenv on MacOS and Linux: + +``` +$ python3 -m venv .venv +``` + +After the init process completes and the virtualenv is created, you can use the following +step to activate your virtualenv. + +``` +$ source .venv/bin/activate +``` + +If you are a Windows platform, you would activate the virtualenv like this: + +``` +% .venv\Scripts\activate.bat +``` + +Once the virtualenv is activated, you can install the required dependencies. + +``` +$ pip install -r requirements.txt +``` + +At this point you can now synthesize the CloudFormation template for this code. + +``` +$ cdk synth +``` + +To add additional dependencies, for example other CDK libraries, just add +them to your `setup.py` file and rerun the `pip install -r requirements.txt` +command. + +## Deploying this sample +To deploy this sample you first need to create an ECR repository and upload the images. To do this you can run +`cdk deploy ECRStack && ./deploy_images` + +Then you can run +`cdk deploy --all` + + +## Useful commands + + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py new file mode 100644 index 0000000000..eca805f7f0 --- /dev/null +++ b/python/appmesh-ecs/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import os + +import aws_cdk as cdk +from aws_cdk import App, Stack +from core_infrastructure.vpc.vpc_stack import VPCStack +from core_infrastructure.ecs.ecs_stack import ECSStack +from core_infrastructure.appmesh.appmesh_stack import AppMeshStack +from colorapp.ecs_colorapp_stack import ColorAppStack +from colorapp.appmesh_colorapp import ServiceMeshColorAppStack +from core_infrastructure.ecr.ecr_stack import ECRStack +from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack +app = cdk.App() + +vpc_stack =VPCStack(app, "VPCStack") +ecs_stack = ECSStack(app, "ECSClusterStack") +appmesh_stack = AppMeshStack(app, "AppMeshStack") +ecr_stack = ECRStack(app, "ECRStack") +color_app_stack = ColorAppStack(app, "ColorAppStack") +appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") +colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack") + +ecs_stack.add_dependency(vpc_stack) +appmesh_stack.add_dependency(ecs_stack) +appmesh_colorapp_stack.add_dependency(appmesh_stack) +colorapp_task_definition_stack.add_dependency(appmesh_colorapp_stack) +color_app_stack.add_dependency(colorapp_task_definition_stack) + + +app.synth() diff --git a/python/appmesh-ecs/appmesh_diagram.png b/python/appmesh-ecs/appmesh_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..984e072f1060b63beb7beabd56ba69281c82b61f GIT binary patch literal 82832 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 CJVK=a literal 0 HcmV?d00001 diff --git a/python/appmesh-ecs/cdk.json b/python/appmesh-ecs/cdk.json new file mode 100644 index 0000000000..db9e766761 --- /dev/null +++ b/python/appmesh-ecs/cdk.json @@ -0,0 +1,70 @@ +{ + "app": "python3 app.py", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "source.bat", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + } +} diff --git a/python/appmesh-ecs/colorapp/__init__.py b/python/appmesh-ecs/colorapp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/colorapp/appmesh_colorapp.py b/python/appmesh-ecs/colorapp/appmesh_colorapp.py new file mode 100644 index 0000000000..1396ab1301 --- /dev/null +++ b/python/appmesh-ecs/colorapp/appmesh_colorapp.py @@ -0,0 +1,280 @@ +# give me a boilerplate cdk class for an appmesh stack with 6 virtual nodes, 1 virtual router, 1 appmesh route, and 2 virtual services. +from aws_cdk import ( + aws_ec2 as ec2, + aws_servicediscovery as servicediscovery, +) +from aws_cdk import Stack, Tags, App +from constructs import Construct +import aws_cdk as core +import aws_cdk.aws_ecs as ecs +import aws_cdk.aws_ecr as ecr +import aws_cdk.aws_dynamodb as dynamodb +import aws_cdk.aws_iam as iam +import aws_cdk.aws_appmesh as appmesh + +class ServiceMeshColorAppStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + # The following creates the appmesh virtual nodes that are associated with each color in the namespace in Cloud Map + ColorTellerBlackVirtualNode = appmesh.CfnVirtualNode( + self, "ColorTellerBlackVirtualNode", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_node_name="colorteller-black-vn", + spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( + service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( + dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( + hostname=core.Fn.sub("colorteller-black.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + ), + listeners=[ + appmesh.CfnVirtualNode.ListenerProperty( + port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + port=9080, + protocol="http", + ), + health_check=appmesh.CfnVirtualNode.HealthCheckProperty( + healthy_threshold=2, + interval_millis=5000, + path="/ping", + port=9080, + protocol="http", + timeout_millis=2000, + unhealthy_threshold=2, + ) + ) + ], + ), + ) + ColorTellerBlueVirtualNode = appmesh.CfnVirtualNode( + self, "ColorTellerBlueVirtualNode", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_node_name="colorteller-blue-vn", + spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( + service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( + dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( + hostname=core.Fn.sub("colorteller-blue.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + ), + listeners=[ + appmesh.CfnVirtualNode.ListenerProperty( + port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + port=9080, + protocol="http", + ), + health_check=appmesh.CfnVirtualNode.HealthCheckProperty( + healthy_threshold=2, + interval_millis=5000, + path="/ping", + port=9080, + protocol="http", + timeout_millis=2000, + unhealthy_threshold=2, + ) + ) + ] + ) + ) + ColorTellerRedVirtualNode = appmesh.CfnVirtualNode( + self, "ColorTellerRedVirtualNode", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_node_name="colorteller-red-vn", + spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( + service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( + dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( + hostname=core.Fn.sub("colorteller-red.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + ), + listeners=[ + appmesh.CfnVirtualNode.ListenerProperty( + port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + port=9080, + protocol="http", + ), + health_check=appmesh.CfnVirtualNode.HealthCheckProperty( + healthy_threshold=2, + interval_millis=5000, + path="/ping", + port=9080, + protocol="http", + timeout_millis=2000, + unhealthy_threshold=2, + ) + ) + ] + ) + ) + ColorTellerWhiteVirtualNode = appmesh.CfnVirtualNode( + self, "ColorTellerWhiteVirtualNode", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_node_name="colorteller-white-vn", + spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( + service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( + dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( + hostname=core.Fn.sub("colorteller.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + ), + listeners=[ + appmesh.CfnVirtualNode.ListenerProperty( + port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + port=9080, + protocol="http", + ), + health_check=appmesh.CfnVirtualNode.HealthCheckProperty( + healthy_threshold=2, + interval_millis=5000, + path="/ping", + port=9080, + protocol="http", + timeout_millis=2000, + unhealthy_threshold=2, + ) + ) + ] + ) + ) + ColorTellerVirtualRouter = appmesh.CfnVirtualRouter( + self, "ColorTellerVirtualRouter", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_router_name="colorteller-vr", + spec=appmesh.CfnVirtualRouter.VirtualRouterSpecProperty( + listeners=[ + appmesh.CfnVirtualRouter.VirtualRouterListenerProperty( + port_mapping=appmesh.CfnVirtualRouter.PortMappingProperty( + port=9080, + protocol="http", + ) + ) + ] + ) + ) + # Creates an app mesh route that distributes traffic across 3 targets when the gateway is hit + ColorTellerRoute = appmesh.CfnRoute( + self, "ColorTellerRoute", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_router_name="colorteller-vr", + route_name="colorteller-route", + spec=appmesh.CfnRoute.RouteSpecProperty( + http_route=appmesh.CfnRoute.HttpRouteProperty( + action=appmesh.CfnRoute.HttpRouteActionProperty( + weighted_targets=[ + appmesh.CfnRoute.WeightedTargetProperty( + virtual_node="colorteller-white-vn", + weight=1, + ), + appmesh.CfnRoute.WeightedTargetProperty( + virtual_node="colorteller-blue-vn", + weight=1, + ), + appmesh.CfnRoute.WeightedTargetProperty( + virtual_node="colorteller-red-vn", + weight=1, + ), + + ], + ), + match=appmesh.CfnRoute.HttpRouteMatchProperty( + prefix="/", + ), + ), + ), + ) + ColorTellerRoute.node.add_dependency(ColorTellerVirtualRouter) + ColorTellerRoute.node.add_dependency(ColorTellerWhiteVirtualNode) + ColorTellerRoute.node.add_dependency(ColorTellerBlueVirtualNode) + ColorTellerRoute.node.add_dependency(ColorTellerRedVirtualNode) + + # Creates a virtual service in app mesh that utilizes the virtual router + ColorTellerVirtualService = appmesh.CfnVirtualService( + self, "ColorTellerVirtualService", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_service_name=core.Fn.sub("colorteller.${ServicesDomain}" , {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), + spec=appmesh.CfnVirtualService.VirtualServiceSpecProperty( + provider=appmesh.CfnVirtualService.VirtualServiceProviderProperty( + virtual_router=appmesh.CfnVirtualService.VirtualRouterServiceProviderProperty( + virtual_router_name="colorteller-vr", + ), + ), + ), + ) + ColorTellerVirtualService.node.add_dependency(ColorTellerVirtualRouter) + + TcpEchoVirtualNode = appmesh.CfnVirtualNode( + self, "TcpEchoVirtualNode", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_node_name="tcpecho-vn", + spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( + service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( + dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( + hostname=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + ), + listeners=[ + appmesh.CfnVirtualNode.ListenerProperty( + port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + port=2701, + protocol="tcp", + ), + health_check=appmesh.CfnVirtualNode.HealthCheckProperty( + healthy_threshold=2, + interval_millis=5000, + protocol="tcp", + timeout_millis=2000, + unhealthy_threshold=2, + ) + ) + ] + ) + ) + TCPEchoVirtualService = appmesh.CfnVirtualService( + self, "TCPEchoVirtualService", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_service_name=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), + spec=appmesh.CfnVirtualService.VirtualServiceSpecProperty( + provider=appmesh.CfnVirtualService.VirtualServiceProviderProperty( + virtual_node=appmesh.CfnVirtualService.VirtualNodeServiceProviderProperty( + virtual_node_name="tcpecho-vn", + ), + ), + ), + ) + TCPEchoVirtualService.node.add_dependency(TcpEchoVirtualNode) + # Creating a virtual node for the color teller gateway + ColorGatewayVirtualNode = appmesh.CfnVirtualNode( + self, "ColorGatewayVirtualNode", + mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), + virtual_node_name="colorgateway-vn", + spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( + service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( + dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( + hostname=core.Fn.sub("colorgateway.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + ), + listeners=[ + appmesh.CfnVirtualNode.ListenerProperty( + port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + port=9080, + protocol="http", + ) + ) + ], + backends=[ + appmesh.CfnVirtualNode.BackendProperty( + virtual_service= + appmesh.CfnVirtualNode.VirtualServiceBackendProperty( + virtual_service_name=core.Fn.sub("colorteller.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), + ) + + + + ), + appmesh.CfnVirtualNode.BackendProperty( + virtual_service= appmesh.CfnVirtualNode.VirtualServiceBackendProperty( + virtual_service_name=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), + ) + ) + ] + ) + ) + \ No newline at end of file diff --git a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py new file mode 100644 index 0000000000..f3d34495a6 --- /dev/null +++ b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py @@ -0,0 +1,469 @@ +from aws_cdk import ( + aws_appmesh as appmesh, + +) +import aws_cdk as core +from aws_cdk import Stack, Tags, App +from constructs import Construct +import aws_cdk.aws_ecs as ecs +import aws_cdk.aws_servicediscovery as servicediscovery +import aws_cdk.aws_elasticloadbalancingv2 as elbv2 +import aws_cdk.aws_ec2 as ec2 +class ColorAppStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + ColorTellerWhiteServiceDiscoveryRecord = servicediscovery.CfnService( + self, "ColorTellerWhiteServiceDiscovery", + description="Service discovery for the white colorteller", + tags=[{"key": "Name", "value": "ColorTellerWhite"}], + name="colorteller", + health_check_custom_config= + servicediscovery.CfnService.HealthCheckCustomConfigProperty( + failure_threshold=1 + ), + dns_config=servicediscovery.CfnService.DnsConfigProperty( + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + # routing_policy="MULTIVALUE", + + dns_records=[ + servicediscovery.CfnService.DnsRecordProperty( + type="A", + ttl=300 + ) + ], + + ) + ) + # Create a service for Color teller white in ECS + ColorTellerWhiteService = ecs.CfnService( + self, "ColorTellerWhiteService", + cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), + service_name="colorteller-white-service", + service_registries=[ + ecs.CfnService.ServiceRegistryProperty( + registry_arn=ColorTellerWhiteServiceDiscoveryRecord.attr_arn + ) + ], + desired_count=1, + launch_type="FARGATE", + deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( + maximum_percent=200, + minimum_healthy_percent=100 + ), + task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-white"), + tags=[{"key": "Name", "value": "ColorTellerWhite"}], + network_configuration=ecs.CfnService.NetworkConfigurationProperty( + awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( + subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], + assign_public_ip="DISABLED", + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] + ) + ) + ) + # Create a service discovery record for the color teller blue service + ColorTellerBlueServiceDiscoveryRecord = servicediscovery.CfnService( + self, "ColorTellerBlueServiceDiscovery", + description="Service discovery for the blue colorteller", + tags=[{"key": "Name", "value": "ColorTellerBlue"}], + name="colorteller-blue", + dns_config=servicediscovery.CfnService.DnsConfigProperty( + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + # routing_policy="MULTIVALUE", + + dns_records=[ + servicediscovery.CfnService.DnsRecordProperty( + type="A", + ttl=300 + ) + ], + + ), + health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( + failure_threshold=1 + ) + ) + # Create a service for the color teller blue service + ColorTellerBlueService = ecs.CfnService( + self, "ColorTellerBlueService", + cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), + service_name="colorteller-blue-service", + service_registries=[ + ecs.CfnService.ServiceRegistryProperty( + registry_arn=ColorTellerBlueServiceDiscoveryRecord.attr_arn + ) + ], + desired_count=1, + launch_type="FARGATE", + deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( + maximum_percent=200, + minimum_healthy_percent=100 + ), + task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-blue"), + tags=[{"key": "Name", "value": "ColorTellerBlue"}], + network_configuration=ecs.CfnService.NetworkConfigurationProperty( + awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( + subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], + assign_public_ip="DISABLED", + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] + ) + ) + ) + # Create a service record for the color teller red service + ColorTellerRedServiceDiscoveryRecord = servicediscovery.CfnService( + self, "ColorTellerRedServiceDiscovery", + description="Service discovery for the red colorteller", + tags=[{"key": "Name", "value": "ColorTellerRed"}], + name="colorteller-red", + dns_config=servicediscovery.CfnService.DnsConfigProperty( + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + # routing_policy="MULTIVALUE", + + dns_records=[ + servicediscovery.CfnService.DnsRecordProperty( + type="A", + ttl=300 + ) + ], + + ), + health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( + failure_threshold=1 + ) + ) + # Create a service for the color teller red service + ColorTellerRedService = ecs.CfnService( + self, "ColorTellerRedService", + cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), + service_name="colorteller-red-service", + service_registries=[ + ecs.CfnService.ServiceRegistryProperty( + registry_arn=ColorTellerRedServiceDiscoveryRecord.attr_arn + ) + ], + desired_count=1, + launch_type="FARGATE", + deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( + maximum_percent=200, + minimum_healthy_percent=100 + ), + task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-red"), + tags=[{"key": "Name", "value": "ColorTellerRed"}], + network_configuration=ecs.CfnService.NetworkConfigurationProperty( + awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( + subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], + assign_public_ip="DISABLED", + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] + ) + ) + ) + # Create a service discovery record for the color teller black service + ColorTellerBlackServiceDiscoveryRecord = servicediscovery.CfnService( + self, "ColorTellerBlackServiceDiscovery", + description="Service discovery for the black colorteller", + tags=[{"key": "Name", "value": "ColorTellerBlack"}], + name="colorteller-black", + dns_config=servicediscovery.CfnService.DnsConfigProperty( + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + # routing_policy="MULTIVALUE", + + dns_records=[ + servicediscovery.CfnService.DnsRecordProperty( + type="A", + ttl=300 + ) + ], + + ), + health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( + failure_threshold=1 + ) + ) + # Create a service for the color teller black service + ColorTellerBlackService = ecs.CfnService( + self, "ColorTellerBlackService", + cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), + service_name="colorteller-black-service", + service_registries=[ + ecs.CfnService.ServiceRegistryProperty( + registry_arn=ColorTellerBlackServiceDiscoveryRecord.attr_arn + ) + ], + deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( + maximum_percent=200, + minimum_healthy_percent=100 + ), + desired_count=1, + launch_type="FARGATE", + task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-black"), + tags=[{"key": "Name", "value": "ColorTellerBlack"}], + network_configuration=ecs.CfnService.NetworkConfigurationProperty( + awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( + subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], + assign_public_ip="DISABLED", + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] + ) + ) + ) + # Create a service discovery record for the color teller blue service + ColorGatewayServiceDiscoveryRecord = servicediscovery.CfnService( + self, "ColorGatewayServiceDiscovery", + description="Service discovery for the gateway colorteller", + tags=[{"key": "Name", "value": "ColorGateway"}], + name="colorgateway", + dns_config=servicediscovery.CfnService.DnsConfigProperty( + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + # routing_policy="MULTIVALUE", + + dns_records=[ + servicediscovery.CfnService.DnsRecordProperty( + type="A", + ttl=300 + ) + ], + + ), + health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( + failure_threshold=1 + ) + ) + ColorGatewayService = ecs.CfnService( + self, "ColorGatewayService", + cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), + service_name="colorteller-gateway-service", + service_registries=[ + ecs.CfnService.ServiceRegistryProperty( + registry_arn=ColorGatewayServiceDiscoveryRecord.attr_arn + ) + ], + desired_count=1, + launch_type="FARGATE", + task_definition=core.Fn.import_value(f"{environment_name}:ColorGatewayTaskDefinitionArn"), + tags=[{"key": "Name", "value": "ColorGateway"}], + deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( + maximum_percent=200, + minimum_healthy_percent=100 + ), + network_configuration=ecs.CfnService.NetworkConfigurationProperty( + awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( + subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], + assign_public_ip="DISABLED", + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] + ) + ), + load_balancers=[ + ecs.CfnService.LoadBalancerProperty( + container_name="app", + container_port=9080, + target_group_arn=WebTargetGroup.attr_target_group_arn + + ) + ] + ) + + + TesterTaskDefinition = ecs.CfnTaskDefinition( + self, "TesterTaskDefinition", + family="tester", + cpu="256", + memory="512", + network_mode="awsvpc", + task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), + execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), + container_definitions=[ + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="app", + image="tstrohmeier/alpine-infinite-curl", + memory_reservation=512, + essential=True, + command=[ + core.Fn.sub("-h http://colorgateway.${ECSServicesDomain}:9080/color", {"ECSServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")} ) + + ], + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": "tester-app" + } + ), + ) + ] + ) + + TcpEchoServiceDiscoveryRecord = servicediscovery.CfnService( + self, "TcpEchoServiceDiscovery", + name="tcpecho", + description="Service discovery for the tcp echo colorteller", + tags=[{"key": "Name", "value": "TcpEcho"}], + dns_config=servicediscovery.CfnService.DnsConfigProperty( + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + # routing_policy="MULTIVALUE", + + dns_records=[ + servicediscovery.CfnService.DnsRecordProperty( + type="A", + ttl=300 + ) + ], + + ), + health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( + failure_threshold=1 + ) + ) + TcpEchoTaskDefinition = ecs.CfnTaskDefinition( + self, "TcpEchoTaskDefinition", + family="tcpecho", + memory="512", + requires_compatibilities=["FARGATE"], + cpu="256", + network_mode="awsvpc", + task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), + execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), + container_definitions=[ + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="app", + image="cjimti/go-echo", + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": "tcpecho-app" + } + ), + port_mappings=[ + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=2701, + host_port=2701, + protocol="tcp" + ) + ], + environment=[ecs.CfnTaskDefinition.KeyValuePairProperty( + name="TCP_PORT", + value="2701" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="NODE_NAME", + value=core.Fn.sub( + "mesh/${AppMeshMeshName}/virtualNode/tcpecho-vn", + {"AppMeshMeshName": f"{environment_name}:Mesh"} + ) + ), + ], + essential=True, + + + ) + ] + ) + TcpEchoService = ecs.CfnService( + self, "TcpEchoService", + cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), + service_name="tcp-echo-service", + service_registries=[ + ecs.CfnService.ServiceRegistryProperty( + registry_arn=TcpEchoServiceDiscoveryRecord.attr_arn + ) + ], + desired_count=1, + launch_type="FARGATE", + task_definition=TcpEchoTaskDefinition.attr_task_definition_arn, + tags=[{"key": "Name", "value": "TcpEcho"}], + deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( + maximum_percent=200, + minimum_healthy_percent=100 + ), + network_configuration=ecs.CfnService.NetworkConfigurationProperty( + awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( + subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], + assign_public_ip="DISABLED", + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] + ) + ) + ) + # The following code creates the front-facing load balancer infrastructure + PublicLoadBalancerSG = ec2.CfnSecurityGroup( + self, "PublicLoadBalancerSG", + vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), + group_description="Access to the public facing load balancer", + security_group_ingress=[ec2.CfnSecurityGroup.IngressProperty( + cidr_ip="0.0.0.0/0", + ip_protocol="tcp", + from_port=80, + to_port=80 + )] + ) + PublicLoadBalancer = elbv2.CfnLoadBalancer( + self, "PublicLoadBalancer", + scheme="internet-facing", + security_groups=[PublicLoadBalancerSG.ref], + subnets=[core.Fn.import_value(f"{environment_name}:PublicSubnet1"), core.Fn.import_value(f"{environment_name}:PublicSubnet2")], + tags=[{"key": "Name", "value": "PublicLoadBalancer"}], + load_balancer_attributes=[elbv2.CfnLoadBalancer.LoadBalancerAttributeProperty( + key="idle_timeout.timeout_seconds", + value="30" + + + ) + ] + + ) + WebTargetGroup = elbv2.CfnTargetGroup( + self, "WebTargetGroup", + health_check_interval_seconds=6, + health_check_path="/ping", + health_check_protocol="HTTP", + health_check_timeout_seconds=5, + healthy_threshold_count=2, + target_type="ip", + unhealthy_threshold_count=2, + vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), + port=80, + protocol="HTTP", + target_group_attributes=[elbv2.CfnTargetGroup.TargetGroupAttributeProperty( + key="deregistration_delay.timeout_seconds", + value="120", + )], + + + tags=[{"key": "Name", "value": "WebTargetGroup"}], + ) + PublicLoadBalancerListener = elbv2.CfnListener( + self, "PublicLoadBalancerListener", + load_balancer_arn=PublicLoadBalancer.ref, + port=80, + protocol="HTTP", + default_actions=[elbv2.CfnListener.ActionProperty( + type="forward", + target_group_arn=WebTargetGroup.attr_target_group_arn + )], + + ) + WebLoadBalancerRule = elbv2.CfnListenerRule( + self, "WebLoadBalancerRule", + listener_arn=PublicLoadBalancerListener.ref, + actions=[elbv2.CfnListenerRule.ActionProperty( + type="forward", + target_group_arn=WebTargetGroup.ref + )], + conditions=[elbv2.CfnListenerRule.RuleConditionProperty( + field="path-pattern", + values=["*"] + )], + priority=1, + ) + PublicLoadBalancerListener.node.add_dependency(PublicLoadBalancerListener) + ColorGatewayService.node.add_dependency(WebLoadBalancerRule) + # Returns the public endpoint for the color app service to cURL against + core.CfnOutput( + self, "ColorAppEndpoint", + description="Public endpoint for Color App Service", + value=core.Fn.join("", ["http://", PublicLoadBalancer.attr_dns_name]), + export_name="ColorAppEndpoint" + ) + + diff --git a/python/appmesh-ecs/container_images/colorteller/.gitignore b/python/appmesh-ecs/container_images/colorteller/.gitignore new file mode 100644 index 0000000000..963c55f7b1 --- /dev/null +++ b/python/appmesh-ecs/container_images/colorteller/.gitignore @@ -0,0 +1 @@ +teller diff --git a/python/appmesh-ecs/container_images/colorteller/Dockerfile b/python/appmesh-ecs/container_images/colorteller/Dockerfile new file mode 100644 index 0000000000..2bae9d4bfc --- /dev/null +++ b/python/appmesh-ecs/container_images/colorteller/Dockerfile @@ -0,0 +1,37 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2 AS builder +RUN yum update -y && \ + yum install -y ca-certificates unzip tar gzip git && \ + yum clean all && \ + rm -rf /var/cache/yum + +RUN curl -LO https://golang.org/dl/go1.17.1.linux-amd64.tar.gz && \ + tar -C /usr/local -xzvf go1.17.1.linux-amd64.tar.gz + +ENV PATH="${PATH}:/usr/local/go/bin" +ENV GOPATH="${HOME}/go" +ENV PATH="${PATH}:${GOPATH}/bin" + +ARG GO_PROXY=https://proxy.golang.org +WORKDIR /go/src/github.com/aws/aws-app-mesh-examples/colorapp/teller + +# go.mod and go.sum go into their own layers. +COPY go.mod . +COPY go.sum . + +# Set the proxies for the go compiler +RUN go env -w GOPROXY=${GO_PROXY} +# This ensures `go mod download` happens only when go.mod and go.sum change. +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /aws-app-mesh-examples-colorapp-teller . + +FROM public.ecr.aws/amazonlinux/amazonlinux:2 +RUN yum update -y && \ + yum install -y ca-certificates && \ + yum clean all && \ + rm -rf /var/cache/yum + +COPY --from=builder /aws-app-mesh-examples-colorapp-teller /bin/aws-app-mesh-examples-colorapp-teller + +ENTRYPOINT ["/bin/aws-app-mesh-examples-colorapp-teller"] diff --git a/python/appmesh-ecs/container_images/colorteller/deploy.sh b/python/appmesh-ecs/container_images/colorteller/deploy.sh new file mode 100755 index 0000000000..4d54d3f293 --- /dev/null +++ b/python/appmesh-ecs/container_images/colorteller/deploy.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -eo pipefail + +source ~/.bashrc + +AWS_ACCOUNT_ID=$1 +AWS_DEFAULT_REGION=$2 + +if [ -z $AWS_ACCOUNT_ID ]; then + echo "AWS_ACCOUNT_ID environment variable is not set." + exit 1 +fi + +if [ -z $AWS_DEFAULT_REGION ]; then + echo "AWS_DEFAULT_REGION environment variable is not set." + exit 1 +fi + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +ECR_URL="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" +COLOR_TELLER_IMAGE=${COLOR_TELLER_IMAGE:-"${ECR_URL}/colorteller"} +GO_PROXY=${GO_PROXY:-"https://proxy.golang.org"} +AWS_CLI_VERSION=$(aws --version 2>&1 | cut -d/ -f2 | cut -d. -f1) + +ecr_login() { + if [ $AWS_CLI_VERSION -gt 1 ]; then + aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | \ + docker login --username AWS --password-stdin ${ECR_URL} + else + $(aws ecr get-login --no-include-email) + fi +} + +describe_create_ecr_registry() { + local repo_name=$1 + local region=$2 + aws ecr describe-repositories --repository-names ${repo_name} --region ${region} \ + || aws ecr create-repository --repository-name ${repo_name} --region ${region} +} + +# build +docker build --build-arg GO_PROXY=$GO_PROXY -t $COLOR_TELLER_IMAGE ${DIR} + +# push +ecr_login +describe_create_ecr_registry colorteller ${AWS_DEFAULT_REGION} +docker push $COLOR_TELLER_IMAGE diff --git a/python/appmesh-ecs/container_images/colorteller/go.mod b/python/appmesh-ecs/container_images/colorteller/go.mod new file mode 100644 index 0000000000..a3f072b905 --- /dev/null +++ b/python/appmesh-ecs/container_images/colorteller/go.mod @@ -0,0 +1,15 @@ +module github.com/aws/aws-app-mesh-examples/colorapp/teller + +go 1.12 + +require ( + github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect + github.com/aws/aws-sdk-go v1.44.209 // indirect + github.com/aws/aws-xray-sdk-go v0.9.4 + github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/net v0.7.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/python/appmesh-ecs/container_images/colorteller/go.sum b/python/appmesh-ecs/container_images/colorteller/go.sum new file mode 100644 index 0000000000..8d1d194072 --- /dev/null +++ b/python/appmesh-ecs/container_images/colorteller/go.sum @@ -0,0 +1,73 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps= +github.com/aws/aws-sdk-go v1.44.209/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-xray-sdk-go v0.9.4 h1:3mtFCrgFR5IefmWFV5pscHp9TTyOWuqaIKJIY0d1Y4g= +github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= +github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf h1:XI2tOTCBqEnMyN2j1yPBI07yQHeywUSCEf8YWqf0oKw= +github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/python/appmesh-ecs/container_images/colorteller/main.go b/python/appmesh-ecs/container_images/colorteller/main.go new file mode 100644 index 0000000000..14dae9d2b7 --- /dev/null +++ b/python/appmesh-ecs/container_images/colorteller/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + + "github.com/aws/aws-xray-sdk-go/xray" +) + +const defaultPort = "8080" +const defaultColor = "black" +const defaultStage = "default" + +func getServerPort() string { + port := os.Getenv("SERVER_PORT") + if port != "" { + return port + } + + return defaultPort +} + +func getColor() string { + color := os.Getenv("COLOR") + if color != "" { + return color + } + + return defaultColor +} + +func getStage() string { + stage := os.Getenv("STAGE") + if stage != "" { + return stage + } + + return defaultStage +} + +type colorHandler struct{} +func (h *colorHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + log.Println("color requested, responding with", getColor()) + fmt.Fprint(writer, getColor()) +} + +type pingHandler struct{} +func (h *pingHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + log.Println("ping requested, reponding with HTTP 200") + writer.WriteHeader(http.StatusOK) +} + +func main() { + log.Println("starting server, listening on port " + getServerPort()) + xraySegmentNamer := xray.NewFixedSegmentNamer(fmt.Sprintf("%s-colorteller-%s", getStage(), getColor())) + http.Handle("/", xray.Handler(xraySegmentNamer, &colorHandler{})) + http.Handle("/ping", xray.Handler(xraySegmentNamer, &pingHandler{})) + http.ListenAndServe(":"+getServerPort(), nil) +} diff --git a/python/appmesh-ecs/container_images/gateway/.gitignore b/python/appmesh-ecs/container_images/gateway/.gitignore new file mode 100644 index 0000000000..ad230ccfee --- /dev/null +++ b/python/appmesh-ecs/container_images/gateway/.gitignore @@ -0,0 +1 @@ +gateway diff --git a/python/appmesh-ecs/container_images/gateway/Dockerfile b/python/appmesh-ecs/container_images/gateway/Dockerfile new file mode 100644 index 0000000000..30b6e83e87 --- /dev/null +++ b/python/appmesh-ecs/container_images/gateway/Dockerfile @@ -0,0 +1,37 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2 AS builder +RUN yum update -y && \ + yum install -y ca-certificates unzip tar gzip git && \ + yum clean all && \ + rm -rf /var/cache/yum + +RUN curl -LO https://golang.org/dl/go1.17.1.linux-amd64.tar.gz && \ + tar -C /usr/local -xzvf go1.17.1.linux-amd64.tar.gz + +ENV PATH="${PATH}:/usr/local/go/bin" +ENV GOPATH="${HOME}/go" +ENV PATH="${PATH}:${GOPATH}/bin" + +ARG GO_PROXY=https://proxy.golang.org +WORKDIR /go/src/github.com/aws/aws-app-mesh-examples/colorapp/gateway + +# go.mod and go.sum go into their own layers. +COPY go.mod . +COPY go.sum . + +# Set the proxies for the go compiler +RUN go env -w GOPROXY=${GO_PROXY} +# This ensures `go mod download` happens only when go.mod and go.sum change. +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /aws-app-mesh-examples-colorapp-gateway . + +FROM public.ecr.aws/amazonlinux/amazonlinux:2 +RUN yum update -y && \ + yum install -y ca-certificates && \ + yum clean all && \ + rm -rf /var/cache/yum + +COPY --from=builder /aws-app-mesh-examples-colorapp-gateway /bin/aws-app-mesh-examples-colorapp-gateway + +ENTRYPOINT ["/bin/aws-app-mesh-examples-colorapp-gateway"] diff --git a/python/appmesh-ecs/container_images/gateway/deploy.sh b/python/appmesh-ecs/container_images/gateway/deploy.sh new file mode 100755 index 0000000000..50c906215c --- /dev/null +++ b/python/appmesh-ecs/container_images/gateway/deploy.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# vim:syn=sh:ts=4:sw=4:et:ai + +set -ex + +source ~/.bashrc +AWS_ACCOUNT_ID=$1 +AWS_DEFAULT_REGION=$2 + +if [ -z $AWS_ACCOUNT_ID ]; then + echo "AWS_ACCOUNT_ID environment variable is not set." + # export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) + exit 1 +fi + +if [ -z $AWS_DEFAULT_REGION ]; then + echo "AWS_DEFAULT_REGION environment variable is not set." + # export AWS_DEFAULT_REGION=$(aws configure get region) + exit 1 +fi + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +ECR_URL="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" +COLOR_GATEWAY_IMAGE=${COLOR_GATEWAY_IMAGE:-"${ECR_URL}/gateway"} +GO_PROXY=${GO_PROXY:-"https://proxy.golang.org"} +AWS_CLI_VERSION=$(aws --version 2>&1 | cut -d/ -f2 | cut -d. -f1) + +ecr_login() { + if [ $AWS_CLI_VERSION -gt 1 ]; then + aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | \ + docker login --username AWS --password-stdin ${ECR_URL} + else + $(aws ecr get-login --no-include-email) + fi +} + +describe_create_ecr_registry() { + local repo_name=$1 + local region=$2 + aws ecr describe-repositories --repository-names ${repo_name} --region ${region} \ + || aws ecr create-repository --repository-name ${repo_name} --region ${region} +} + +# build - for Mac M1 based on https://stackoverflow.com/questions/68630526/lib64-ld-linux-x86-64-so-2-no-such-file-or-directory-error +docker build --platform linux/x86_64 --build-arg GO_PROXY=$GO_PROXY -t $COLOR_GATEWAY_IMAGE ${DIR} + +# push +ecr_login +describe_create_ecr_registry gateway ${AWS_DEFAULT_REGION} +docker push $COLOR_GATEWAY_IMAGE diff --git a/python/appmesh-ecs/container_images/gateway/go.mod b/python/appmesh-ecs/container_images/gateway/go.mod new file mode 100644 index 0000000000..e2d3c69279 --- /dev/null +++ b/python/appmesh-ecs/container_images/gateway/go.mod @@ -0,0 +1,16 @@ +module github.com/aws/aws-app-mesh-examples/colorapp/gateway + +go 1.12 + +require ( + github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect + github.com/aws/aws-sdk-go v1.44.209 // indirect + github.com/aws/aws-xray-sdk-go v0.9.4 + github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/net v0.7.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/python/appmesh-ecs/container_images/gateway/go.sum b/python/appmesh-ecs/container_images/gateway/go.sum new file mode 100644 index 0000000000..8d1d194072 --- /dev/null +++ b/python/appmesh-ecs/container_images/gateway/go.sum @@ -0,0 +1,73 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps= +github.com/aws/aws-sdk-go v1.44.209/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-xray-sdk-go v0.9.4 h1:3mtFCrgFR5IefmWFV5pscHp9TTyOWuqaIKJIY0d1Y4g= +github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= +github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf h1:XI2tOTCBqEnMyN2j1yPBI07yQHeywUSCEf8YWqf0oKw= +github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/python/appmesh-ecs/container_images/gateway/main.go b/python/appmesh-ecs/container_images/gateway/main.go new file mode 100644 index 0000000000..7bf08e563c --- /dev/null +++ b/python/appmesh-ecs/container_images/gateway/main.go @@ -0,0 +1,228 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math" + "net" + "net/http" + "os" + "strings" + "sync" + + "github.com/aws/aws-xray-sdk-go/xray" + "github.com/pkg/errors" +) + +const defaultPort = "8080" +const defaultStage = "default" +const maxColors = 1000 + +var colors [maxColors]string +var colorsIdx int +var colorsMutext = &sync.Mutex{} + +func getServerPort() string { + port := os.Getenv("SERVER_PORT") + if port != "" { + return port + } + + return defaultPort +} + +func getStage() string { + stage := os.Getenv("STAGE") + if stage != "" { + return stage + } + + return defaultStage +} + +func getColorTellerEndpoint() (string, error) { + colorTellerEndpoint := os.Getenv("COLOR_TELLER_ENDPOINT") + if colorTellerEndpoint == "" { + return "", errors.New("COLOR_TELLER_ENDPOINT is not set") + } + return colorTellerEndpoint, nil +} + +type colorHandler struct{} + +func (h *colorHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + color, err := getColorFromColorTeller(request) + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte("500 - Unexpected Error")) + return + } + + colorsMutext.Lock() + defer colorsMutext.Unlock() + + addColor(color) + statsJson, err := json.Marshal(getRatios()) + if err != nil { + fmt.Fprintf(writer, `{"color":"%s", "error":"%s"}`, color, err) + return + } + fmt.Fprintf(writer, `{"color":"%s", "stats": %s}`, color, statsJson) +} + +func addColor(color string) { + colors[colorsIdx] = color + + colorsIdx += 1 + if colorsIdx >= maxColors { + colorsIdx = 0 + } +} + +func getRatios() map[string]float64 { + counts := make(map[string]int) + var total = 0 + + for _, c := range colors { + if c != "" { + counts[c] += 1 + total += 1 + } + } + + ratios := make(map[string]float64) + for k, v := range counts { + ratio := float64(v) / float64(total) + ratios[k] = math.Round(ratio*100) / 100 + } + + return ratios +} + +type clearColorStatsHandler struct{} + +func (h *clearColorStatsHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + colorsMutext.Lock() + defer colorsMutext.Unlock() + + colorsIdx = 0 + for i := range colors { + colors[i] = "" + } + + fmt.Fprint(writer, "cleared") +} + +func getColorFromColorTeller(request *http.Request) (string, error) { + colorTellerEndpoint, err := getColorTellerEndpoint() + if err != nil { + return "-n/a-", err + } + + client := xray.Client(&http.Client{}) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", colorTellerEndpoint), nil) + log.Printf("Requesting color from %s", colorTellerEndpoint) + log.Printf("Gateway Request is : %v", req) + if err != nil { + return "-n/a-", err + } + + resp, err := client.Do(req.WithContext(request.Context())) + log.Printf("Gateway Response is : %v", resp) + if err != nil { + return "-n/a-", err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "-n/a-", err + } + + color := strings.TrimSpace(string(body)) + if len(color) < 1 { + return "-n/a-", errors.New("Empty response from colorTeller") + } + + return color, nil +} + +func getTCPEchoEndpoint() (string, error) { + tcpEchoEndpoint := os.Getenv("TCP_ECHO_ENDPOINT") + if tcpEchoEndpoint == "" { + return "", errors.New("TCP_ECHO_ENDPOINT is not set") + } + return tcpEchoEndpoint, nil +} + +type tcpEchoHandler struct{} + +func (h *tcpEchoHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + endpoint, err := getTCPEchoEndpoint() + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(writer, "tcpecho endpoint is not set") + return + } + + log.Printf("Dialing tcp endpoint %s", endpoint) + conn, err := net.Dial("tcp", endpoint) + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(writer, "Dial failed, err:%s", err.Error()) + return + } + defer conn.Close() + + strEcho := "Hello from gateway" + log.Printf("Writing '%s'", strEcho) + _, err = fmt.Fprintf(conn, strEcho) + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(writer, "Write to server failed, err:%s", err.Error()) + return + } + + reply, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + writer.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(writer, "Read from server failed, err:%s", err.Error()) + return + } + + fmt.Fprintf(writer, "Response from tcpecho server: %s", reply) +} + +type pingHandler struct{} + +func (h *pingHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + log.Println("ping requested, reponding with HTTP 200") + writer.WriteHeader(http.StatusOK) +} + +func main() { + log.Println("Starting server, listening on port " + getServerPort()) + + colorTellerEndpoint, err := getColorTellerEndpoint() + if err != nil { + log.Fatalln(err) + } + tcpEchoEndpoint, err := getTCPEchoEndpoint() + if err != nil { + log.Println(err) + } + + log.Println("Using color-teller at " + colorTellerEndpoint) + log.Println("Using tcp-echo at " + tcpEchoEndpoint) + + xraySegmentNamer := xray.NewFixedSegmentNamer(fmt.Sprintf("%s-gateway", getStage())) + + http.Handle("/color", xray.Handler(xraySegmentNamer, &colorHandler{})) + http.Handle("/color/clear", xray.Handler(xraySegmentNamer, &clearColorStatsHandler{})) + http.Handle("/tcpecho", xray.Handler(xraySegmentNamer, &tcpEchoHandler{})) + http.Handle("/ping", xray.Handler(xraySegmentNamer, &pingHandler{})) + log.Fatal(http.ListenAndServe(":"+getServerPort(), nil)) +} diff --git a/python/appmesh-ecs/core_infrastructure/appmesh/__init__.py b/python/appmesh-ecs/core_infrastructure/appmesh/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py b/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py new file mode 100644 index 0000000000..8c50a664d4 --- /dev/null +++ b/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py @@ -0,0 +1,24 @@ +# write a boilerplate class for an appmesh mesh with cdk +from aws_cdk import ( + aws_appmesh as appmesh, + +) +import aws_cdk as core +from aws_cdk import Stack, Tags, App +from constructs import Construct +class AppMeshStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + # Creates an AWS App Mesh mesh + mesh = appmesh.CfnMesh(self, "ecs-mesh", + mesh_name=environment_name + + ) + core.CfnOutput( + self, "MeshName", + value=mesh.attr_mesh_name, + description="A reference to the AppMesh Meshs", + export_name=f"{environment_name}:Mesh" + ) diff --git a/python/appmesh-ecs/core_infrastructure/ecr/__init__.py b/python/appmesh-ecs/core_infrastructure/ecr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py b/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py new file mode 100644 index 0000000000..e00ee77f18 --- /dev/null +++ b/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py @@ -0,0 +1,51 @@ +# write a boilerplate class for an appmesh mesh with cdk +from aws_cdk import ( + aws_appmesh as appmesh, + +) +import aws_cdk as core +from aws_cdk import Stack, Tags, App +from constructs import Construct +import aws_cdk.aws_ecs as ecs +import aws_cdk.aws_servicediscovery as servicediscovery +import aws_cdk.aws_elasticloadbalancingv2 as elbv2 +import aws_cdk.aws_ec2 as ec2 +import aws_cdk.aws_ecr as ecr +import subprocess +class ECRStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + # Creates two ecr repositories that will host the docker images for the color teller gateway app and color teller app + ColorAppGatewayRepository = ecr.CfnRepository( + self, "gateway", + repository_name="gateway", + tags=[{ + "key": "Name", + "value": "gateway" + }] + ) + ColorAppColorTellerRepository = ecr.CfnRepository( + self, "colorteller", + repository_name="colorteller", + tags=[{ + "key": "Name", + "value": "colorteller" + }] + ) + + core.CfnOutput( + self, "ColorAppGatewayRepository", + value=ColorAppGatewayRepository.attr_repository_uri, + description="ColorAppRepository", + export_name=f"{environment_name}:ColorAppRepository" + ) + core.CfnOutput( + self, "ColorAppColorTellerRepository", + value=ColorAppColorTellerRepository.attr_repository_uri, + description="ColorAppColorTellerRepository", + export_name=f"{environment_name}:ColorAppColorTellerRepository" + ) + + \ No newline at end of file diff --git a/python/appmesh-ecs/core_infrastructure/ecs/__init__.py b/python/appmesh-ecs/core_infrastructure/ecs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py b/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py new file mode 100644 index 0000000000..ece56cb63d --- /dev/null +++ b/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py @@ -0,0 +1,162 @@ +import aws_cdk.aws_ecs as ecs +import aws_cdk.aws_ec2 as ec2 +import aws_cdk.aws_autoscaling as autoscaling +from aws_cdk import Stack, Tags, App +from constructs import Construct +import aws_cdk as core +import aws_cdk.aws_ssm as ssm +import aws_cdk.aws_iam as iam +import aws_cdk.aws_logs as logs +import aws_cdk.aws_servicediscovery as servicediscovery +class ECSStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + + AWSRegion=core.Stack.of(self).region + AWSStackId=core.Stack.of(self).stack_id + # The following creates the ECS Cluster along with its security groups. It also creates the proper roles that the ECS tasks will assume + # as well as the log group that the tasks log to and the service discovery namespace (created under AWS Cloud Map in the console) + ecsCluster = ecs.CfnCluster( + self, "ecsCluster", + cluster_name="App-Mesh-ECS-Cluster", + capacity_providers=["FARGATE"], + default_capacity_provider_strategy=[{"capacityProvider": "FARGATE", "weight": 1, "base": 1}], + ) + ecsSGG = ec2.CfnSecurityGroup( + self, "ECS_SG", + group_description="Security Group for ECS instances", + vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), + group_name="ecs-sg", + security_group_ingress=[ + ec2.CfnSecurityGroup.IngressProperty( + cidr_ip=core.Fn.import_value(f"{environment_name}:VpcCidr"), + ip_protocol="-1" + # ip protocol -1? not sure + # ip_protocol="tcp", + # from_port=8080, + # to_port=8080 + ) + ], + + ) + ECSServiceSecurityGroup = ec2.CfnSecurityGroup( + self, "ECSSecurityGroup", + group_name="ecs-service-sg", + group_description="Security group for ECS service", + vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), + security_group_ingress=[ + ec2.CfnSecurityGroup.IngressProperty( + cidr_ip=core.Fn.import_value(f"{environment_name}:VpcCidr"), + + ip_protocol="-1", + # from_port=8080, + # to_port=8080 + ) + ] + ) + ECSTaskIamRole = iam.CfnRole( + self, "ECSTaskIamRole", + role_name="TaskIamRole", + assume_role_policy_document={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "ecs-tasks.amazonaws.com" + ] + }, + "Action": "sts:AssumeRole" + } + ] + }, + managed_policy_arns=[ + "arn:aws:iam::aws:policy/CloudWatchFullAccess", + "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess", + "arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess" + ] + + ) + TaskExecutionIamRole = iam.CfnRole( + self, "TaskExecutionIamRole", + role_name="ecs-task-execution-role", + assume_role_policy_document={ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "ecs-tasks.amazonaws.com" + ] + }, + "Action": "sts:AssumeRole" + } + ] + }, + managed_policy_arns=[ + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", + "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" + ] + ) + ECSServicelogGroup = logs.CfnLogGroup( + self, "ECSServiceLogGroup", + log_group_name=f"{ecsCluster.cluster_name}-service", + retention_in_days=7 + ) + ECSServiceDiscoveryNamespace = servicediscovery.CfnPrivateDnsNamespace( + self, "ServiceDiscoveryNamespace", + name="appmesh.local", + vpc=core.Fn.import_value(f"{environment_name}:VPCID") + ) + + core.CfnOutput( + self, "ECSCluster", + value=ecsCluster.ref, + export_name=f"{environment_name}:ECSCluster" + ) + + core.CfnOutput( + self, "ECSServiceDiscoveryNamespace", + value=ECSServiceDiscoveryNamespace.name, + export_name=f"{environment_name}:ECSServiceDiscoveryNamespace" + ) + core.CfnOutput( + self, "ECSServiceDiscoveryId", + value=ECSServiceDiscoveryNamespace.attr_id, + export_name=f"{environment_name}:ECSServiceDiscoveryNamespaceID" + ) + core.CfnOutput( + self, "ECSServicelogGroup", + value=ECSServicelogGroup.ref, + export_name=f"{environment_name}:ECSServicelogGroup" + ) + core.CfnOutput(self, "ECSServiceSecurityGroup", + value=ECSServiceSecurityGroup.ref, + export_name=f"{environment_name}:ECSServiceSecurityGroup") + + core.CfnOutput( + self, "Task_Execution_Iam_Role", + value=TaskExecutionIamRole.attr_arn, + export_name=f"{environment_name}:TaskExecutionIamRole" + ) + + core.CfnOutput( + self, "ECS_Task_Iam_Role", + value=ECSTaskIamRole.attr_arn, + export_name=f"{environment_name}:ECSTaskIamRole" + ) + + # core.CfnOutput( + # self, "BastionHostId", + # value=BastionHost.ref, + # export_name=f"{environment_name}:BastionHostId" + # ) + # core.CfnOutput( + # self, "BastionHostPublicIp", + # value=BastionHost.attr_public_ip, + # export_name=f"{environment_name}:BastionHostPublicIp" + # ) \ No newline at end of file diff --git a/python/appmesh-ecs/core_infrastructure/vpc/__init__.py b/python/appmesh-ecs/core_infrastructure/vpc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py b/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py new file mode 100644 index 0000000000..b2b46e47b2 --- /dev/null +++ b/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py @@ -0,0 +1,177 @@ +import aws_cdk.aws_ec2 as ec2 +import aws_cdk.aws_s3 as s3 +from aws_cdk import Stack, Tags, App +from constructs import Construct +import aws_cdk as core +class VPCStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + # The following code creates the core VPC infrastructure with two public subnets and two private subnets, as well as route tables + # that route private subnets to the internet using NAT gateways + vpc = ec2.CfnVPC(self, "appmesh-vpc", + cidr_block="192.168.0.0/16", + enable_dns_support=True, + enable_dns_hostnames=True, + tags=[{ + "key": "Name", + "value": "appmesh-vpc" + }] + ) + public_subnet_1 = ec2.CfnSubnet(self, "PublicSubnet1", + cidr_block="192.168.1.0/24", + vpc_id=vpc.ref, + availability_zone=f"{core.Aws.REGION}a", + map_public_ip_on_launch=True, + tags=[{ + "key": "Name", + "value": "PublicSubnet1" + }] + ) + public_subnet_2 = ec2.CfnSubnet(self, "PublicSubnet2", + cidr_block="192.168.2.0/24", + vpc_id=vpc.ref, + availability_zone=f"{core.Aws.REGION}b", + map_public_ip_on_launch=True, + tags=[{ + "key": "Name", + "value": "PublicSubnet2" + }] + ) + private_subnet_1 = ec2.CfnSubnet(self, "PrivateSubnet1", + cidr_block="192.168.3.0/24", + vpc_id=vpc.ref, + availability_zone=f"{core.Aws.REGION}a", + tags=[{ + "key": "Name", + "value": "PrivateSubnet1" + }] + ) + + private_subnet_2 = ec2.CfnSubnet(self, "PrivateSubnet2", + cidr_block="192.168.4.0/24", + vpc_id=vpc.ref, + availability_zone=f"{core.Aws.REGION}b", + tags=[{ + "key": "Name", + "value": "PrivateSubnet2" + }] + ) + internet_gateway = ec2.CfnInternetGateway(self, "InternetGateway", + tags=[{ + "key": "Name", + "value": "InternetGateway" + }] + ) + gateway_attachment = ec2.CfnVPCGatewayAttachment(self, "IGWAttachment", + vpc_id=vpc.ref, + internet_gateway_id=internet_gateway.ref + ) + + + NatGateway1EIP = ec2.CfnEIP(self, "NatGateway1EIP", domain="vpc") + NatGateway2EIP = ec2.CfnEIP(self, "NatGateway2EIP", domain="vpc") + + + cfn_nat_gateway1 = ec2.CfnNatGateway(self, "MyCfnNatGateway1", + allocation_id=NatGateway1EIP.attr_allocation_id, + subnet_id=public_subnet_1.attr_subnet_id) + + cfn_nat_gateway2 = ec2.CfnNatGateway(self, "MyCfnNatGateway2", + allocation_id=NatGateway2EIP.attr_allocation_id, + subnet_id=public_subnet_1.attr_subnet_id) + NatGateway1EIP.node.add_dependency(gateway_attachment) + NatGateway2EIP.node.add_dependency(gateway_attachment) + cfn_nat_gateway1.node.add_dependency(NatGateway1EIP) + cfn_nat_gateway2.node.add_dependency(NatGateway2EIP) + + public_route_table = ec2.CfnRouteTable(self, "PublicRouteTable", + vpc_id=vpc.attr_vpc_id, + tags=[{ + "key": "Name", + "value": f"{environment_name} Public Routes" + }]) + ec2.CfnRoute(self, "DefaultPublicRoute", + route_table_id=public_route_table.attr_route_table_id, + destination_cidr_block="0.0.0.0/0", + gateway_id=gateway_attachment.internet_gateway_id + + ) + + ec2.CfnSubnetRouteTableAssociation(self, f"PublicSubnetRouteTableAssociation1", + route_table_id=public_route_table.attr_route_table_id, + subnet_id=public_subnet_1.attr_subnet_id) + ec2.CfnSubnetRouteTableAssociation(self, f"PublicSubnetRouteTableAssociation2", + route_table_id=public_route_table.attr_route_table_id, + subnet_id=public_subnet_2.attr_subnet_id) + privateRouteTable = ec2.CfnRouteTable(self, "PrivateRouteTable1", + vpc_id=vpc.attr_vpc_id, + tags=[{ + "key": "Name", + "value": f"{environment_name} Private Routes (AZ1)" + }]) + + ec2.CfnRoute(self, "DefaultPrivateRoute1", + route_table_id=privateRouteTable.ref, + destination_cidr_block="0.0.0.0/0", + nat_gateway_id=cfn_nat_gateway1.ref) + + ec2.CfnSubnetRouteTableAssociation(self, "PrivateSubnetRouteTableAssociation", + subnet_id=private_subnet_1.attr_subnet_id, + route_table_id=privateRouteTable.ref) + + privateRouteTable2 = ec2.CfnRouteTable(self, "PrivateRouteTable2", + vpc_id=vpc.attr_vpc_id, + tags=[{ + "key": "Name", + "value": f"{environment_name} Private Routes (AZ2)" + }]) + + ec2.CfnRoute(self, "DefaultPrivateRoute2", + route_table_id=privateRouteTable2.ref, + destination_cidr_block="0.0.0.0/0", + nat_gateway_id=cfn_nat_gateway2.ref) + + ec2.CfnSubnetRouteTableAssociation(self, "PrivateSubnet2RouteTableAssociation", + subnet_id=private_subnet_2.attr_subnet_id, + route_table_id=privateRouteTable2.ref) + core.CfnOutput( + self, "VPCID", + value=vpc.attr_vpc_id, + export_name=f"{environment_name}:VPCID" + ) + core.CfnOutput( + self, "Public Subnet 1", + value=public_subnet_1.attr_subnet_id, + export_name=f"{environment_name}:PublicSubnet1" + ) + core.CfnOutput( + self, "Public Subnet 2", + value=public_subnet_2.attr_subnet_id, + export_name=f"{environment_name}:PublicSubnet2" + ) + core.CfnOutput( + self, "Private Subnet 1", + value=private_subnet_1.attr_subnet_id, + export_name=f"{environment_name}:PrivateSubnet1" + ) + core.CfnOutput( + self, "Private Subnet 2", + value=private_subnet_2.attr_subnet_id, + export_name=f"{environment_name}:PrivateSubnet2" + ) + core.CfnOutput(self, "VpcCidr", + value=vpc.attr_cidr_block, + export_name=f"{environment_name}:VpcCidr" + ) + + + + + + + + + + \ No newline at end of file diff --git a/python/appmesh-ecs/deploy_images.sh b/python/appmesh-ecs/deploy_images.sh new file mode 100755 index 0000000000..8d0fd20827 --- /dev/null +++ b/python/appmesh-ecs/deploy_images.sh @@ -0,0 +1,32 @@ +#!/bin/bash + + +# Paths to the deployment scripts +GatewayScriptPath="./container_images/gateway/deploy.sh" +ColorTellerScriptPath="./container_images/colorteller/deploy.sh" + +# Retrieve the AWS account ID and default region +account_id=$(aws sts get-caller-identity --query Account --output text) +default_region=$(aws configure get region) + +# Print the retrieved account ID +echo "Account ID: $account_id" +echo "Default Region: $default_region" + +# Execute the deployment scripts +{ + gateway_result=$(bash "$GatewayScriptPath" "$account_id" "$default_region" 2>&1) + echo "Gateway Result: $gateway_result" +} || { + echo "Error occurred while running the Gateway script" + exit 1 +} + +{ + colorteller_result=$(bash "$ColorTellerScriptPath" "$account_id" "$default_region" 2>&1) + echo "ColorTeller Result: $colorteller_result" +} || { + echo "Error occurred while running the ColorTeller script" + exit 1 +} + diff --git a/python/appmesh-ecs/requirements-dev.txt b/python/appmesh-ecs/requirements-dev.txt new file mode 100644 index 0000000000..927094516e --- /dev/null +++ b/python/appmesh-ecs/requirements-dev.txt @@ -0,0 +1 @@ +pytest==6.2.5 diff --git a/python/appmesh-ecs/requirements.txt b/python/appmesh-ecs/requirements.txt new file mode 100644 index 0000000000..b1bcff241f --- /dev/null +++ b/python/appmesh-ecs/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib==2.147.1 +constructs>=10.0.0,<11.0.0 diff --git a/python/appmesh-ecs/source.bat b/python/appmesh-ecs/source.bat new file mode 100644 index 0000000000..9e1a83442a --- /dev/null +++ b/python/appmesh-ecs/source.bat @@ -0,0 +1,13 @@ +@echo off + +rem The sole purpose of this script is to make the command +rem +rem source .venv/bin/activate +rem +rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. +rem On Windows, this command just runs this batch file (the argument is ignored). +rem +rem Now we don't need to document a Windows command for activating a virtualenv. + +echo Executing .venv\Scripts\activate.bat for you +.venv\Scripts\activate.bat diff --git a/python/appmesh-ecs/task_definitions/__init__.py b/python/appmesh-ecs/task_definitions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py new file mode 100644 index 0000000000..30c51412de --- /dev/null +++ b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py @@ -0,0 +1,388 @@ +# give me a boilerplate cdk class +import aws_cdk as core +import aws_cdk.aws_ec2 as ec2 +import aws_cdk.aws_ecs as ecs +from aws_cdk import Stack, Tags, App +from constructs import Construct +class ColorAppTaskDefinitionStack(Stack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + + color_teller_colors = ["red", "black", "blue", "white"] + for color in color_teller_colors: + task_definition = self.create_color_task_definition(color, environment_name) + core.CfnOutput( + self, f"ColorTellerTaskDefinitionArn-{color}", + value=task_definition.attr_task_definition_arn, + export_name=f"{environment_name}:ColorTellerTaskDefinitionArn-{color}" + ) + + + + colorGatewayTaskDefinition = ecs.CfnTaskDefinition( + self, "colorGatewayTask", + family=f"{environment_name}-ColorGateway", + network_mode="awsvpc", + memory="512", + requires_compatibilities=["FARGATE"], + cpu="256", + proxy_configuration= + ecs.CfnTaskDefinition.ProxyConfigurationProperty( + container_name="envoy", + type="APPMESH", + proxy_configuration_properties=[ + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="IgnoredUID", + value="1337" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ProxyIngressPort", + value="15000" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ProxyEgressPort", + value="15001" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="AppPorts", + value="9080" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="EgressIgnoredIPs", + value="169.254.170.2,169.254.169.254" + ) + ] + ), + container_definitions=[ + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="app", + image=f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/gateway:latest", + # memory=256, + # cpu=128, + port_mappings=[ + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=9080, + host_port=9080, + protocol="tcp" + ) + ], + environment=[ + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="SERVER_PORT", + value="9080" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="COLOR_TELLER_ENDPOINT", + value=core.Fn.sub("colorteller.${SERVICES_DOMAIN}:9080", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="TCP_ECHO_ENDPOINT", + value=core.Fn.sub("tcpecho.${SERVICES_DOMAIN}:2701", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) + + ], + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": "colorteller-gateway" + } + ), + essential=True, + depends_on=[ + ecs.CfnTaskDefinition.ContainerDependencyProperty( + container_name="envoy", + condition="HEALTHY" + ) + ], + + + ), + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="envoy", + user="1337", + essential=True, + # cpu=128, + image=core.Fn.sub("840364872350.dkr.ecr.${Region}.amazonaws.com/aws-appmesh-envoy:v1.29.5.0-prod",{"Region": core.Aws.REGION}), + port_mappings=[ + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=9901, + host_port=9901, + protocol="tcp" + ), + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=15000, + host_port=15000, + protocol="tcp" + ), + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=15001, + host_port=15001, + protocol="tcp" + ), + + ], + ulimits=[ecs.CfnTaskDefinition.UlimitProperty( + hard_limit=15000, + name="nofile", + soft_limit=15000 + )], + environment=[ + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="APPMESH_RESOURCE_ARN", + value=core.Fn.sub("mesh/${MeshName}/virtualNode/${app_name}-vn", {"MeshName": core.Fn.import_value(f"{environment_name}:Mesh"),"app_name": "colorgateway" }) + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ENVOY_LOG_LEVEL", + value="DEBUG" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ENABLE_ENVOY_XRAY_TRACING", + value="1" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ENABLE_ENVOY_STATS_TAGS", + value="1" + ) + ], + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": f"{environment_name}-envoy" + } + + ), + health_check=ecs.CfnTaskDefinition.HealthCheckProperty( + command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], + interval=5, + retries=3, + timeout=2 + + ), + + + ), + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="xrayContainer", + # cpu=32, + image="amazon/aws-xray-daemon", + user="1337", + port_mappings=[ecs.CfnTaskDefinition.PortMappingProperty( + container_port=2000, + host_port=2000, + protocol="udp" + )], + memory_reservation=256, + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": f"colorteller={color}-xray" + } + ), + ) + ], + task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), + execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), + + + ) + core.CfnOutput( + self, "ColorGatewayTaskDefinitionArn", + value=colorGatewayTaskDefinition.attr_task_definition_arn, + export_name=f"{environment_name}:ColorGatewayTaskDefinitionArn" + ) + + + + def create_color_task_definition(self, color, environment_name): + return ecs.CfnTaskDefinition( + self, f"colorTellerTask-{color}", + # name=f"{environment_name}-ColorTeller-{color}", + family=f"{environment_name}-ColorTeller-{color}", + network_mode="awsvpc", + memory="512", + requires_compatibilities=["FARGATE"], + cpu="256", + proxy_configuration=ecs.CfnTaskDefinition.ProxyConfigurationProperty( + container_name="envoy", + type="APPMESH", + proxy_configuration_properties=[ + ecs.CfnTaskDefinition.KeyValuePairProperty( + + name="IgnoredUID", + value="1337" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ProxyIngressPort", + value="15000" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ProxyEgressPort", + value="15001" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="AppPorts", + value="9080" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="EgressIgnoredIPs", + value="169.254.170.2,169.254.169.254" + ) + + + ] + ), + container_definitions=[ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="app", + image=f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/colorteller:latest", + essential=True, + # cpu=128, + port_mappings=[ecs.CfnTaskDefinition.PortMappingProperty( + container_port=9080, + host_port=9080, + protocol="tcp" + )], + environment=[ + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="COLOR", + value=color + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="SERVER_PORT", + value="9080" + ), + # ecs.CfnTaskDefinition.KeyValuePairProperty( + # name="STAGE", + # value=F"{app_mesh_stage}" + # ), + + ], + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": f"colorteller={color}-app" + } + ), + depends_on=[ + ecs.CfnTaskDefinition.ContainerDependencyProperty( + container_name="envoy", + condition="HEALTHY" + ) + ] + + ), + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="envoy", + user="1337", + # cpu=128, + essential=True, + image=core.Fn.sub("840364872350.dkr.ecr.${Region}.amazonaws.com/aws-appmesh-envoy:v1.29.5.0-prod",{"Region": core.Aws.REGION}), + port_mappings=[ + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=9901, + host_port=9901, + protocol="tcp" + ), + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=15000, + host_port=15000, + protocol="tcp" + ), + ecs.CfnTaskDefinition.PortMappingProperty( + container_port=15001, + host_port=15001, + protocol="tcp" + ), + + ], + ulimits=[ecs.CfnTaskDefinition.UlimitProperty( + hard_limit=15000, + name="nofile", + soft_limit=15000 + )], + environment=[ + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="APPMESH_RESOURCE_ARN", + value=core.Fn.sub("mesh/${MeshName}/virtualNode/colorteller-${app_name}-vn", {"MeshName": core.Fn.import_value(f"{environment_name}:Mesh"),"app_name": color }) + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ENVOY_LOG_LEVEL", + value="DEBUG" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ENABLE_ENVOY_XRAY_TRACING", + value="1" + ), + ecs.CfnTaskDefinition.KeyValuePairProperty( + name="ENABLE_ENVOY_STATS_TAGS", + value="1" + ) + ], + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": f"{environment_name}-envoy" + } + + ), + health_check=ecs.CfnTaskDefinition.HealthCheckProperty( + command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], + interval=5, + retries=3, + timeout=2 + + ) + ), + ecs.CfnTaskDefinition.ContainerDefinitionProperty( + name="xrayContainer", + # cpu=32, + image="amazon/aws-xray-daemon", + user="1337", + port_mappings=[ecs.CfnTaskDefinition.PortMappingProperty( + container_port=2000, + host_port=2000, + protocol="udp" + )], + memory_reservation=256, + log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + log_driver="awslogs", + options={ + "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), + "awslogs-region": core.Aws.REGION, + "awslogs-stream-prefix": f"colorteller={color}-xray" + } + ), + ) + ], + + # log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( + # log_driver="awslogs", + # options={ + # "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServiceLogGroup"), + # "awslogs-region": core.Aws.REGION, + # "awslogs-stream-prefix": f"{environment_name}-colorTeller-app" + # } + # ), + task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), + execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), + ) + + # task_role_arn=core.Fn.import_value(f"{environment_name}:TaskRoleArn"), + # execution_role_arn=core.Fn.import_value(f"{environment_name}:ExecutionRoleArn"), + # tags={ + # "Name": "xrayContainer" + # }) + + + \ No newline at end of file From 60a437b6220f4d2bcf56184f992063e88dc10782 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Wed, 17 Jul 2024 15:42:59 -0500 Subject: [PATCH 02/24] Adding example for creating AppMesh resources with tasks in ECS Fargate --- python/appmesh-ecs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/appmesh-ecs/README.md b/python/appmesh-ecs/README.md index fb0f152262..61bb2aef41 100644 --- a/python/appmesh-ecs/README.md +++ b/python/appmesh-ecs/README.md @@ -2,7 +2,7 @@ # App Mesh + ECS Fargate This stack will create core infrastructure resources and then create ECS services and tasks along with -the appropriate app mesh resources. You can view the traces of the application in AWS X-Ray. +the appropriate app mesh resources. You can view the traces of the application in AWS X-Ray. This is based off of the ColorTeller CloudFormation Example. ![Detailed diagram](appmesh_diagram.png) From b15f796da9fd9b4d75700cad4b1810db60bfa2d2 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Wed, 17 Jul 2024 16:20:03 -0500 Subject: [PATCH 03/24] Adding example for creating AppMesh resources with tasks in ECS Fargate --- python/appmesh-ecs/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index eca805f7f0..ee535a7fec 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -20,6 +20,7 @@ appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack") + ecs_stack.add_dependency(vpc_stack) appmesh_stack.add_dependency(ecs_stack) appmesh_colorapp_stack.add_dependency(appmesh_stack) From ccbae568cac442062d3e175e4aa59ffcb41d7a1c Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Wed, 17 Jul 2024 16:23:11 -0500 Subject: [PATCH 04/24] Adding example for creating AppMesh resources with tasks in ECS Fargate --- python/appmesh-ecs/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index ee535a7fec..eca805f7f0 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -20,7 +20,6 @@ appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack") - ecs_stack.add_dependency(vpc_stack) appmesh_stack.add_dependency(ecs_stack) appmesh_colorapp_stack.add_dependency(appmesh_stack) From e2eba5cd47bd08f61443366b6c37dc2412fc7283 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Wed, 17 Jul 2024 16:33:15 -0500 Subject: [PATCH 05/24] Adding example for creating AppMesh resources with tasks in ECS Fargate, re-ordered some resources --- .../colorapp/ecs_colorapp_stack.py | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py index f3d34495a6..5f4c63c55d 100644 --- a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py +++ b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py @@ -228,6 +228,26 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: failure_threshold=1 ) ) + WebTargetGroup = elbv2.CfnTargetGroup( + self, "WebTargetGroup", + health_check_interval_seconds=6, + health_check_path="/ping", + health_check_protocol="HTTP", + health_check_timeout_seconds=5, + healthy_threshold_count=2, + target_type="ip", + unhealthy_threshold_count=2, + vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), + port=80, + protocol="HTTP", + target_group_attributes=[elbv2.CfnTargetGroup.TargetGroupAttributeProperty( + key="deregistration_delay.timeout_seconds", + value="120", + )], + + + tags=[{"key": "Name", "value": "WebTargetGroup"}], + ) ColorGatewayService = ecs.CfnService( self, "ColorGatewayService", cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), @@ -412,26 +432,7 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: ] ) - WebTargetGroup = elbv2.CfnTargetGroup( - self, "WebTargetGroup", - health_check_interval_seconds=6, - health_check_path="/ping", - health_check_protocol="HTTP", - health_check_timeout_seconds=5, - healthy_threshold_count=2, - target_type="ip", - unhealthy_threshold_count=2, - vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), - port=80, - protocol="HTTP", - target_group_attributes=[elbv2.CfnTargetGroup.TargetGroupAttributeProperty( - key="deregistration_delay.timeout_seconds", - value="120", - )], - - - tags=[{"key": "Name", "value": "WebTargetGroup"}], - ) + PublicLoadBalancerListener = elbv2.CfnListener( self, "PublicLoadBalancerListener", load_balancer_arn=PublicLoadBalancer.ref, From 9795dffef75b010cf9bb018cab1fd8f2b8ec4c55 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:16:02 -0500 Subject: [PATCH 06/24] Added appmesh-ecs to implmement CDK example of using Appmesh with ECS --- python/appmesh-ecs/.gitignore | 9 + python/appmesh-ecs/README.md | 15 +- python/appmesh-ecs/app.py | 21 +- python/appmesh-ecs/appmesh_diagram.png | Bin 82832 -> 0 bytes .../appmesh-ecs/colorapp/appmesh_colorapp.py | 368 +++------ .../colorapp/ecs_colorapp_stack.py | 487 +---------- .../container_images/colorteller/Dockerfile | 11 +- .../container_images/gateway/Dockerfile | 12 +- .../appmesh/appmesh_stack.py | 8 +- .../core_infrastructure/ecr/ecr_stack.py | 66 +- .../core_infrastructure/ecs/ecs_stack.py | 240 +++--- .../core_infrastructure/vpc/vpc_stack.py | 173 +--- python/appmesh-ecs/deploy_images.sh | 32 - .../color_app_task_definition_stack.py | 759 ++++++++++-------- python/appmesh-ecs/tests/__init__.py | 0 python/appmesh-ecs/tests/unit/__init__.py | 0 .../unit/test_new_cdk_appmesh_redo_stack.py | 15 + 17 files changed, 834 insertions(+), 1382 deletions(-) delete mode 100644 python/appmesh-ecs/appmesh_diagram.png delete mode 100755 python/appmesh-ecs/deploy_images.sh create mode 100644 python/appmesh-ecs/tests/__init__.py create mode 100644 python/appmesh-ecs/tests/unit/__init__.py create mode 100644 python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py diff --git a/python/appmesh-ecs/.gitignore b/python/appmesh-ecs/.gitignore index 37833f8beb..9572748ce1 100644 --- a/python/appmesh-ecs/.gitignore +++ b/python/appmesh-ecs/.gitignore @@ -8,3 +8,12 @@ __pycache__ # CDK asset staging directory .cdk.staging cdk.out +/colorapp/ecs_colorapp_stack.py +new_cdk_appmesh_redo/ +vpc/ +ecsstackbackup.py.back +xray-container.json +envoy-container.json +create-task-defs.sh +colorteller-base-task-def.json +colorgateway-base-task-def.json \ No newline at end of file diff --git a/python/appmesh-ecs/README.md b/python/appmesh-ecs/README.md index 61bb2aef41..6da0c4cb43 100644 --- a/python/appmesh-ecs/README.md +++ b/python/appmesh-ecs/README.md @@ -1,11 +1,7 @@ -# App Mesh + ECS Fargate - -This stack will create core infrastructure resources and then create ECS services and tasks along with -the appropriate app mesh resources. You can view the traces of the application in AWS X-Ray. This is based off of the ColorTeller CloudFormation Example. - -![Detailed diagram](appmesh_diagram.png) +# Welcome to your CDK Python project! +This is a blank project for CDK development with Python. The `cdk.json` file tells the CDK Toolkit how to execute your app. @@ -52,12 +48,7 @@ them to your `setup.py` file and rerun the `pip install -r requirements.txt` command. ## Deploying this sample -To deploy this sample you first need to create an ECR repository and upload the images. To do this you can run -`cdk deploy ECRStack && ./deploy_images` - -Then you can run -`cdk deploy --all` - +To deploy this sample, run `cdk deploy --all` ## Useful commands diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index eca805f7f0..dd2a23b0fc 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -2,29 +2,28 @@ import os import aws_cdk as cdk -from aws_cdk import App, Stack -from core_infrastructure.vpc.vpc_stack import VPCStack +# from core_infrastructure.vpc.vpc_stack import VPCStack from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack -from colorapp.ecs_colorapp_stack import ColorAppStack -from colorapp.appmesh_colorapp import ServiceMeshColorAppStack + from core_infrastructure.ecr.ecr_stack import ECRStack + +from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack -app = cdk.App() +from colorapp.appmesh_colorapp import ServiceMeshColorAppStack -vpc_stack =VPCStack(app, "VPCStack") +app = cdk.App() ecs_stack = ECSStack(app, "ECSClusterStack") appmesh_stack = AppMeshStack(app, "AppMeshStack") ecr_stack = ECRStack(app, "ECRStack") -color_app_stack = ColorAppStack(app, "ColorAppStack") appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") -colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack") +colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ + "account": os.environ["CDK_DEFAULT_ACCOUNT"], + "region": os.environ["CDK_DEFAULT_REGION"] +}) -ecs_stack.add_dependency(vpc_stack) appmesh_stack.add_dependency(ecs_stack) appmesh_colorapp_stack.add_dependency(appmesh_stack) colorapp_task_definition_stack.add_dependency(appmesh_colorapp_stack) -color_app_stack.add_dependency(colorapp_task_definition_stack) - app.synth() diff --git a/python/appmesh-ecs/appmesh_diagram.png b/python/appmesh-ecs/appmesh_diagram.png deleted file mode 100644 index 984e072f1060b63beb7beabd56ba69281c82b61f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82832 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 CJVK=a diff --git a/python/appmesh-ecs/colorapp/appmesh_colorapp.py b/python/appmesh-ecs/colorapp/appmesh_colorapp.py index 1396ab1301..3c503fad30 100644 --- a/python/appmesh-ecs/colorapp/appmesh_colorapp.py +++ b/python/appmesh-ecs/colorapp/appmesh_colorapp.py @@ -13,268 +13,158 @@ import aws_cdk.aws_appmesh as appmesh class ServiceMeshColorAppStack(Stack): + # This function creates the virtual nodes in app mesh that correspond to each color + def createVirtualNodes(stack: core.Stack, color: str, environment_name: str, mesh): + environment_name ="appmesh-env" + return appmesh.VirtualNode( + scope=stack, + id=f"VirtualNode-{color}", + mesh=mesh, + virtual_node_name=f"colorteller-{color}-vn", + + listeners=[ + appmesh.VirtualNodeListener.http( + port=9080, + health_check=appmesh.HealthCheck.http( + healthy_threshold=3, + unhealthy_threshold=2, + timeout=core.Duration.millis(2000), + interval=core.Duration.millis(5000), + path="/ping", + ), + )], + service_discovery=appmesh.ServiceDiscovery.dns( + hostname=core.Fn.sub( + "colorteller-${color}.${ServicesDomain}", + { + "color": str(color), + "ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace") + } + ) + + + ) + ) + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs ) environment_name ="appmesh-env" - # The following creates the appmesh virtual nodes that are associated with each color in the namespace in Cloud Map - ColorTellerBlackVirtualNode = appmesh.CfnVirtualNode( - self, "ColorTellerBlackVirtualNode", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_node_name="colorteller-black-vn", - spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( - service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( - dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( - hostname=core.Fn.sub("colorteller-black.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ) - ), - listeners=[ - appmesh.CfnVirtualNode.ListenerProperty( - port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( - port=9080, - protocol="http", - ), - health_check=appmesh.CfnVirtualNode.HealthCheckProperty( - healthy_threshold=2, - interval_millis=5000, - path="/ping", - port=9080, - protocol="http", - timeout_millis=2000, - unhealthy_threshold=2, - ) - ) - ], - ), + my_mesh = appmesh.Mesh.from_mesh_name(self, "Mesh", mesh_name=core.Fn.import_value(f"{environment_name}:Mesh")) + color_teller_colors = ["red", "black", "blue"] + virtual_nodes = [] + for color in color_teller_colors: + virtual_nodes.append(self.createVirtualNodes(color, environment_name, my_mesh)) + + ColorTellerVirtualRouter = appmesh.VirtualRouter( + self, "ColorTellerVirtualRouter", + mesh=my_mesh, + virtual_router_name="colorteller-vr", + listeners=[appmesh.VirtualRouterListener.http(port=9080)] + ) - ColorTellerBlueVirtualNode = appmesh.CfnVirtualNode( - self, "ColorTellerBlueVirtualNode", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_node_name="colorteller-blue-vn", - spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( - service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( - dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( - hostname=core.Fn.sub("colorteller-blue.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ) - ), - listeners=[ - appmesh.CfnVirtualNode.ListenerProperty( - port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( + + # Creates an app mesh route that distributes traffic across 3 targets when the gateway is hit + + ColorTellerVirtualRouter.add_route( + "MyRoute", + route_name="colorteller-route", + route_spec=appmesh.RouteSpec.http( + weighted_targets=[ + appmesh.WeightedTarget( + virtual_node=virtual_nodes[0], port=9080, - protocol="http", + weight=1 ), - health_check=appmesh.CfnVirtualNode.HealthCheckProperty( - healthy_threshold=2, - interval_millis=5000, - path="/ping", + appmesh.WeightedTarget( + virtual_node=virtual_nodes[1], port=9080, - protocol="http", - timeout_millis=2000, - unhealthy_threshold=2, - ) - ) - ] - ) - ) - ColorTellerRedVirtualNode = appmesh.CfnVirtualNode( - self, "ColorTellerRedVirtualNode", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_node_name="colorteller-red-vn", - spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( - service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( - dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( - hostname=core.Fn.sub("colorteller-red.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ) - ), - listeners=[ - appmesh.CfnVirtualNode.ListenerProperty( - port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( - port=9080, - protocol="http", + weight=1 ), - health_check=appmesh.CfnVirtualNode.HealthCheckProperty( - healthy_threshold=2, - interval_millis=5000, - path="/ping", + appmesh.WeightedTarget( + virtual_node=virtual_nodes[2], port=9080, - protocol="http", - timeout_millis=2000, - unhealthy_threshold=2, - ) + weight=1 ) - ] - ) - ) - ColorTellerWhiteVirtualNode = appmesh.CfnVirtualNode( - self, "ColorTellerWhiteVirtualNode", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_node_name="colorteller-white-vn", - spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( - service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( - dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( - hostname=core.Fn.sub("colorteller.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ], + match=appmesh.HttpRouteMatch( + path=appmesh.HttpRoutePathMatch.starts_with("/") ) - ), - listeners=[ - appmesh.CfnVirtualNode.ListenerProperty( - port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( - port=9080, - protocol="http", - ), - health_check=appmesh.CfnVirtualNode.HealthCheckProperty( - healthy_threshold=2, - interval_millis=5000, - path="/ping", - port=9080, - protocol="http", - timeout_millis=2000, - unhealthy_threshold=2, - ) - ) - ] ) ) - ColorTellerVirtualRouter = appmesh.CfnVirtualRouter( - self, "ColorTellerVirtualRouter", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_router_name="colorteller-vr", - spec=appmesh.CfnVirtualRouter.VirtualRouterSpecProperty( - listeners=[ - appmesh.CfnVirtualRouter.VirtualRouterListenerProperty( - port_mapping=appmesh.CfnVirtualRouter.PortMappingProperty( - port=9080, - protocol="http", - ) - ) - ] + + CollorTellerVirtualService = appmesh.VirtualService( + self, "ColorTellerService", + virtual_service_provider=appmesh.VirtualServiceProvider.virtual_router(virtual_router=ColorTellerVirtualRouter), + virtual_service_name=core.Fn.sub("colorteller.${ServicesDomain}" , {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), + + ) - ) - # Creates an app mesh route that distributes traffic across 3 targets when the gateway is hit - ColorTellerRoute = appmesh.CfnRoute( - self, "ColorTellerRoute", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_router_name="colorteller-vr", - route_name="colorteller-route", - spec=appmesh.CfnRoute.RouteSpecProperty( - http_route=appmesh.CfnRoute.HttpRouteProperty( - action=appmesh.CfnRoute.HttpRouteActionProperty( - weighted_targets=[ - appmesh.CfnRoute.WeightedTargetProperty( - virtual_node="colorteller-white-vn", - weight=1, - ), - appmesh.CfnRoute.WeightedTargetProperty( - virtual_node="colorteller-blue-vn", - weight=1, - ), - appmesh.CfnRoute.WeightedTargetProperty( - virtual_node="colorteller-red-vn", - weight=1, - ), + ColorTellerWhiteVirtualNode = appmesh.VirtualNode( + self, "ColorTellerWhiteVirtualNode", + mesh=my_mesh, + virtual_node_name=f"colorteller-white-vn", + + listeners=[ + appmesh.VirtualNodeListener.http( + port=9080, + health_check=appmesh.HealthCheck.http( + healthy_threshold=3, + unhealthy_threshold=2, + timeout=core.Duration.millis(2000), + interval=core.Duration.millis(5000), + path="/ping", + ), + )], + service_discovery=appmesh.ServiceDiscovery.dns( + hostname=core.Fn.sub("colorteller.${ServicesDomain}",{"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + ) - ], - ), - match=appmesh.CfnRoute.HttpRouteMatchProperty( - prefix="/", - ), - ), - ), + ) - ColorTellerRoute.node.add_dependency(ColorTellerVirtualRouter) - ColorTellerRoute.node.add_dependency(ColorTellerWhiteVirtualNode) - ColorTellerRoute.node.add_dependency(ColorTellerBlueVirtualNode) - ColorTellerRoute.node.add_dependency(ColorTellerRedVirtualNode) - # Creates a virtual service in app mesh that utilizes the virtual router - ColorTellerVirtualService = appmesh.CfnVirtualService( - self, "ColorTellerVirtualService", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_service_name=core.Fn.sub("colorteller.${ServicesDomain}" , {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), - spec=appmesh.CfnVirtualService.VirtualServiceSpecProperty( - provider=appmesh.CfnVirtualService.VirtualServiceProviderProperty( - virtual_router=appmesh.CfnVirtualService.VirtualRouterServiceProviderProperty( - virtual_router_name="colorteller-vr", - ), - ), - ), - ) - ColorTellerVirtualService.node.add_dependency(ColorTellerVirtualRouter) + + TcpEchoVirtualNode = appmesh.VirtualNode( + self, "TcpEchoVirtualNode", + mesh=my_mesh, + virtual_node_name=f"tcpecho-vn", - TcpEchoVirtualNode = appmesh.CfnVirtualNode( - self, "TcpEchoVirtualNode", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_node_name="tcpecho-vn", - spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( - service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( - dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( - hostname=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ) - ), listeners=[ - appmesh.CfnVirtualNode.ListenerProperty( - port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( - port=2701, - protocol="tcp", - ), - health_check=appmesh.CfnVirtualNode.HealthCheckProperty( + appmesh.VirtualNodeListener.tcp(port=2701), + appmesh.VirtualNodeListener.tcp( + health_check=appmesh.HealthCheck.tcp( healthy_threshold=2, - interval_millis=5000, - protocol="tcp", - timeout_millis=2000, unhealthy_threshold=2, - ) - ) - ] - ) - ) - TCPEchoVirtualService = appmesh.CfnVirtualService( - self, "TCPEchoVirtualService", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_service_name=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), - spec=appmesh.CfnVirtualService.VirtualServiceSpecProperty( - provider=appmesh.CfnVirtualService.VirtualServiceProviderProperty( - virtual_node=appmesh.CfnVirtualService.VirtualNodeServiceProviderProperty( - virtual_node_name="tcpecho-vn", - ), - ), - ), - ) - TCPEchoVirtualService.node.add_dependency(TcpEchoVirtualNode) - # Creating a virtual node for the color teller gateway - ColorGatewayVirtualNode = appmesh.CfnVirtualNode( - self, "ColorGatewayVirtualNode", - mesh_name=core.Fn.import_value(f"{environment_name}:Mesh"), - virtual_node_name="colorgateway-vn", - spec=appmesh.CfnVirtualNode.VirtualNodeSpecProperty( - service_discovery=appmesh.CfnVirtualNode.ServiceDiscoveryProperty( - dns=appmesh.CfnVirtualNode.DnsServiceDiscoveryProperty( - hostname=core.Fn.sub("colorgateway.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + timeout=core.Duration.millis(2000), + interval=core.Duration.millis(5000), + + ), ) + + ], + service_discovery=appmesh.ServiceDiscovery.dns( + hostname=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) ), - listeners=[ - appmesh.CfnVirtualNode.ListenerProperty( - port_mapping=appmesh.CfnVirtualNode.PortMappingProperty( - port=9080, - protocol="http", - ) - ) - ], - backends=[ - appmesh.CfnVirtualNode.BackendProperty( - virtual_service= - appmesh.CfnVirtualNode.VirtualServiceBackendProperty( - virtual_service_name=core.Fn.sub("colorteller.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), - ) - - - + + ) + TCPEchoVirtualService = appmesh.VirtualService( + self, "TCPEchoVirtualService", + virtual_service_provider=appmesh.VirtualServiceProvider.virtual_node(TcpEchoVirtualNode), + virtual_service_name=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), + + ) + + ColorGatewayVirtualNode = appmesh.VirtualNode( + self, "ColorGatewayVirtualNode", + mesh=my_mesh, + virtual_node_name=f"colorgateway-vn", + + listeners=[appmesh.VirtualNodeListener.http(port=9080)], + service_discovery=appmesh.ServiceDiscovery.dns( + hostname=core.Fn.sub("colorgateway.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) ), - appmesh.CfnVirtualNode.BackendProperty( - virtual_service= appmesh.CfnVirtualNode.VirtualServiceBackendProperty( - virtual_service_name=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), - ) - ) - ] - ) + + ) - \ No newline at end of file + ColorGatewayVirtualNode.add_backend(appmesh.Backend.virtual_service(CollorTellerVirtualService)) + ColorGatewayVirtualNode.add_backend(appmesh.Backend.virtual_service(TCPEchoVirtualService)) \ No newline at end of file diff --git a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py index 5f4c63c55d..0e0aff7bd6 100644 --- a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py +++ b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py @@ -10,461 +10,56 @@ import aws_cdk.aws_elasticloadbalancingv2 as elbv2 import aws_cdk.aws_ec2 as ec2 class ColorAppStack(Stack): - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) + def create_color_discovery_record(self, color, namespace): environment_name ="appmesh-env" - ColorTellerWhiteServiceDiscoveryRecord = servicediscovery.CfnService( - self, "ColorTellerWhiteServiceDiscovery", - description="Service discovery for the white colorteller", - tags=[{"key": "Name", "value": "ColorTellerWhite"}], - name="colorteller", - health_check_custom_config= - servicediscovery.CfnService.HealthCheckCustomConfigProperty( - failure_threshold=1 - ), - dns_config=servicediscovery.CfnService.DnsConfigProperty( - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - # routing_policy="MULTIVALUE", - - dns_records=[ - servicediscovery.CfnService.DnsRecordProperty( - type="A", - ttl=300 - ) - ], - - ) - ) - # Create a service for Color teller white in ECS - ColorTellerWhiteService = ecs.CfnService( - self, "ColorTellerWhiteService", - cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name="colorteller-white-service", - service_registries=[ - ecs.CfnService.ServiceRegistryProperty( - registry_arn=ColorTellerWhiteServiceDiscoveryRecord.attr_arn - ) - ], - desired_count=1, - launch_type="FARGATE", - deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( - maximum_percent=200, - minimum_healthy_percent=100 - ), - task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-white"), - tags=[{"key": "Name", "value": "ColorTellerWhite"}], - network_configuration=ecs.CfnService.NetworkConfigurationProperty( - awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( - subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], - assign_public_ip="DISABLED", - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] - ) - ) - ) - # Create a service discovery record for the color teller blue service - ColorTellerBlueServiceDiscoveryRecord = servicediscovery.CfnService( - self, "ColorTellerBlueServiceDiscovery", - description="Service discovery for the blue colorteller", - tags=[{"key": "Name", "value": "ColorTellerBlue"}], - name="colorteller-blue", - dns_config=servicediscovery.CfnService.DnsConfigProperty( - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - # routing_policy="MULTIVALUE", - - dns_records=[ - servicediscovery.CfnService.DnsRecordProperty( - type="A", - ttl=300 - ) - ], - - ), - health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( - failure_threshold=1 - ) - ) - # Create a service for the color teller blue service - ColorTellerBlueService = ecs.CfnService( - self, "ColorTellerBlueService", - cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name="colorteller-blue-service", - service_registries=[ - ecs.CfnService.ServiceRegistryProperty( - registry_arn=ColorTellerBlueServiceDiscoveryRecord.attr_arn - ) - ], - desired_count=1, - launch_type="FARGATE", - deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( - maximum_percent=200, - minimum_healthy_percent=100 - ), - task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-blue"), - tags=[{"key": "Name", "value": "ColorTellerBlue"}], - network_configuration=ecs.CfnService.NetworkConfigurationProperty( - awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( - subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], - assign_public_ip="DISABLED", - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] - ) - ) - ) - # Create a service record for the color teller red service - ColorTellerRedServiceDiscoveryRecord = servicediscovery.CfnService( - self, "ColorTellerRedServiceDiscovery", - description="Service discovery for the red colorteller", - tags=[{"key": "Name", "value": "ColorTellerRed"}], - name="colorteller-red", - dns_config=servicediscovery.CfnService.DnsConfigProperty( - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - # routing_policy="MULTIVALUE", - - dns_records=[ - servicediscovery.CfnService.DnsRecordProperty( - type="A", - ttl=300 - ) - ], - - ), - health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( - failure_threshold=1 - ) - ) - # Create a service for the color teller red service - ColorTellerRedService = ecs.CfnService( - self, "ColorTellerRedService", - cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name="colorteller-red-service", - service_registries=[ - ecs.CfnService.ServiceRegistryProperty( - registry_arn=ColorTellerRedServiceDiscoveryRecord.attr_arn - ) - ], - desired_count=1, - launch_type="FARGATE", - deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( - maximum_percent=200, - minimum_healthy_percent=100 - ), - task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-red"), - tags=[{"key": "Name", "value": "ColorTellerRed"}], - network_configuration=ecs.CfnService.NetworkConfigurationProperty( - awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( - subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], - assign_public_ip="DISABLED", - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] - ) - ) - ) - # Create a service discovery record for the color teller black service - ColorTellerBlackServiceDiscoveryRecord = servicediscovery.CfnService( - self, "ColorTellerBlackServiceDiscovery", - description="Service discovery for the black colorteller", - tags=[{"key": "Name", "value": "ColorTellerBlack"}], - name="colorteller-black", - dns_config=servicediscovery.CfnService.DnsConfigProperty( - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - # routing_policy="MULTIVALUE", + return servicediscovery.Service( + self, f"ColorTeller{color}ServiceDiscoveryRecord", + name=f"colorteller-{color}-service", + namespace=namespace, + dns_record_type=servicediscovery.DnsRecordType.A, + dns_ttl=core.Duration.seconds(30), + custom_health_check=servicediscovery.HealthCheckCustomConfig(failure_threshold=1) - dns_records=[ - servicediscovery.CfnService.DnsRecordProperty( - type="A", - ttl=300 - ) - ], - ), - health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( - failure_threshold=1 - ) ) - # Create a service for the color teller black service - ColorTellerBlackService = ecs.CfnService( - self, "ColorTellerBlackService", + def create_color_service(self, color, dr): + environment_name ="appmesh-env" + task_definition_arn = core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-{color}") + task_definition = ecs.TaskDefinition.from_task_definition_arn(self, f"ColorTeller{color}TaskDefinition", task_definition_arn=task_definition_arn) + return ecs.FargateService( + self, f"ColorTeller{color}Service", cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name="colorteller-black-service", - service_registries=[ - ecs.CfnService.ServiceRegistryProperty( - registry_arn=ColorTellerBlackServiceDiscoveryRecord.attr_arn - ) - ], - deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( - maximum_percent=200, - minimum_healthy_percent=100 - ), + service_name=f'colorteller-{color}-service', desired_count=1, - launch_type="FARGATE", - task_definition=core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-black"), - tags=[{"key": "Name", "value": "ColorTellerBlack"}], - network_configuration=ecs.CfnService.NetworkConfigurationProperty( - awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( - subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], - assign_public_ip="DISABLED", - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] - ) - ) + max_healthy_percent=200, + min_healthy_percent=100, + task_definition=task_definition, + vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), + assign_public_ip=False, + security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")], + cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=dr) ) - # Create a service discovery record for the color teller blue service - ColorGatewayServiceDiscoveryRecord = servicediscovery.CfnService( - self, "ColorGatewayServiceDiscovery", - description="Service discovery for the gateway colorteller", - tags=[{"key": "Name", "value": "ColorGateway"}], - name="colorgateway", - dns_config=servicediscovery.CfnService.DnsConfigProperty( - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - # routing_policy="MULTIVALUE", - - dns_records=[ - servicediscovery.CfnService.DnsRecordProperty( - type="A", - ttl=300 - ) - ], + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + environment_name ="appmesh-env" + colors = ["blue", "red", "black"] + namespace = servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes(self, "Namespace", + namespace_arn=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceARN"), + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + namespace_name=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceName")) + for color in colors: + dr = self.create_color_discovery_record(color, namespace) - ), - health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( - failure_threshold=1 - ) - ) - WebTargetGroup = elbv2.CfnTargetGroup( - self, "WebTargetGroup", - health_check_interval_seconds=6, - health_check_path="/ping", - health_check_protocol="HTTP", - health_check_timeout_seconds=5, - healthy_threshold_count=2, - target_type="ip", - unhealthy_threshold_count=2, - vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), - port=80, - protocol="HTTP", - target_group_attributes=[elbv2.CfnTargetGroup.TargetGroupAttributeProperty( - key="deregistration_delay.timeout_seconds", - value="120", - )], - - - tags=[{"key": "Name", "value": "WebTargetGroup"}], - ) - ColorGatewayService = ecs.CfnService( - self, "ColorGatewayService", - cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name="colorteller-gateway-service", - service_registries=[ - ecs.CfnService.ServiceRegistryProperty( - registry_arn=ColorGatewayServiceDiscoveryRecord.attr_arn - ) - ], - desired_count=1, - launch_type="FARGATE", - task_definition=core.Fn.import_value(f"{environment_name}:ColorGatewayTaskDefinitionArn"), - tags=[{"key": "Name", "value": "ColorGateway"}], - deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( - maximum_percent=200, - minimum_healthy_percent=100 - ), - network_configuration=ecs.CfnService.NetworkConfigurationProperty( - awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( - subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], - assign_public_ip="DISABLED", - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] - ) - ), - load_balancers=[ - ecs.CfnService.LoadBalancerProperty( - container_name="app", - container_port=9080, - target_group_arn=WebTargetGroup.attr_target_group_arn - - ) - ] - ) - + service = self.create_color_service(color, dr) - TesterTaskDefinition = ecs.CfnTaskDefinition( - self, "TesterTaskDefinition", - family="tester", - cpu="256", - memory="512", - network_mode="awsvpc", - task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), - execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), - container_definitions=[ - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="app", - image="tstrohmeier/alpine-infinite-curl", - memory_reservation=512, - essential=True, - command=[ - core.Fn.sub("-h http://colorgateway.${ECSServicesDomain}:9080/color", {"ECSServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")} ) - - ], - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": "tester-app" - } - ), - ) - ] - ) - - TcpEchoServiceDiscoveryRecord = servicediscovery.CfnService( - self, "TcpEchoServiceDiscovery", - name="tcpecho", - description="Service discovery for the tcp echo colorteller", - tags=[{"key": "Name", "value": "TcpEcho"}], - dns_config=servicediscovery.CfnService.DnsConfigProperty( - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - # routing_policy="MULTIVALUE", - - dns_records=[ - servicediscovery.CfnService.DnsRecordProperty( - type="A", - ttl=300 - ) - ], - - ), - health_check_custom_config=servicediscovery.CfnService.HealthCheckCustomConfigProperty( - failure_threshold=1 - ) - ) - TcpEchoTaskDefinition = ecs.CfnTaskDefinition( - self, "TcpEchoTaskDefinition", - family="tcpecho", - memory="512", - requires_compatibilities=["FARGATE"], - cpu="256", - network_mode="awsvpc", - task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), - execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), - container_definitions=[ - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="app", - image="cjimti/go-echo", - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": "tcpecho-app" - } - ), - port_mappings=[ - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=2701, - host_port=2701, - protocol="tcp" - ) - ], - environment=[ecs.CfnTaskDefinition.KeyValuePairProperty( - name="TCP_PORT", - value="2701" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="NODE_NAME", - value=core.Fn.sub( - "mesh/${AppMeshMeshName}/virtualNode/tcpecho-vn", - {"AppMeshMeshName": f"{environment_name}:Mesh"} - ) - ), - ], - essential=True, - - - ) - ] - ) - TcpEchoService = ecs.CfnService( - self, "TcpEchoService", - cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name="tcp-echo-service", - service_registries=[ - ecs.CfnService.ServiceRegistryProperty( - registry_arn=TcpEchoServiceDiscoveryRecord.attr_arn - ) - ], - desired_count=1, - launch_type="FARGATE", - task_definition=TcpEchoTaskDefinition.attr_task_definition_arn, - tags=[{"key": "Name", "value": "TcpEcho"}], - deployment_configuration=ecs.CfnService.DeploymentConfigurationProperty( - maximum_percent=200, - minimum_healthy_percent=100 - ), - network_configuration=ecs.CfnService.NetworkConfigurationProperty( - awsvpc_configuration=ecs.CfnService.AwsVpcConfigurationProperty( - subnets=[core.Fn.import_value(f"{environment_name}:PrivateSubnet1"), core.Fn.import_value(f"{environment_name}:PrivateSubnet2")], - assign_public_ip="DISABLED", - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")] - ) - ) - ) - # The following code creates the front-facing load balancer infrastructure - PublicLoadBalancerSG = ec2.CfnSecurityGroup( - self, "PublicLoadBalancerSG", - vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), - group_description="Access to the public facing load balancer", - security_group_ingress=[ec2.CfnSecurityGroup.IngressProperty( - cidr_ip="0.0.0.0/0", - ip_protocol="tcp", - from_port=80, - to_port=80 - )] - ) - PublicLoadBalancer = elbv2.CfnLoadBalancer( - self, "PublicLoadBalancer", - scheme="internet-facing", - security_groups=[PublicLoadBalancerSG.ref], - subnets=[core.Fn.import_value(f"{environment_name}:PublicSubnet1"), core.Fn.import_value(f"{environment_name}:PublicSubnet2")], - tags=[{"key": "Name", "value": "PublicLoadBalancer"}], - load_balancer_attributes=[elbv2.CfnLoadBalancer.LoadBalancerAttributeProperty( - key="idle_timeout.timeout_seconds", - value="30" - + ColorTellerWhiteServiceDiscoveryRecord = servicediscovery.Service( + self, "ColorTellerWhiteServiceDiscoveryRecord", + name="colorteller", + namespace=namespace, + dns_record_type=servicediscovery.DnsRecordType.A, + dns_ttl=core.Duration.seconds(30), + custom_health_check=servicediscovery.HealthCheckCustomConfig(failure_threshold=1) - ) - ] - - ) - PublicLoadBalancerListener = elbv2.CfnListener( - self, "PublicLoadBalancerListener", - load_balancer_arn=PublicLoadBalancer.ref, - port=80, - protocol="HTTP", - default_actions=[elbv2.CfnListener.ActionProperty( - type="forward", - target_group_arn=WebTargetGroup.attr_target_group_arn - )], - - ) - WebLoadBalancerRule = elbv2.CfnListenerRule( - self, "WebLoadBalancerRule", - listener_arn=PublicLoadBalancerListener.ref, - actions=[elbv2.CfnListenerRule.ActionProperty( - type="forward", - target_group_arn=WebTargetGroup.ref - )], - conditions=[elbv2.CfnListenerRule.RuleConditionProperty( - field="path-pattern", - values=["*"] - )], - priority=1, ) - PublicLoadBalancerListener.node.add_dependency(PublicLoadBalancerListener) - ColorGatewayService.node.add_dependency(WebLoadBalancerRule) - # Returns the public endpoint for the color app service to cURL against - core.CfnOutput( - self, "ColorAppEndpoint", - description="Public endpoint for Color App Service", - value=core.Fn.join("", ["http://", PublicLoadBalancer.attr_dns_name]), - export_name="ColorAppEndpoint" - ) - - + colorColorTellerWhiteService = self.create_color_service('white', ColorTellerWhiteServiceDiscoveryRecord) diff --git a/python/appmesh-ecs/container_images/colorteller/Dockerfile b/python/appmesh-ecs/container_images/colorteller/Dockerfile index 2bae9d4bfc..73b77cbdb5 100644 --- a/python/appmesh-ecs/container_images/colorteller/Dockerfile +++ b/python/appmesh-ecs/container_images/colorteller/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/amazonlinux/amazonlinux:2 AS builder +FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 AS builder RUN yum update -y && \ yum install -y ca-certificates unzip tar gzip git && \ yum clean all && \ @@ -11,7 +11,8 @@ ENV PATH="${PATH}:/usr/local/go/bin" ENV GOPATH="${HOME}/go" ENV PATH="${PATH}:${GOPATH}/bin" -ARG GO_PROXY=https://proxy.golang.org +# ARG GO_PROXY=https://proxy.golang.org +ARG GO_PROXY=direct WORKDIR /go/src/github.com/aws/aws-app-mesh-examples/colorapp/teller # go.mod and go.sum go into their own layers. @@ -19,14 +20,16 @@ COPY go.mod . COPY go.sum . # Set the proxies for the go compiler -RUN go env -w GOPROXY=${GO_PROXY} +# RUN go env -w GOPROXY=${GO_PROXY} -- This line is commented out in favor of GOPRIVATE, but if GOPROXY works on your machine you can use this line instead. +RUN go env -w GOPRIVATE=* + # This ensures `go mod download` happens only when go.mod and go.sum change. RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /aws-app-mesh-examples-colorapp-teller . -FROM public.ecr.aws/amazonlinux/amazonlinux:2 +FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 RUN yum update -y && \ yum install -y ca-certificates && \ yum clean all && \ diff --git a/python/appmesh-ecs/container_images/gateway/Dockerfile b/python/appmesh-ecs/container_images/gateway/Dockerfile index 30b6e83e87..ea3fb95be4 100644 --- a/python/appmesh-ecs/container_images/gateway/Dockerfile +++ b/python/appmesh-ecs/container_images/gateway/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/amazonlinux/amazonlinux:2 AS builder +FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 AS builder RUN yum update -y && \ yum install -y ca-certificates unzip tar gzip git && \ yum clean all && \ @@ -11,7 +11,9 @@ ENV PATH="${PATH}:/usr/local/go/bin" ENV GOPATH="${HOME}/go" ENV PATH="${PATH}:${GOPATH}/bin" -ARG GO_PROXY=https://proxy.golang.org +# ARG GO_PROXY=https://proxy.golang.org +ARG GO_PROXY=direct + WORKDIR /go/src/github.com/aws/aws-app-mesh-examples/colorapp/gateway # go.mod and go.sum go into their own layers. @@ -19,14 +21,16 @@ COPY go.mod . COPY go.sum . # Set the proxies for the go compiler -RUN go env -w GOPROXY=${GO_PROXY} +# RUN go env -w GOPROXY=${GO_PROXY} -- This line is commented out in favor of GOPRIVATE, but if GOPROXY works on your machine you can use this line instead. +RUN go env -w GOPRIVATE=* # This ensures `go mod download` happens only when go.mod and go.sum change. RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /aws-app-mesh-examples-colorapp-gateway . -FROM public.ecr.aws/amazonlinux/amazonlinux:2 +# Because this was built on an M1 Mac, you may be able to remove the --platform flag +FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 RUN yum update -y && \ yum install -y ca-certificates && \ yum clean all && \ diff --git a/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py b/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py index 8c50a664d4..693ae16836 100644 --- a/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py +++ b/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py @@ -12,13 +12,11 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs ) environment_name ="appmesh-env" # Creates an AWS App Mesh mesh - mesh = appmesh.CfnMesh(self, "ecs-mesh", - mesh_name=environment_name - - ) + mesh = appmesh.Mesh(self, "ecs-mesh", mesh_name=environment_name) + core.CfnOutput( self, "MeshName", - value=mesh.attr_mesh_name, + value=mesh.mesh_name, description="A reference to the AppMesh Meshs", export_name=f"{environment_name}:Mesh" ) diff --git a/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py b/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py index e00ee77f18..f19aab90a7 100644 --- a/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py +++ b/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py @@ -3,6 +3,7 @@ aws_appmesh as appmesh, ) +import os import aws_cdk as core from aws_cdk import Stack, Tags, App from constructs import Construct @@ -11,6 +12,8 @@ import aws_cdk.aws_elasticloadbalancingv2 as elbv2 import aws_cdk.aws_ec2 as ec2 import aws_cdk.aws_ecr as ecr +from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform +import cdk_ecr_deployment as ecrdeploy import subprocess class ECRStack(Stack): @@ -18,34 +21,59 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs ) environment_name ="appmesh-env" # Creates two ecr repositories that will host the docker images for the color teller gateway app and color teller app - ColorAppGatewayRepository = ecr.CfnRepository( - self, "gateway", - repository_name="gateway", - tags=[{ - "key": "Name", - "value": "gateway" - }] - ) - ColorAppColorTellerRepository = ecr.CfnRepository( - self, "colorteller", - repository_name="colorteller", - tags=[{ - "key": "Name", - "value": "colorteller" - }] + ColorAppGatewayRepository = ecr.Repository(self, "GatewayRepository", repository_name="gateway") + ColorAppColorTellerRepository = ecr.Repository(self, "ColorTellerRepository", repository_name="colorteller") + + # The docker images were built on a M1 Macbook Pro, you may have to rebuild your images + gatewayAsset = DockerImageAsset(self, "gatewayAsset", + directory="./container_images/gateway", + build_args={ + "GOPROXY": 'direct' + }, + platform=Platform.LINUX_AMD64 + ) + colortellerAsset = DockerImageAsset(self, "colortellerAsset", + directory="./container_images/colorteller", + build_args={ + "GOPROXY": 'direct' + } + + ) + ecrdeploy.ECRDeployment(self, "DeployGatewayImage", + src=ecrdeploy.DockerImageName(gatewayAsset.image_uri), + dest=ecrdeploy.DockerImageName(f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/gateway:latest") + ) + + + + ecrdeploy.ECRDeployment(self, "DeployColorTellerImage", + src=ecrdeploy.DockerImageName(colortellerAsset.image_uri), + dest=ecrdeploy.DockerImageName(f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/colorteller:latest") + ) core.CfnOutput( self, "ColorAppGatewayRepository", - value=ColorAppGatewayRepository.attr_repository_uri, + value=ColorAppGatewayRepository.repository_uri, description="ColorAppRepository", export_name=f"{environment_name}:ColorAppRepository" ) + core.CfnOutput( + self, "ColorAppGatewayRepositoryName", + value=ColorAppGatewayRepository.repository_name, + description="ColorAppGatewayRepository", + export_name=f"{environment_name}:ColorAppGatewayRepository" + ) core.CfnOutput( self, "ColorAppColorTellerRepository", - value=ColorAppColorTellerRepository.attr_repository_uri, + value=ColorAppColorTellerRepository.repository_uri, description="ColorAppColorTellerRepository", export_name=f"{environment_name}:ColorAppColorTellerRepository" ) - - \ No newline at end of file + core.CfnOutput( + self, "ColorAppColorTellerRepositoryName", + value=ColorAppColorTellerRepository.repository_name, + description="ColorAppColorTellerRepository", + export_name=f"{environment_name}:ColorAppColorTellerRepositoryName" + ) + diff --git a/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py b/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py index ece56cb63d..f7a9f52f3d 100644 --- a/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py +++ b/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py @@ -13,150 +13,162 @@ class ECSStack(Stack): def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs ) environment_name ="appmesh-env" + vpc = ec2.Vpc(self, "AppMeshVPC", + ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"), + create_internet_gateway=True, + max_azs=2, + nat_gateways=2, + enable_dns_hostnames=True, + enable_dns_support=True, + vpc_name="App-Mesh-VPC", + subnet_configuration=[ + ec2.SubnetConfiguration( + subnet_type=ec2.SubnetType.PUBLIC, + name="Public", + cidr_mask=24 + ), + ec2.SubnetConfiguration( + subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, + name="Private", + cidr_mask=24 + ) + ] + ) + AWSRegion=core.Stack.of(self).region AWSStackId=core.Stack.of(self).stack_id # The following creates the ECS Cluster along with its security groups. It also creates the proper roles that the ECS tasks will assume # as well as the log group that the tasks log to and the service discovery namespace (created under AWS Cloud Map in the console) - ecsCluster = ecs.CfnCluster( - self, "ecsCluster", - cluster_name="App-Mesh-ECS-Cluster", - capacity_providers=["FARGATE"], - default_capacity_provider_strategy=[{"capacityProvider": "FARGATE", "weight": 1, "base": 1}], - ) - ecsSGG = ec2.CfnSecurityGroup( - self, "ECS_SG", - group_description="Security Group for ECS instances", - vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), - group_name="ecs-sg", - security_group_ingress=[ - ec2.CfnSecurityGroup.IngressProperty( - cidr_ip=core.Fn.import_value(f"{environment_name}:VpcCidr"), - ip_protocol="-1" - # ip protocol -1? not sure - # ip_protocol="tcp", - # from_port=8080, - # to_port=8080 - ) - ], - - ) - ECSServiceSecurityGroup = ec2.CfnSecurityGroup( - self, "ECSSecurityGroup", - group_name="ecs-service-sg", - group_description="Security group for ECS service", - vpc_id=core.Fn.import_value(f"{environment_name}:VPCID"), - security_group_ingress=[ - ec2.CfnSecurityGroup.IngressProperty( - cidr_ip=core.Fn.import_value(f"{environment_name}:VpcCidr"), - - ip_protocol="-1", - # from_port=8080, - # to_port=8080 - ) - ] - ) - ECSTaskIamRole = iam.CfnRole( - self, "ECSTaskIamRole", - role_name="TaskIamRole", - assume_role_policy_document={ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ - "ecs-tasks.amazonaws.com" - ] - }, - "Action": "sts:AssumeRole" - } - ] - }, - managed_policy_arns=[ - "arn:aws:iam::aws:policy/CloudWatchFullAccess", - "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess", - "arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess" - ] - - ) - TaskExecutionIamRole = iam.CfnRole( - self, "TaskExecutionIamRole", - role_name="ecs-task-execution-role", - assume_role_policy_document={ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Service": [ - "ecs-tasks.amazonaws.com" - ] - }, - "Action": "sts:AssumeRole" - } - ] - }, - managed_policy_arns=[ - "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly", - "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" - ] - ) - ECSServicelogGroup = logs.CfnLogGroup( - self, "ECSServiceLogGroup", - log_group_name=f"{ecsCluster.cluster_name}-service", - retention_in_days=7 - ) - ECSServiceDiscoveryNamespace = servicediscovery.CfnPrivateDnsNamespace( - self, "ServiceDiscoveryNamespace", - name="appmesh.local", - vpc=core.Fn.import_value(f"{environment_name}:VPCID") - ) - + ecsCluster = ecs.Cluster(self, "ecsCluster", + cluster_name="App-Mesh-ECS-Cluster", + vpc=vpc + + ) + + ECSServiceDiscoveryNamespace = servicediscovery.PrivateDnsNamespace(self, "ServiceDiscoveryNamespace", + name="appmesh.local", + vpc=vpc + ) + ECSServiceLogGroup = logs.LogGroup(self, "ECSServiceLogGroup", + log_group_name=f"{ecsCluster.cluster_name}-service", + removal_policy=core.RemovalPolicy.DESTROY, + retention=logs.RetentionDays.FIVE_DAYS, + ) + ECSTaskIamRole = iam.Role(self, "ECSTaskIamRole", + assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + managed_policies=[ + iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchFullAccess"), + iam.ManagedPolicy.from_aws_managed_policy_name("AWSAppMeshEnvoyAccess"), + iam.ManagedPolicy.from_aws_managed_policy_name("AWSXRayDaemonWriteAccess"), + ], + ) + TaskExecutionRole = iam.Role(self, "TaskexecutionRole", + assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + managed_policies=[ + iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly"), + iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchLogsFullAccess"), + ], + ) + ECSSecurityGroup = ec2.SecurityGroup(self, "ECSSecurityGroup", + vpc=vpc, + description="ECS Security Group", + allow_all_outbound=True, + ) + ECSSecurityGroup.add_ingress_rule(peer=ec2.Peer.ipv4(vpc.vpc_cidr_block), connection=ec2.Port.all_traffic(), description="allow any from LAN") + vpc_id_ssm_store = ssm.StringParameter(self, "vpcid", parameter_name="vpc_id", string_value=vpc.vpc_id) core.CfnOutput( self, "ECSCluster", - value=ecsCluster.ref, + value=ecsCluster.cluster_name, export_name=f"{environment_name}:ECSCluster" ) + core.CfnOutput( + self, "ECSClusterARN", + value=ecsCluster.cluster_arn, + export_name=f"{environment_name}:ECSClusterARN" + ) core.CfnOutput( self, "ECSServiceDiscoveryNamespace", - value=ECSServiceDiscoveryNamespace.name, + value=ECSServiceDiscoveryNamespace.namespace_name, export_name=f"{environment_name}:ECSServiceDiscoveryNamespace" ) + core.CfnOutput( + self, "ECSServiceDiscoveryNamespaceARN", + value=ECSServiceDiscoveryNamespace.namespace_arn, + export_name=f"{environment_name}:ECSServiceDiscoveryNamespaceARN" + ) core.CfnOutput( self, "ECSServiceDiscoveryId", - value=ECSServiceDiscoveryNamespace.attr_id, + value=ECSServiceDiscoveryNamespace.namespace_id, export_name=f"{environment_name}:ECSServiceDiscoveryNamespaceID" ) core.CfnOutput( self, "ECSServicelogGroup", - value=ECSServicelogGroup.ref, - export_name=f"{environment_name}:ECSServicelogGroup" + value=ECSServiceLogGroup.log_group_name, + export_name=f"{environment_name}:ECSServicelogGroupName" + ) + core.CfnOutput( + self, "ECSServicelogGroupARN", + value=ECSServiceLogGroup.log_group_arn, + export_name=f"{environment_name}:ECSServicelogGroupARN" ) core.CfnOutput(self, "ECSServiceSecurityGroup", - value=ECSServiceSecurityGroup.ref, + value=ECSSecurityGroup.security_group_id, export_name=f"{environment_name}:ECSServiceSecurityGroup") core.CfnOutput( self, "Task_Execution_Iam_Role", - value=TaskExecutionIamRole.attr_arn, + value=TaskExecutionRole.role_arn, export_name=f"{environment_name}:TaskExecutionIamRole" ) core.CfnOutput( self, "ECS_Task_Iam_Role", - value=ECSTaskIamRole.attr_arn, + value=ECSTaskIamRole.role_arn, export_name=f"{environment_name}:ECSTaskIamRole" ) - - # core.CfnOutput( - # self, "BastionHostId", - # value=BastionHost.ref, - # export_name=f"{environment_name}:BastionHostId" - # ) - # core.CfnOutput( - # self, "BastionHostPublicIp", - # value=BastionHost.attr_public_ip, - # export_name=f"{environment_name}:BastionHostPublicIp" - # ) \ No newline at end of file + core.CfnOutput( + self, "VPCID", + value=vpc.vpc_id, + export_name=f"{environment_name}:VPCID" + ) + core.CfnOutput( + self, "VpcAvailabilityZones", + value=core.Fn.join(',', vpc.availability_zones), + export_name=f"{environment_name}:VpcAvailabilityZones" + ) + private_subnet_ids = [subnet.subnet_id for subnet in vpc.private_subnets] + core.CfnOutput(self, "PrivateSubnetIds", + value=core.Fn.join(',', private_subnet_ids), + export_name=f"{environment_name}:MyPrivateSubnetIds" + ) + public_subnet_ids = [subnet.subnet_id for subnet in vpc.public_subnets] + core.CfnOutput(self, "PublicSubnetIds", + value=core.Fn.join(',', public_subnet_ids), + export_name=f"{environment_name}:MyPublicSubnetIds" + ) + core.CfnOutput( + self, "Public Subnet 1", + value=vpc.public_subnets[0].subnet_id, + export_name=f"{environment_name}:PublicSubnet1" + ) + core.CfnOutput( + self, "Public Subnet 2", + value=vpc.public_subnets[1].subnet_id, + export_name=f"{environment_name}:PublicSubnet2" + ) + core.CfnOutput( + self, "Private Subnet 1", + value=vpc.private_subnets[0].subnet_id, + export_name=f"{environment_name}:PrivateSubnet1" + ) + core.CfnOutput( + self, "Private Subnet 2", + value=vpc.private_subnets[1].subnet_id, + export_name=f"{environment_name}:PrivateSubnet2" + ) + core.CfnOutput(self, "VpcCidr", + value=vpc.vpc_cidr_block, + export_name=f"{environment_name}:VpcCidr" + ) \ No newline at end of file diff --git a/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py b/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py index b2b46e47b2..c2940b3733 100644 --- a/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py +++ b/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py @@ -1,5 +1,6 @@ import aws_cdk.aws_ec2 as ec2 import aws_cdk.aws_s3 as s3 +import aws_cdk.aws_ssm as ssm from aws_cdk import Stack, Tags, App from constructs import Construct import aws_cdk as core @@ -10,168 +11,56 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: environment_name ="appmesh-env" # The following code creates the core VPC infrastructure with two public subnets and two private subnets, as well as route tables # that route private subnets to the internet using NAT gateways - vpc = ec2.CfnVPC(self, "appmesh-vpc", - cidr_block="192.168.0.0/16", - enable_dns_support=True, - enable_dns_hostnames=True, - tags=[{ - "key": "Name", - "value": "appmesh-vpc" - }] - ) - public_subnet_1 = ec2.CfnSubnet(self, "PublicSubnet1", - cidr_block="192.168.1.0/24", - vpc_id=vpc.ref, - availability_zone=f"{core.Aws.REGION}a", - map_public_ip_on_launch=True, - tags=[{ - "key": "Name", - "value": "PublicSubnet1" - }] - ) - public_subnet_2 = ec2.CfnSubnet(self, "PublicSubnet2", - cidr_block="192.168.2.0/24", - vpc_id=vpc.ref, - availability_zone=f"{core.Aws.REGION}b", - map_public_ip_on_launch=True, - tags=[{ - "key": "Name", - "value": "PublicSubnet2" - }] - ) - private_subnet_1 = ec2.CfnSubnet(self, "PrivateSubnet1", - cidr_block="192.168.3.0/24", - vpc_id=vpc.ref, - availability_zone=f"{core.Aws.REGION}a", - tags=[{ - "key": "Name", - "value": "PrivateSubnet1" - }] - ) - - private_subnet_2 = ec2.CfnSubnet(self, "PrivateSubnet2", - cidr_block="192.168.4.0/24", - vpc_id=vpc.ref, - availability_zone=f"{core.Aws.REGION}b", - tags=[{ - "key": "Name", - "value": "PrivateSubnet2" - }] - ) - internet_gateway = ec2.CfnInternetGateway(self, "InternetGateway", - tags=[{ - "key": "Name", - "value": "InternetGateway" - }] - ) - gateway_attachment = ec2.CfnVPCGatewayAttachment(self, "IGWAttachment", - vpc_id=vpc.ref, - internet_gateway_id=internet_gateway.ref - ) - - - NatGateway1EIP = ec2.CfnEIP(self, "NatGateway1EIP", domain="vpc") - NatGateway2EIP = ec2.CfnEIP(self, "NatGateway2EIP", domain="vpc") - - - cfn_nat_gateway1 = ec2.CfnNatGateway(self, "MyCfnNatGateway1", - allocation_id=NatGateway1EIP.attr_allocation_id, - subnet_id=public_subnet_1.attr_subnet_id) - cfn_nat_gateway2 = ec2.CfnNatGateway(self, "MyCfnNatGateway2", - allocation_id=NatGateway2EIP.attr_allocation_id, - subnet_id=public_subnet_1.attr_subnet_id) - NatGateway1EIP.node.add_dependency(gateway_attachment) - NatGateway2EIP.node.add_dependency(gateway_attachment) - cfn_nat_gateway1.node.add_dependency(NatGateway1EIP) - cfn_nat_gateway2.node.add_dependency(NatGateway2EIP) - - public_route_table = ec2.CfnRouteTable(self, "PublicRouteTable", - vpc_id=vpc.attr_vpc_id, - tags=[{ - "key": "Name", - "value": f"{environment_name} Public Routes" - }]) - ec2.CfnRoute(self, "DefaultPublicRoute", - route_table_id=public_route_table.attr_route_table_id, - destination_cidr_block="0.0.0.0/0", - gateway_id=gateway_attachment.internet_gateway_id - - ) - - ec2.CfnSubnetRouteTableAssociation(self, f"PublicSubnetRouteTableAssociation1", - route_table_id=public_route_table.attr_route_table_id, - subnet_id=public_subnet_1.attr_subnet_id) - ec2.CfnSubnetRouteTableAssociation(self, f"PublicSubnetRouteTableAssociation2", - route_table_id=public_route_table.attr_route_table_id, - subnet_id=public_subnet_2.attr_subnet_id) - privateRouteTable = ec2.CfnRouteTable(self, "PrivateRouteTable1", - vpc_id=vpc.attr_vpc_id, - tags=[{ - "key": "Name", - "value": f"{environment_name} Private Routes (AZ1)" - }]) - - ec2.CfnRoute(self, "DefaultPrivateRoute1", - route_table_id=privateRouteTable.ref, - destination_cidr_block="0.0.0.0/0", - nat_gateway_id=cfn_nat_gateway1.ref) - - ec2.CfnSubnetRouteTableAssociation(self, "PrivateSubnetRouteTableAssociation", - subnet_id=private_subnet_1.attr_subnet_id, - route_table_id=privateRouteTable.ref) - - privateRouteTable2 = ec2.CfnRouteTable(self, "PrivateRouteTable2", - vpc_id=vpc.attr_vpc_id, - tags=[{ - "key": "Name", - "value": f"{environment_name} Private Routes (AZ2)" - }]) - - ec2.CfnRoute(self, "DefaultPrivateRoute2", - route_table_id=privateRouteTable2.ref, - destination_cidr_block="0.0.0.0/0", - nat_gateway_id=cfn_nat_gateway2.ref) - - ec2.CfnSubnetRouteTableAssociation(self, "PrivateSubnet2RouteTableAssociation", - subnet_id=private_subnet_2.attr_subnet_id, - route_table_id=privateRouteTable2.ref) + vpc = ec2.Vpc(self, "AppMeshVPC", + ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"), + create_internet_gateway=True, + max_azs=2, + nat_gateways=2, + enable_dns_hostnames=True, + enable_dns_support=True, + vpc_name="App-Mesh-VPC", + subnet_configuration=[ + ec2.SubnetConfiguration( + subnet_type=ec2.SubnetType.PUBLIC, + name="Public", + cidr_mask=24 + ), + ec2.SubnetConfiguration( + subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, + name="Private", + cidr_mask=24 + ) + ] + ) + vpc_id_ssm_store = ssm.StringParameter(self, "vpcid", parameter_name="vpc_id", string_value=vpc.vpc_id) core.CfnOutput( + self, "VPCID", - value=vpc.attr_vpc_id, + value=vpc.vpc_id, export_name=f"{environment_name}:VPCID" ) core.CfnOutput( self, "Public Subnet 1", - value=public_subnet_1.attr_subnet_id, + value=vpc.public_subnets[0].subnet_id, export_name=f"{environment_name}:PublicSubnet1" ) core.CfnOutput( self, "Public Subnet 2", - value=public_subnet_2.attr_subnet_id, + value=vpc.public_subnets[1].subnet_id, export_name=f"{environment_name}:PublicSubnet2" ) core.CfnOutput( self, "Private Subnet 1", - value=private_subnet_1.attr_subnet_id, + value=vpc.private_subnets[0].subnet_id, export_name=f"{environment_name}:PrivateSubnet1" ) core.CfnOutput( self, "Private Subnet 2", - value=private_subnet_2.attr_subnet_id, + value=vpc.private_subnets[1].subnet_id, export_name=f"{environment_name}:PrivateSubnet2" ) core.CfnOutput(self, "VpcCidr", - value=vpc.attr_cidr_block, + value=vpc.vpc_cidr_block, export_name=f"{environment_name}:VpcCidr" - ) - - - - - - - - - - \ No newline at end of file + ) \ No newline at end of file diff --git a/python/appmesh-ecs/deploy_images.sh b/python/appmesh-ecs/deploy_images.sh deleted file mode 100755 index 8d0fd20827..0000000000 --- a/python/appmesh-ecs/deploy_images.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - - -# Paths to the deployment scripts -GatewayScriptPath="./container_images/gateway/deploy.sh" -ColorTellerScriptPath="./container_images/colorteller/deploy.sh" - -# Retrieve the AWS account ID and default region -account_id=$(aws sts get-caller-identity --query Account --output text) -default_region=$(aws configure get region) - -# Print the retrieved account ID -echo "Account ID: $account_id" -echo "Default Region: $default_region" - -# Execute the deployment scripts -{ - gateway_result=$(bash "$GatewayScriptPath" "$account_id" "$default_region" 2>&1) - echo "Gateway Result: $gateway_result" -} || { - echo "Error occurred while running the Gateway script" - exit 1 -} - -{ - colorteller_result=$(bash "$ColorTellerScriptPath" "$account_id" "$default_region" 2>&1) - echo "ColorTeller Result: $colorteller_result" -} || { - echo "Error occurred while running the ColorTeller script" - exit 1 -} - diff --git a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py index 30c51412de..dc6dd49cb2 100644 --- a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py +++ b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py @@ -2,387 +2,438 @@ import aws_cdk as core import aws_cdk.aws_ec2 as ec2 import aws_cdk.aws_ecs as ecs -from aws_cdk import Stack, Tags, App +import aws_cdk.aws_iam as iam +import aws_cdk.aws_ecr as ecr +import aws_cdk.aws_logs as logs +import aws_cdk.aws_appmesh as appmesh +import aws_cdk.aws_ssm as ssm +from aws_cdk.aws_ecs import BaseService +import aws_cdk.aws_elasticloadbalancingv2 as elbv2 +from aws_cdk import Stack, Tags, App, Duration from constructs import Construct + +from aws_cdk import ( + aws_appmesh as appmesh, + +) +import aws_cdk as core +import aws_cdk.aws_servicediscovery as servicediscovery class ColorAppTaskDefinitionStack(Stack): + # This function creates the task definitions for each color service + def create_color_task_definition(self, color, environment_name): + task_definition = ecs.FargateTaskDefinition( + self, f"colorTellerTask-{color}", + family=f"{environment_name}-ColorTeller-{color}", + cpu=256, + memory_limit_mib=512, + task_role=iam.Role.from_role_arn( + self, f"TaskDefinitionTaskRole-{color}", + role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole") + ), + execution_role=iam.Role.from_role_arn( + self, f"ExecutionRole-{color}", + role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole") + ) + ) + proxy_config = ecs.AppMeshProxyConfiguration( + container_name="envoy", + properties=ecs.AppMeshProxyConfigurationProps( + ignored_uid=1337, + app_ports=[9080], + proxy_ingress_port=15000, + proxy_egress_port=15001, + egress_ignored_i_ps=["169.254.170.2,169.254.169.254"] + ) + + ) + envoy = task_definition.add_container( + "envoy", + image=ecs.ContainerImage.from_registry(f"840364872350.dkr.ecr.{core.Aws.REGION}.amazonaws.com/aws-appmesh-envoy:v1.29.6.1-prod"), + user="1337", + container_name="envoy", + memory_reservation_mib=256, + environment={ + "APPMESH_RESOURCE_ARN": core.Fn.sub("mesh/${MeshName}/virtualNode/colorteller-${app_name}-vn", {"MeshName": core.Fn.import_value(f"{environment_name}:Mesh"), "app_name": color }), + "ENVOY_LOG_LEVEL": "DEBUG", + "ENABLE_ENVOY_XRAY_TRACING": "1", + "ENABLE_ENVOY_STATS_TAGS": "1" + }, + health_check=ecs.HealthCheck( + command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], + interval=Duration.seconds(5), + retries=3, + timeout=Duration.seconds(2) + ), + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"colorteller-{color}-envoy", + log_group=logs.LogGroup.from_log_group_arn( + self, f"AppLogGroup-{color}-envoy", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") + ) + ), + essential=True, + port_mappings=[ecs.PortMapping(container_port=15000, host_port=15000, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=15001, host_port=15001, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=9901, host_port=9901, protocol=ecs.Protocol.TCP),], + ulimits=[ecs.Ulimit(hard_limit=15000, name=ecs.UlimitName.NOFILE, soft_limit=15000)], + ) + + # Container Definitions + repo = ecr.Repository.from_repository_name(self, f"AppECRRepo-{color}", repository_name="colorteller") + app_container = task_definition.add_container( + "app", + image=ecs.ContainerImage.from_ecr_repository(repository=repo, tag='latest'), + # ecr.Repository.from_repository_arn( + # self, "AppECRRepo", + # repository_arn=f"arn:aws:ecr:{core.Aws.REGION}:{core.Aws.ACCOUNT_ID}:repository/colorteller", + + # ), + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"colorteller-{color}-app", + log_group=logs.LogGroup.from_log_group_arn( + self, f"AppLogGroup-{color}", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") + ) + ), + environment={ + "COLOR": color, + "SERVER_PORT": "9080" + }, + port_mappings=[ecs.PortMapping(container_port=9080, host_port=9080, protocol=ecs.Protocol.TCP)], + # container_depends_on=[ + # ecs.ContainerDependency( + + # container=envoy, + # condition=ecs.ContainerDependencyCondition.HEALTHY + + # ) + # ] + ) + app_container.add_container_dependencies(ecs.ContainerDependency( + container=envoy, + condition=ecs.ContainerDependencyCondition.HEALTHY + )) + + xray_container = task_definition.add_container( + "xrayContainer", + image=ecs.ContainerImage.from_registry("amazon/aws-xray-daemon"), + user="1337", + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"colorteller-{color}-xray", + log_group=logs.LogGroup.from_log_group_arn( + self, f"XrayLogGroup-{color}", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") + ) + ), + memory_reservation_mib=256, + port_mappings=[ecs.PortMapping(container_port=2000, host_port=2000, protocol=ecs.Protocol.TCP)], + + ) + return task_definition + # This function creates the ECS service for each corresponding color + def create_color_service(self, color, namespace, taskdef, ): + environment_name ="appmesh-env" + vpc_id = core.Fn.import_value(f"{environment_name}:VPCID") + + availability_zones = core.Fn.split(',', f"{environment_name}:VpcAvailabilityZones") + private_subnet_ids = core.Fn.split(',', core.Fn.import_value(f"{environment_name}:MyPrivateSubnetIds")) + imported_vpc = ec2.Vpc.from_vpc_attributes(self, f"ImportedVPC-{color}", vpc_id=vpc_id, availability_zones=availability_zones, private_subnet_ids=private_subnet_ids) + cluster = ecs.Cluster.from_cluster_attributes(self, f"Cluster-{color}", + cluster_name=core.Fn.import_value(f"{environment_name}:ECSCluster"), + vpc=imported_vpc, + + ) + security_group_id = ec2.SecurityGroup.from_security_group_id( + self, f"SecurityGroup-{color}", + security_group_id=core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup") + ) + namespace_name = f'colorteller-{color}' + if color=='white': + namespace_name = 'colorteller' + + + return ecs.FargateService( + self, f"ColorTeller{color}Service", + cluster=cluster, + service_name=f'colorteller-{color}-service', + desired_count=1, + max_healthy_percent=200, + min_healthy_percent=100, + task_definition=taskdef, + vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), + assign_public_ip=False, + security_groups=[security_group_id], + cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=namespace, dns_record_type=servicediscovery.DnsRecordType.A, dns_ttl=core.Duration.seconds(300), failure_threshold=1, name=namespace_name) + ) + # This function creates the load balancer infrastructure + def create_load_balanced_service(self, vpc, color_gateway_service): + PublicLoadBalancerSG = ec2.SecurityGroup( + self, "PublicLoadBalancerSG", + vpc=vpc, + description="ECS Security Group", + + allow_all_outbound=True, + ) + TargetGroup = elbv2.ApplicationTargetGroup( + self, "WebTargetGroup", + target_group_name="color-teller-target-group2", + port=80, + vpc=vpc, + targets=[color_gateway_service], + target_type=elbv2.TargetType.IP, + protocol=elbv2.ApplicationProtocol.HTTP, + health_check=elbv2.HealthCheck( + path="/ping", + port="9080", + interval=Duration.seconds(6), + timeout=Duration.seconds(5), + healthy_threshold_count=2, + unhealthy_threshold_count=2, + ), + + + ) + TargetGroup.set_attribute(key="deregistration_delay.timeout_seconds", + value="120") + PublicLoadBalancerSG.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(80), "Allow HTTP Ingress") + lb = elbv2.ApplicationLoadBalancer(self, "PublicLoadBalancer", + + vpc=vpc, + internet_facing=True, + security_group=PublicLoadBalancerSG, + vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), + ) + lb.set_attribute(key="idle_timeout.timeout_seconds", + value="30") + listener = lb.add_listener("Public Listener", + port=80, + + default_action=elbv2.ListenerAction.forward( + target_groups=[TargetGroup] + ), + ) + + # listener.add_targets("ECS", + # port=80, + # targets=[BaseService.load_balancer_target( + # self, + # container_name="app", + # container_port=9080 + # )], + + # )xw + WebLoadBalancerRule = elbv2.ApplicationListenerRule( + self, "WebLoadBalancerRule", + listener=listener, + priority=1, + action=elbv2.ListenerAction.forward(target_groups=[TargetGroup]), + conditions=[elbv2.ListenerCondition.path_patterns(["*"])], + ) + return lb.load_balancer_dns_name, TargetGroup + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: super().__init__(scope, id, **kwargs ) environment_name ="appmesh-env" - + mesh_name = core.Fn.import_value(f"{environment_name}:Mesh") + vpc_id = core.Fn.import_value(f"{environment_name}:VPCID") + vpc_id = ssm.StringParameter.value_from_lookup(self, "vpc_id") + imported_vpc = ec2.Vpc.from_lookup(self, "ImportedVPC", vpc_id=vpc_id) + imported_cluster = ecs.Cluster.from_cluster_attributes(self, f"ColorGatewayCluster", cluster_name=core.Fn.import_value(f"{environment_name}:ECSCluster"),vpc=imported_vpc) color_teller_colors = ["red", "black", "blue", "white"] + + namespace = servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes(self, "Namespace", + namespace_arn=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceARN"), + namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), + namespace_name=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceName")) + + # Loop through all the colors to create their task definitions and ECS services for color in color_teller_colors: task_definition = self.create_color_task_definition(color, environment_name) + service = self.create_color_service(color, namespace, taskdef=task_definition) + core.CfnOutput( self, f"ColorTellerTaskDefinitionArn-{color}", - value=task_definition.attr_task_definition_arn, + value=task_definition.task_definition_arn, export_name=f"{environment_name}:ColorTellerTaskDefinitionArn-{color}" ) - + colorGatewayProxyConfiguration = ecs.AppMeshProxyConfiguration( + container_name="envoy", + properties=ecs.AppMeshProxyConfigurationProps( + proxy_ingress_port=15000, + proxy_egress_port=15001, + app_ports=[9080], + ignored_uid=1337, + egress_ignored_i_ps=["169.254.170.2,169.254.169.254"] + ) + + ) + colorGatewayTaskDefinition = ecs.FargateTaskDefinition(self, + "colorGatewayTaskDefinition", + cpu=256, + memory_limit_mib=512, + proxy_configuration=colorGatewayProxyConfiguration, + task_role=iam.Role.from_role_arn( + self, f"TaskDefinitionTaskRoleGateway", + role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole") + ), + execution_role=iam.Role.from_role_arn( + self, f"ExecutionRoleGateway", + role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole") + ) + ) - colorGatewayTaskDefinition = ecs.CfnTaskDefinition( - self, "colorGatewayTask", - family=f"{environment_name}-ColorGateway", - network_mode="awsvpc", - memory="512", - requires_compatibilities=["FARGATE"], - cpu="256", - proxy_configuration= - ecs.CfnTaskDefinition.ProxyConfigurationProperty( - container_name="envoy", - type="APPMESH", - proxy_configuration_properties=[ - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="IgnoredUID", - value="1337" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ProxyIngressPort", - value="15000" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ProxyEgressPort", - value="15001" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="AppPorts", - value="9080" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="EgressIgnoredIPs", - value="169.254.170.2,169.254.169.254" - ) - ] - ), - container_definitions=[ - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="app", - image=f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/gateway:latest", - # memory=256, - # cpu=128, - port_mappings=[ - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=9080, - host_port=9080, - protocol="tcp" - ) - ], - environment=[ - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="SERVER_PORT", - value="9080" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="COLOR_TELLER_ENDPOINT", - value=core.Fn.sub("colorteller.${SERVICES_DOMAIN}:9080", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="TCP_ECHO_ENDPOINT", - value=core.Fn.sub("tcpecho.${SERVICES_DOMAIN}:2701", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) + color_gateway_repo = ecr.Repository.from_repository_name(self, f"AppECRRepoGateway", repository_name="gateway") + + # The following creates the containers for the gateway ECS service + # don't change the order of containers, the LB will use the app one by default since it is the first essential container + gateway_app_td = colorGatewayTaskDefinition.add_container( + "app", + image=ecs.ContainerImage.from_ecr_repository(repository=color_gateway_repo, tag='latest'), + port_mappings=[ecs.PortMapping(container_port=9080, host_port=9080, protocol=ecs.Protocol.TCP)], + environment= + {"SERVER_PORT": "9080", + "COLOR_TELLER_ENDPOINT": core.Fn.sub("colorteller.${SERVICES_DOMAIN}:9080", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) , + "TCP_ECHO_ENDPOINT": core.Fn.sub("tcpecho.${SERVICES_DOMAIN}:2701", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")})} + + , + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"colorteller-gateway-app", + log_group=logs.LogGroup.from_log_group_arn( + self, f"AppLogGroupGateway-app", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") ) - - ], - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": "colorteller-gateway" - } - ), - essential=True, - depends_on=[ - ecs.CfnTaskDefinition.ContainerDependencyProperty( - container_name="envoy", - condition="HEALTHY" - ) - ], - - ), - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="envoy", - user="1337", - essential=True, - # cpu=128, - image=core.Fn.sub("840364872350.dkr.ecr.${Region}.amazonaws.com/aws-appmesh-envoy:v1.29.5.0-prod",{"Region": core.Aws.REGION}), - port_mappings=[ - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=9901, - host_port=9901, - protocol="tcp" - ), - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=15000, - host_port=15000, - protocol="tcp" - ), - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=15001, - host_port=15001, - protocol="tcp" - ), - - ], - ulimits=[ecs.CfnTaskDefinition.UlimitProperty( - hard_limit=15000, - name="nofile", - soft_limit=15000 - )], - environment=[ - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="APPMESH_RESOURCE_ARN", - value=core.Fn.sub("mesh/${MeshName}/virtualNode/${app_name}-vn", {"MeshName": core.Fn.import_value(f"{environment_name}:Mesh"),"app_name": "colorgateway" }) - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ENVOY_LOG_LEVEL", - value="DEBUG" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ENABLE_ENVOY_XRAY_TRACING", - value="1" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ENABLE_ENVOY_STATS_TAGS", - value="1" + essential=True, + ) + gateway_envoy_td = colorGatewayTaskDefinition.add_container( + "envoy", + user="1337", + memory_reservation_mib=256, + image=ecs.ContainerImage.from_registry(f"840364872350.dkr.ecr.{core.Aws.REGION}.amazonaws.com/aws-appmesh-envoy:v1.29.6.1-prod"), + port_mappings=[ecs.PortMapping(container_port=15000, host_port=15000, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=15001, host_port=15001, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=9901, host_port=9901, protocol=ecs.Protocol.TCP),], + ulimits=[ecs.Ulimit(name=ecs.UlimitName.NOFILE, soft_limit=15000, hard_limit=15000)], + + environment={ + "APPMESH_RESOURCE_ARN": f"mesh/{mesh_name}/virtualNode/colorgateway-vn", + "ENVOY_LOG_LEVEL": "DEBUG", + "ENABLE_ENVOY_XRAY_TRACING": "1", + "ENABLE_ENVOY_STATS_TAGS": "1"}, + + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"colorteller-gateway-envoy", + log_group=logs.LogGroup.from_log_group_arn( + self, f"AppLogGroupGateway-envoy", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") ) - ], - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": f"{environment_name}-envoy" - } - - ), - health_check=ecs.CfnTaskDefinition.HealthCheckProperty( + ), + health_check=ecs.HealthCheck( command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], - interval=5, + interval=Duration.seconds(5), retries=3, - timeout=2 - + timeout=Duration.seconds(2), ), - - - ), - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="xrayContainer", - # cpu=32, - image="amazon/aws-xray-daemon", - user="1337", - port_mappings=[ecs.CfnTaskDefinition.PortMappingProperty( - container_port=2000, - host_port=2000, - protocol="udp" - )], - memory_reservation=256, - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": f"colorteller={color}-xray" - } + essential=True, + ) + gateway_xray_td = colorGatewayTaskDefinition.add_container( + "xray", + image=ecs.ContainerImage.from_registry("amazon/aws-xray-daemon"), + user="1337", + port_mappings=[ecs.PortMapping(container_port=2000, host_port=2000, protocol=ecs.Protocol.TCP)], + memory_reservation_mib=256, + # logging=ecs.LogDrivers.aws_logs( + # stream_prefix="colorteller-gateway-", + # log_group=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN"), + + # ), + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"colorteller-gateway-xray", + log_group=logs.LogGroup.from_log_group_arn( + self, f"AppLogGroupGateway-xray", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") + ) ), + essential=True, + ) + + gateway_app_td.add_container_dependencies(ecs.ContainerDependency( + container=gateway_envoy_td, + condition=ecs.ContainerDependencyCondition.HEALTHY + )) + + TcpEchoTaskDefinition = ecs.FargateTaskDefinition( + self, f"TesterTaskDefinition", + family="tester", + cpu=256, + memory_limit_mib=512, + task_role=iam.Role.from_role_arn( + self, "TcpEchoTaskDefinition", + role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole") + ), + execution_role=iam.Role.from_role_arn( + self, f"ExecutionRoleTCP", + role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole") ) - ], - task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), - execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), - - + + + + ) + TcpEchoTaskDefinition.add_container( + "app", + image=ecs.ContainerImage.from_registry("cjimti/go-echo"), + port_mappings=[ecs.PortMapping(container_port=2701, host_port=2701, protocol=ecs.Protocol.TCP)], + logging=ecs.LogDriver.aws_logs( + stream_prefix=f"tcp-echo", + log_group=logs.LogGroup.from_log_group_arn( + self, "tcpecho-app", + log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") + ) + ), + environment={"NODE_NAME": core.Fn.sub( + "mesh/${AppMeshMeshName}/virtualNode/tcpecho-vn", + {"AppMeshMeshName": f"{environment_name}:Mesh"} + )}, + essential=True, + ) + security_group_id = ec2.SecurityGroup.from_security_group_id( + self, f"SecurityGroupTcp", + security_group_id=core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup") + ) + colorGatewayService = ecs.FargateService( + self, "ColorGatewayECSService", + cluster=imported_cluster, + service_name=f'colorgateway-service', + desired_count=1, + max_healthy_percent=200, + min_healthy_percent=100, + task_definition=colorGatewayTaskDefinition, + vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), + security_groups=[security_group_id], + cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=namespace, name='colorgateway',dns_ttl=core.Duration.seconds(300), dns_record_type=servicediscovery.DnsRecordType.A, failure_threshold=1) + ) + TcpEchoService = ecs.FargateService(self, f"TcpEchoService", + cluster=imported_cluster, + task_definition=TcpEchoTaskDefinition, + service_name=f"tcpecho", + max_healthy_percent=200, + min_healthy_percent=100, + desired_count=1, + security_groups=[security_group_id], + vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), + cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=namespace,dns_record_type=servicediscovery.DnsRecordType.A, + dns_ttl=core.Duration.seconds(300), failure_threshold=1, name="tcpecho")) + lb, tg = self.create_load_balanced_service(imported_vpc, color_gateway_service=colorGatewayService) + core.CfnOutput( + self, "ColorAppEndpoint", + description="Public endpoint for Color App Service", + value=core.Fn.join("", ["http://", lb]), + export_name="ColorAppEndpoint" ) core.CfnOutput( self, "ColorGatewayTaskDefinitionArn", - value=colorGatewayTaskDefinition.attr_task_definition_arn, + value=colorGatewayTaskDefinition.task_definition_arn, export_name=f"{environment_name}:ColorGatewayTaskDefinitionArn" ) - - def create_color_task_definition(self, color, environment_name): - return ecs.CfnTaskDefinition( - self, f"colorTellerTask-{color}", - # name=f"{environment_name}-ColorTeller-{color}", - family=f"{environment_name}-ColorTeller-{color}", - network_mode="awsvpc", - memory="512", - requires_compatibilities=["FARGATE"], - cpu="256", - proxy_configuration=ecs.CfnTaskDefinition.ProxyConfigurationProperty( - container_name="envoy", - type="APPMESH", - proxy_configuration_properties=[ - ecs.CfnTaskDefinition.KeyValuePairProperty( - - name="IgnoredUID", - value="1337" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ProxyIngressPort", - value="15000" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ProxyEgressPort", - value="15001" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="AppPorts", - value="9080" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="EgressIgnoredIPs", - value="169.254.170.2,169.254.169.254" - ) - - - ] - ), - container_definitions=[ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="app", - image=f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/colorteller:latest", - essential=True, - # cpu=128, - port_mappings=[ecs.CfnTaskDefinition.PortMappingProperty( - container_port=9080, - host_port=9080, - protocol="tcp" - )], - environment=[ - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="COLOR", - value=color - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="SERVER_PORT", - value="9080" - ), - # ecs.CfnTaskDefinition.KeyValuePairProperty( - # name="STAGE", - # value=F"{app_mesh_stage}" - # ), - - ], - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": f"colorteller={color}-app" - } - ), - depends_on=[ - ecs.CfnTaskDefinition.ContainerDependencyProperty( - container_name="envoy", - condition="HEALTHY" - ) - ] - - ), - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="envoy", - user="1337", - # cpu=128, - essential=True, - image=core.Fn.sub("840364872350.dkr.ecr.${Region}.amazonaws.com/aws-appmesh-envoy:v1.29.5.0-prod",{"Region": core.Aws.REGION}), - port_mappings=[ - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=9901, - host_port=9901, - protocol="tcp" - ), - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=15000, - host_port=15000, - protocol="tcp" - ), - ecs.CfnTaskDefinition.PortMappingProperty( - container_port=15001, - host_port=15001, - protocol="tcp" - ), - - ], - ulimits=[ecs.CfnTaskDefinition.UlimitProperty( - hard_limit=15000, - name="nofile", - soft_limit=15000 - )], - environment=[ - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="APPMESH_RESOURCE_ARN", - value=core.Fn.sub("mesh/${MeshName}/virtualNode/colorteller-${app_name}-vn", {"MeshName": core.Fn.import_value(f"{environment_name}:Mesh"),"app_name": color }) - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ENVOY_LOG_LEVEL", - value="DEBUG" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ENABLE_ENVOY_XRAY_TRACING", - value="1" - ), - ecs.CfnTaskDefinition.KeyValuePairProperty( - name="ENABLE_ENVOY_STATS_TAGS", - value="1" - ) - ], - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": f"{environment_name}-envoy" - } - - ), - health_check=ecs.CfnTaskDefinition.HealthCheckProperty( - command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], - interval=5, - retries=3, - timeout=2 - - ) - ), - ecs.CfnTaskDefinition.ContainerDefinitionProperty( - name="xrayContainer", - # cpu=32, - image="amazon/aws-xray-daemon", - user="1337", - port_mappings=[ecs.CfnTaskDefinition.PortMappingProperty( - container_port=2000, - host_port=2000, - protocol="udp" - )], - memory_reservation=256, - log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - log_driver="awslogs", - options={ - "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServicelogGroup"), - "awslogs-region": core.Aws.REGION, - "awslogs-stream-prefix": f"colorteller={color}-xray" - } - ), - ) - ], - - # log_configuration=ecs.CfnTaskDefinition.LogConfigurationProperty( - # log_driver="awslogs", - # options={ - # "awslogs-group": core.Fn.import_value(f"{environment_name}:ECSServiceLogGroup"), - # "awslogs-region": core.Aws.REGION, - # "awslogs-stream-prefix": f"{environment_name}-colorTeller-app" - # } - # ), - task_role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole"), - execution_role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole"), - ) - - # task_role_arn=core.Fn.import_value(f"{environment_name}:TaskRoleArn"), - # execution_role_arn=core.Fn.import_value(f"{environment_name}:ExecutionRoleArn"), - # tags={ - # "Name": "xrayContainer" - # }) - - - \ No newline at end of file + \ No newline at end of file diff --git a/python/appmesh-ecs/tests/__init__.py b/python/appmesh-ecs/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/tests/unit/__init__.py b/python/appmesh-ecs/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py b/python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py new file mode 100644 index 0000000000..e49aa6162c --- /dev/null +++ b/python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py @@ -0,0 +1,15 @@ +import aws_cdk as core +import aws_cdk.assertions as assertions + +from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack + +# example tests. To run these tests, uncomment this file along with the example +# resource in new_cdk_appmesh_redo/new_cdk_appmesh_redo_stack.py +def test_sqs_queue_created(): + app = core.App() + stack = NewCdkAppmeshRedoStack(app, "new-cdk-appmesh-redo") + template = assertions.Template.from_stack(stack) + +# template.has_resource_properties("AWS::SQS::Queue", { +# "VisibilityTimeout": 300 +# }) From a14c49472bd31cf04d0b4db1421b4a56534062f6 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:31:12 -0500 Subject: [PATCH 07/24] added requirement for cdk-ecr-deployment --- python/appmesh-ecs/app.py | 1 - python/appmesh-ecs/requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index dd2a23b0fc..add1bc4d10 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -2,7 +2,6 @@ import os import aws_cdk as cdk -# from core_infrastructure.vpc.vpc_stack import VPCStack from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack diff --git a/python/appmesh-ecs/requirements.txt b/python/appmesh-ecs/requirements.txt index b1bcff241f..018b6f3e4b 100644 --- a/python/appmesh-ecs/requirements.txt +++ b/python/appmesh-ecs/requirements.txt @@ -1,2 +1,3 @@ aws-cdk-lib==2.147.1 constructs>=10.0.0,<11.0.0 +cdk-ecr-deployment \ No newline at end of file From 22cbc4b257dbc9e79c6f4242bc2639df6aba2bf4 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:35:02 -0500 Subject: [PATCH 08/24] removed unused folder --- python/appmesh-ecs/app.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index add1bc4d10..9064e23e34 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -4,10 +4,7 @@ import aws_cdk as cdk from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack - from core_infrastructure.ecr.ecr_stack import ECRStack - -from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack from colorapp.appmesh_colorapp import ServiceMeshColorAppStack From c2379969b38d7bdfe436346aed0d40a56f498eaa Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:40:47 -0500 Subject: [PATCH 09/24] commented out env for task definition stack --- python/appmesh-ecs/README.md | 2 +- python/appmesh-ecs/app.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/python/appmesh-ecs/README.md b/python/appmesh-ecs/README.md index 6da0c4cb43..da16725763 100644 --- a/python/appmesh-ecs/README.md +++ b/python/appmesh-ecs/README.md @@ -48,7 +48,7 @@ them to your `setup.py` file and rerun the `pip install -r requirements.txt` command. ## Deploying this sample -To deploy this sample, run `cdk deploy --all` +To deploy this sample, navigate to app.py and uncomment lines 19-20, then run `cdk deploy --all` ## Useful commands diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index 9064e23e34..ffda8e82ba 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -14,8 +14,10 @@ ecr_stack = ECRStack(app, "ECRStack") appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ - "account": os.environ["CDK_DEFAULT_ACCOUNT"], - "region": os.environ["CDK_DEFAULT_REGION"] + # Uncomment these when you deploy + + # "account": os.environ["CDK_DEFAULT_ACCOUNT"], + # "region": os.environ["CDK_DEFAULT_REGION"] }) appmesh_stack.add_dependency(ecs_stack) From 6ec94688a84bff2d5a1a6d6602c5a3bc38c0d77e Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:46:55 -0500 Subject: [PATCH 10/24] need env variables for stack --- python/appmesh-ecs/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index ffda8e82ba..b032b3e72e 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -16,8 +16,8 @@ colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ # Uncomment these when you deploy - # "account": os.environ["CDK_DEFAULT_ACCOUNT"], - # "region": os.environ["CDK_DEFAULT_REGION"] + "account": os.environ["CDK_DEFAULT_ACCOUNT"], + "region": os.environ["CDK_DEFAULT_REGION"] }) appmesh_stack.add_dependency(ecs_stack) From 162b1b66f14f53b77898a952dd6e72d8df712683 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:52:49 -0500 Subject: [PATCH 11/24] removed env variable for account id and region, replaced with direct implementation from cdk --- python/appmesh-ecs/app.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index b032b3e72e..a671fd6d06 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -2,9 +2,13 @@ import os import aws_cdk as cdk +from aws_cdk import Aws from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack + from core_infrastructure.ecr.ecr_stack import ECRStack + +from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack from colorapp.appmesh_colorapp import ServiceMeshColorAppStack @@ -14,10 +18,9 @@ ecr_stack = ECRStack(app, "ECRStack") appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ - # Uncomment these when you deploy - - "account": os.environ["CDK_DEFAULT_ACCOUNT"], - "region": os.environ["CDK_DEFAULT_REGION"] + "account": Aws.ACCOUNT_ID, + "region": Aws.REGION + }) appmesh_stack.add_dependency(ecs_stack) From a1cb3eb59fdedee93624ac73e99189082202bdd7 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 11:54:26 -0500 Subject: [PATCH 12/24] removed env variable for account id and region, replaced with direct implementation from cdk --- python/appmesh-ecs/app.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index a671fd6d06..00e37200ca 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -1,14 +1,10 @@ #!/usr/bin/env python3 -import os import aws_cdk as cdk from aws_cdk import Aws from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack - from core_infrastructure.ecr.ecr_stack import ECRStack - -from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack from colorapp.appmesh_colorapp import ServiceMeshColorAppStack From 4177fc3d24004f25a59881ae5d3447cbd4f31ddc Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 12:51:06 -0500 Subject: [PATCH 13/24] added cdk environment for variables --- python/appmesh-ecs/app.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index 00e37200ca..c96f7c1c77 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 +import os import aws_cdk as cdk -from aws_cdk import Aws from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack + from core_infrastructure.ecr.ecr_stack import ECRStack + +from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack from colorapp.appmesh_colorapp import ServiceMeshColorAppStack @@ -13,11 +16,11 @@ appmesh_stack = AppMeshStack(app, "AppMeshStack") ecr_stack = ECRStack(app, "ECRStack") appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") -colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ - "account": Aws.ACCOUNT_ID, - "region": Aws.REGION - -}) + +colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env=cdk.Environment( + account=cdk.Aws.ACCOUNT_ID, + region=cdk.Aws.REGION +)) appmesh_stack.add_dependency(ecs_stack) appmesh_colorapp_stack.add_dependency(appmesh_stack) From 7bd7837a87ecc6afc7d1371eb94cfdcf2fd13c71 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 12:53:23 -0500 Subject: [PATCH 14/24] added cdk environment for variables --- python/appmesh-ecs/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index c96f7c1c77..72bf087abe 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -6,8 +6,6 @@ from core_infrastructure.appmesh.appmesh_stack import AppMeshStack from core_infrastructure.ecr.ecr_stack import ECRStack - -from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack from colorapp.appmesh_colorapp import ServiceMeshColorAppStack From ff4758e4f7d89759c9a0656dfc797eb9bdd49add Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 12:55:17 -0500 Subject: [PATCH 15/24] added cdk environment for variables, again --- python/appmesh-ecs/app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index 72bf087abe..a5ec71b659 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -4,7 +4,6 @@ import aws_cdk as cdk from core_infrastructure.ecs.ecs_stack import ECSStack from core_infrastructure.appmesh.appmesh_stack import AppMeshStack - from core_infrastructure.ecr.ecr_stack import ECRStack from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack from colorapp.appmesh_colorapp import ServiceMeshColorAppStack @@ -16,8 +15,8 @@ appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env=cdk.Environment( - account=cdk.Aws.ACCOUNT_ID, - region=cdk.Aws.REGION + account=os.getenv('CDK_DEFAULT_ACCOUNT'), + region=os.getenv('CDK_DEFAULT_REGION') )) appmesh_stack.add_dependency(ecs_stack) From 71dcf0fb17bff4a2cdf96770f8795df7417d8b51 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 13:15:02 -0500 Subject: [PATCH 16/24] . --- python/appmesh-ecs/app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index a5ec71b659..26b97f907d 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -14,10 +14,11 @@ ecr_stack = ECRStack(app, "ECRStack") appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") -colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env=cdk.Environment( - account=os.getenv('CDK_DEFAULT_ACCOUNT'), - region=os.getenv('CDK_DEFAULT_REGION') -)) +colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ + 'account': os.getenv('CDK_DEFAULT_ACCOUNT'), + 'region': os.getenv('CDK_DEFAULT_REGION') +} +) appmesh_stack.add_dependency(ecs_stack) appmesh_colorapp_stack.add_dependency(appmesh_stack) From c3dbefe09a43bc75d170e448a739ef83a94437bd Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 13:22:11 -0500 Subject: [PATCH 17/24] .. --- python/appmesh-ecs/README.md | 3 --- python/appmesh-ecs/requirements.txt | 1 - .../color_app_task_definition_stack.py | 10 +++------- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/python/appmesh-ecs/README.md b/python/appmesh-ecs/README.md index da16725763..c53f0b50cc 100644 --- a/python/appmesh-ecs/README.md +++ b/python/appmesh-ecs/README.md @@ -47,9 +47,6 @@ To add additional dependencies, for example other CDK libraries, just add them to your `setup.py` file and rerun the `pip install -r requirements.txt` command. -## Deploying this sample -To deploy this sample, navigate to app.py and uncomment lines 19-20, then run `cdk deploy --all` - ## Useful commands * `cdk ls` list all stacks in the app diff --git a/python/appmesh-ecs/requirements.txt b/python/appmesh-ecs/requirements.txt index 018b6f3e4b..b1bcff241f 100644 --- a/python/appmesh-ecs/requirements.txt +++ b/python/appmesh-ecs/requirements.txt @@ -1,3 +1,2 @@ aws-cdk-lib==2.147.1 constructs>=10.0.0,<11.0.0 -cdk-ecr-deployment \ No newline at end of file diff --git a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py index dc6dd49cb2..4ffd7938c9 100644 --- a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py +++ b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py @@ -130,13 +130,9 @@ def create_color_task_definition(self, color, environment_name): ) return task_definition # This function creates the ECS service for each corresponding color - def create_color_service(self, color, namespace, taskdef, ): + def create_color_service(self, color, namespace, taskdef, imported_vpc ): environment_name ="appmesh-env" - vpc_id = core.Fn.import_value(f"{environment_name}:VPCID") - - availability_zones = core.Fn.split(',', f"{environment_name}:VpcAvailabilityZones") - private_subnet_ids = core.Fn.split(',', core.Fn.import_value(f"{environment_name}:MyPrivateSubnetIds")) - imported_vpc = ec2.Vpc.from_vpc_attributes(self, f"ImportedVPC-{color}", vpc_id=vpc_id, availability_zones=availability_zones, private_subnet_ids=private_subnet_ids) + cluster = ecs.Cluster.from_cluster_attributes(self, f"Cluster-{color}", cluster_name=core.Fn.import_value(f"{environment_name}:ECSCluster"), vpc=imported_vpc, @@ -249,7 +245,7 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: # Loop through all the colors to create their task definitions and ECS services for color in color_teller_colors: task_definition = self.create_color_task_definition(color, environment_name) - service = self.create_color_service(color, namespace, taskdef=task_definition) + service = self.create_color_service(color, namespace, taskdef=task_definition, imported_vpc=imported_vpc) core.CfnOutput( self, f"ColorTellerTaskDefinitionArn-{color}", From 0937d8cafd1343e1102ab2bf41c79017b6575b27 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 13:24:21 -0500 Subject: [PATCH 18/24] ... --- python/appmesh-ecs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/python/appmesh-ecs/requirements.txt b/python/appmesh-ecs/requirements.txt index b1bcff241f..4a9cf8bb14 100644 --- a/python/appmesh-ecs/requirements.txt +++ b/python/appmesh-ecs/requirements.txt @@ -1,2 +1,3 @@ aws-cdk-lib==2.147.1 constructs>=10.0.0,<11.0.0 +cdk_ecr_deployment \ No newline at end of file From ffc0a273a382f95a4280a1c59c33098777afcd86 Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 5 Sep 2024 13:26:11 -0500 Subject: [PATCH 19/24] fixed requirements --- python/appmesh-ecs/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py index 26b97f907d..434c8abd90 100644 --- a/python/appmesh-ecs/app.py +++ b/python/appmesh-ecs/app.py @@ -13,7 +13,6 @@ appmesh_stack = AppMeshStack(app, "AppMeshStack") ecr_stack = ECRStack(app, "ECRStack") appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") - colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ 'account': os.getenv('CDK_DEFAULT_ACCOUNT'), 'region': os.getenv('CDK_DEFAULT_REGION') From 41832f1106bdac6439d8c2843ebfce5d9b4b1f9f Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 24 Oct 2024 11:26:42 -0500 Subject: [PATCH 20/24] adding ecs service connect cdk example --- python/appmesh-ecs/.gitignore | 19 - python/appmesh-ecs/app.py | 26 -- .../appmesh-ecs/colorapp/appmesh_colorapp.py | 170 ------- .../colorapp/ecs_colorapp_stack.py | 65 --- .../container_images/colorteller/.gitignore | 1 - .../container_images/colorteller/Dockerfile | 40 -- .../container_images/colorteller/deploy.sh | 48 -- .../container_images/colorteller/go.mod | 15 - .../container_images/colorteller/go.sum | 73 --- .../container_images/colorteller/main.go | 61 --- .../container_images/gateway/.gitignore | 1 - .../container_images/gateway/Dockerfile | 41 -- .../container_images/gateway/deploy.sh | 50 -- .../container_images/gateway/go.mod | 16 - .../container_images/gateway/go.sum | 73 --- .../container_images/gateway/main.go | 228 --------- .../appmesh/appmesh_stack.py | 22 - .../core_infrastructure/ecr/ecr_stack.py | 79 ---- .../core_infrastructure/ecs/__init__.py | 0 .../core_infrastructure/ecs/ecs_stack.py | 174 ------- .../core_infrastructure/vpc/__init__.py | 0 .../core_infrastructure/vpc/vpc_stack.py | 66 --- .../appmesh-ecs/task_definitions/__init__.py | 0 .../color_app_task_definition_stack.py | 435 ------------------ python/appmesh-ecs/tests/__init__.py | 0 python/appmesh-ecs/tests/unit/__init__.py | 0 .../unit/test_new_cdk_appmesh_redo_stack.py | 15 - python/ecs-serviceconnect/.gitignore | 10 + .../README.md | 15 +- python/ecs-serviceconnect/app.py | 9 + .../cdk.json | 0 .../cdk_examples_service_connect}/__init__.py | 0 .../cdk_examples_service_connect_stack.py | 39 ++ .../ecr}/__init__.py | 0 python/ecs-serviceconnect/ecr/ecr_stack.py | 72 +++ .../ecs}/__init__.py | 0 python/ecs-serviceconnect/ecs/ecs_stack.py | 163 +++++++ .../requirements-dev.txt | 0 .../requirements.txt | 0 .../services/data/Dockerfile | 20 + .../ecs-serviceconnect/services/data/data.py | 18 + .../services/data/requirements.txt | 1 + .../services/frontend/Dockerfile | 22 + .../services/frontend/frontend.py | 26 ++ .../services/frontend/requirements.txt | 2 + .../source.bat | 0 46 files changed, 388 insertions(+), 1727 deletions(-) delete mode 100644 python/appmesh-ecs/.gitignore delete mode 100644 python/appmesh-ecs/app.py delete mode 100644 python/appmesh-ecs/colorapp/appmesh_colorapp.py delete mode 100644 python/appmesh-ecs/colorapp/ecs_colorapp_stack.py delete mode 100644 python/appmesh-ecs/container_images/colorteller/.gitignore delete mode 100644 python/appmesh-ecs/container_images/colorteller/Dockerfile delete mode 100755 python/appmesh-ecs/container_images/colorteller/deploy.sh delete mode 100644 python/appmesh-ecs/container_images/colorteller/go.mod delete mode 100644 python/appmesh-ecs/container_images/colorteller/go.sum delete mode 100644 python/appmesh-ecs/container_images/colorteller/main.go delete mode 100644 python/appmesh-ecs/container_images/gateway/.gitignore delete mode 100644 python/appmesh-ecs/container_images/gateway/Dockerfile delete mode 100755 python/appmesh-ecs/container_images/gateway/deploy.sh delete mode 100644 python/appmesh-ecs/container_images/gateway/go.mod delete mode 100644 python/appmesh-ecs/container_images/gateway/go.sum delete mode 100644 python/appmesh-ecs/container_images/gateway/main.go delete mode 100644 python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py delete mode 100644 python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py delete mode 100644 python/appmesh-ecs/core_infrastructure/ecs/__init__.py delete mode 100644 python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py delete mode 100644 python/appmesh-ecs/core_infrastructure/vpc/__init__.py delete mode 100644 python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py delete mode 100644 python/appmesh-ecs/task_definitions/__init__.py delete mode 100644 python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py delete mode 100644 python/appmesh-ecs/tests/__init__.py delete mode 100644 python/appmesh-ecs/tests/unit/__init__.py delete mode 100644 python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py create mode 100644 python/ecs-serviceconnect/.gitignore rename python/{appmesh-ecs => ecs-serviceconnect}/README.md (66%) create mode 100644 python/ecs-serviceconnect/app.py rename python/{appmesh-ecs => ecs-serviceconnect}/cdk.json (100%) rename python/{appmesh-ecs/colorapp => ecs-serviceconnect/cdk_examples_service_connect}/__init__.py (100%) create mode 100644 python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py rename python/{appmesh-ecs/core_infrastructure/appmesh => ecs-serviceconnect/ecr}/__init__.py (100%) create mode 100644 python/ecs-serviceconnect/ecr/ecr_stack.py rename python/{appmesh-ecs/core_infrastructure/ecr => ecs-serviceconnect/ecs}/__init__.py (100%) create mode 100644 python/ecs-serviceconnect/ecs/ecs_stack.py rename python/{appmesh-ecs => ecs-serviceconnect}/requirements-dev.txt (100%) rename python/{appmesh-ecs => ecs-serviceconnect}/requirements.txt (100%) create mode 100644 python/ecs-serviceconnect/services/data/Dockerfile create mode 100644 python/ecs-serviceconnect/services/data/data.py create mode 100644 python/ecs-serviceconnect/services/data/requirements.txt create mode 100644 python/ecs-serviceconnect/services/frontend/Dockerfile create mode 100644 python/ecs-serviceconnect/services/frontend/frontend.py create mode 100644 python/ecs-serviceconnect/services/frontend/requirements.txt rename python/{appmesh-ecs => ecs-serviceconnect}/source.bat (100%) diff --git a/python/appmesh-ecs/.gitignore b/python/appmesh-ecs/.gitignore deleted file mode 100644 index 9572748ce1..0000000000 --- a/python/appmesh-ecs/.gitignore +++ /dev/null @@ -1,19 +0,0 @@ -*.swp -package-lock.json -__pycache__ -.pytest_cache -.venv -*.egg-info - -# CDK asset staging directory -.cdk.staging -cdk.out -/colorapp/ecs_colorapp_stack.py -new_cdk_appmesh_redo/ -vpc/ -ecsstackbackup.py.back -xray-container.json -envoy-container.json -create-task-defs.sh -colorteller-base-task-def.json -colorgateway-base-task-def.json \ No newline at end of file diff --git a/python/appmesh-ecs/app.py b/python/appmesh-ecs/app.py deleted file mode 100644 index 434c8abd90..0000000000 --- a/python/appmesh-ecs/app.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -import os - -import aws_cdk as cdk -from core_infrastructure.ecs.ecs_stack import ECSStack -from core_infrastructure.appmesh.appmesh_stack import AppMeshStack -from core_infrastructure.ecr.ecr_stack import ECRStack -from task_definitions.color_app_task_definition_stack import ColorAppTaskDefinitionStack -from colorapp.appmesh_colorapp import ServiceMeshColorAppStack - -app = cdk.App() -ecs_stack = ECSStack(app, "ECSClusterStack") -appmesh_stack = AppMeshStack(app, "AppMeshStack") -ecr_stack = ECRStack(app, "ECRStack") -appmesh_colorapp_stack = ServiceMeshColorAppStack(app, "AppmeshColorappStack") -colorapp_task_definition_stack = ColorAppTaskDefinitionStack(app, "ColorAppTaskDefinitionStack",env={ - 'account': os.getenv('CDK_DEFAULT_ACCOUNT'), - 'region': os.getenv('CDK_DEFAULT_REGION') -} -) - -appmesh_stack.add_dependency(ecs_stack) -appmesh_colorapp_stack.add_dependency(appmesh_stack) -colorapp_task_definition_stack.add_dependency(appmesh_colorapp_stack) - -app.synth() diff --git a/python/appmesh-ecs/colorapp/appmesh_colorapp.py b/python/appmesh-ecs/colorapp/appmesh_colorapp.py deleted file mode 100644 index 3c503fad30..0000000000 --- a/python/appmesh-ecs/colorapp/appmesh_colorapp.py +++ /dev/null @@ -1,170 +0,0 @@ -# give me a boilerplate cdk class for an appmesh stack with 6 virtual nodes, 1 virtual router, 1 appmesh route, and 2 virtual services. -from aws_cdk import ( - aws_ec2 as ec2, - aws_servicediscovery as servicediscovery, -) -from aws_cdk import Stack, Tags, App -from constructs import Construct -import aws_cdk as core -import aws_cdk.aws_ecs as ecs -import aws_cdk.aws_ecr as ecr -import aws_cdk.aws_dynamodb as dynamodb -import aws_cdk.aws_iam as iam -import aws_cdk.aws_appmesh as appmesh - -class ServiceMeshColorAppStack(Stack): - # This function creates the virtual nodes in app mesh that correspond to each color - def createVirtualNodes(stack: core.Stack, color: str, environment_name: str, mesh): - environment_name ="appmesh-env" - return appmesh.VirtualNode( - scope=stack, - id=f"VirtualNode-{color}", - mesh=mesh, - virtual_node_name=f"colorteller-{color}-vn", - - listeners=[ - appmesh.VirtualNodeListener.http( - port=9080, - health_check=appmesh.HealthCheck.http( - healthy_threshold=3, - unhealthy_threshold=2, - timeout=core.Duration.millis(2000), - interval=core.Duration.millis(5000), - path="/ping", - ), - )], - service_discovery=appmesh.ServiceDiscovery.dns( - hostname=core.Fn.sub( - "colorteller-${color}.${ServicesDomain}", - { - "color": str(color), - "ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace") - } - ) - - - ) - ) - - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - my_mesh = appmesh.Mesh.from_mesh_name(self, "Mesh", mesh_name=core.Fn.import_value(f"{environment_name}:Mesh")) - color_teller_colors = ["red", "black", "blue"] - virtual_nodes = [] - for color in color_teller_colors: - virtual_nodes.append(self.createVirtualNodes(color, environment_name, my_mesh)) - - ColorTellerVirtualRouter = appmesh.VirtualRouter( - self, "ColorTellerVirtualRouter", - mesh=my_mesh, - virtual_router_name="colorteller-vr", - listeners=[appmesh.VirtualRouterListener.http(port=9080)] - - ) - - # Creates an app mesh route that distributes traffic across 3 targets when the gateway is hit - - ColorTellerVirtualRouter.add_route( - "MyRoute", - route_name="colorteller-route", - route_spec=appmesh.RouteSpec.http( - weighted_targets=[ - appmesh.WeightedTarget( - virtual_node=virtual_nodes[0], - port=9080, - weight=1 - ), - appmesh.WeightedTarget( - virtual_node=virtual_nodes[1], - port=9080, - weight=1 - ), - appmesh.WeightedTarget( - virtual_node=virtual_nodes[2], - port=9080, - weight=1 - ) - ], - match=appmesh.HttpRouteMatch( - path=appmesh.HttpRoutePathMatch.starts_with("/") - ) - ) - ) - - CollorTellerVirtualService = appmesh.VirtualService( - self, "ColorTellerService", - virtual_service_provider=appmesh.VirtualServiceProvider.virtual_router(virtual_router=ColorTellerVirtualRouter), - virtual_service_name=core.Fn.sub("colorteller.${ServicesDomain}" , {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), - - - ) - ColorTellerWhiteVirtualNode = appmesh.VirtualNode( - self, "ColorTellerWhiteVirtualNode", - mesh=my_mesh, - virtual_node_name=f"colorteller-white-vn", - - listeners=[ - appmesh.VirtualNodeListener.http( - port=9080, - health_check=appmesh.HealthCheck.http( - healthy_threshold=3, - unhealthy_threshold=2, - timeout=core.Duration.millis(2000), - interval=core.Duration.millis(5000), - path="/ping", - ), - )], - service_discovery=appmesh.ServiceDiscovery.dns( - hostname=core.Fn.sub("colorteller.${ServicesDomain}",{"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ) - - - ) - - - TcpEchoVirtualNode = appmesh.VirtualNode( - self, "TcpEchoVirtualNode", - mesh=my_mesh, - virtual_node_name=f"tcpecho-vn", - - listeners=[ - appmesh.VirtualNodeListener.tcp(port=2701), - appmesh.VirtualNodeListener.tcp( - health_check=appmesh.HealthCheck.tcp( - healthy_threshold=2, - unhealthy_threshold=2, - timeout=core.Duration.millis(2000), - interval=core.Duration.millis(5000), - - ), - ) - - ], - service_discovery=appmesh.ServiceDiscovery.dns( - hostname=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ), - - ) - TCPEchoVirtualService = appmesh.VirtualService( - self, "TCPEchoVirtualService", - virtual_service_provider=appmesh.VirtualServiceProvider.virtual_node(TcpEchoVirtualNode), - virtual_service_name=core.Fn.sub("tcpecho.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}), - - ) - - ColorGatewayVirtualNode = appmesh.VirtualNode( - self, "ColorGatewayVirtualNode", - mesh=my_mesh, - virtual_node_name=f"colorgateway-vn", - - listeners=[appmesh.VirtualNodeListener.http(port=9080)], - service_discovery=appmesh.ServiceDiscovery.dns( - hostname=core.Fn.sub("colorgateway.${ServicesDomain}", {"ServicesDomain": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) - ), - - - ) - ColorGatewayVirtualNode.add_backend(appmesh.Backend.virtual_service(CollorTellerVirtualService)) - ColorGatewayVirtualNode.add_backend(appmesh.Backend.virtual_service(TCPEchoVirtualService)) \ No newline at end of file diff --git a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py b/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py deleted file mode 100644 index 0e0aff7bd6..0000000000 --- a/python/appmesh-ecs/colorapp/ecs_colorapp_stack.py +++ /dev/null @@ -1,65 +0,0 @@ -from aws_cdk import ( - aws_appmesh as appmesh, - -) -import aws_cdk as core -from aws_cdk import Stack, Tags, App -from constructs import Construct -import aws_cdk.aws_ecs as ecs -import aws_cdk.aws_servicediscovery as servicediscovery -import aws_cdk.aws_elasticloadbalancingv2 as elbv2 -import aws_cdk.aws_ec2 as ec2 -class ColorAppStack(Stack): - def create_color_discovery_record(self, color, namespace): - environment_name ="appmesh-env" - return servicediscovery.Service( - self, f"ColorTeller{color}ServiceDiscoveryRecord", - name=f"colorteller-{color}-service", - namespace=namespace, - dns_record_type=servicediscovery.DnsRecordType.A, - dns_ttl=core.Duration.seconds(30), - custom_health_check=servicediscovery.HealthCheckCustomConfig(failure_threshold=1) - - - ) - def create_color_service(self, color, dr): - environment_name ="appmesh-env" - task_definition_arn = core.Fn.import_value(f"{environment_name}:ColorTellerTaskDefinitionArn-{color}") - task_definition = ecs.TaskDefinition.from_task_definition_arn(self, f"ColorTeller{color}TaskDefinition", task_definition_arn=task_definition_arn) - return ecs.FargateService( - self, f"ColorTeller{color}Service", - cluster=core.Fn.import_value(f"{environment_name}:ECSCluster"), - service_name=f'colorteller-{color}-service', - desired_count=1, - max_healthy_percent=200, - min_healthy_percent=100, - task_definition=task_definition, - vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), - assign_public_ip=False, - security_groups=[core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup")], - cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=dr) - ) - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - colors = ["blue", "red", "black"] - namespace = servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes(self, "Namespace", - namespace_arn=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceARN"), - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - namespace_name=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceName")) - for color in colors: - dr = self.create_color_discovery_record(color, namespace) - - service = self.create_color_service(color, dr) - - ColorTellerWhiteServiceDiscoveryRecord = servicediscovery.Service( - self, "ColorTellerWhiteServiceDiscoveryRecord", - name="colorteller", - namespace=namespace, - dns_record_type=servicediscovery.DnsRecordType.A, - dns_ttl=core.Duration.seconds(30), - custom_health_check=servicediscovery.HealthCheckCustomConfig(failure_threshold=1) - - - ) - colorColorTellerWhiteService = self.create_color_service('white', ColorTellerWhiteServiceDiscoveryRecord) diff --git a/python/appmesh-ecs/container_images/colorteller/.gitignore b/python/appmesh-ecs/container_images/colorteller/.gitignore deleted file mode 100644 index 963c55f7b1..0000000000 --- a/python/appmesh-ecs/container_images/colorteller/.gitignore +++ /dev/null @@ -1 +0,0 @@ -teller diff --git a/python/appmesh-ecs/container_images/colorteller/Dockerfile b/python/appmesh-ecs/container_images/colorteller/Dockerfile deleted file mode 100644 index 73b77cbdb5..0000000000 --- a/python/appmesh-ecs/container_images/colorteller/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 AS builder -RUN yum update -y && \ - yum install -y ca-certificates unzip tar gzip git && \ - yum clean all && \ - rm -rf /var/cache/yum - -RUN curl -LO https://golang.org/dl/go1.17.1.linux-amd64.tar.gz && \ - tar -C /usr/local -xzvf go1.17.1.linux-amd64.tar.gz - -ENV PATH="${PATH}:/usr/local/go/bin" -ENV GOPATH="${HOME}/go" -ENV PATH="${PATH}:${GOPATH}/bin" - -# ARG GO_PROXY=https://proxy.golang.org -ARG GO_PROXY=direct -WORKDIR /go/src/github.com/aws/aws-app-mesh-examples/colorapp/teller - -# go.mod and go.sum go into their own layers. -COPY go.mod . -COPY go.sum . - -# Set the proxies for the go compiler -# RUN go env -w GOPROXY=${GO_PROXY} -- This line is commented out in favor of GOPRIVATE, but if GOPROXY works on your machine you can use this line instead. -RUN go env -w GOPRIVATE=* - -# This ensures `go mod download` happens only when go.mod and go.sum change. -RUN go mod download - -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /aws-app-mesh-examples-colorapp-teller . - -FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 -RUN yum update -y && \ - yum install -y ca-certificates && \ - yum clean all && \ - rm -rf /var/cache/yum - -COPY --from=builder /aws-app-mesh-examples-colorapp-teller /bin/aws-app-mesh-examples-colorapp-teller - -ENTRYPOINT ["/bin/aws-app-mesh-examples-colorapp-teller"] diff --git a/python/appmesh-ecs/container_images/colorteller/deploy.sh b/python/appmesh-ecs/container_images/colorteller/deploy.sh deleted file mode 100755 index 4d54d3f293..0000000000 --- a/python/appmesh-ecs/container_images/colorteller/deploy.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -source ~/.bashrc - -AWS_ACCOUNT_ID=$1 -AWS_DEFAULT_REGION=$2 - -if [ -z $AWS_ACCOUNT_ID ]; then - echo "AWS_ACCOUNT_ID environment variable is not set." - exit 1 -fi - -if [ -z $AWS_DEFAULT_REGION ]; then - echo "AWS_DEFAULT_REGION environment variable is not set." - exit 1 -fi - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -ECR_URL="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" -COLOR_TELLER_IMAGE=${COLOR_TELLER_IMAGE:-"${ECR_URL}/colorteller"} -GO_PROXY=${GO_PROXY:-"https://proxy.golang.org"} -AWS_CLI_VERSION=$(aws --version 2>&1 | cut -d/ -f2 | cut -d. -f1) - -ecr_login() { - if [ $AWS_CLI_VERSION -gt 1 ]; then - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | \ - docker login --username AWS --password-stdin ${ECR_URL} - else - $(aws ecr get-login --no-include-email) - fi -} - -describe_create_ecr_registry() { - local repo_name=$1 - local region=$2 - aws ecr describe-repositories --repository-names ${repo_name} --region ${region} \ - || aws ecr create-repository --repository-name ${repo_name} --region ${region} -} - -# build -docker build --build-arg GO_PROXY=$GO_PROXY -t $COLOR_TELLER_IMAGE ${DIR} - -# push -ecr_login -describe_create_ecr_registry colorteller ${AWS_DEFAULT_REGION} -docker push $COLOR_TELLER_IMAGE diff --git a/python/appmesh-ecs/container_images/colorteller/go.mod b/python/appmesh-ecs/container_images/colorteller/go.mod deleted file mode 100644 index a3f072b905..0000000000 --- a/python/appmesh-ecs/container_images/colorteller/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module github.com/aws/aws-app-mesh-examples/colorapp/teller - -go 1.12 - -require ( - github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect - github.com/aws/aws-sdk-go v1.44.209 // indirect - github.com/aws/aws-xray-sdk-go v0.9.4 - github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/stretchr/testify v1.8.1 // indirect - golang.org/x/net v0.7.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) diff --git a/python/appmesh-ecs/container_images/colorteller/go.sum b/python/appmesh-ecs/container_images/colorteller/go.sum deleted file mode 100644 index 8d1d194072..0000000000 --- a/python/appmesh-ecs/container_images/colorteller/go.sum +++ /dev/null @@ -1,73 +0,0 @@ -github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps= -github.com/aws/aws-sdk-go v1.44.209/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-xray-sdk-go v0.9.4 h1:3mtFCrgFR5IefmWFV5pscHp9TTyOWuqaIKJIY0d1Y4g= -github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= -github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf h1:XI2tOTCBqEnMyN2j1yPBI07yQHeywUSCEf8YWqf0oKw= -github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/python/appmesh-ecs/container_images/colorteller/main.go b/python/appmesh-ecs/container_images/colorteller/main.go deleted file mode 100644 index 14dae9d2b7..0000000000 --- a/python/appmesh-ecs/container_images/colorteller/main.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "fmt" - "log" - "net/http" - "os" - - "github.com/aws/aws-xray-sdk-go/xray" -) - -const defaultPort = "8080" -const defaultColor = "black" -const defaultStage = "default" - -func getServerPort() string { - port := os.Getenv("SERVER_PORT") - if port != "" { - return port - } - - return defaultPort -} - -func getColor() string { - color := os.Getenv("COLOR") - if color != "" { - return color - } - - return defaultColor -} - -func getStage() string { - stage := os.Getenv("STAGE") - if stage != "" { - return stage - } - - return defaultStage -} - -type colorHandler struct{} -func (h *colorHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - log.Println("color requested, responding with", getColor()) - fmt.Fprint(writer, getColor()) -} - -type pingHandler struct{} -func (h *pingHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - log.Println("ping requested, reponding with HTTP 200") - writer.WriteHeader(http.StatusOK) -} - -func main() { - log.Println("starting server, listening on port " + getServerPort()) - xraySegmentNamer := xray.NewFixedSegmentNamer(fmt.Sprintf("%s-colorteller-%s", getStage(), getColor())) - http.Handle("/", xray.Handler(xraySegmentNamer, &colorHandler{})) - http.Handle("/ping", xray.Handler(xraySegmentNamer, &pingHandler{})) - http.ListenAndServe(":"+getServerPort(), nil) -} diff --git a/python/appmesh-ecs/container_images/gateway/.gitignore b/python/appmesh-ecs/container_images/gateway/.gitignore deleted file mode 100644 index ad230ccfee..0000000000 --- a/python/appmesh-ecs/container_images/gateway/.gitignore +++ /dev/null @@ -1 +0,0 @@ -gateway diff --git a/python/appmesh-ecs/container_images/gateway/Dockerfile b/python/appmesh-ecs/container_images/gateway/Dockerfile deleted file mode 100644 index ea3fb95be4..0000000000 --- a/python/appmesh-ecs/container_images/gateway/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 AS builder -RUN yum update -y && \ - yum install -y ca-certificates unzip tar gzip git && \ - yum clean all && \ - rm -rf /var/cache/yum - -RUN curl -LO https://golang.org/dl/go1.17.1.linux-amd64.tar.gz && \ - tar -C /usr/local -xzvf go1.17.1.linux-amd64.tar.gz - -ENV PATH="${PATH}:/usr/local/go/bin" -ENV GOPATH="${HOME}/go" -ENV PATH="${PATH}:${GOPATH}/bin" - -# ARG GO_PROXY=https://proxy.golang.org -ARG GO_PROXY=direct - -WORKDIR /go/src/github.com/aws/aws-app-mesh-examples/colorapp/gateway - -# go.mod and go.sum go into their own layers. -COPY go.mod . -COPY go.sum . - -# Set the proxies for the go compiler -# RUN go env -w GOPROXY=${GO_PROXY} -- This line is commented out in favor of GOPRIVATE, but if GOPROXY works on your machine you can use this line instead. -RUN go env -w GOPRIVATE=* -# This ensures `go mod download` happens only when go.mod and go.sum change. -RUN go mod download - -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /aws-app-mesh-examples-colorapp-gateway . - -# Because this was built on an M1 Mac, you may be able to remove the --platform flag -FROM --platform=linux/amd64 public.ecr.aws/amazonlinux/amazonlinux:2 -RUN yum update -y && \ - yum install -y ca-certificates && \ - yum clean all && \ - rm -rf /var/cache/yum - -COPY --from=builder /aws-app-mesh-examples-colorapp-gateway /bin/aws-app-mesh-examples-colorapp-gateway - -ENTRYPOINT ["/bin/aws-app-mesh-examples-colorapp-gateway"] diff --git a/python/appmesh-ecs/container_images/gateway/deploy.sh b/python/appmesh-ecs/container_images/gateway/deploy.sh deleted file mode 100755 index 50c906215c..0000000000 --- a/python/appmesh-ecs/container_images/gateway/deploy.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# vim:syn=sh:ts=4:sw=4:et:ai - -set -ex - -source ~/.bashrc -AWS_ACCOUNT_ID=$1 -AWS_DEFAULT_REGION=$2 - -if [ -z $AWS_ACCOUNT_ID ]; then - echo "AWS_ACCOUNT_ID environment variable is not set." - # export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) - exit 1 -fi - -if [ -z $AWS_DEFAULT_REGION ]; then - echo "AWS_DEFAULT_REGION environment variable is not set." - # export AWS_DEFAULT_REGION=$(aws configure get region) - exit 1 -fi - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" -ECR_URL="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" -COLOR_GATEWAY_IMAGE=${COLOR_GATEWAY_IMAGE:-"${ECR_URL}/gateway"} -GO_PROXY=${GO_PROXY:-"https://proxy.golang.org"} -AWS_CLI_VERSION=$(aws --version 2>&1 | cut -d/ -f2 | cut -d. -f1) - -ecr_login() { - if [ $AWS_CLI_VERSION -gt 1 ]; then - aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | \ - docker login --username AWS --password-stdin ${ECR_URL} - else - $(aws ecr get-login --no-include-email) - fi -} - -describe_create_ecr_registry() { - local repo_name=$1 - local region=$2 - aws ecr describe-repositories --repository-names ${repo_name} --region ${region} \ - || aws ecr create-repository --repository-name ${repo_name} --region ${region} -} - -# build - for Mac M1 based on https://stackoverflow.com/questions/68630526/lib64-ld-linux-x86-64-so-2-no-such-file-or-directory-error -docker build --platform linux/x86_64 --build-arg GO_PROXY=$GO_PROXY -t $COLOR_GATEWAY_IMAGE ${DIR} - -# push -ecr_login -describe_create_ecr_registry gateway ${AWS_DEFAULT_REGION} -docker push $COLOR_GATEWAY_IMAGE diff --git a/python/appmesh-ecs/container_images/gateway/go.mod b/python/appmesh-ecs/container_images/gateway/go.mod deleted file mode 100644 index e2d3c69279..0000000000 --- a/python/appmesh-ecs/container_images/gateway/go.mod +++ /dev/null @@ -1,16 +0,0 @@ -module github.com/aws/aws-app-mesh-examples/colorapp/gateway - -go 1.12 - -require ( - github.com/DATA-DOG/go-sqlmock v1.3.3 // indirect - github.com/aws/aws-sdk-go v1.44.209 // indirect - github.com/aws/aws-xray-sdk-go v0.9.4 - github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.8.1 // indirect - golang.org/x/net v0.7.0 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect -) diff --git a/python/appmesh-ecs/container_images/gateway/go.sum b/python/appmesh-ecs/container_images/gateway/go.sum deleted file mode 100644 index 8d1d194072..0000000000 --- a/python/appmesh-ecs/container_images/gateway/go.sum +++ /dev/null @@ -1,73 +0,0 @@ -github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/aws/aws-sdk-go v1.44.209 h1:wZuiaA4eaqYZmoZXqGgNHqVD7y7kUGFvACDGBgowTps= -github.com/aws/aws-sdk-go v1.44.209/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-xray-sdk-go v0.9.4 h1:3mtFCrgFR5IefmWFV5pscHp9TTyOWuqaIKJIY0d1Y4g= -github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= -github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf h1:XI2tOTCBqEnMyN2j1yPBI07yQHeywUSCEf8YWqf0oKw= -github.com/cihub/seelog v0.0.0-20151216151435-d2c6e5aa9fbf/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/python/appmesh-ecs/container_images/gateway/main.go b/python/appmesh-ecs/container_images/gateway/main.go deleted file mode 100644 index 7bf08e563c..0000000000 --- a/python/appmesh-ecs/container_images/gateway/main.go +++ /dev/null @@ -1,228 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "io/ioutil" - "log" - "math" - "net" - "net/http" - "os" - "strings" - "sync" - - "github.com/aws/aws-xray-sdk-go/xray" - "github.com/pkg/errors" -) - -const defaultPort = "8080" -const defaultStage = "default" -const maxColors = 1000 - -var colors [maxColors]string -var colorsIdx int -var colorsMutext = &sync.Mutex{} - -func getServerPort() string { - port := os.Getenv("SERVER_PORT") - if port != "" { - return port - } - - return defaultPort -} - -func getStage() string { - stage := os.Getenv("STAGE") - if stage != "" { - return stage - } - - return defaultStage -} - -func getColorTellerEndpoint() (string, error) { - colorTellerEndpoint := os.Getenv("COLOR_TELLER_ENDPOINT") - if colorTellerEndpoint == "" { - return "", errors.New("COLOR_TELLER_ENDPOINT is not set") - } - return colorTellerEndpoint, nil -} - -type colorHandler struct{} - -func (h *colorHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - color, err := getColorFromColorTeller(request) - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - writer.Write([]byte("500 - Unexpected Error")) - return - } - - colorsMutext.Lock() - defer colorsMutext.Unlock() - - addColor(color) - statsJson, err := json.Marshal(getRatios()) - if err != nil { - fmt.Fprintf(writer, `{"color":"%s", "error":"%s"}`, color, err) - return - } - fmt.Fprintf(writer, `{"color":"%s", "stats": %s}`, color, statsJson) -} - -func addColor(color string) { - colors[colorsIdx] = color - - colorsIdx += 1 - if colorsIdx >= maxColors { - colorsIdx = 0 - } -} - -func getRatios() map[string]float64 { - counts := make(map[string]int) - var total = 0 - - for _, c := range colors { - if c != "" { - counts[c] += 1 - total += 1 - } - } - - ratios := make(map[string]float64) - for k, v := range counts { - ratio := float64(v) / float64(total) - ratios[k] = math.Round(ratio*100) / 100 - } - - return ratios -} - -type clearColorStatsHandler struct{} - -func (h *clearColorStatsHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - colorsMutext.Lock() - defer colorsMutext.Unlock() - - colorsIdx = 0 - for i := range colors { - colors[i] = "" - } - - fmt.Fprint(writer, "cleared") -} - -func getColorFromColorTeller(request *http.Request) (string, error) { - colorTellerEndpoint, err := getColorTellerEndpoint() - if err != nil { - return "-n/a-", err - } - - client := xray.Client(&http.Client{}) - req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", colorTellerEndpoint), nil) - log.Printf("Requesting color from %s", colorTellerEndpoint) - log.Printf("Gateway Request is : %v", req) - if err != nil { - return "-n/a-", err - } - - resp, err := client.Do(req.WithContext(request.Context())) - log.Printf("Gateway Response is : %v", resp) - if err != nil { - return "-n/a-", err - } - - defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "-n/a-", err - } - - color := strings.TrimSpace(string(body)) - if len(color) < 1 { - return "-n/a-", errors.New("Empty response from colorTeller") - } - - return color, nil -} - -func getTCPEchoEndpoint() (string, error) { - tcpEchoEndpoint := os.Getenv("TCP_ECHO_ENDPOINT") - if tcpEchoEndpoint == "" { - return "", errors.New("TCP_ECHO_ENDPOINT is not set") - } - return tcpEchoEndpoint, nil -} - -type tcpEchoHandler struct{} - -func (h *tcpEchoHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - endpoint, err := getTCPEchoEndpoint() - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(writer, "tcpecho endpoint is not set") - return - } - - log.Printf("Dialing tcp endpoint %s", endpoint) - conn, err := net.Dial("tcp", endpoint) - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(writer, "Dial failed, err:%s", err.Error()) - return - } - defer conn.Close() - - strEcho := "Hello from gateway" - log.Printf("Writing '%s'", strEcho) - _, err = fmt.Fprintf(conn, strEcho) - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(writer, "Write to server failed, err:%s", err.Error()) - return - } - - reply, err := bufio.NewReader(conn).ReadString('\n') - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - fmt.Fprintf(writer, "Read from server failed, err:%s", err.Error()) - return - } - - fmt.Fprintf(writer, "Response from tcpecho server: %s", reply) -} - -type pingHandler struct{} - -func (h *pingHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - log.Println("ping requested, reponding with HTTP 200") - writer.WriteHeader(http.StatusOK) -} - -func main() { - log.Println("Starting server, listening on port " + getServerPort()) - - colorTellerEndpoint, err := getColorTellerEndpoint() - if err != nil { - log.Fatalln(err) - } - tcpEchoEndpoint, err := getTCPEchoEndpoint() - if err != nil { - log.Println(err) - } - - log.Println("Using color-teller at " + colorTellerEndpoint) - log.Println("Using tcp-echo at " + tcpEchoEndpoint) - - xraySegmentNamer := xray.NewFixedSegmentNamer(fmt.Sprintf("%s-gateway", getStage())) - - http.Handle("/color", xray.Handler(xraySegmentNamer, &colorHandler{})) - http.Handle("/color/clear", xray.Handler(xraySegmentNamer, &clearColorStatsHandler{})) - http.Handle("/tcpecho", xray.Handler(xraySegmentNamer, &tcpEchoHandler{})) - http.Handle("/ping", xray.Handler(xraySegmentNamer, &pingHandler{})) - log.Fatal(http.ListenAndServe(":"+getServerPort(), nil)) -} diff --git a/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py b/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py deleted file mode 100644 index 693ae16836..0000000000 --- a/python/appmesh-ecs/core_infrastructure/appmesh/appmesh_stack.py +++ /dev/null @@ -1,22 +0,0 @@ -# write a boilerplate class for an appmesh mesh with cdk -from aws_cdk import ( - aws_appmesh as appmesh, - -) -import aws_cdk as core -from aws_cdk import Stack, Tags, App -from constructs import Construct -class AppMeshStack(Stack): - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - # Creates an AWS App Mesh mesh - mesh = appmesh.Mesh(self, "ecs-mesh", mesh_name=environment_name) - - core.CfnOutput( - self, "MeshName", - value=mesh.mesh_name, - description="A reference to the AppMesh Meshs", - export_name=f"{environment_name}:Mesh" - ) diff --git a/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py b/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py deleted file mode 100644 index f19aab90a7..0000000000 --- a/python/appmesh-ecs/core_infrastructure/ecr/ecr_stack.py +++ /dev/null @@ -1,79 +0,0 @@ -# write a boilerplate class for an appmesh mesh with cdk -from aws_cdk import ( - aws_appmesh as appmesh, - -) -import os -import aws_cdk as core -from aws_cdk import Stack, Tags, App -from constructs import Construct -import aws_cdk.aws_ecs as ecs -import aws_cdk.aws_servicediscovery as servicediscovery -import aws_cdk.aws_elasticloadbalancingv2 as elbv2 -import aws_cdk.aws_ec2 as ec2 -import aws_cdk.aws_ecr as ecr -from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform -import cdk_ecr_deployment as ecrdeploy -import subprocess -class ECRStack(Stack): - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - # Creates two ecr repositories that will host the docker images for the color teller gateway app and color teller app - ColorAppGatewayRepository = ecr.Repository(self, "GatewayRepository", repository_name="gateway") - ColorAppColorTellerRepository = ecr.Repository(self, "ColorTellerRepository", repository_name="colorteller") - - # The docker images were built on a M1 Macbook Pro, you may have to rebuild your images - gatewayAsset = DockerImageAsset(self, "gatewayAsset", - directory="./container_images/gateway", - build_args={ - "GOPROXY": 'direct' - }, - platform=Platform.LINUX_AMD64 - ) - colortellerAsset = DockerImageAsset(self, "colortellerAsset", - directory="./container_images/colorteller", - build_args={ - "GOPROXY": 'direct' - } - - - ) - - ecrdeploy.ECRDeployment(self, "DeployGatewayImage", - src=ecrdeploy.DockerImageName(gatewayAsset.image_uri), - dest=ecrdeploy.DockerImageName(f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/gateway:latest") - ) - - - - ecrdeploy.ECRDeployment(self, "DeployColorTellerImage", - src=ecrdeploy.DockerImageName(colortellerAsset.image_uri), - dest=ecrdeploy.DockerImageName(f"{core.Aws.ACCOUNT_ID}.dkr.ecr.{core.Aws.REGION}.amazonaws.com/colorteller:latest") - ) - core.CfnOutput( - self, "ColorAppGatewayRepository", - value=ColorAppGatewayRepository.repository_uri, - description="ColorAppRepository", - export_name=f"{environment_name}:ColorAppRepository" - ) - core.CfnOutput( - self, "ColorAppGatewayRepositoryName", - value=ColorAppGatewayRepository.repository_name, - description="ColorAppGatewayRepository", - export_name=f"{environment_name}:ColorAppGatewayRepository" - ) - core.CfnOutput( - self, "ColorAppColorTellerRepository", - value=ColorAppColorTellerRepository.repository_uri, - description="ColorAppColorTellerRepository", - export_name=f"{environment_name}:ColorAppColorTellerRepository" - ) - core.CfnOutput( - self, "ColorAppColorTellerRepositoryName", - value=ColorAppColorTellerRepository.repository_name, - description="ColorAppColorTellerRepository", - export_name=f"{environment_name}:ColorAppColorTellerRepositoryName" - ) - diff --git a/python/appmesh-ecs/core_infrastructure/ecs/__init__.py b/python/appmesh-ecs/core_infrastructure/ecs/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py b/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py deleted file mode 100644 index f7a9f52f3d..0000000000 --- a/python/appmesh-ecs/core_infrastructure/ecs/ecs_stack.py +++ /dev/null @@ -1,174 +0,0 @@ -import aws_cdk.aws_ecs as ecs -import aws_cdk.aws_ec2 as ec2 -import aws_cdk.aws_autoscaling as autoscaling -from aws_cdk import Stack, Tags, App -from constructs import Construct -import aws_cdk as core -import aws_cdk.aws_ssm as ssm -import aws_cdk.aws_iam as iam -import aws_cdk.aws_logs as logs -import aws_cdk.aws_servicediscovery as servicediscovery -class ECSStack(Stack): - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - vpc = ec2.Vpc(self, "AppMeshVPC", - ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"), - create_internet_gateway=True, - max_azs=2, - nat_gateways=2, - enable_dns_hostnames=True, - enable_dns_support=True, - vpc_name="App-Mesh-VPC", - subnet_configuration=[ - ec2.SubnetConfiguration( - subnet_type=ec2.SubnetType.PUBLIC, - name="Public", - cidr_mask=24 - ), - ec2.SubnetConfiguration( - subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, - name="Private", - cidr_mask=24 - ) - ] - ) - - - AWSRegion=core.Stack.of(self).region - AWSStackId=core.Stack.of(self).stack_id - # The following creates the ECS Cluster along with its security groups. It also creates the proper roles that the ECS tasks will assume - # as well as the log group that the tasks log to and the service discovery namespace (created under AWS Cloud Map in the console) - ecsCluster = ecs.Cluster(self, "ecsCluster", - cluster_name="App-Mesh-ECS-Cluster", - vpc=vpc - - ) - - ECSServiceDiscoveryNamespace = servicediscovery.PrivateDnsNamespace(self, "ServiceDiscoveryNamespace", - name="appmesh.local", - vpc=vpc - ) - ECSServiceLogGroup = logs.LogGroup(self, "ECSServiceLogGroup", - log_group_name=f"{ecsCluster.cluster_name}-service", - removal_policy=core.RemovalPolicy.DESTROY, - retention=logs.RetentionDays.FIVE_DAYS, - ) - ECSTaskIamRole = iam.Role(self, "ECSTaskIamRole", - assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"), - managed_policies=[ - iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchFullAccess"), - iam.ManagedPolicy.from_aws_managed_policy_name("AWSAppMeshEnvoyAccess"), - iam.ManagedPolicy.from_aws_managed_policy_name("AWSXRayDaemonWriteAccess"), - ], - ) - TaskExecutionRole = iam.Role(self, "TaskexecutionRole", - assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"), - managed_policies=[ - iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly"), - iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchLogsFullAccess"), - ], - ) - ECSSecurityGroup = ec2.SecurityGroup(self, "ECSSecurityGroup", - vpc=vpc, - description="ECS Security Group", - allow_all_outbound=True, - ) - ECSSecurityGroup.add_ingress_rule(peer=ec2.Peer.ipv4(vpc.vpc_cidr_block), connection=ec2.Port.all_traffic(), description="allow any from LAN") - vpc_id_ssm_store = ssm.StringParameter(self, "vpcid", parameter_name="vpc_id", string_value=vpc.vpc_id) - core.CfnOutput( - self, "ECSCluster", - value=ecsCluster.cluster_name, - export_name=f"{environment_name}:ECSCluster" - ) - core.CfnOutput( - self, "ECSClusterARN", - value=ecsCluster.cluster_arn, - export_name=f"{environment_name}:ECSClusterARN" - ) - - core.CfnOutput( - self, "ECSServiceDiscoveryNamespace", - value=ECSServiceDiscoveryNamespace.namespace_name, - export_name=f"{environment_name}:ECSServiceDiscoveryNamespace" - ) - core.CfnOutput( - self, "ECSServiceDiscoveryNamespaceARN", - value=ECSServiceDiscoveryNamespace.namespace_arn, - export_name=f"{environment_name}:ECSServiceDiscoveryNamespaceARN" - ) - core.CfnOutput( - self, "ECSServiceDiscoveryId", - value=ECSServiceDiscoveryNamespace.namespace_id, - export_name=f"{environment_name}:ECSServiceDiscoveryNamespaceID" - ) - core.CfnOutput( - self, "ECSServicelogGroup", - value=ECSServiceLogGroup.log_group_name, - export_name=f"{environment_name}:ECSServicelogGroupName" - ) - core.CfnOutput( - self, "ECSServicelogGroupARN", - value=ECSServiceLogGroup.log_group_arn, - export_name=f"{environment_name}:ECSServicelogGroupARN" - ) - core.CfnOutput(self, "ECSServiceSecurityGroup", - value=ECSSecurityGroup.security_group_id, - export_name=f"{environment_name}:ECSServiceSecurityGroup") - - core.CfnOutput( - self, "Task_Execution_Iam_Role", - value=TaskExecutionRole.role_arn, - export_name=f"{environment_name}:TaskExecutionIamRole" - ) - - core.CfnOutput( - self, "ECS_Task_Iam_Role", - value=ECSTaskIamRole.role_arn, - export_name=f"{environment_name}:ECSTaskIamRole" - ) - core.CfnOutput( - self, "VPCID", - value=vpc.vpc_id, - export_name=f"{environment_name}:VPCID" - ) - core.CfnOutput( - self, "VpcAvailabilityZones", - value=core.Fn.join(',', vpc.availability_zones), - export_name=f"{environment_name}:VpcAvailabilityZones" - ) - private_subnet_ids = [subnet.subnet_id for subnet in vpc.private_subnets] - core.CfnOutput(self, "PrivateSubnetIds", - value=core.Fn.join(',', private_subnet_ids), - export_name=f"{environment_name}:MyPrivateSubnetIds" - ) - public_subnet_ids = [subnet.subnet_id for subnet in vpc.public_subnets] - core.CfnOutput(self, "PublicSubnetIds", - value=core.Fn.join(',', public_subnet_ids), - export_name=f"{environment_name}:MyPublicSubnetIds" - ) - core.CfnOutput( - self, "Public Subnet 1", - value=vpc.public_subnets[0].subnet_id, - export_name=f"{environment_name}:PublicSubnet1" - ) - core.CfnOutput( - self, "Public Subnet 2", - value=vpc.public_subnets[1].subnet_id, - export_name=f"{environment_name}:PublicSubnet2" - ) - core.CfnOutput( - self, "Private Subnet 1", - value=vpc.private_subnets[0].subnet_id, - export_name=f"{environment_name}:PrivateSubnet1" - ) - core.CfnOutput( - self, "Private Subnet 2", - value=vpc.private_subnets[1].subnet_id, - export_name=f"{environment_name}:PrivateSubnet2" - ) - core.CfnOutput(self, "VpcCidr", - value=vpc.vpc_cidr_block, - export_name=f"{environment_name}:VpcCidr" - ) \ No newline at end of file diff --git a/python/appmesh-ecs/core_infrastructure/vpc/__init__.py b/python/appmesh-ecs/core_infrastructure/vpc/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py b/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py deleted file mode 100644 index c2940b3733..0000000000 --- a/python/appmesh-ecs/core_infrastructure/vpc/vpc_stack.py +++ /dev/null @@ -1,66 +0,0 @@ -import aws_cdk.aws_ec2 as ec2 -import aws_cdk.aws_s3 as s3 -import aws_cdk.aws_ssm as ssm -from aws_cdk import Stack, Tags, App -from constructs import Construct -import aws_cdk as core -class VPCStack(Stack): - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - # The following code creates the core VPC infrastructure with two public subnets and two private subnets, as well as route tables - # that route private subnets to the internet using NAT gateways - - vpc = ec2.Vpc(self, "AppMeshVPC", - ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"), - create_internet_gateway=True, - max_azs=2, - nat_gateways=2, - enable_dns_hostnames=True, - enable_dns_support=True, - vpc_name="App-Mesh-VPC", - subnet_configuration=[ - ec2.SubnetConfiguration( - subnet_type=ec2.SubnetType.PUBLIC, - name="Public", - cidr_mask=24 - ), - ec2.SubnetConfiguration( - subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, - name="Private", - cidr_mask=24 - ) - ] - ) - vpc_id_ssm_store = ssm.StringParameter(self, "vpcid", parameter_name="vpc_id", string_value=vpc.vpc_id) - core.CfnOutput( - - self, "VPCID", - value=vpc.vpc_id, - export_name=f"{environment_name}:VPCID" - ) - core.CfnOutput( - self, "Public Subnet 1", - value=vpc.public_subnets[0].subnet_id, - export_name=f"{environment_name}:PublicSubnet1" - ) - core.CfnOutput( - self, "Public Subnet 2", - value=vpc.public_subnets[1].subnet_id, - export_name=f"{environment_name}:PublicSubnet2" - ) - core.CfnOutput( - self, "Private Subnet 1", - value=vpc.private_subnets[0].subnet_id, - export_name=f"{environment_name}:PrivateSubnet1" - ) - core.CfnOutput( - self, "Private Subnet 2", - value=vpc.private_subnets[1].subnet_id, - export_name=f"{environment_name}:PrivateSubnet2" - ) - core.CfnOutput(self, "VpcCidr", - value=vpc.vpc_cidr_block, - export_name=f"{environment_name}:VpcCidr" - ) \ No newline at end of file diff --git a/python/appmesh-ecs/task_definitions/__init__.py b/python/appmesh-ecs/task_definitions/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py b/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py deleted file mode 100644 index 4ffd7938c9..0000000000 --- a/python/appmesh-ecs/task_definitions/color_app_task_definition_stack.py +++ /dev/null @@ -1,435 +0,0 @@ -# give me a boilerplate cdk class -import aws_cdk as core -import aws_cdk.aws_ec2 as ec2 -import aws_cdk.aws_ecs as ecs -import aws_cdk.aws_iam as iam -import aws_cdk.aws_ecr as ecr -import aws_cdk.aws_logs as logs -import aws_cdk.aws_appmesh as appmesh -import aws_cdk.aws_ssm as ssm -from aws_cdk.aws_ecs import BaseService -import aws_cdk.aws_elasticloadbalancingv2 as elbv2 -from aws_cdk import Stack, Tags, App, Duration -from constructs import Construct - -from aws_cdk import ( - aws_appmesh as appmesh, - -) -import aws_cdk as core -import aws_cdk.aws_servicediscovery as servicediscovery -class ColorAppTaskDefinitionStack(Stack): - - # This function creates the task definitions for each color service - def create_color_task_definition(self, color, environment_name): - task_definition = ecs.FargateTaskDefinition( - self, f"colorTellerTask-{color}", - family=f"{environment_name}-ColorTeller-{color}", - cpu=256, - memory_limit_mib=512, - task_role=iam.Role.from_role_arn( - self, f"TaskDefinitionTaskRole-{color}", - role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole") - ), - execution_role=iam.Role.from_role_arn( - self, f"ExecutionRole-{color}", - role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole") - ) - ) - proxy_config = ecs.AppMeshProxyConfiguration( - container_name="envoy", - properties=ecs.AppMeshProxyConfigurationProps( - ignored_uid=1337, - app_ports=[9080], - proxy_ingress_port=15000, - proxy_egress_port=15001, - egress_ignored_i_ps=["169.254.170.2,169.254.169.254"] - ) - - ) - envoy = task_definition.add_container( - "envoy", - image=ecs.ContainerImage.from_registry(f"840364872350.dkr.ecr.{core.Aws.REGION}.amazonaws.com/aws-appmesh-envoy:v1.29.6.1-prod"), - user="1337", - container_name="envoy", - memory_reservation_mib=256, - environment={ - "APPMESH_RESOURCE_ARN": core.Fn.sub("mesh/${MeshName}/virtualNode/colorteller-${app_name}-vn", {"MeshName": core.Fn.import_value(f"{environment_name}:Mesh"), "app_name": color }), - "ENVOY_LOG_LEVEL": "DEBUG", - "ENABLE_ENVOY_XRAY_TRACING": "1", - "ENABLE_ENVOY_STATS_TAGS": "1" - }, - health_check=ecs.HealthCheck( - command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], - interval=Duration.seconds(5), - retries=3, - timeout=Duration.seconds(2) - ), - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"colorteller-{color}-envoy", - log_group=logs.LogGroup.from_log_group_arn( - self, f"AppLogGroup-{color}-envoy", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - essential=True, - port_mappings=[ecs.PortMapping(container_port=15000, host_port=15000, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=15001, host_port=15001, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=9901, host_port=9901, protocol=ecs.Protocol.TCP),], - ulimits=[ecs.Ulimit(hard_limit=15000, name=ecs.UlimitName.NOFILE, soft_limit=15000)], - ) - - # Container Definitions - repo = ecr.Repository.from_repository_name(self, f"AppECRRepo-{color}", repository_name="colorteller") - app_container = task_definition.add_container( - "app", - image=ecs.ContainerImage.from_ecr_repository(repository=repo, tag='latest'), - # ecr.Repository.from_repository_arn( - # self, "AppECRRepo", - # repository_arn=f"arn:aws:ecr:{core.Aws.REGION}:{core.Aws.ACCOUNT_ID}:repository/colorteller", - - # ), - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"colorteller-{color}-app", - log_group=logs.LogGroup.from_log_group_arn( - self, f"AppLogGroup-{color}", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - environment={ - "COLOR": color, - "SERVER_PORT": "9080" - }, - port_mappings=[ecs.PortMapping(container_port=9080, host_port=9080, protocol=ecs.Protocol.TCP)], - # container_depends_on=[ - # ecs.ContainerDependency( - - # container=envoy, - # condition=ecs.ContainerDependencyCondition.HEALTHY - - # ) - # ] - ) - app_container.add_container_dependencies(ecs.ContainerDependency( - container=envoy, - condition=ecs.ContainerDependencyCondition.HEALTHY - )) - - xray_container = task_definition.add_container( - "xrayContainer", - image=ecs.ContainerImage.from_registry("amazon/aws-xray-daemon"), - user="1337", - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"colorteller-{color}-xray", - log_group=logs.LogGroup.from_log_group_arn( - self, f"XrayLogGroup-{color}", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - memory_reservation_mib=256, - port_mappings=[ecs.PortMapping(container_port=2000, host_port=2000, protocol=ecs.Protocol.TCP)], - - ) - return task_definition - # This function creates the ECS service for each corresponding color - def create_color_service(self, color, namespace, taskdef, imported_vpc ): - environment_name ="appmesh-env" - - cluster = ecs.Cluster.from_cluster_attributes(self, f"Cluster-{color}", - cluster_name=core.Fn.import_value(f"{environment_name}:ECSCluster"), - vpc=imported_vpc, - - ) - security_group_id = ec2.SecurityGroup.from_security_group_id( - self, f"SecurityGroup-{color}", - security_group_id=core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup") - ) - namespace_name = f'colorteller-{color}' - if color=='white': - namespace_name = 'colorteller' - - - return ecs.FargateService( - self, f"ColorTeller{color}Service", - cluster=cluster, - service_name=f'colorteller-{color}-service', - desired_count=1, - max_healthy_percent=200, - min_healthy_percent=100, - task_definition=taskdef, - vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), - assign_public_ip=False, - security_groups=[security_group_id], - cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=namespace, dns_record_type=servicediscovery.DnsRecordType.A, dns_ttl=core.Duration.seconds(300), failure_threshold=1, name=namespace_name) - ) - # This function creates the load balancer infrastructure - def create_load_balanced_service(self, vpc, color_gateway_service): - PublicLoadBalancerSG = ec2.SecurityGroup( - self, "PublicLoadBalancerSG", - vpc=vpc, - description="ECS Security Group", - - allow_all_outbound=True, - ) - TargetGroup = elbv2.ApplicationTargetGroup( - self, "WebTargetGroup", - target_group_name="color-teller-target-group2", - port=80, - vpc=vpc, - targets=[color_gateway_service], - target_type=elbv2.TargetType.IP, - protocol=elbv2.ApplicationProtocol.HTTP, - health_check=elbv2.HealthCheck( - path="/ping", - port="9080", - interval=Duration.seconds(6), - timeout=Duration.seconds(5), - healthy_threshold_count=2, - unhealthy_threshold_count=2, - ), - - - ) - TargetGroup.set_attribute(key="deregistration_delay.timeout_seconds", - value="120") - PublicLoadBalancerSG.add_ingress_rule(ec2.Peer.any_ipv4(), ec2.Port.tcp(80), "Allow HTTP Ingress") - lb = elbv2.ApplicationLoadBalancer(self, "PublicLoadBalancer", - - vpc=vpc, - internet_facing=True, - security_group=PublicLoadBalancerSG, - vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC), - ) - lb.set_attribute(key="idle_timeout.timeout_seconds", - value="30") - listener = lb.add_listener("Public Listener", - port=80, - - default_action=elbv2.ListenerAction.forward( - target_groups=[TargetGroup] - ), - ) - - # listener.add_targets("ECS", - # port=80, - # targets=[BaseService.load_balancer_target( - # self, - # container_name="app", - # container_port=9080 - # )], - - # )xw - WebLoadBalancerRule = elbv2.ApplicationListenerRule( - self, "WebLoadBalancerRule", - listener=listener, - priority=1, - action=elbv2.ListenerAction.forward(target_groups=[TargetGroup]), - conditions=[elbv2.ListenerCondition.path_patterns(["*"])], - ) - return lb.load_balancer_dns_name, TargetGroup - - - def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: - super().__init__(scope, id, **kwargs ) - environment_name ="appmesh-env" - mesh_name = core.Fn.import_value(f"{environment_name}:Mesh") - vpc_id = core.Fn.import_value(f"{environment_name}:VPCID") - vpc_id = ssm.StringParameter.value_from_lookup(self, "vpc_id") - imported_vpc = ec2.Vpc.from_lookup(self, "ImportedVPC", vpc_id=vpc_id) - imported_cluster = ecs.Cluster.from_cluster_attributes(self, f"ColorGatewayCluster", cluster_name=core.Fn.import_value(f"{environment_name}:ECSCluster"),vpc=imported_vpc) - color_teller_colors = ["red", "black", "blue", "white"] - - namespace = servicediscovery.PrivateDnsNamespace.from_private_dns_namespace_attributes(self, "Namespace", - namespace_arn=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceARN"), - namespace_id=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceID"), - namespace_name=core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespaceName")) - - # Loop through all the colors to create their task definitions and ECS services - for color in color_teller_colors: - task_definition = self.create_color_task_definition(color, environment_name) - service = self.create_color_service(color, namespace, taskdef=task_definition, imported_vpc=imported_vpc) - - core.CfnOutput( - self, f"ColorTellerTaskDefinitionArn-{color}", - value=task_definition.task_definition_arn, - export_name=f"{environment_name}:ColorTellerTaskDefinitionArn-{color}" - ) - - colorGatewayProxyConfiguration = ecs.AppMeshProxyConfiguration( - container_name="envoy", - properties=ecs.AppMeshProxyConfigurationProps( - proxy_ingress_port=15000, - proxy_egress_port=15001, - app_ports=[9080], - ignored_uid=1337, - egress_ignored_i_ps=["169.254.170.2,169.254.169.254"] - ) - - ) - colorGatewayTaskDefinition = ecs.FargateTaskDefinition(self, - "colorGatewayTaskDefinition", - cpu=256, - memory_limit_mib=512, - proxy_configuration=colorGatewayProxyConfiguration, - task_role=iam.Role.from_role_arn( - self, f"TaskDefinitionTaskRoleGateway", - role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole") - ), - execution_role=iam.Role.from_role_arn( - self, f"ExecutionRoleGateway", - role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole") - ) - ) - - color_gateway_repo = ecr.Repository.from_repository_name(self, f"AppECRRepoGateway", repository_name="gateway") - - # The following creates the containers for the gateway ECS service - # don't change the order of containers, the LB will use the app one by default since it is the first essential container - gateway_app_td = colorGatewayTaskDefinition.add_container( - "app", - image=ecs.ContainerImage.from_ecr_repository(repository=color_gateway_repo, tag='latest'), - port_mappings=[ecs.PortMapping(container_port=9080, host_port=9080, protocol=ecs.Protocol.TCP)], - environment= - {"SERVER_PORT": "9080", - "COLOR_TELLER_ENDPOINT": core.Fn.sub("colorteller.${SERVICES_DOMAIN}:9080", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")}) , - "TCP_ECHO_ENDPOINT": core.Fn.sub("tcpecho.${SERVICES_DOMAIN}:2701", {"SERVICES_DOMAIN": core.Fn.import_value(f"{environment_name}:ECSServiceDiscoveryNamespace")})} - - , - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"colorteller-gateway-app", - log_group=logs.LogGroup.from_log_group_arn( - self, f"AppLogGroupGateway-app", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - essential=True, - ) - gateway_envoy_td = colorGatewayTaskDefinition.add_container( - "envoy", - user="1337", - memory_reservation_mib=256, - image=ecs.ContainerImage.from_registry(f"840364872350.dkr.ecr.{core.Aws.REGION}.amazonaws.com/aws-appmesh-envoy:v1.29.6.1-prod"), - port_mappings=[ecs.PortMapping(container_port=15000, host_port=15000, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=15001, host_port=15001, protocol=ecs.Protocol.TCP), ecs.PortMapping(container_port=9901, host_port=9901, protocol=ecs.Protocol.TCP),], - ulimits=[ecs.Ulimit(name=ecs.UlimitName.NOFILE, soft_limit=15000, hard_limit=15000)], - - environment={ - "APPMESH_RESOURCE_ARN": f"mesh/{mesh_name}/virtualNode/colorgateway-vn", - "ENVOY_LOG_LEVEL": "DEBUG", - "ENABLE_ENVOY_XRAY_TRACING": "1", - "ENABLE_ENVOY_STATS_TAGS": "1"}, - - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"colorteller-gateway-envoy", - log_group=logs.LogGroup.from_log_group_arn( - self, f"AppLogGroupGateway-envoy", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - health_check=ecs.HealthCheck( - command=["CMD-SHELL", "curl -s http://localhost:9901/server_info | grep state | grep -q LIVE"], - interval=Duration.seconds(5), - retries=3, - timeout=Duration.seconds(2), - ), - essential=True, - ) - gateway_xray_td = colorGatewayTaskDefinition.add_container( - "xray", - image=ecs.ContainerImage.from_registry("amazon/aws-xray-daemon"), - user="1337", - port_mappings=[ecs.PortMapping(container_port=2000, host_port=2000, protocol=ecs.Protocol.TCP)], - memory_reservation_mib=256, - # logging=ecs.LogDrivers.aws_logs( - # stream_prefix="colorteller-gateway-", - # log_group=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN"), - - # ), - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"colorteller-gateway-xray", - log_group=logs.LogGroup.from_log_group_arn( - self, f"AppLogGroupGateway-xray", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - essential=True, - ) - - gateway_app_td.add_container_dependencies(ecs.ContainerDependency( - container=gateway_envoy_td, - condition=ecs.ContainerDependencyCondition.HEALTHY - )) - - TcpEchoTaskDefinition = ecs.FargateTaskDefinition( - self, f"TesterTaskDefinition", - family="tester", - cpu=256, - memory_limit_mib=512, - task_role=iam.Role.from_role_arn( - self, "TcpEchoTaskDefinition", - role_arn=core.Fn.import_value(f"{environment_name}:ECSTaskIamRole") - ), - execution_role=iam.Role.from_role_arn( - self, f"ExecutionRoleTCP", - role_arn=core.Fn.import_value(f"{environment_name}:TaskExecutionIamRole") - ) - - - - ) - TcpEchoTaskDefinition.add_container( - "app", - image=ecs.ContainerImage.from_registry("cjimti/go-echo"), - port_mappings=[ecs.PortMapping(container_port=2701, host_port=2701, protocol=ecs.Protocol.TCP)], - logging=ecs.LogDriver.aws_logs( - stream_prefix=f"tcp-echo", - log_group=logs.LogGroup.from_log_group_arn( - self, "tcpecho-app", - log_group_arn=core.Fn.import_value(f"{environment_name}:ECSServicelogGroupARN") - ) - ), - environment={"NODE_NAME": core.Fn.sub( - "mesh/${AppMeshMeshName}/virtualNode/tcpecho-vn", - {"AppMeshMeshName": f"{environment_name}:Mesh"} - )}, - essential=True, - ) - security_group_id = ec2.SecurityGroup.from_security_group_id( - self, f"SecurityGroupTcp", - security_group_id=core.Fn.import_value(f"{environment_name}:ECSServiceSecurityGroup") - ) - colorGatewayService = ecs.FargateService( - self, "ColorGatewayECSService", - cluster=imported_cluster, - service_name=f'colorgateway-service', - desired_count=1, - max_healthy_percent=200, - min_healthy_percent=100, - task_definition=colorGatewayTaskDefinition, - vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), - security_groups=[security_group_id], - cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=namespace, name='colorgateway',dns_ttl=core.Duration.seconds(300), dns_record_type=servicediscovery.DnsRecordType.A, failure_threshold=1) - ) - TcpEchoService = ecs.FargateService(self, f"TcpEchoService", - cluster=imported_cluster, - task_definition=TcpEchoTaskDefinition, - service_name=f"tcpecho", - max_healthy_percent=200, - min_healthy_percent=100, - desired_count=1, - security_groups=[security_group_id], - vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), - cloud_map_options=ecs.CloudMapOptions(cloud_map_namespace=namespace,dns_record_type=servicediscovery.DnsRecordType.A, - dns_ttl=core.Duration.seconds(300), failure_threshold=1, name="tcpecho")) - lb, tg = self.create_load_balanced_service(imported_vpc, color_gateway_service=colorGatewayService) - core.CfnOutput( - self, "ColorAppEndpoint", - description="Public endpoint for Color App Service", - value=core.Fn.join("", ["http://", lb]), - export_name="ColorAppEndpoint" - ) - core.CfnOutput( - self, "ColorGatewayTaskDefinitionArn", - value=colorGatewayTaskDefinition.task_definition_arn, - export_name=f"{environment_name}:ColorGatewayTaskDefinitionArn" - ) - - - \ No newline at end of file diff --git a/python/appmesh-ecs/tests/__init__.py b/python/appmesh-ecs/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/appmesh-ecs/tests/unit/__init__.py b/python/appmesh-ecs/tests/unit/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py b/python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py deleted file mode 100644 index e49aa6162c..0000000000 --- a/python/appmesh-ecs/tests/unit/test_new_cdk_appmesh_redo_stack.py +++ /dev/null @@ -1,15 +0,0 @@ -import aws_cdk as core -import aws_cdk.assertions as assertions - -from new_cdk_appmesh_redo.new_cdk_appmesh_redo_stack import NewCdkAppmeshRedoStack - -# example tests. To run these tests, uncomment this file along with the example -# resource in new_cdk_appmesh_redo/new_cdk_appmesh_redo_stack.py -def test_sqs_queue_created(): - app = core.App() - stack = NewCdkAppmeshRedoStack(app, "new-cdk-appmesh-redo") - template = assertions.Template.from_stack(stack) - -# template.has_resource_properties("AWS::SQS::Queue", { -# "VisibilityTimeout": 300 -# }) diff --git a/python/ecs-serviceconnect/.gitignore b/python/ecs-serviceconnect/.gitignore new file mode 100644 index 0000000000..37833f8beb --- /dev/null +++ b/python/ecs-serviceconnect/.gitignore @@ -0,0 +1,10 @@ +*.swp +package-lock.json +__pycache__ +.pytest_cache +.venv +*.egg-info + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/python/appmesh-ecs/README.md b/python/ecs-serviceconnect/README.md similarity index 66% rename from python/appmesh-ecs/README.md rename to python/ecs-serviceconnect/README.md index c53f0b50cc..60223f448e 100644 --- a/python/appmesh-ecs/README.md +++ b/python/ecs-serviceconnect/README.md @@ -1,16 +1,8 @@ # Welcome to your CDK Python project! -This is a blank project for CDK development with Python. +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. -The `cdk.json` file tells the CDK Toolkit how to execute your app. - -This project is set up like a standard Python project. The initialization -process also creates a virtualenv within this project, stored under the `.venv` -directory. To create the virtualenv it assumes that there is a `python3` -(or `python` for Windows) executable in your path with access to the `venv` -package. If for any reason the automatic creation of the virtualenv fails, -you can create the virtualenv manually. To manually create a virtualenv on MacOS and Linux: @@ -55,4 +47,9 @@ command. * `cdk diff` compare deployed stack with current state * `cdk docs` open CDK documentation + +To deploy this stack run `cdk deploy --all` + +You can then see how the two containers by running `curl /get-data`, which will return an array from the backend service at its local domain. + Enjoy! diff --git a/python/ecs-serviceconnect/app.py b/python/ecs-serviceconnect/app.py new file mode 100644 index 0000000000..9b947aeac9 --- /dev/null +++ b/python/ecs-serviceconnect/app.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +import aws_cdk as cdk +from cdk_examples_service_connect.cdk_examples_service_connect_stack import CdkExamplesServiceConnectStack + + +app = cdk.App() +CdkExamplesServiceConnectStack(app, "CdkExamplesServiceConnectStack") + +app.synth() diff --git a/python/appmesh-ecs/cdk.json b/python/ecs-serviceconnect/cdk.json similarity index 100% rename from python/appmesh-ecs/cdk.json rename to python/ecs-serviceconnect/cdk.json diff --git a/python/appmesh-ecs/colorapp/__init__.py b/python/ecs-serviceconnect/cdk_examples_service_connect/__init__.py similarity index 100% rename from python/appmesh-ecs/colorapp/__init__.py rename to python/ecs-serviceconnect/cdk_examples_service_connect/__init__.py diff --git a/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py b/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py new file mode 100644 index 0000000000..32ba9a281a --- /dev/null +++ b/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py @@ -0,0 +1,39 @@ +from aws_cdk import ( + # Duration, + Stack, + aws_ec2 as ec2, + # aws_sqs as sqs, +) +from constructs import Construct +from ecs.ecs_stack import EcsStack +from ecr.ecr_stack import EcrStack +class CdkExamplesServiceConnectStack(Stack): + + def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + # Creating a shared VPC with public subnets and private subnets with NAT Gateways + vpc = ec2.Vpc(self, "ServiceConnectVPC", + ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"), + create_internet_gateway=True, + max_azs=2, + nat_gateways=2, + enable_dns_hostnames=True, + enable_dns_support=True, + vpc_name="App-Mesh-VPC", + subnet_configuration=[ + ec2.SubnetConfiguration( + subnet_type=ec2.SubnetType.PUBLIC, + name="Public", + cidr_mask=24 + ), + ec2.SubnetConfiguration( + subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS, + name="Private", + cidr_mask=24 + ) + ] + ) + AWSRegion=Stack.of(self).region + AWSStackId=Stack.of(self).stack_id + ecr_stack = EcrStack(self, "EcrStack") + ecs_stack = EcsStack(self, "EcsStack", vpc=vpc, frontend_repository=ecr_stack.frontend_docker_asset, backend_data_repository=ecr_stack.backend_data_docker_asset) diff --git a/python/appmesh-ecs/core_infrastructure/appmesh/__init__.py b/python/ecs-serviceconnect/ecr/__init__.py similarity index 100% rename from python/appmesh-ecs/core_infrastructure/appmesh/__init__.py rename to python/ecs-serviceconnect/ecr/__init__.py diff --git a/python/ecs-serviceconnect/ecr/ecr_stack.py b/python/ecs-serviceconnect/ecr/ecr_stack.py new file mode 100644 index 0000000000..6d337f3a7f --- /dev/null +++ b/python/ecs-serviceconnect/ecr/ecr_stack.py @@ -0,0 +1,72 @@ +# write a boilerplate class for an appmesh mesh with cdk +# from aws_cdk import ( +# aws_appmesh as appmesh, + +# ) +# import os +# import aws_cdk as core +# from aws_cdk import Stack, Tags, App +from constructs import Construct +# import aws_cdk.aws_ecs as ecs +# import aws_cdk.aws_servicediscovery as servicediscovery +# import aws_cdk.aws_elasticloadbalancingv2 as elbv2 +# import aws_cdk.aws_ec2 as ec2 +# import aws_cdk.aws_ecr as ecr +# from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform +# import subprocess +from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform +import cdk_ecr_deployment as ecrdeploy + +from aws_cdk import ( + # Duration, + NestedStack, + Stack, + aws_ec2 as ec2, + aws_ecr as ecr, + aws_ecs as ecs, + Aws + # aws_sqs as sqs, +) +class EcrStack(NestedStack): + + def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: + super().__init__(scope, id, **kwargs ) + # Creates two ecr repositories that will host the docker images for the color teller gateway app and color teller app + FrontendRepository = ecr.Repository(self, "FrontendRepository", repository_name="frontend") + BackendDataRepository = ecr.Repository(self, "BackendDataRepository", repository_name="backend_data") + + # The docker images were built on a M1 Macbook Pro, you may have to rebuild your images + frontendAsset = DockerImageAsset(self, "frontendAsset", + directory="./services/frontend", + build_args={ + "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 + }, + + platform=Platform.LINUX_AMD64 + ) + dataAsset = DockerImageAsset(self, "dataAsset", + directory="./services/data", + + ) + # Deploying images to ECR + ecrdeploy.ECRDeployment(self, "DeployFrontendImage", + src=ecrdeploy.DockerImageName(frontendAsset.image_uri), + dest=ecrdeploy.DockerImageName(f"{Aws.ACCOUNT_ID}.dkr.ecr.{Aws.REGION}.amazonaws.com/frontend:latest") + ) + + + + ecrdeploy.ECRDeployment(self, "DeployBackendImage", + src=ecrdeploy.DockerImageName(dataAsset.image_uri), + dest=ecrdeploy.DockerImageName(f"{Aws.ACCOUNT_ID}.dkr.ecr.{Aws.REGION}.amazonaws.com/backend_data:latest") + ) + + # Exporting values to be used in other stacks + self.frontend_docker_asset = frontendAsset + self.backend_data_docker_asset = dataAsset + self.frontend_repo = FrontendRepository + self.backend_repo = BackendDataRepository + self.frontend_repository_uri = FrontendRepository.repository_uri + self.frontend_repository_name = FrontendRepository.repository_name + self.backend_data_repository_uri = BackendDataRepository.repository_uri + self.backend_data_repository_name = BackendDataRepository.repository_name \ No newline at end of file diff --git a/python/appmesh-ecs/core_infrastructure/ecr/__init__.py b/python/ecs-serviceconnect/ecs/__init__.py similarity index 100% rename from python/appmesh-ecs/core_infrastructure/ecr/__init__.py rename to python/ecs-serviceconnect/ecs/__init__.py diff --git a/python/ecs-serviceconnect/ecs/ecs_stack.py b/python/ecs-serviceconnect/ecs/ecs_stack.py new file mode 100644 index 0000000000..e587033c8d --- /dev/null +++ b/python/ecs-serviceconnect/ecs/ecs_stack.py @@ -0,0 +1,163 @@ +from aws_cdk import ( + NestedStack, + aws_ec2 as ec2, + aws_servicediscovery as servicediscovery, + aws_ecs as ecs, + Duration, + aws_logs as logs, + aws_iam as iam, + aws_iam as iam, + aws_ecr_assets as ecr_assets, + aws_elasticloadbalancingv2 as elbv2, + RemovalPolicy, + CfnOutput + +) +from constructs import Construct + +class EcsStack(NestedStack): + + def __init__(self, scope: Construct, construct_id: str, vpc: ec2.Vpc, frontend_repository: ecr_assets.DockerImageAsset, backend_data_repository: ecr_assets.DockerImageAsset, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) + # Creating the ECS Cluster and the cloud map namespace + ecs_cluster = ecs.Cluster(self, "ECSCluster", + vpc=vpc, + cluster_name="App-Service-Connect-Cluster", + container_insights=True) + default_cloud_map_namespace=ecs_cluster.add_default_cloud_map_namespace(name="scapp.local", use_for_service_connect=True, type=servicediscovery.NamespaceType.DNS_PRIVATE) + # Creating the Cloudwatch log group where ECS Logs will be stored + ECSServiceLogGroup = logs.LogGroup(self, "ECSServiceLogGroup", + log_group_name=f"{ecs_cluster.cluster_name}-service", + removal_policy=RemovalPolicy.DESTROY, + retention=logs.RetentionDays.FIVE_DAYS, + ) + # Creating the task and execution IAM roles that the containers will assume to read and write to cloudwatch, Task Execution + # Role will read from ECR + ECSTaskIamRole = iam.Role(self, "ECSTaskIamRole", + assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + managed_policies=[ + iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchFullAccess"), + ], + ) + TaskExecutionRole = iam.Role(self, "TaskexecutionRole", + assumed_by=iam.ServicePrincipal("ecs-tasks.amazonaws.com"), + managed_policies=[ + iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEC2ContainerRegistryReadOnly"), + iam.ManagedPolicy.from_aws_managed_policy_name("CloudWatchLogsFullAccess"), + ], + ) + # ECS Security group, this will allow access from the Load Balancer and allow LAN access so that the + # ECS containers can talk to eachother on port 5001 (which is the port that the backend uses) + ECSSecurityGroup = ec2.SecurityGroup(self, "ECSSecurityGroup", + vpc=vpc, + description="ECS Security Group", + allow_all_outbound=True, + ) + ECSSecurityGroup.add_ingress_rule(ec2.Peer.ipv4(vpc.vpc_cidr_block), ec2.Port.tcp(5001), description="All traffic within VPC",) + # Task definitions for the frontend and backend + frontend_definition = ecs.FargateTaskDefinition( + self, f"FrontendTaskDefinition", + family="frontend", + cpu=256, + memory_limit_mib=512, + task_role=TaskExecutionRole, + execution_role=ECSTaskIamRole + ) + backend_definition = ecs.FargateTaskDefinition( + self, f"BackendTaskDefinition", + family="backend", + cpu=256, + memory_limit_mib=512, + task_role=TaskExecutionRole, + execution_role=ECSTaskIamRole + ) + + # Containers for each application, when the frontend is hit on /get-data it makes a call to the backend endpoint /data + frontend_container = frontend_definition.add_container("FrontendContainer", + container_name="frontend-app", + image=ecs.ContainerImage.from_docker_image_asset(frontend_repository), + port_mappings=[ + ecs.PortMapping( + container_port=5000, # Flask app is running on 5001 + host_port=5000, + name="frontend" # Name of the port mapping + ) + ], + logging=ecs.LogDriver.aws_logs(stream_prefix="ecs-logs")) + backend_container = backend_definition.add_container("BackendContainer", + image=ecs.ContainerImage.from_docker_image_asset(backend_data_repository), + port_mappings=[ + ecs.PortMapping( + container_port=5001, # Flask app is running on 5001 + host_port=5001, + name="data" # Name of the port mapping + + ) + ], + container_name="backend", + logging=ecs.LogDriver.aws_logs(stream_prefix="ecs-logs")) + # Creating the service definitions and port mappings + frontend_service = ecs.FargateService(self, "FrontendService", + cluster=ecs_cluster, + task_definition=frontend_definition, + desired_count=1, + max_healthy_percent=200, + min_healthy_percent=100, + vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), + security_groups=[ECSSecurityGroup], + service_connect_configuration=ecs.ServiceConnectProps( + namespace=default_cloud_map_namespace.namespace_name, + services=[ecs.ServiceConnectService( + port_mapping_name="frontend", # Logical name for the service + port=5000, # Container port + )]), + service_name="frontend-service") + backend_service = ecs.FargateService(self, "BackendService", + cluster=ecs_cluster, + task_definition=backend_definition, + desired_count=1, + max_healthy_percent=200, + min_healthy_percent=100, + vpc_subnets=ec2.SubnetSelection(one_per_az=True, subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS), + security_groups=[ECSSecurityGroup], + service_connect_configuration=ecs.ServiceConnectProps( + namespace=default_cloud_map_namespace.namespace_name, + services=[ecs.ServiceConnectService( + port_mapping_name="data", # Logical name for the service + port=5001, # Container port + )]), + service_name="backend-service") + # Creating a public load balancer that will listen on port 80 and forward requests to the frontend ecs container, + # healthchecks are established on port 5000 + public_lb_sg = ec2.SecurityGroup(self, "PublicLBSG", vpc=vpc, description="Public LB SG", allow_all_outbound=True) + target_group = elbv2.ApplicationTargetGroup( + self, "TargetGroup", + target_group_name="ecs-target-group", + vpc=vpc, + port=80, + targets=[frontend_service], + target_type=elbv2.TargetType.IP, + protocol=elbv2.ApplicationProtocol.HTTP, + health_check=elbv2.HealthCheck( + path="/", + port="5000", + interval=Duration.seconds(6), + timeout=Duration.seconds(5), + healthy_threshold_count=2, + unhealthy_threshold_count=2, + ), + ) + target_group.set_attribute(key="deregistration_delay.timeout_seconds", + value="120") + public_lb_sg.add_ingress_rule(peer=ec2.Peer.any_ipv4(), connection=ec2.Port.tcp(80), description="Allow HTTP traffic") + 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)) + public_lb.set_attribute(key="idle_timeout.timeout_seconds", value="30") + listener = public_lb.add_listener("Listener", port=80, default_action=elbv2.ListenerAction.forward(target_groups=[target_group])) + lb_rule = elbv2.ApplicationListenerRule( + self, "ListenerRule", + listener=listener, + priority=1, + action=elbv2.ListenerAction.forward(target_groups=[target_group]), + conditions=[elbv2.ListenerCondition.path_patterns(["*"])], + ) + CfnOutput(self, "Load Balancer URL", value=f"http://{public_lb.load_balancer_dns_name}") diff --git a/python/appmesh-ecs/requirements-dev.txt b/python/ecs-serviceconnect/requirements-dev.txt similarity index 100% rename from python/appmesh-ecs/requirements-dev.txt rename to python/ecs-serviceconnect/requirements-dev.txt diff --git a/python/appmesh-ecs/requirements.txt b/python/ecs-serviceconnect/requirements.txt similarity index 100% rename from python/appmesh-ecs/requirements.txt rename to python/ecs-serviceconnect/requirements.txt diff --git a/python/ecs-serviceconnect/services/data/Dockerfile b/python/ecs-serviceconnect/services/data/Dockerfile new file mode 100644 index 0000000000..ce05b21d42 --- /dev/null +++ b/python/ecs-serviceconnect/services/data/Dockerfile @@ -0,0 +1,20 @@ +# Use the official Python image +FROM --platform=linux/amd64 python:3.9-slim + +# Set the working directory +WORKDIR /app + +# Copy the current directory contents into the container +COPY . . + +# Install all dependencies +RUN pip install -r requirements.txt + +# Set environment variables (optional) +ENV SERVICE_B_PORT=5001 + +# Expose the port your Flask app will run on +# EXPOSE 5001 + +# Command to run the application +CMD ["python", "data.py"] diff --git a/python/ecs-serviceconnect/services/data/data.py b/python/ecs-serviceconnect/services/data/data.py new file mode 100644 index 0000000000..8e4f8622d0 --- /dev/null +++ b/python/ecs-serviceconnect/services/data/data.py @@ -0,0 +1,18 @@ +from flask import Flask, jsonify +import os + +app = Flask(__name__) + +@app.route('/data', methods=['GET']) +def get_data(): + """Returns some sample data from the backend.""" + sample_data = { + "message": "Hello from Service B!", + "data": [1, 2, 3, 4, 5] + } + return jsonify(sample_data) +@app.route('/', methods=['GET']) +def main(): + return jsonify({"message": "Hello from backend Flask!"}), 200 +if __name__ == '__main__': + app.run(debug=True, port=int(os.environ.get("SERVICE_B_PORT", 5001))) # Use environment variable for port diff --git a/python/ecs-serviceconnect/services/data/requirements.txt b/python/ecs-serviceconnect/services/data/requirements.txt new file mode 100644 index 0000000000..2077213c37 --- /dev/null +++ b/python/ecs-serviceconnect/services/data/requirements.txt @@ -0,0 +1 @@ +Flask \ No newline at end of file diff --git a/python/ecs-serviceconnect/services/frontend/Dockerfile b/python/ecs-serviceconnect/services/frontend/Dockerfile new file mode 100644 index 0000000000..4681673044 --- /dev/null +++ b/python/ecs-serviceconnect/services/frontend/Dockerfile @@ -0,0 +1,22 @@ +# Use the official Python image +FROM --platform=linux/amd64 python:3.9-slim + +ARG SERVICE_B_URL_BUILD_ARG +ENV SERVICE_B_URL=$SERVICE_B_URL_BUILD_ARG + +# Set the working directory +WORKDIR /app + +# Copy the current directory contents into the container +COPY . . + +# Install all dependencies +RUN pip install -r requirements.txt + +# Set environment variables (optional) + +# Expose the port your Flask app will run on +# EXPOSE 5001 + +# Command to run the application +CMD ["python", "frontend.py"] \ No newline at end of file diff --git a/python/ecs-serviceconnect/services/frontend/frontend.py b/python/ecs-serviceconnect/services/frontend/frontend.py new file mode 100644 index 0000000000..bc3000f037 --- /dev/null +++ b/python/ecs-serviceconnect/services/frontend/frontend.py @@ -0,0 +1,26 @@ +from flask import Flask, jsonify +import requests +import os + +app = Flask(__name__) + +# URL for the backend service (service_b) from the environment variable +# service B URL will data.scapp.local +SERVICE_B_URL = os.environ.get('SERVICE_B_URL', 'http://localhost:5001/data') + +@app.route('/get-data', methods=['GET']) +def get_data(): + """Fetch data from the backend service (service_b).""" + try: + response = requests.get("http://"+SERVICE_B_URL+":5001/data") + response.raise_for_status() # Raise an error for bad responses + data = response.json() # Parse the JSON response + return jsonify(data), 200 # Return data from service_b + except requests.exceptions.RequestException as e: + return jsonify({"error": str(e)}), 500 # Return error if the request fails + +@app.route('/', methods=['GET']) +def main(): + return jsonify({"message": "Hello from frontend Flask!"}), 200 +if __name__ == '__main__': + app.run(debug=True, port=5000) diff --git a/python/ecs-serviceconnect/services/frontend/requirements.txt b/python/ecs-serviceconnect/services/frontend/requirements.txt new file mode 100644 index 0000000000..0eb56cde55 --- /dev/null +++ b/python/ecs-serviceconnect/services/frontend/requirements.txt @@ -0,0 +1,2 @@ +Flask +requests \ No newline at end of file diff --git a/python/appmesh-ecs/source.bat b/python/ecs-serviceconnect/source.bat similarity index 100% rename from python/appmesh-ecs/source.bat rename to python/ecs-serviceconnect/source.bat From 9aa0d639acbe62de1707a4dbb3b343c8ba601f3e Mon Sep 17 00:00:00 2001 From: Eldyn Castillo Date: Thu, 24 Oct 2024 11:36:55 -0500 Subject: [PATCH 21/24] removed extra comments --- .../cdk_examples_service_connect_stack.py | 2 -- python/ecs-serviceconnect/ecr/ecr_stack.py | 28 +------------------ 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py b/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py index 32ba9a281a..3d889afd56 100644 --- a/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py +++ b/python/ecs-serviceconnect/cdk_examples_service_connect/cdk_examples_service_connect_stack.py @@ -1,8 +1,6 @@ from aws_cdk import ( - # Duration, Stack, aws_ec2 as ec2, - # aws_sqs as sqs, ) from constructs import Construct from ecs.ecs_stack import EcsStack diff --git a/python/ecs-serviceconnect/ecr/ecr_stack.py b/python/ecs-serviceconnect/ecr/ecr_stack.py index 6d337f3a7f..52de9669d5 100644 --- a/python/ecs-serviceconnect/ecr/ecr_stack.py +++ b/python/ecs-serviceconnect/ecr/ecr_stack.py @@ -1,31 +1,11 @@ -# write a boilerplate class for an appmesh mesh with cdk -# from aws_cdk import ( -# aws_appmesh as appmesh, - -# ) -# import os -# import aws_cdk as core -# from aws_cdk import Stack, Tags, App from constructs import Construct -# import aws_cdk.aws_ecs as ecs -# import aws_cdk.aws_servicediscovery as servicediscovery -# import aws_cdk.aws_elasticloadbalancingv2 as elbv2 -# import aws_cdk.aws_ec2 as ec2 -# import aws_cdk.aws_ecr as ecr -# from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform -# import subprocess from aws_cdk.aws_ecr_assets import DockerImageAsset, Platform import cdk_ecr_deployment as ecrdeploy from aws_cdk import ( - # Duration, NestedStack, - Stack, - aws_ec2 as ec2, aws_ecr as ecr, - aws_ecs as ecs, Aws - # aws_sqs as sqs, ) class EcrStack(NestedStack): @@ -63,10 +43,4 @@ def __init__(self, scope: Construct, id: str, **kwargs, ) -> None: # Exporting values to be used in other stacks self.frontend_docker_asset = frontendAsset - self.backend_data_docker_asset = dataAsset - self.frontend_repo = FrontendRepository - self.backend_repo = BackendDataRepository - self.frontend_repository_uri = FrontendRepository.repository_uri - self.frontend_repository_name = FrontendRepository.repository_name - self.backend_data_repository_uri = BackendDataRepository.repository_uri - self.backend_data_repository_name = BackendDataRepository.repository_name \ No newline at end of file + self.backend_data_docker_asset = dataAsset \ No newline at end of file From ef101d129273db6c6d6d11667d17458780112f7f Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Sat, 2 Nov 2024 09:19:22 -0500 Subject: [PATCH 22/24] Update Dockerfile Update to python 3.12 --- python/ecs-serviceconnect/services/data/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ecs-serviceconnect/services/data/Dockerfile b/python/ecs-serviceconnect/services/data/Dockerfile index ce05b21d42..dd3e8eee67 100644 --- a/python/ecs-serviceconnect/services/data/Dockerfile +++ b/python/ecs-serviceconnect/services/data/Dockerfile @@ -1,5 +1,5 @@ # Use the official Python image -FROM --platform=linux/amd64 python:3.9-slim +FROM --platform=linux/amd64 python:3.12-slim # Set the working directory WORKDIR /app From 10f5205702f3099471fe6d5377fbe3835160b751 Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Sat, 2 Nov 2024 09:20:24 -0500 Subject: [PATCH 23/24] Update Dockerfile Update python to 3.12 --- python/ecs-serviceconnect/services/frontend/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ecs-serviceconnect/services/frontend/Dockerfile b/python/ecs-serviceconnect/services/frontend/Dockerfile index 4681673044..3ec2fec85d 100644 --- a/python/ecs-serviceconnect/services/frontend/Dockerfile +++ b/python/ecs-serviceconnect/services/frontend/Dockerfile @@ -1,5 +1,5 @@ # Use the official Python image -FROM --platform=linux/amd64 python:3.9-slim +FROM --platform=linux/amd64 python:3.12-slim ARG SERVICE_B_URL_BUILD_ARG ENV SERVICE_B_URL=$SERVICE_B_URL_BUILD_ARG @@ -19,4 +19,4 @@ RUN pip install -r requirements.txt # EXPOSE 5001 # Command to run the application -CMD ["python", "frontend.py"] \ No newline at end of file +CMD ["python", "frontend.py"] From d92840cd54dfb44ffc319a6332da70f9301681ed Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Sat, 2 Nov 2024 09:21:53 -0500 Subject: [PATCH 24/24] Delete python/ecs-serviceconnect/source.bat Delete file that is created by venv --- python/ecs-serviceconnect/source.bat | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 python/ecs-serviceconnect/source.bat diff --git a/python/ecs-serviceconnect/source.bat b/python/ecs-serviceconnect/source.bat deleted file mode 100644 index 9e1a83442a..0000000000 --- a/python/ecs-serviceconnect/source.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off - -rem The sole purpose of this script is to make the command -rem -rem source .venv/bin/activate -rem -rem (which activates a Python virtualenv on Linux or Mac OS X) work on Windows. -rem On Windows, this command just runs this batch file (the argument is ignored). -rem -rem Now we don't need to document a Windows command for activating a virtualenv. - -echo Executing .venv\Scripts\activate.bat for you -.venv\Scripts\activate.bat