diff --git a/walkthroughs/howto-proxy/README.md b/walkthroughs/howto-proxy/README.md new file mode 100644 index 00000000..907310c6 --- /dev/null +++ b/walkthroughs/howto-proxy/README.md @@ -0,0 +1,35 @@ +## Overview +This example shows how services that are behind ALB can be accessed by clients using Envoy with the help of App Mesh. + +![System Diagram](./howto-alb.png "System Diagram") + +### Backend +There are two versions of Backend, V1 that is registered behind internal ALB and V2 that is registered under a CloudMap service (backend-v2.howto-alb.pvt.local). Additionally, there is a Route53 hosted zone (howto-alb.hosted.local) with a alias target to V1 ALB's DNS name (backend.howto-alb.hosted.local). + +V1 is registered as a virtual-node with service-discovery set to DNS (ALB's DNS). V2 on the otherhand is registered as a virtual-node with CloudMap service-discovery. Backend is registered as a virtual-service (name: backend.howto-alb.hosted.local) with router that routes to V1 and V2. + +### Frontend +Frontend app is ECS service that runs in private subnet behind internet-facing ALB. Frontend is registered with App Mesh as virtual-node with backends set to Backend's virtual-service. Frontend is deployed with Envoy sidecar that communicates with Backend. + +## Prerequisites +1. Install Docker. It is needed to build the demo application images. + +## Setup + +1. Clone this repository and navigate to the walkthrough/howto-alb folder, all commands will be ran from this location +2. **Your** account id: + ``` + export AWS_ACCOUNT_ID= + ``` +3. **Region** e.g. us-west-2 + ``` + export AWS_DEFAULT_REGION=us-west-2 + ``` +4. **ENVOY_IMAGE** environment variable is not set to App Mesh Envoy, see https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html + ``` + export ENVOY_IMAGE=... + ``` +5. Setup using cloudformation + ``` + ./deploy.sh + ``` diff --git a/walkthroughs/howto-proxy/app.yaml b/walkthroughs/howto-proxy/app.yaml new file mode 100644 index 00000000..d454b93b --- /dev/null +++ b/walkthroughs/howto-proxy/app.yaml @@ -0,0 +1,553 @@ +Parameters: + ProjectName: + Type: String + Description: Project name to link stacks + + AppMeshXdsEndpoint: + Type: String + Description: App Mesh XDS Endpoint Override + Default: "" + + EnvoyImage: + Type: String + Description: Envoy container image + + FrontAppImage: + Type: String + Description: Front app container image + + ContainerPort: + Type: Number + Description: Port number to use for applications + Default: 8080 + + ColorProxyImage: + Type: String + Description: Color proxy container image + + ColorAppSslImage: + Type: String + Description: Color app ssl container image + +Resources: + + TaskSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: "Security group for the tasks" + VpcId: + Fn::ImportValue: !Sub '${ProjectName}:VPC' + SecurityGroupIngress: + - CidrIp: + Fn::ImportValue: !Sub '${ProjectName}:VpcCIDR' + IpProtocol: -1 + + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Sub '${ProjectName}-log-group' + RetentionInDays: 30 + + TaskIamRole: + Type: AWS::IAM::Role + Properties: + Path: / + AssumeRolePolicyDocument: | + { + "Statement": [{ + "Effect": "Allow", + "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, + "Action": [ "sts:AssumeRole" ] + }] + } + ManagedPolicyArns: + - arn:aws:iam::aws:policy/CloudWatchFullAccess + - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess + - arn:aws:iam::aws:policy/AWSAppMeshEnvoyAccess + + TaskExecutionIamRole: + Type: AWS::IAM::Role + Properties: + Path: / + AssumeRolePolicyDocument: | + { + "Statement": [{ + "Effect": "Allow", + "Principal": { "Service": [ "ecs-tasks.amazonaws.com" ]}, + "Action": [ "sts:AssumeRole" ] + }] + } + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly + - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess + + BackendRecordSet: + Type: AWS::Route53::RecordSet + DependsOn: + - BackendV1LoadBalancer + Properties: + AliasTarget: + HostedZoneId: !GetAtt 'BackendV1LoadBalancer.CanonicalHostedZoneID' + DNSName: !GetAtt 'BackendV1LoadBalancer.DNSName' + HostedZoneId: + Fn::ImportValue: + !Sub '${ProjectName}:DnsHostedZoneId' + Name: + Fn::Join: + - '.' + - + - backend + - Fn::ImportValue: + !Sub '${ProjectName}:DnsHostedZoneName' + Type: A + + # Backend V1 behind ALB + BackendV1LoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internal + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '30' + Subnets: + - Fn::ImportValue: + !Sub '${ProjectName}:PrivateSubnet1' + - Fn::ImportValue: + !Sub '${ProjectName}:PrivateSubnet2' + SecurityGroups: + - !Ref TaskSecurityGroup + + # Backend V2 with CloudMap service registry + BackendV2ServiceRegistry: + Type: AWS::ServiceDiscovery::Service + Properties: + Name: 'backend-v2' + DnsConfig: + NamespaceId: + Fn::ImportValue: + !Sub '${ProjectName}:DnsNamespaceId' + DnsRecords: + - Type: A + TTL: 300 + HealthCheckCustomConfig: + FailureThreshold: 1 + + BackendV2TaskDef: + Type: AWS::ECS::TaskDefinition + Properties: + RequiresCompatibilities: + - 'FARGATE' + Family: 'green' + NetworkMode: 'awsvpc' + Cpu: 256 + Memory: 512 + TaskRoleArn: !Ref TaskIamRole + ExecutionRoleArn: !Ref TaskExecutionIamRole + ContainerDefinitions: + - Name: 'app' + Image: !Ref ColorProxyImage + Essential: true + DependsOn: + - ContainerName: 'envoy' + Condition: 'HEALTHY' + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Sub ${ProjectName}-log-group + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: 'backend-v2' + PortMappings: + - ContainerPort: !Ref ContainerPort + HostPort: !Ref ContainerPort + Protocol: 'tcp' + Environment: + - Name: COLOR + Value: 'green' + - Name: PORT + Value: !Sub '${ContainerPort}' + - Name: 'colorappssl' + Image: !Ref ColorAppSslImage + Essential: false + DependsOn: + - ContainerName: 'envoy' + Condition: 'HEALTHY' + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Sub ${ProjectName}-log-group + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: 'backend-v2' + PortMappings: + - ContainerPort: 8443 + HostPort: 8443 + Protocol: 'tcp' + Environment: + - Name: COLOR + Value: 'green' + - Name: PORT + Value: 8443 + - Name: envoy + Image: !Ref EnvoyImage + Essential: true + User: '1337' + Ulimits: + - Name: "nofile" + HardLimit: 15000 + SoftLimit: 15000 + PortMappings: + - ContainerPort: 9901 + Protocol: 'tcp' + - ContainerPort: 15000 + Protocol: 'tcp' + - ContainerPort: 15001 + Protocol: 'tcp' + HealthCheck: + Command: + - 'CMD-SHELL' + - 'curl -s http://localhost:9901/server_info | grep state | grep -q LIVE' + Interval: 5 + Timeout: 10 + Retries: 10 + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Sub '${ProjectName}-log-group' + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: 'backend-v2' + Environment: + - Name: 'ENVOY_LOG_LEVEL' + Value: 'debug' + - Name: 'ENABLE_ENVOY_STATS_TAGS' + Value: '1' + - Name: 'APPMESH_VIRTUAL_NODE_NAME' + Value: + Fn::Join: + - '' + - + - 'mesh/' + - Fn::ImportValue: + !Sub '${ProjectName}:Mesh' + - '/virtualNode/' + - !GetAtt 'BackendV2VirtualNode.VirtualNodeName' + + BackendV2Service: + Type: AWS::ECS::Service + DependsOn: + - BackendV2ServiceRegistry + Properties: + Cluster: + Fn::ImportValue: + !Sub "${ProjectName}:ECSCluster" + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 100 + DesiredCount: 1 + LaunchType: 'FARGATE' + ServiceRegistries: + - RegistryArn: !GetAtt 'BackendV2ServiceRegistry.Arn' + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: DISABLED + SecurityGroups: + - !Ref TaskSecurityGroup + Subnets: + - Fn::ImportValue: + !Sub '${ProjectName}:PrivateSubnet1' + - Fn::ImportValue: + !Sub '${ProjectName}:PrivateSubnet2' + TaskDefinition: !Ref BackendV2TaskDef + + BackendV2VirtualNode: + Type: AWS::AppMesh::VirtualNode + Properties: + MeshName: + Fn::ImportValue: + !Sub '${ProjectName}:Mesh' + VirtualNodeName: !Sub '${ProjectName}-backend-v2-node' + Spec: + Listeners: + - PortMapping: + Port: !Ref ContainerPort + Protocol: http + ServiceDiscovery: + AWSCloudMap: + NamespaceName: + Fn::ImportValue: + !Sub '${ProjectName}:DnsNamespaceName' + ServiceName: !GetAtt BackendV2ServiceRegistry.Name + Attributes: + - Key: 'ECS_TASK_DEFINITION_FAMILY' + Value: 'green' + + BackendVirtualService: + Type: AWS::AppMesh::VirtualService + Properties: + MeshName: + Fn::ImportValue: + !Sub '${ProjectName}:Mesh' + VirtualServiceName: !Ref BackendRecordSet + Spec: + Provider: + VirtualNode: + VirtualNodeName: !GetAtt BackendV2VirtualNode.VirtualNodeName + + # Frontend + SecurityGroupIngressFromPublicALB: + Type: AWS::EC2::SecurityGroupIngress + Properties: + Description: Ingress from the public ALB + GroupId: !Ref TaskSecurityGroup + IpProtocol: -1 + SourceSecurityGroupId: !Ref PublicLoadBalancerSecurityGroup + + PublicLoadBalancerSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: 'Access to the public facing load balancer' + VpcId: + Fn::ImportValue: + !Sub "${ProjectName}:VPC" + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 80 + ToPort: 80 + + PublicLoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Scheme: internet-facing + LoadBalancerAttributes: + - Key: idle_timeout.timeout_seconds + Value: '30' + Subnets: + - Fn::ImportValue: + !Sub '${ProjectName}:PublicSubnet1' + - Fn::ImportValue: + !Sub '${ProjectName}:PublicSubnet2' + SecurityGroups: + - !Ref PublicLoadBalancerSecurityGroup + + WebTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 60 + HealthCheckPath: '/ping' + HealthCheckProtocol: HTTP + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + TargetType: ip + Name: !Sub '${ProjectName}-webtarget' + Port: 80 + Protocol: HTTP + UnhealthyThresholdCount: 2 + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: 120 + VpcId: + Fn::ImportValue: + !Sub "${ProjectName}:VPC" + + PublicLoadBalancerListener: + DependsOn: + - PublicLoadBalancer + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + DefaultActions: + - TargetGroupArn: !Ref WebTargetGroup + Type: 'forward' + LoadBalancerArn: !Ref PublicLoadBalancer + Port: 80 + Protocol: HTTP + + WebLoadBalancerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - TargetGroupArn: !Ref WebTargetGroup + Type: 'forward' + Conditions: + - Field: path-pattern + Values: + - '*' + ListenerArn: !Ref PublicLoadBalancerListener + Priority: 1 + + FrontVirtualNode: + Type: AWS::AppMesh::VirtualNode + DependsOn: + - BackendVirtualService + Properties: + MeshName: + Fn::ImportValue: + !Sub '${ProjectName}:Mesh' + VirtualNodeName: !Sub "${ProjectName}-front-node" + Spec: + Listeners: + - PortMapping: + Port: !Sub '${ContainerPort}' + Protocol: http + ServiceDiscovery: + DNS: + Hostname: !GetAtt PublicLoadBalancer.DNSName + Backends: + - VirtualService: + VirtualServiceName: !GetAtt 'BackendVirtualService.VirtualServiceName' + + FrontTaskDef: + Type: AWS::ECS::TaskDefinition + DependsOn: + - BackendVirtualService + - FrontVirtualNode + Properties: + RequiresCompatibilities: + - 'FARGATE' + Family: 'front' + NetworkMode: 'awsvpc' + Cpu: 256 + Memory: 512 + TaskRoleArn: !Ref TaskIamRole + ExecutionRoleArn: !Ref TaskExecutionIamRole + ProxyConfiguration: + Type: 'APPMESH' + ContainerName: 'envoy' + ProxyConfigurationProperties: + - Name: 'IgnoredUID' + Value: '1337' + - Name: 'ProxyIngressPort' + Value: '15000' + - Name: 'ProxyEgressPort' + Value: '15001' + - Name: 'AppPorts' + Value: !Sub '${ContainerPort}' + - Name: 'EgressIgnoredIPs' + Value: '169.254.170.2,169.254.169.254' + ContainerDefinitions: + - Name: 'app' + Image: !Ref FrontAppImage + Essential: true + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Sub '${ProjectName}-log-group' + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: 'front' + PortMappings: + - ContainerPort: !Ref ContainerPort + Protocol: 'tcp' + DependsOn: + - ContainerName: 'envoy' + Condition: 'HEALTHY' + - ContainerName: 'xray' + Condition: 'START' + Environment: + - Name: 'COLOR_HOST' + Value: !Join ['', [!GetAtt 'BackendVirtualService.VirtualServiceName', ':', !Sub '${ContainerPort}']] + - Name: PORT + Value: !Sub '${ContainerPort}' + - Name: XRAY_APP_NAME + Value: + Fn::Join: + - '' + - + - Fn::ImportValue: + !Sub '${ProjectName}:Mesh' + - '/' + - !GetAtt 'FrontVirtualNode.VirtualNodeName' + - Name: 'xray' + Image: "public.ecr.aws/xray/aws-xray-daemon" + Essential: true + User: '1337' + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Sub '${ProjectName}-log-group' + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: 'front' + PortMappings: + - ContainerPort: 2000 + Protocol: 'udp' + - Name: envoy + Image: !Ref EnvoyImage + Essential: true + User: '1337' + DependsOn: + - ContainerName: 'xray' + Condition: 'START' + Ulimits: + - Name: "nofile" + HardLimit: 15000 + SoftLimit: 15000 + PortMappings: + - ContainerPort: 9901 + Protocol: 'tcp' + - ContainerPort: 15000 + Protocol: 'tcp' + - ContainerPort: 15001 + Protocol: 'tcp' + HealthCheck: + Command: + - 'CMD-SHELL' + - 'curl -s http://localhost:9901/server_info | grep state | grep -q LIVE' + Interval: 5 + Timeout: 10 + Retries: 10 + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Sub '${ProjectName}-log-group' + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: 'front' + Environment: + - Name: 'ENVOY_LOG_LEVEL' + Value: 'debug' + - Name: 'ENABLE_ENVOY_XRAY_TRACING' + Value: '1' + - Name: 'ENABLE_ENVOY_STATS_TAGS' + Value: '1' + - Name: 'APPMESH_VIRTUAL_NODE_NAME' + Value: + Fn::Join: + - '' + - + - 'mesh/' + - Fn::ImportValue: + !Sub '${ProjectName}:Mesh' + - '/virtualNode/' + - !GetAtt 'FrontVirtualNode.VirtualNodeName' + + FrontService: + Type: AWS::ECS::Service + DependsOn: + - WebLoadBalancerRule + Properties: + Cluster: + Fn::ImportValue: + !Sub "${ProjectName}:ECSCluster" + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 100 + DesiredCount: 1 + LaunchType: 'FARGATE' + TaskDefinition: !Ref FrontTaskDef + LoadBalancers: + - ContainerName: app + ContainerPort: !Ref ContainerPort + TargetGroupArn: !Ref WebTargetGroup + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: DISABLED + SecurityGroups: + - !Ref TaskSecurityGroup + Subnets: + - Fn::ImportValue: + !Sub '${ProjectName}:PrivateSubnet1' + - Fn::ImportValue: + !Sub '${ProjectName}:PrivateSubnet2' + +Outputs: + FrontEndpoint: + Description: 'Public endpoint for Front service' + Value: !Join ['', ['http://', !GetAtt 'PublicLoadBalancer.DNSName']] + Export: + Name: !Sub "${ProjectName}:FrontEndpoint" diff --git a/walkthroughs/howto-proxy/colorappssl/Dockerfile b/walkthroughs/howto-proxy/colorappssl/Dockerfile new file mode 100644 index 00000000..ff8c4792 --- /dev/null +++ b/walkthroughs/howto-proxy/colorappssl/Dockerfile @@ -0,0 +1,23 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2 + +COPY requirements.txt ./ + +RUN yum update -y && \ + yum install -y python3 && \ + pip3 install --no-cache-dir -r requirements.txt && \ + yum clean all && \ + rm -rf /var/cache/yum + +WORKDIR /usr/src/app + +RUN yum install -y openssl && \ + openssl req -x509 -nodes -days 365 \ + -subj "/C=CA/ST=QC/O=Company Inc/CN=example.com" \ + -newkey rsa:2048 -keyout key.pem \ + -out cert.pem; + +COPY . . + +ENV PORT 8443 + +CMD ["gunicorn", "--certfile", "cert.pem", "--keyfile", "key.pem", "app:app", "--config=config.py"] diff --git a/walkthroughs/howto-proxy/colorappssl/app.py b/walkthroughs/howto-proxy/colorappssl/app.py new file mode 100755 index 00000000..b28497dc --- /dev/null +++ b/walkthroughs/howto-proxy/colorappssl/app.py @@ -0,0 +1,29 @@ +import os +import config +from flask import Flask, request +from aws_xray_sdk.core import patch_all, xray_recorder +from aws_xray_sdk.ext.flask.middleware import XRayMiddleware + +app = Flask(__name__) + +xray_recorder.configure( + context_missing='LOG_ERROR', + service=config.XRAY_APP_NAME, +) +patch_all() + +XRayMiddleware(app, xray_recorder) + +@app.route('/ping') +def ping(): + return 'Pong' + +@app.route('/') +def color(): + print('----------------') + print(request.headers) + print('----------------') + return config.COLOR + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=config.PORT, debug=config.DEBUG_MODE) diff --git a/walkthroughs/howto-proxy/colorappssl/config.py b/walkthroughs/howto-proxy/colorappssl/config.py new file mode 100644 index 00000000..68c7a81d --- /dev/null +++ b/walkthroughs/howto-proxy/colorappssl/config.py @@ -0,0 +1,12 @@ +from os import environ as env +import multiprocessing + +PORT = int(env.get("PORT", 8443)) +DEBUG_MODE = int(env.get("DEBUG_MODE", 0)) +XRAY_APP_NAME = env.get('XRAY_APP_NAME', 'feapp') +COLOR = env.get('COLOR', 'n/a') + +# Gunicorn config +bind = ":" + str(PORT) +workers = multiprocessing.cpu_count() * 2 + 1 +threads = 2 * multiprocessing.cpu_count() diff --git a/walkthroughs/howto-proxy/colorappssl/requirements.txt b/walkthroughs/howto-proxy/colorappssl/requirements.txt new file mode 100644 index 00000000..47f05a04 --- /dev/null +++ b/walkthroughs/howto-proxy/colorappssl/requirements.txt @@ -0,0 +1,3 @@ +flask +aws-xray-sdk +gunicorn==19.9.0 diff --git a/walkthroughs/howto-proxy/colorproxy/Dockerfile b/walkthroughs/howto-proxy/colorproxy/Dockerfile new file mode 100644 index 00000000..70144c5f --- /dev/null +++ b/walkthroughs/howto-proxy/colorproxy/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:latest + +EXPOSE 8080 + +COPY default.conf /etc/nginx/conf.d/ diff --git a/walkthroughs/howto-proxy/colorproxy/default.conf b/walkthroughs/howto-proxy/colorproxy/default.conf new file mode 100644 index 00000000..43fb151c --- /dev/null +++ b/walkthroughs/howto-proxy/colorproxy/default.conf @@ -0,0 +1,7 @@ +server { + listen 8080; + server_name colorproxy; + location / { + proxy_pass https://localhost:8443; + } +} diff --git a/walkthroughs/howto-proxy/deploy.sh b/walkthroughs/howto-proxy/deploy.sh new file mode 100755 index 00000000..d60f8e7c --- /dev/null +++ b/walkthroughs/howto-proxy/deploy.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +set -e + +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 + +if [ -z $ENVOY_IMAGE ]; then + echo "ENVOY_IMAGE environment variable is not set to App Mesh Envoy, see https://docs.aws.amazon.com/app-mesh/latest/userguide/envoy.html" + exit 1 +fi + +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +PROJECT_NAME="howto-proxy" +ECR_URL="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" +ECR_IMAGE_PREFIX="${ECR_URL}/${PROJECT_NAME}" +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 +} + +deploy_images() { + ecr_login + for app in colorproxy colorappssl feapp; do + aws ecr describe-repositories --repository-name $PROJECT_NAME/$app >/dev/null 2>&1 || aws ecr create-repository --repository-name $PROJECT_NAME/$app >/dev/null + docker build -t ${ECR_IMAGE_PREFIX}/${app} ${DIR}/${app} + docker push ${ECR_IMAGE_PREFIX}/${app} + done +} + +deploy_infra() { + stack_name="${PROJECT_NAME}-infra" + aws cloudformation deploy \ + --no-fail-on-empty-changeset \ + --stack-name $stack_name\ + --template-file "${DIR}/infra.yaml" \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides "ProjectName=${PROJECT_NAME}" +} + +deploy_app() { + aws cloudformation deploy \ + --no-fail-on-empty-changeset \ + --stack-name "${PROJECT_NAME}-app" \ + --template-file "${DIR}/app.yaml" \ + --capabilities CAPABILITY_IAM \ + --parameter-overrides "ProjectName=${PROJECT_NAME}" "EnvoyImage=${ENVOY_IMAGE}" "ColorAppSslImage=${ECR_IMAGE_PREFIX}/colorappssl" "ColorProxyImage=${ECR_IMAGE_PREFIX}/colorproxy" "FrontAppImage=${ECR_IMAGE_PREFIX}/feapp" +} + +delete_cfn_stack() { + stack_name=$1 + aws cloudformation delete-stack --stack-name $stack_name + echo 'Waiting for the stack to be deleted, this may take a few minutes...' + aws cloudformation wait stack-delete-complete --stack-name $stack_name + echo 'Done' +} + +confirm_service_linked_role() { + aws iam get-role --role-name AWSServiceRoleForAppMesh >/dev/null + [[ $? -eq 0 ]] || + (echo "Error: no service linked role for App Mesh" && exit 1) +} + +print_endpoint() { + echo "Public endpoint:" + prefix=$(aws cloudformation describe-stacks \ + --stack-name="${PROJECT_NAME}-app" \ + --query="Stacks[0].Outputs[?OutputKey=='FrontEndpoint'].OutputValue" \ + --output=text) + echo "${prefix}/color" +} + +deploy_resources() { + + if [ -z $SKIP_IMAGES ]; then + echo "deploy images..." + deploy_images + fi + + echo "deploy infra..." + deploy_infra + + echo "deploy app..." + deploy_app + + confirm_service_linked_role + print_endpoint +} + +delete_images() { + for app in colorproxy colorappssl feapp; do + echo "deleting repository..." + aws ecr delete-repository \ + --repository-name $PROJECT_NAME/$app \ + --force >/dev/null + done +} + +delete_resources() { + echo "delete app..." + delete_cfn_stack "${PROJECT_NAME}-app" + + echo "delete infra..." + delete_cfn_stack "${PROJECT_NAME}-infra" + + echo "delete images..." + delete_images + + echo "all resources from this tutorial have been removed" +} + +action=${1:-"deploy"} +if [ "$action" == "delete" ]; then + delete_resources + exit 0 +fi + +deploy_resources diff --git a/walkthroughs/howto-proxy/feapp/Dockerfile b/walkthroughs/howto-proxy/feapp/Dockerfile new file mode 100644 index 00000000..e25684b5 --- /dev/null +++ b/walkthroughs/howto-proxy/feapp/Dockerfile @@ -0,0 +1,17 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2 + +COPY requirements.txt ./ + +RUN yum update -y && \ + yum install -y python3 && \ + pip3 install --no-cache-dir -r requirements.txt && \ + yum clean all && \ + rm -rf /var/cache/yum + +WORKDIR /usr/src/app + +COPY . . + +ENV PORT 8080 + +CMD ["gunicorn", "app:app", "--config=config.py"] \ No newline at end of file diff --git a/walkthroughs/howto-proxy/feapp/app.py b/walkthroughs/howto-proxy/feapp/app.py new file mode 100755 index 00000000..b0014a3f --- /dev/null +++ b/walkthroughs/howto-proxy/feapp/app.py @@ -0,0 +1,28 @@ +import os +import requests +import config +from flask import Flask, request +from aws_xray_sdk.core import xray_recorder, patch_all +from aws_xray_sdk.ext.flask.middleware import XRayMiddleware + +app = Flask(__name__) + +xray_recorder.configure( + context_missing='LOG_ERROR', + service=config.XRAY_APP_NAME, +) +patch_all() +XRayMiddleware(app, xray_recorder) + +@app.route('/ping') +def ping(): + return 'Pong' + +@app.route('/color') +def color(): + print(request.headers) + response = requests.get(f'http://{config.COLOR_HOST}') + return response.text + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=config.PORT, debug=config.DEBUG_MODE) \ No newline at end of file diff --git a/walkthroughs/howto-proxy/feapp/config.py b/walkthroughs/howto-proxy/feapp/config.py new file mode 100644 index 00000000..a8018e01 --- /dev/null +++ b/walkthroughs/howto-proxy/feapp/config.py @@ -0,0 +1,12 @@ +from os import environ as env +import multiprocessing + +PORT = int(env.get("PORT", 8080)) +DEBUG_MODE = int(env.get("DEBUG_MODE", 0)) +XRAY_APP_NAME = env.get('XRAY_APP_NAME', 'feapp') +COLOR_HOST = env.get('COLOR_HOST') + +# Gunicorn config +bind = ":" + str(PORT) +workers = multiprocessing.cpu_count() * 2 + 1 +threads = 2 * multiprocessing.cpu_count() \ No newline at end of file diff --git a/walkthroughs/howto-proxy/feapp/requirements.txt b/walkthroughs/howto-proxy/feapp/requirements.txt new file mode 100644 index 00000000..56d7f135 --- /dev/null +++ b/walkthroughs/howto-proxy/feapp/requirements.txt @@ -0,0 +1,4 @@ +flask +requests +aws-xray-sdk +gunicorn==19.9.0 diff --git a/walkthroughs/howto-proxy/howto-alb.png b/walkthroughs/howto-proxy/howto-alb.png new file mode 100644 index 00000000..be28b9ab Binary files /dev/null and b/walkthroughs/howto-proxy/howto-alb.png differ diff --git a/walkthroughs/howto-proxy/infra.yaml b/walkthroughs/howto-proxy/infra.yaml new file mode 100644 index 00000000..72f9a16d --- /dev/null +++ b/walkthroughs/howto-proxy/infra.yaml @@ -0,0 +1,289 @@ +Parameters: + ProjectName: + Type: String + Description: Project name to link stacks + + VpcCIDR: + Description: Please enter the IP range (CIDR notation) for this VPC + Type: String + Default: 10.0.0.0/16 + + PublicSubnet1CIDR: + Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone + Type: String + Default: 10.0.0.0/19 + + PublicSubnet2CIDR: + Description: Please enter the IP range (CIDR notation) for the public subnet in the second Availability Zone + Type: String + Default: 10.0.32.0/19 + + PrivateSubnet1CIDR: + Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone + Type: String + Default: 10.0.64.0/19 + + PrivateSubnet2CIDR: + Description: Please enter the IP range (CIDR notation) for the private subnet in the second Availability Zone + Type: String + Default: 10.0.96.0/19 + +Resources: + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: !Ref VpcCIDR + EnableDnsHostnames: true + Tags: + - Key: Name + Value: !Ref ProjectName + + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: !Ref ProjectName + + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref InternetGateway + VpcId: !Ref VPC + + PublicSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [ 0, !GetAZs '' ] + CidrBlock: !Ref PublicSubnet1CIDR + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub '${ProjectName} Public Subnet (AZ1)' + + PublicSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [ 1, !GetAZs '' ] + CidrBlock: !Ref PublicSubnet2CIDR + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: !Sub '${ProjectName} Public Subnet (AZ2)' + + PrivateSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [ 0, !GetAZs '' ] + CidrBlock: !Ref PrivateSubnet1CIDR + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub '${ProjectName} Private Subnet (AZ1)' + + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [ 1, !GetAZs '' ] + CidrBlock: !Ref PrivateSubnet2CIDR + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: !Sub '${ProjectName} Private Subnet (AZ2)' + + NatGateway1EIP: + Type: AWS::EC2::EIP + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + + NatGateway2EIP: + Type: AWS::EC2::EIP + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + + NatGateway1: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGateway1EIP.AllocationId + SubnetId: !Ref PublicSubnet1 + + NatGateway2: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGateway2EIP.AllocationId + SubnetId: !Ref PublicSubnet2 + + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub '${ProjectName} Public Routes' + + DefaultPublicRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + PublicSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet1 + + PublicSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet2 + + PrivateRouteTable1: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub '${ProjectName} Private Routes (AZ1)' + + DefaultPrivateRoute1: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable1 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway1 + + PrivateSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PrivateRouteTable1 + SubnetId: !Ref PrivateSubnet1 + + PrivateRouteTable2: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: !Sub '${ProjectName} Private Routes (AZ2)' + + DefaultPrivateRoute2: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable2 + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway2 + + PrivateSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PrivateRouteTable2 + SubnetId: !Ref PrivateSubnet2 + + Mesh: + Type: AWS::AppMesh::Mesh + Properties: + MeshName: !Ref ProjectName + + ECSCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Ref ProjectName + + DnsNamespace: + Type: AWS::ServiceDiscovery::PrivateDnsNamespace + Properties: + Name: !Sub '${ProjectName}.pvt.local' + Vpc: !Ref VPC + + DnsHostedZone: + Type: AWS::Route53::HostedZone + Properties: + Name: !Sub '${ProjectName}.hosted.local' + HostedZoneConfig: + Comment: Private hosted zone + VPCs: + - VPCId: !Ref VPC + VPCRegion: !Ref 'AWS::Region' + +Outputs: + VPC: + Description: A reference to the created VPC + Value: !Ref VPC + Export: + Name: !Sub '${ProjectName}:VPC' + + PublicSubnet1: + Description: A reference to the public subnet in the 1st Availability Zone + Value: !Ref PublicSubnet1 + Export: + Name: !Sub '${ProjectName}:PublicSubnet1' + + PublicSubnet2: + Description: A reference to the public subnet in the 2nd Availability Zone + Value: !Ref PublicSubnet2 + Export: + Name: !Sub '${ProjectName}:PublicSubnet2' + + PrivateSubnet1: + Description: A reference to the private subnet in the 1st Availability Zone + Value: !Ref PrivateSubnet1 + Export: + Name: !Sub '${ProjectName}:PrivateSubnet1' + + PrivateSubnet2: + Description: A reference to the private subnet in the 2nd Availability Zone + Value: !Ref PrivateSubnet2 + Export: + Name: !Sub '${ProjectName}:PrivateSubnet2' + + VpcCIDR: + Description: VPC CIDR + Value: !Ref VpcCIDR + Export: + Name: !Sub '${ProjectName}:VpcCIDR' + + Mesh: + Description: A reference to the App Mesh mesh + Value: !GetAtt Mesh.MeshName + Export: + Name: !Sub "${ProjectName}:Mesh" + + Cluster: + Description: A reference to the ECS cluster + Value: !Ref ECSCluster + Export: + Name: !Sub "${ProjectName}:ECSCluster" + + DnsNamespaceName: + Description: Dns Namespace + Value: !Sub '${ProjectName}.pvt.local' + Export: + Name: !Sub "${ProjectName}:DnsNamespaceName" + + DnsNamespaceId: + Description: Dns Namespace + Value: !GetAtt DnsNamespace.Id + Export: + Name: !Sub "${ProjectName}:DnsNamespaceId" + + DnsHostedZoneId: + Description: Dns Namespace + Value: !Ref DnsHostedZone + Export: + Name: !Sub "${ProjectName}:DnsHostedZoneId" + + DnsHostedZoneName: + Description: Dns Namespace + Value: !Sub '${ProjectName}.hosted.local' + Export: + Name: !Sub "${ProjectName}:DnsHostedZoneName" \ No newline at end of file