# Example for AWS X-Ray in Amazon ECS ## 1. Overview ### 1.1. Use case - This pattern describes implementation guides and example codes for the developers who are planning to implement distributed tracing system using AWS X-Ray, under distributed architecture (i.e. Microservices architecture) that consists of multiple containers based on Amazon ECS. ### 1.2. Assumed requirements - System adminstrators can monitor system performance and reliability across multiple containers based on Amazon ECS, and can identify the bottlenecks of them. ### 1.3. Limitations - This pattern contains only content about _ECS on Fargate_. The contents specific to _ECS on EC2_ are not mentioned here. - Some application codes are included as example codes, but not all the languages or frameworks supported in AWS X-Ray are described here, only for _Express framework for Node.js_, _Flask and Django framework for Python_. - For the sake of simplicity, it has not been properly designed except for the settings mentioned in this article, such as communication encryption, capacity tuning, redundancy, etc. ### 1.4. Architecture - The example codes assumes an architecture consisting of 4 containers based on ECS Fargate. 1. **Frontend (nginx, html)** Returns simple html file that sends requests to BFF and displays the result. 1. **BFF (Express, Node.js)** Requests to Backend #1, #2 sequentially and responds with the combined result in JSON format. 1. **Backend #1 (Flask, Python)** Responds current date information in JSON format, writing log (last accessed time) to DynamoDB. 1. **Backend #2 (Django, Python)** Responds current time information in JSON format, writing log (last accessed time) to DynamoDB. ### 1.5. Distributed tracing mechanism - Trace data is obtained and associated through **BFF, Backend #1, Backend #2** using AWS X-Ray SDK and AWS X-Ray Daemon on Amazon ECS tasks. _Note: In this case, trace data of **Frontend** was excluded from the collection, because it can be obtained from developer tools of common browser._ - Trace data can be monitored on AWS X-Ray Console like below. ## 2. Prerequisites ### 2.1. Tools - The following tools need to be installed on your machine. - [AWS CLI version 2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) - [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) - [docker](https://docs.docker.com/get-docker/) ## 3. Usage ### 3.1. Installation 1. Download source codes from this repository to your machine. 1. Edit `envs/default.conf`. You can skip this step if you proceed with default settings. 1. Run `install.sh` with the following command. This might take 15 minutes or more. ```bash $ bash scripts/install.sh ``` 1. After installation, get the frontend endpoint URL with the following command. ```bash $ bash scripts/get_url.sh ``` 1. Access to the URL via browser, then current date and time are shown like below. 1. After trying to access the URL some times, access to [X-Ray console](https://ap-northeast-1.console.aws.amazon.com/xray/home) then you can see the trace map. ### 3.2. Update 1. Run `install.sh` again after modifications with the following command. ```bash $ bash scripts/install.sh ``` ### 3.3. Uninstallation 1. Run `uninstall.sh` with the following command. ```bash $ bash scripts/uninstall.sh ``` ## 4. Implementation guide - This pattern describes how to implement distributed tracing mechanism under the Amazon ECS container-based distributed system using AWS X-Ray. - To obtain trace data in AWS X-Ray from Amazon ECS containers, 2 types of configurations need to be configured properly. 1. Application codes 1. CloudFormation templates ### 4.1. Application codes #### 4.1.1. Add X-Ray SDK middlewares (for tracing incoming requests) - To enable tracing incoming requests in application codes, you can add X-Ray SDK middlewares for supported framework. - In the following sections, we share examples for Express(Node.js), Flask(Python) and Django(Python). ##### 4.1.1.1. Example for Express(Node.js): [app/bff/server.js](app/bff/server.js) ```javascript //------------------------------------------------------------ // Add X-Ray SDK for Node.js with the middleware (Express) // - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-middleware.html#xray-sdk-nodejs-middleware-express //------------------------------------------------------------ const AWSXRay = require('aws-xray-sdk'); //------------------------------------------------------------ const express = require('express'); ... const app = express(); //------------------------------------------------------------ // Open segment for X-Ray with the middleware (Express) // - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-middleware.html#xray-sdk-nodejs-middleware-express //------------------------------------------------------------ app.use(AWSXRay.express.openSegment('BFF')); //------------------------------------------------------------ ... //------------------------------------------------------------ // Close segment for X-Ray with the middleware (Express) // - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-middleware.html#xray-sdk-nodejs-middleware-express //------------------------------------------------------------ app.use(AWSXRay.express.closeSegment()); //------------------------------------------------------------ ... ``` References: - [Tracing incoming requests with Express](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-middleware.html#xray-sdk-nodejs-middleware-express) ##### 4.1.1.2. Example for Flask(Python): [app/backend1/app.py](app/backend1/app.py) ```python from flask import * from flask_cors import CORS import time, datetime, os, math, logging import boto3 ... #------------------------------------------------------------# # Add X-Ray SDK for Python with the middleware (Flask) # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-adding-middleware-flask #------------------------------------------------------------# from aws_xray_sdk.core import xray_recorder from aws_xray_sdk.ext.flask.middleware import XRayMiddleware #------------------------------------------------------------# ... app = Flask(__name__) CORS(app) #------------------------------------------------------------# # Setup X-Ray SDK and apply patch to Flask application # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-adding-middleware-flask #------------------------------------------------------------# xray_recorder.configure(service='Backend #1') XRayMiddleware(app, xray_recorder) #------------------------------------------------------------# ... ``` References: - [Adding the middleware to your application (flask)](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-adding-middleware-flask) ##### 4.1.1.3. Example for Django(Python): [app/backend2/project/project/settings.py](app/backend2/project/project/settings.py) ```python ... #------------------------------------------------------------# # Add X-Ray SDK for Python with the middleware (Django) # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-adding-middleware-django #------------------------------------------------------------# INSTALLED_APPS = [ 'aws_xray_sdk.ext.django', # Added 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] MIDDLEWARE = [ 'aws_xray_sdk.ext.django.middleware.XRayMiddleware', # Added 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] #------------------------------------------------------------# ... #------------------------------------------------------------# # Recorder configuration for X-Ray # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-configuration.html#xray-sdk-python-middleware-configuration-django #------------------------------------------------------------# XRAY_RECORDER = { 'AWS_XRAY_TRACING_NAME': 'Backend #2', 'AWS_XRAY_CONTEXT_MISSING': 'LOG_ERROR', } #------------------------------------------------------------# ``` References: - [Adding the middleware to your application (Django)](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-adding-middleware-django) - [Recorder configuration with Django](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-configuration.html#xray-sdk-python-middleware-configuration-django) #### 4.1.2. Apply patches to libraries (for tracing downstream calls) - If you want to trace downstream calls from applications and associate them, you can apply patches to the supported libraries for each language. - In the following sections, we share examples for Node.js and Python. ##### 4.1.2.1. Example for Node.js: [app/bff/server.js](app/bff/server.js) ```javascript ... //------------------------------------------------------------ // Apply patches to Node.js libraries for tracing downstream HTTP requests // - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-httpclients.html // - https://github.com/aws-samples/aws-xray-sdk-node-sample/blob/master/index.js //------------------------------------------------------------ // HTTP Client AWSXRay.captureHTTPsGlobal(require('http')); AWSXRay.capturePromise(); const axios = require('axios'); // // AWS SDK (not used in this sample) // const AWS = AWSXRay.captureAWS(require('aws-sdk')); //------------------------------------------------------------ ... ``` References: - [Tracing calls to downstream HTTP web services using the X-Ray SDK for Node.js](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-nodejs-httpclients.html) - [X-Ray SDK for Node.js Sample App](https://github.com/aws-samples/aws-xray-sdk-node-sample/tree/56edb37a5fae46c14eb74793509de3ed6e2f5c5c) ##### 4.1.2.2. Example for Python: [app/backend1/app.py](app/backend1/app.py), [app/backend2/project/api/views.py](app/backend2/project/api/views.py) ```python ... #------------------------------------------------------------# # Apply patches to Python libraries for tracing downstream HTTP requests # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-patching.html #------------------------------------------------------------# from aws_xray_sdk.core import patch_all patch_all() #------------------------------------------------------------# ... ``` References: - [Patching libraries to instrument downstream calls](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-patching.html) #### 4.1.3. Configure X-Ray segments manually - Without using X-Ray SDK with the middleware and patches for the libraries, you can manually configure X-Ray segments and subsegments. - It enables us to associate services and obtain trace data through the communication between them, but you need to take care header information to be set and passed. ##### 4.1.3.1. Example for Python in BFF ```python from flask import * import time, datetime, os, math, logging import boto3 #------------------------------------------------------------# # Add X-Ray SDK for Python # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-middleware-manual #------------------------------------------------------------# from aws_xray_sdk.core import xray_recorder #------------------------------------------------------------# app = Flask(__name__) @app.route('/health', methods=['GET']) def health_check(): return jsonify({ "status": "ok" }) @app.route("/", methods=["GET"]) def main(): #--------------------------------------------------------# # Get trace_id and parent_id from headers # - https://docs.aws.amazon.com/xray/latest/devguide/xray-services-elb.html #--------------------------------------------------------# trace_id = request.headers.get('X_RAY_HEADER_TRACE') parent_id = request.headers.get('X_RAY_HEADER_PARENT') #--------------------------------------------------------# #--------------------------------------------------------# # Begin "segment" # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-middleware-manual #--------------------------------------------------------# xray_recorder.begin_segment(name='Backend #1', parent_id=parent_id ,traceid=trace_id) #--------------------------------------------------------# #--------------------------------------------------------# # Create new headers using current segment information # - https://docs.aws.amazon.com/xray-sdk-for-python/latest/reference/basic.html #--------------------------------------------------------# current_segment = xray_recorder.current_segment() headers = { 'X_RAY_HEADER_TRACE': current_segment.trace_id, 'X_RAY_HEADER_PARENT': current_segment.id } #--------------------------------------------------------# #----------------------------------------------------# # Begin "subsegment" #1 # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-subsegments.html #----------------------------------------------------# xray_recorder.begin_subsegment(name='Call Backend #1') #----------------------------------------------------# # Write to DynamoDB table dyname_db_table_name = os.environ.get('DYNAMO_DB_TABLE_NAME', '') region_name = os.environ.get('AWS_DEFAULT_REGION', '') try: dynamodb = boto3.resource('dynamodb', region_name=region_name) table = dynamodb.Table(dyname_db_table_name) table.put_item(Item={ "SubAppId": "backend1", "LastAccessed": str(math.floor(time.time())) }) except Exception as e: logger.exception(e) #----------------------------------------------------# # End "subsegment" #1 # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-subsegments.html #----------------------------------------------------# xray_recorder.end_subsegment() #----------------------------------------------------# #----------------------------------------------------# # Begin "subsegment" #2 # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-subsegments.html #----------------------------------------------------# xray_recorder.begin_subsegment(name='Call Backend #2') #----------------------------------------------------# #----------------------------------------------------# # End "subsegment" #2 # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-subsegments.html #----------------------------------------------------# xray_recorder.end_subsegment() #----------------------------------------------------# #--------------------------------------------------------# # End "segment" # - https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-middleware-manual #--------------------------------------------------------# xray_recorder.end_segment() #--------------------------------------------------------# # Return current date current_datetime = datetime.datetime.fromtimestamp(time.time()).astimezone(datetime.timezone(datetime.timedelta(hours=9))) return jsonify({ "currentDate": current_datetime.strftime('%Y/%m/%d') }) if __name__ == '__main__': app.run(host="0.0.0.0", port=80) ``` References: - [Instrumenting Python code manually](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-middleware.html#xray-sdk-python-middleware-manual) - [Generating custom subsegments with the X-Ray SDK for Python](https://docs.aws.amazon.com/xray/latest/devguide/xray-sdk-python-subsegments.html) - [aws-xray-sdk - Basic Usage](https://docs.aws.amazon.com/xray-sdk-for-python/latest/reference/basic.html) - [Elastic Load Balancing and AWS X-Ray](https://docs.aws.amazon.com/xray/latest/devguide/xray-services-elb.html) ### 4.2. CloudFormation templates #### 4.2.1. Add X-Ray daemon container settings to ECS task definition - X-Ray daemon container settings are needed in ECS task definition. - About X-Ray daemon container, please see section 1.5. ##### 4.2.1.1. Example for CFn template: [templates/2-svc-bff.yml](templates/2-svc-bff.yml) ```yaml BffEcsTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: ContainerDefinitions: - Name: "bff-app" ... #------------------------------------------------------------# # X-Ray daemon container (side-car) definition for ECS Task # - https://docs.aws.amazon.com/xray/latest/devguide/xray-daemon-ecs.html # - https://gallery.ecr.aws/xray/aws-xray-daemon #------------------------------------------------------------# - Name: "bff-xray-daemon" Image: public.ecr.aws/xray/aws-xray-daemon PortMappings: - ContainerPort: 2000 Protocol: udp LogConfiguration: LogDriver: awslogs Options: awslogs-create-group: True awslogs-group: {"Fn::ImportValue": !Sub "${AppId}-${EnvId}-ecs-log-group-name"} awslogs-region: !Ref AWS::Region awslogs-stream-prefix: bff #------------------------------------------------------------# ... TaskRoleArn: !GetAtt EcsTaskRole.Arn ``` References: - [Running the X-Ray daemon on Amazon ECS](https://docs.aws.amazon.com/xray/latest/devguide/xray-daemon-ecs.html) - [Amazon ECR Public Gallery > xray > xray/aws-xray-daemon](https://gallery.ecr.aws/xray/aws-xray-daemon) #### 4.2.2. Add a policy to ECS task role - A policy is needed in ECS task role for writing data into X-Ray through the X-Ray daemon container. ##### 4.2.2.1. Example for CFn template: [templates/1-svc-base.yml](templates/1-svc-base.yml) ```yaml EcsTaskRole: Type: AWS::IAM::Role Properties: ... ManagedPolicyArns: #------------------------------------------------------------# # Required role for X-Ray daemon container in ECS Task # - https://docs.aws.amazon.com/ja_jp/xray/latest/devguide/security_iam_id-based-policy-examples.html #------------------------------------------------------------# - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess #------------------------------------------------------------# ... ``` References: - [AWS X-Ray identity-based policy examples](https://docs.aws.amazon.com/xray/latest/devguide/security_iam_id-based-policy-examples.html) #### 4.2.3. Configure X-Ray Sampling Rule - In X-Ray, sampling rules can be adjusted mainly for the cost perspectives. - In this case we add a sampling rule to disable sampling healthcheck traffic in each service using CloudFormation. ##### 4.2.3.1. Example for CFn template: [templates/1-svc-base.yml](templates/1-svc-base.yml) ```yaml #------------------------------------------------------------# # X-Ray Sampling rules to disable sampling healthcheck traffic # - https://docs.aws.amazon.com/xray/latest/devguide/xray-console-sampling.html#xray-console-sampling-options #------------------------------------------------------------# XRaySamplingRule: Type: AWS::XRay::SamplingRule Properties: SamplingRule: Version: 1 RuleName: !Sub "ignore-healthcheck" Priority: 1 ReservoirSize: 0 FixedRate: 0 ServiceName: "*" ServiceType: "*" HTTPMethod: "GET" URLPath: "/health" ResourceARN: "*" Host: "*" #------------------------------------------------------------# ``` References: - [Sampling rule options](https://docs.aws.amazon.com/xray/latest/devguide/xray-console-sampling.html#xray-console-sampling-options) ## 5. Security See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. ## 6. License This library is licensed under the MIT-0 License. See the LICENSE file.