From ea6f6feb65dd9ca7ec00fdf1443b9bca1f6bf792 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Fri, 6 Jul 2018 11:40:10 -0400 Subject: [PATCH 01/39] Using localstack/localstack to mimic multiple AWS services locally --- Makefile | 2 +- .../Dockerfile | 4 +-- .../bootstrap.sh} | 16 +++++++-- deps/sqs-local/Dockerfile | 9 +++++ deps/sqs-local/custom.conf | 8 +++++ docker-compose.yml | 36 ++++++++++++------- 6 files changed, 58 insertions(+), 17 deletions(-) rename deps/{dynamodb-tables => bootstrap-resources}/Dockerfile (85%) rename deps/{dynamodb-tables/tables.sh => bootstrap-resources/bootstrap.sh} (80%) create mode 100644 deps/sqs-local/Dockerfile create mode 100644 deps/sqs-local/custom.conf diff --git a/Makefile b/Makefile index eb2cf1f..aa8dc76 100644 --- a/Makefile +++ b/Makefile @@ -8,5 +8,5 @@ build: test: docker-compose build client docker-compose build test - docker-compose run --no-deps -d client + docker-compose up --no-deps -d client docker-compose run --no-deps test diff --git a/deps/dynamodb-tables/Dockerfile b/deps/bootstrap-resources/Dockerfile similarity index 85% rename from deps/dynamodb-tables/Dockerfile rename to deps/bootstrap-resources/Dockerfile index c2a332e..9a0711c 100644 --- a/deps/dynamodb-tables/Dockerfile +++ b/deps/bootstrap-resources/Dockerfile @@ -12,5 +12,5 @@ RUN apk -v --update add \ VOLUME /root/.aws VOLUME /project WORKDIR /project -ADD tables.sh . -ENTRYPOINT ["sh", "tables.sh"] +ADD bootstrap.sh . +ENTRYPOINT ["sh", "bootstrap.sh"] diff --git a/deps/dynamodb-tables/tables.sh b/deps/bootstrap-resources/bootstrap.sh similarity index 80% rename from deps/dynamodb-tables/tables.sh rename to deps/bootstrap-resources/bootstrap.sh index 237976f..897329d 100755 --- a/deps/dynamodb-tables/tables.sh +++ b/deps/bootstrap-resources/bootstrap.sh @@ -3,7 +3,6 @@ RESULT=$(aws dynamodb describe-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ --table-name test_Users) -CODE=$? if [ $? -eq 0 ]; then aws dynamodb delete-table \ --region us-east-1 \ @@ -23,7 +22,6 @@ RESULT=$(aws dynamodb describe-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ --table-name test_Messages) -CODE=$? if [ $? -eq 0 ]; then aws dynamodb delete-table \ --region us-east-1 \ @@ -37,3 +35,17 @@ aws dynamodb create-table \ --key-schema AttributeName=room,KeyType=HASH AttributeName=message,KeyType=RANGE \ --attribute-definitions AttributeName=room,AttributeType=S AttributeName=message,AttributeType=S \ --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10 + +# Local SQS queue +RESULT=$(aws sqs get-queue-url \ + --region us-east-1 \ + --endpoint-url $SQS_ENDPOINT \ + --queue-name test_Messages) +if [ $? -eq 0 ]; then + echo "Delete queue"; + echo $RESULT; +fi +aws sqs create-queue \ + --region us-east-1 \ + --endpoint-url $SQS_ENDPOINT \ + --queue-name test_Messages diff --git a/deps/sqs-local/Dockerfile b/deps/sqs-local/Dockerfile new file mode 100644 index 0000000..a09215b --- /dev/null +++ b/deps/sqs-local/Dockerfile @@ -0,0 +1,9 @@ +FROM java:8 + +ADD https://s3-eu-west-1.amazonaws.com/softwaremill-public/elasticmq-server-0.13.8.jar / +COPY custom.conf / +ENTRYPOINT ["/usr/bin/java", "-Dconfig.file=custom.conf", "-jar", "/elasticmq-server-0.13.8.jar"] + +EXPOSE 9324 + +CMD ["-help"] diff --git a/deps/sqs-local/custom.conf b/deps/sqs-local/custom.conf new file mode 100644 index 0000000..9be7730 --- /dev/null +++ b/deps/sqs-local/custom.conf @@ -0,0 +1,8 @@ +include classpath("application.conf") + +node-address { + protocol = http + host = "*" + port = 9324 + context-path = "" +} diff --git a/docker-compose.yml b/docker-compose.yml index f30a37f..c78a340 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,25 +9,35 @@ services: networks: - chat ports: - - 6379:6379 + - 6379 - # Launch a local version of DynamoDB - dynamodb-local: + # Launch localstack (https://github.com/localstack/localstack) + # This container mimics AWS services locally for offline testing + # and development purposes. + local-stack: + image: localstack/localstack networks: - chat - build: ./deps/dynamodb-local ports: - - 8000:8000 + # DyanmoDB Local + - 4569 + # SNS local + - 4575 + # SQS local + - 4576 - # Ephemeral container used for creating the tables in DynamoDB - dynamodb-tables: + # Ephemeral container used for creating the tables in + # the local DynamoDB tables, SNS topics, and SQS queues + bootstrap-resources: depends_on: - - dynamodb-local + - local-stack networks: - chat - build: ./deps/dynamodb-tables + build: ./deps/bootstrap-resources environment: - DYNAMODB_ENDPOINT: http://dynamodb-local:8000 + DYNAMODB_ENDPOINT: http://local-stack:4569 + SQS_ENDPOINT: http://local-stack:4576 + SNS_ENDPOINT: http://local-stack:4575 AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test @@ -35,7 +45,7 @@ services: client: depends_on: - redis - - dynamodb-tables + - bootstrap-resources networks: - chat build: ./services/client @@ -43,7 +53,9 @@ services: LOCAL: "true" ENV_NAME: test REDIS_ENDPOINT: redis - DYNAMODB_ENDPOINT: http://dynamodb-local:8000 + DYNAMODB_ENDPOINT: http://local-stack:4569 + SQS_ENDPOINT: http://local-stack:4576 + SNS_ENDPOINT: http://local-stack:4575 AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test ports: From e1a0ca0da72edc7ca8e3616d6227c6a388bfca1b Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Fri, 6 Jul 2018 16:07:57 -0400 Subject: [PATCH 02/39] Another service behind an SNS topic and SQS queue --- Makefile | 2 ++ deps/bootstrap-resources/bootstrap.sh | 50 +++++++++++++++++++++------ docker-compose.yml | 18 ++++++++++ services/client/lib/config.js | 3 ++ services/client/lib/message.js | 33 +++++++++++++++--- services/client/package.json | 12 +------ services/message-cards/Dockerfile | 10 ++++++ services/message-cards/index.js | 24 +++++++++++++ services/message-cards/lib/config.js | 9 +++++ services/message-cards/package.json | 13 +++++++ 10 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 services/message-cards/Dockerfile create mode 100644 services/message-cards/index.js create mode 100644 services/message-cards/lib/config.js create mode 100644 services/message-cards/package.json diff --git a/Makefile b/Makefile index aa8dc76..e205526 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,8 @@ build: test: docker-compose build client + docker-compose build message-cards docker-compose build test docker-compose up --no-deps -d client + docker-compose up --no-deps -d message-cards docker-compose run --no-deps test diff --git a/deps/bootstrap-resources/bootstrap.sh b/deps/bootstrap-resources/bootstrap.sh index 897329d..e17b2f1 100755 --- a/deps/bootstrap-resources/bootstrap.sh +++ b/deps/bootstrap-resources/bootstrap.sh @@ -1,51 +1,81 @@ +ENV_NAME="test"; + # Local Users table +TABLE_NAME="Users"; RESULT=$(aws dynamodb describe-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ - --table-name test_Users) + --table-name ${ENV_NAME}_${TABLE_NAME}) if [ $? -eq 0 ]; then + echo "Delete existing table ${TABLE_NAME}"; aws dynamodb delete-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ - --table-name test_Users + --table-name ${ENV_NAME}_${TABLE_NAME} fi +echo "Create table ${TABLE_NAME}"; aws dynamodb create-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ - --table-name test_Users \ + --table-name ${ENV_NAME}_${TABLE_NAME} \ --key-schema AttributeName=username,KeyType=HASH \ --attribute-definitions AttributeName=username,AttributeType=S \ --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10 # Local Messages table +TABLE_NAME="Messages"; RESULT=$(aws dynamodb describe-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ - --table-name test_Messages) + --table-name ${ENV_NAME}_${TABLE_NAME}) if [ $? -eq 0 ]; then + echo "Delete existing table ${TABLE_NAME}"; aws dynamodb delete-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ - --table-name test_Messages + --table-name ${ENV_NAME}_${TABLE_NAME} fi +echo "Create table ${TABLE_NAME}"; aws dynamodb create-table \ --region us-east-1 \ --endpoint-url $DYNAMODB_ENDPOINT \ - --table-name test_Messages \ + --table-name ${ENV_NAME}_${TABLE_NAME} \ --key-schema AttributeName=room,KeyType=HASH AttributeName=message,KeyType=RANGE \ --attribute-definitions AttributeName=room,AttributeType=S AttributeName=message,AttributeType=S \ --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=10 # Local SQS queue +QUEUE_NAME="MessagesToCardify"; RESULT=$(aws sqs get-queue-url \ --region us-east-1 \ --endpoint-url $SQS_ENDPOINT \ - --queue-name test_Messages) + --queue-name ${ENV_NAME}_${QUEUE_NAME}) if [ $? -eq 0 ]; then - echo "Delete queue"; - echo $RESULT; + echo "Delete queue ${QUEUE_NAME}"; + aws sqs delete-queue \ + --region us-east-1 \ + --endpoint-url $SQS_ENDPOINT \ + --queue-url $SQS_ENDPOINT/queue/${ENV_NAME}_${QUEUE_NAME} fi +echo "Create queue ${QUEUE_NAME}"; aws sqs create-queue \ --region us-east-1 \ --endpoint-url $SQS_ENDPOINT \ - --queue-name test_Messages + --queue-name ${ENV_NAME}_${QUEUE_NAME} + +# Local SNS topic +TOPIC_NAME="MessageSent" +echo "Create topic ${TOPIC_NAME}"; +aws sns create-topic \ + --region us-east-1 \ + --endpoint-url $SNS_ENDPOINT \ + --name ${ENV_NAME}_${TOPIC_NAME} + +# Subscription between the SQS queue and the SNS topic +echo "Create subscription between topic ${TOPIC_NAME} and queue ${QUEUE_NAME}" +aws sns subscribe \ + --region us-east-1 \ + --endpoint-url $SNS_ENDPOINT \ + --topic-arn arn:aws:sns:us-east-1:123456789012:${ENV_NAME}_${TOPIC_NAME} \ + --protocol sqs \ + --notification-endpoint $SQS_ENDPOINT/queue/${ENV_NAME}_${QUEUE_NAME} diff --git a/docker-compose.yml b/docker-compose.yml index c78a340..64ce6c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,15 +56,33 @@ services: DYNAMODB_ENDPOINT: http://local-stack:4569 SQS_ENDPOINT: http://local-stack:4576 SNS_ENDPOINT: http://local-stack:4575 + MESSAGE_SENT_SNS_ARN: arn:aws:sns:us-east-1:123456789012:test_MessageSent AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test ports: - 3000:3000 + # Background worker that cardifies messages + message-cards: + depends_on: + - redis + - bootstrap-resources + networks: + - chat + build: ./services/message-cards + environment: + LOCAL: "true" + ENV_NAME: test + REDIS_ENDPOINT: redis + QUEUE_URL: http://local-stack:4576/queue/test_MessagesToCardify + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + # The test suite test: depends_on: - client + - message-cards networks: - chat build: ./services/test-suite diff --git a/services/client/lib/config.js b/services/client/lib/config.js index 992afbd..ddd0ab8 100644 --- a/services/client/lib/config.js +++ b/services/client/lib/config.js @@ -8,6 +8,9 @@ module.exports = { REDIS_ENDPOINT: process.env.REDIS_ENDPOINT, DYNAMODB_ENDPOINT: new AWS.Endpoint(process.env.DYNAMODB_ENDPOINT), + SNS_ENDPOINT: new AWS.Endpoint(process.env.SNS_ENDPOINT), + + MESSAGE_SENT_SNS_ARN: process.env.MESSAGE_SENT_SNS_ARN, // Controls how often clients ping back and forth HEARTBEAT_TIMEOUT: 8000, diff --git a/services/client/lib/message.js b/services/client/lib/message.js index f925162..289cd84 100644 --- a/services/client/lib/message.js +++ b/services/client/lib/message.js @@ -9,6 +9,11 @@ function Message() { endpoint: config.DYNAMODB_ENDPOINT }); this.tableName = `${config.ENV_NAME}_Messages`; + + this.SNS = new AWS.SNS({ + region: config.REGION, + endpoint: config.SNS_ENDPOINT + }); } module.exports = new Message(); @@ -23,9 +28,9 @@ module.exports = new Message(); * @param {Date} message.time **/ Message.prototype.add = async function(message) { - try { - var id = message.time + ':' + crypto.randomBytes(7).toString('hex'); + var id = message.time + ':' + crypto.randomBytes(7).toString('hex'); + try { await this.dynamoDB.put({ TableName: this.tableName, Item: { @@ -37,13 +42,31 @@ Message.prototype.add = async function(message) { message: id } }).promise(); - - return id; } catch (e) { console.error(e); + throw new Error('Failed to insert new message in database'); + } - throw new Error('Failed to insert new user in database'); + // Publish onto the SNS topic so that subscribing services get notified + // Happens aysnchronously and does not block the return. + try { + this.SNS.publish({ + Message: JSON.stringify({ + room: message.room, + username: message.username, + avatar: message.avatar, + content: message.content, + time: message.time, + message: id + }), + TopicArn: config.MESSAGE_SENT_SNS_ARN + }).promise(); + } catch (e) { + console.error(e); + throw new Error('Failed to publish MessageSent notification'); } + + return id; }; /** diff --git a/services/client/package.json b/services/client/package.json index 91c47f0..f302f95 100644 --- a/services/client/package.json +++ b/services/client/package.json @@ -3,8 +3,7 @@ "version": "0.0.0", "description": "A simple chat client using socket.io", "main": "index.js", - "private": true, - "license": "BSD", + "license": "MIT", "dependencies": { "async": "^2.6.0", "aws-sdk": "2.262.1", @@ -17,15 +16,6 @@ "socket.io": "2.1.1", "socket.io-redis": "5.2.0" }, - "devDependencies": { - "chai": "4.1.2", - "mocha": "5.2.0", - "request-promise-native": "1.0.5", - "request": "2.87.0" - }, - "scripts": { - "start": "LOCAL=true REGION=us-east-1 ENV_NAME=test REDIS_ENDPOINT=localhost DYNAMODB_ENDPOINT=http://localhost:8000 AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test node index.js" - }, "eslintConfig": { "globals": { "Vue": true diff --git a/services/message-cards/Dockerfile b/services/message-cards/Dockerfile new file mode 100644 index 0000000..0d5e6b1 --- /dev/null +++ b/services/message-cards/Dockerfile @@ -0,0 +1,10 @@ +FROM node:9 AS build +WORKDIR /srv +ADD package.json . +RUN npm install + +FROM node:9-slim +COPY --from=build /srv . +ADD . . +EXPOSE 3000 +CMD ["node", "index.js"] diff --git a/services/message-cards/index.js b/services/message-cards/index.js new file mode 100644 index 0000000..0222ee0 --- /dev/null +++ b/services/message-cards/index.js @@ -0,0 +1,24 @@ +var config = require('./lib/config'); +var Squiss = require('squiss'); + +var poller = new Squiss({ + queueUrl: config.QUEUE_URL, + awsConfig: { + region: config.REGION + }, + bodyFormat: 'json', + unwrapSns: true, + maxInFlight: 50 // Handle a max of 50 messages concurrently. +}); + +poller.start(); + +poller.on('message', (msg) => { + console.log(JSON.stringify(msg.body)); + msg.del(); +}); + +process.on('SIGTERM', function() { + console.log('Received SIGTERM, shutting down polling'); + poller.stop(); +}); diff --git a/services/message-cards/lib/config.js b/services/message-cards/lib/config.js new file mode 100644 index 0000000..8e686ed --- /dev/null +++ b/services/message-cards/lib/config.js @@ -0,0 +1,9 @@ +module.exports = { + ENV_NAME: process.env.ENV_NAME, + REGION: process.env.REGION || 'us-east-1', + + REDIS_ENDPOINT: process.env.REDIS_ENDPOINT, + SQS_ENDPOINT: process.env.SQS_ENDPOINT, + + QUEUE_URL: process.env.QUEUE_URL +}; diff --git a/services/message-cards/package.json b/services/message-cards/package.json new file mode 100644 index 0000000..50a0179 --- /dev/null +++ b/services/message-cards/package.json @@ -0,0 +1,13 @@ +{ + "name": "message-cards", + "version": "0.0.0", + "description": "A backend service which parses messages for links and fetches metadata for cards", + "main": "index.js", + "license": "MIT", + "dependencies": { + "async": "^2.6.0", + "redis": "2.8.0", + "socket.io-redis": "5.2.0", + "squiss": "2.2.1" + } +} From d554b0337fc53d868917b8525117673a54006f1b Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Mon, 23 Jul 2018 15:15:24 -0700 Subject: [PATCH 03/39] Adding deployment for new background service --- Makefile | 18 +- deps/bootstrap-resources/Dockerfile | 2 + deps/bootstrap-resources/bootstrap.sh | 40 +++- deps/bootstrap-resources/wait-for-it.sh | 32 +++ docker-compose.yml | 20 +- message-cards-service.yml | 270 ++++++++++++++++++++++++ pipeline.yml | 68 +++++- services/message-cards/index.js | 6 +- services/message-cards/package.json | 1 + 9 files changed, 431 insertions(+), 26 deletions(-) create mode 100755 deps/bootstrap-resources/wait-for-it.sh create mode 100644 message-cards-service.yml diff --git a/Makefile b/Makefile index e205526..9f6608e 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ -run: - docker-compose up -d - -build: - docker-compose build client +up: + docker-compose up -d redis + docker-compose up -d localstack + docker-compose run bootstrap-resources docker-compose up --no-deps -d client + docker-compose up --no-deps -d message-cards + +down: + docker-compose down test: docker-compose build client @@ -12,3 +15,8 @@ test: docker-compose up --no-deps -d client docker-compose up --no-deps -d message-cards docker-compose run --no-deps test + +cards: + docker-compose build message-cards + docker-compose up --no-deps -d message-cards + docker-compose run --no-deps test diff --git a/deps/bootstrap-resources/Dockerfile b/deps/bootstrap-resources/Dockerfile index 9a0711c..61a7e12 100644 --- a/deps/bootstrap-resources/Dockerfile +++ b/deps/bootstrap-resources/Dockerfile @@ -5,6 +5,7 @@ RUN apk -v --update add \ groff \ less \ mailcap \ + curl \ && \ pip install --upgrade awscli==1.14.5 s3cmd==2.0.1 python-magic && \ apk -v --purge del py-pip && \ @@ -12,5 +13,6 @@ RUN apk -v --update add \ VOLUME /root/.aws VOLUME /project WORKDIR /project +ADD wait-for-it.sh . ADD bootstrap.sh . ENTRYPOINT ["sh", "bootstrap.sh"] diff --git a/deps/bootstrap-resources/bootstrap.sh b/deps/bootstrap-resources/bootstrap.sh index e17b2f1..99d420d 100755 --- a/deps/bootstrap-resources/bootstrap.sh +++ b/deps/bootstrap-resources/bootstrap.sh @@ -1,3 +1,14 @@ +# Make sure that the localstack resources are up first. +./wait-for-it.sh $SNS_ENDPOINT +./wait-for-it.sh $SQS_ENDPOINT +./wait-for-it.sh $DYNAMODB_ENDPOINT + +aws dynamodb list-tables \ + --region us-east-1 \ + --endpoint-url $DYNAMODB_ENDPOINT \ + --cli-connect-timeout 1 \ + --cli-read-timeout 1 + ENV_NAME="test"; # Local Users table @@ -58,14 +69,34 @@ if [ $? -eq 0 ]; then --queue-url $SQS_ENDPOINT/queue/${ENV_NAME}_${QUEUE_NAME} fi echo "Create queue ${QUEUE_NAME}"; +aws sqs delete-queue \ + --region us-east-1 \ + --endpoint-url $SQS_ENDPOINT \ + --queue-url $SQS_ENDPOINT/queue/${ENV_NAME}_${QUEUE_NAME} aws sqs create-queue \ --region us-east-1 \ --endpoint-url $SQS_ENDPOINT \ + --attributes VisibilityTimeout=60 \ --queue-name ${ENV_NAME}_${QUEUE_NAME} # Local SNS topic TOPIC_NAME="MessageSent" +TOPIC_ARN="arn:aws:sns:us-east-1:123456789012:${ENV_NAME}_${TOPIC_NAME}" echo "Create topic ${TOPIC_NAME}"; +SUBSCRIPTION_ARN=$(aws sns list-subscriptions-by-topic \ + --region us-east-1 \ + --endpoint-url $SNS_ENDPOINT \ + --topic-arn $TOPIC_ARN \ + --query Subscriptions[0].SubscriptionArn \ + --output text) +aws sns unsubscribe \ + --region us-east-1 \ + --endpoint-url $SNS_ENDPOINT \ + --subscription-arn $SUBSCRIPTION_ARN +aws sns delete-topic \ + --region us-east-1 \ + --endpoint-url $SNS_ENDPOINT \ + --topic-arn $TOPIC_ARN aws sns create-topic \ --region us-east-1 \ --endpoint-url $SNS_ENDPOINT \ @@ -76,6 +107,13 @@ echo "Create subscription between topic ${TOPIC_NAME} and queue ${QUEUE_NAME}" aws sns subscribe \ --region us-east-1 \ --endpoint-url $SNS_ENDPOINT \ - --topic-arn arn:aws:sns:us-east-1:123456789012:${ENV_NAME}_${TOPIC_NAME} \ + --topic-arn $TOPIC_ARN \ --protocol sqs \ --notification-endpoint $SQS_ENDPOINT/queue/${ENV_NAME}_${QUEUE_NAME} +aws sns list-subscriptions-by-topic \ + --region us-east-1 \ + --endpoint-url $SNS_ENDPOINT \ + --topic-arn $TOPIC_ARN + + + diff --git a/deps/bootstrap-resources/wait-for-it.sh b/deps/bootstrap-resources/wait-for-it.sh new file mode 100755 index 0000000..7ffc186 --- /dev/null +++ b/deps/bootstrap-resources/wait-for-it.sh @@ -0,0 +1,32 @@ +#!/bin/sh +echo "Waiting for $1 (This may take a few moments)"; + +#clean=${1//http:\/\//} +#hostport=${clean//:/ } + +#set -f; IFS=' ' +#set -- $hostport +#second=$2; fourth=$4 +#set +f; unset IFS + +#HOST=$1 +#PORT=$2 + +#while ! nc -w 1 -z $HOST $PORT; do sleep 1 && echo -n .; done; + +while true +do + echo "pre poll"; + STATUS=$(curl -s -o /dev/null -w '%{http_code}' $1) + echo $STATUS + if [ $STATUS -eq 404 ]; then + echo "ready" + break + else + echo -n "." + fi + sleep 1 +done + + +#--output /dev/null --silent --head diff --git a/docker-compose.yml b/docker-compose.yml index 64ce6c8..f3e039a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,12 +14,12 @@ services: # Launch localstack (https://github.com/localstack/localstack) # This container mimics AWS services locally for offline testing # and development purposes. - local-stack: + localstack: image: localstack/localstack networks: - chat ports: - # DyanmoDB Local + # DynamoDB Local - 4569 # SNS local - 4575 @@ -30,14 +30,14 @@ services: # the local DynamoDB tables, SNS topics, and SQS queues bootstrap-resources: depends_on: - - local-stack + - localstack networks: - chat build: ./deps/bootstrap-resources environment: - DYNAMODB_ENDPOINT: http://local-stack:4569 - SQS_ENDPOINT: http://local-stack:4576 - SNS_ENDPOINT: http://local-stack:4575 + DYNAMODB_ENDPOINT: http://localstack:4569 + SQS_ENDPOINT: http://localstack:4576 + SNS_ENDPOINT: http://localstack:4575 AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test @@ -53,9 +53,9 @@ services: LOCAL: "true" ENV_NAME: test REDIS_ENDPOINT: redis - DYNAMODB_ENDPOINT: http://local-stack:4569 - SQS_ENDPOINT: http://local-stack:4576 - SNS_ENDPOINT: http://local-stack:4575 + DYNAMODB_ENDPOINT: http://localstack:4569 + SQS_ENDPOINT: http://localstack:4576 + SNS_ENDPOINT: http://localstack:4575 MESSAGE_SENT_SNS_ARN: arn:aws:sns:us-east-1:123456789012:test_MessageSent AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test @@ -74,7 +74,7 @@ services: LOCAL: "true" ENV_NAME: test REDIS_ENDPOINT: redis - QUEUE_URL: http://local-stack:4576/queue/test_MessagesToCardify + QUEUE_URL: http://localstack:4576/queue/test_MessagesToCardify AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test diff --git a/message-cards-service.yml b/message-cards-service.yml new file mode 100644 index 0000000..67c6b7e --- /dev/null +++ b/message-cards-service.yml @@ -0,0 +1,270 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: Socket.io chat service +Parameters: + EnvironmentName: + Type: String + Default: production + Description: A name for the environment that this cloudformation will be part of. + Used to locate other resources in the same environment. + ServiceName: + Type: String + Default: chat + Description: A name for the service + ImageUrl: + Type: String + Default: nginx + Description: The url of a docker image that contains the application process that + will handle the traffic for this service + ContainerPort: + Type: Number + Default: 3000 + Description: What port number the application inside the docker container is binding to + ContainerCpu: + Type: Number + Default: 1024 + Description: How much CPU to give the container. 1024 is 1 CPU + ContainerMemory: + Type: Number + Default: 2048 + Description: How much memory in megabytes to give the container + Path: + Type: String + Default: "*" + Description: A path on the public load balancer that this service + should be connected to. Use * to send all load balancer + traffic to this service. + Priority: + Type: Number + Default: 1 + Description: The priority for the routing rule added to the load balancer. + This only applies if your have multiple services which have been + assigned to different paths on the load balancer. + DesiredCount: + Type: Number + Default: 2 + Description: How many copies of the service task to run + +Resources: + # A log group for storing the container logs for this service + LogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: !Join ['-', [!Ref 'EnvironmentName', 'service', !Ref 'ServiceName']] + + # The task definition. This is a simple metadata description of what + # container to run, and what resource requirements it has. + TaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + Family: !Ref 'ServiceName' + Cpu: !Ref 'ContainerCpu' + Memory: !Ref 'ContainerMemory' + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + ExecutionRoleArn: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'ECSTaskExecutionRole']] + TaskRoleArn: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'ChatServiceRole']] + ContainerDefinitions: + - Name: !Ref 'ServiceName' + Cpu: !Ref 'ContainerCpu' + Memory: !Ref 'ContainerMemory' + Image: !Ref 'ImageUrl' + Environment: + - Name: REGION + Value: !Ref 'AWS::Region' + - Name: NODE_ENV + Value: production + - Name: REDIS_ENDPOINT + Value: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'RedisEndpoint']] + - Name: ENV_NAME + Value: !Ref 'EnvironmentName' + LogConfiguration: + LogDriver: 'awslogs' + Options: + awslogs-group: !Join ['-', [!Ref 'EnvironmentName', 'service', !Ref 'ServiceName']] + awslogs-region: !Ref 'AWS::Region' + awslogs-stream-prefix: !Ref 'ServiceName' + + # The service. The service is a resource which allows you to run multiple + # copies of a type of task, and gather up their logs and metrics, as well + # as monitor the number of running tasks and replace any that have crashed + Service: + Type: AWS::ECS::Service + DependsOn: + - HTTPRule + - HTTPSRule + Properties: + ServiceName: !Ref 'ServiceName' + Cluster: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + LaunchType: FARGATE + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 75 + DesiredCount: !Ref 'DesiredCount' + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: ENABLED + SecurityGroups: + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'FargateContainerSecurityGroup']] + Subnets: + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicSubnetOne']] + - Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'PublicSubnetTwo']] + TaskDefinition: !Ref 'TaskDefinition' + + # Enable autoscaling for this service + ScalableTarget: + Type: AWS::ApplicationAutoScaling::ScalableTarget + DependsOn: Service + Properties: + ServiceNamespace: 'ecs' + ScalableDimension: 'ecs:service:DesiredCount' + ResourceId: + Fn::Join: + - '/' + - - service + - Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + - !Ref 'ServiceName' + MinCapacity: 2 + MaxCapacity: 10 + RoleARN: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'AutoscalingRole']] + + # Create scaling policies for the service + ScaleDownPolicy: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ScalableTarget + Properties: + PolicyName: + Fn::Join: + - '/' + - - scale + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + - down + PolicyType: StepScaling + ResourceId: + Fn::Join: + - '/' + - - service + - Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + - !Ref 'ServiceName' + ScalableDimension: 'ecs:service:DesiredCount' + ServiceNamespace: 'ecs' + StepScalingPolicyConfiguration: + AdjustmentType: 'ChangeInCapacity' + StepAdjustments: + - MetricIntervalUpperBound: 0 + ScalingAdjustment: -1 + MetricAggregationType: 'Average' + Cooldown: 60 + + ScaleUpPolicy: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ScalableTarget + Properties: + PolicyName: + Fn::Join: + - '/' + - - scale + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + - up + PolicyType: StepScaling + ResourceId: + Fn::Join: + - '/' + - - service + - Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + - !Ref 'ServiceName' + ScalableDimension: 'ecs:service:DesiredCount' + ServiceNamespace: 'ecs' + StepScalingPolicyConfiguration: + AdjustmentType: 'ChangeInCapacity' + StepAdjustments: + - MetricIntervalLowerBound: 0 + MetricIntervalUpperBound: 15 + ScalingAdjustment: 1 + - MetricIntervalLowerBound: 15 + MetricIntervalUpperBound: 25 + ScalingAdjustment: 2 + - MetricIntervalLowerBound: 25 + ScalingAdjustment: 3 + MetricAggregationType: 'Average' + Cooldown: 60 + + # Create alarms to trigger these policies + LowCpuUsageAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: + Fn::Join: + - '-' + - - low-cpu + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + AlarmDescription: + Fn::Join: + - ' ' + - - "Low CPU utilization for service" + - !Ref 'ServiceName' + - "in stack" + - !Ref 'EnvironmentName' + MetricName: CPUUtilization + Namespace: AWS/ECS + Dimensions: + - Name: ServiceName + Value: !Ref 'ServiceName' + - Name: ClusterName + Value: + Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + Statistic: Average + Period: 60 + EvaluationPeriods: 1 + Threshold: 20 + ComparisonOperator: LessThanOrEqualToThreshold + AlarmActions: + - !Ref ScaleDownPolicy + + HighCpuUsageAlarm: + Type: AWS::CloudWatch::Alarm + Properties: + AlarmName: + Fn::Join: + - '-' + - - high-cpu + - !Ref 'EnvironmentName' + - !Ref 'ServiceName' + AlarmDescription: + Fn::Join: + - ' ' + - - "High CPU utilization for service" + - !Ref 'ServiceName' + - "in stack" + - !Ref 'EnvironmentName' + MetricName: CPUUtilization + Namespace: AWS/ECS + Dimensions: + - Name: ServiceName + Value: !Ref 'ServiceName' + - Name: ClusterName + Value: + Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ClusterName']] + Statistic: Average + Period: 60 + EvaluationPeriods: 1 + Threshold: 70 + ComparisonOperator: GreaterThanOrEqualToThreshold + AlarmActions: + - !Ref ScaleUpPolicy diff --git a/pipeline.yml b/pipeline.yml index 9d4ab27..2f6ca39 100644 --- a/pipeline.yml +++ b/pipeline.yml @@ -148,7 +148,7 @@ Resources: DeletionPolicy: Retain # This is the definition of how to build the code in the repository - CodeBuildProject: + ChatFrontendBuild: Type: AWS::CodeBuild::Project Properties: Artifacts: @@ -184,6 +184,43 @@ Resources: Name: !Ref AWS::StackName ServiceRole: !Ref CodeBuildServiceRole + # This is the definition of how to build the code in the repository + MessageCardsBuild: + Type: AWS::CodeBuild::Project + Properties: + Artifacts: + Type: CODEPIPELINE + Source: + Type: CODEPIPELINE + BuildSpec: | + version: 0.2 + phases: + pre_build: + commands: + - $(aws ecr get-login --no-include-email) + - TAG="$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | head -c 8)" + - IMAGE_URI="${REPOSITORY_URI}:${TAG}" + build: + commands: + - docker build --tag "${IMAGE_URI}" ./services/message-cards + post_build: + commands: + - docker push "$IMAGE_URI" + - printf '{"ImageUri":"%s"}' "$IMAGE_URI" > build.json + artifacts: + files: build.json + Environment: + ComputeType: BUILD_GENERAL1_SMALL + Image: aws/codebuild/docker:17.09.0 + Type: LINUX_CONTAINER + EnvironmentVariables: + - Name: AWS_DEFAULT_REGION + Value: !Ref AWS::Region + - Name: REPOSITORY_URI + Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} + Name: !Ref AWS::StackName + ServiceRole: !Ref CodeBuildServiceRole + # This pipeline defines the steps to build, deploy, and release the application Pipeline: Type: AWS::CodePipeline::Pipeline @@ -241,7 +278,7 @@ Resources: - Name: ChatResources Actions: # Deploy the chat resources - - Name: Deploy + - Name: Build architecture and containers ActionTypeId: Category: Deploy Owner: AWS @@ -262,7 +299,7 @@ Resources: OutputArtifacts: - Name: ChatResources - # And build the chat docker image + # Build the chat frontend docker image - Name: Build ActionTypeId: Category: Build @@ -270,17 +307,32 @@ Resources: Version: 1 Provider: CodeBuild Configuration: - ProjectName: !Ref CodeBuildProject + ProjectName: !Ref ChatFrontendBuild InputArtifacts: - Name: Source OutputArtifacts: - - Name: BuildOutput + - Name: FrontendBuildOutput + RunOrder: 1 + + # Build the backend worker image + - Name: BuildMessageCards + ActionTypeId: + Category: Build + Owner: AWS + Version: 1 + Provider: CodeBuild + Configuration: + ProjectName: !Ref MessageCardsBuild + InputArtifacts: + - Name: Source + OutputArtifacts: + - Name: MessageCardsBuildOutput RunOrder: 1 # Finally we deploy the Fargate service to the cluster - - Name: Deploy + - Name: Deploy containers Actions: - - Name: Deploy + - Name: Deploy frontend container ActionTypeId: Category: Deploy Owner: AWS @@ -301,7 +353,7 @@ Resources: } InputArtifacts: - Name: Source - - Name: BuildOutput + - Name: FrontendBuildOutput Outputs: PipelineUrl: diff --git a/services/message-cards/index.js b/services/message-cards/index.js index 0222ee0..936152b 100644 --- a/services/message-cards/index.js +++ b/services/message-cards/index.js @@ -11,14 +11,16 @@ var poller = new Squiss({ maxInFlight: 50 // Handle a max of 50 messages concurrently. }); +console.log('Message card service up and ready to poll'); + poller.start(); -poller.on('message', (msg) => { +poller.on('message', function(msg) { console.log(JSON.stringify(msg.body)); msg.del(); }); process.on('SIGTERM', function() { - console.log('Received SIGTERM, shutting down polling'); + console.log('Received SIGTERM, shutting down message card polling'); poller.stop(); }); diff --git a/services/message-cards/package.json b/services/message-cards/package.json index 50a0179..03654a9 100644 --- a/services/message-cards/package.json +++ b/services/message-cards/package.json @@ -6,6 +6,7 @@ "license": "MIT", "dependencies": { "async": "^2.6.0", + "cheerio": "1.0.0-rc.2", "redis": "2.8.0", "socket.io-redis": "5.2.0", "squiss": "2.2.1" From 5404864ef6f48ae3eb2369524bc44020b0415249 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Mon, 23 Jul 2018 15:18:57 -0700 Subject: [PATCH 04/39] Fixing a duped name in the code builds --- pipeline.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipeline.yml b/pipeline.yml index 2f6ca39..f047230 100644 --- a/pipeline.yml +++ b/pipeline.yml @@ -181,7 +181,7 @@ Resources: Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} - Name: !Ref AWS::StackName + Name: !Ref AWS::StackName-chat-frontend ServiceRole: !Ref CodeBuildServiceRole # This is the definition of how to build the code in the repository @@ -218,7 +218,7 @@ Resources: Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} - Name: !Ref AWS::StackName + Name: !Ref AWS::StackName-message-cards ServiceRole: !Ref CodeBuildServiceRole # This pipeline defines the steps to build, deploy, and release the application From 558af3479b3f806810ee20b59057eafaa31acdf2 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Mon, 23 Jul 2018 15:32:03 -0700 Subject: [PATCH 05/39] Fixing missing bits in the pipeline --- .gitignore | 1 + pipeline.yml | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 825fc67..44fddde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .DS_Store npm-debug.log +deploy-pipeline.sh diff --git a/pipeline.yml b/pipeline.yml index f047230..06b88fa 100644 --- a/pipeline.yml +++ b/pipeline.yml @@ -181,7 +181,7 @@ Resources: Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} - Name: !Ref AWS::StackName-chat-frontend + Name: !Sub ${AWS::StackName}-chat-frontend ServiceRole: !Ref CodeBuildServiceRole # This is the definition of how to build the code in the repository @@ -218,7 +218,7 @@ Resources: Value: !Ref AWS::Region - Name: REPOSITORY_URI Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} - Name: !Ref AWS::StackName-message-cards + Name: !Sub ${AWS::StackName}-message-cards ServiceRole: !Ref CodeBuildServiceRole # This pipeline defines the steps to build, deploy, and release the application @@ -234,7 +234,7 @@ Resources: # First we have to pull the source code from the Github repository. - Name: Source Actions: - - Name: App + - Name: Fetch-source ActionTypeId: Category: Source Owner: ThirdParty @@ -250,7 +250,7 @@ Resources: RunOrder: 1 # Now we deploy the base resources: the cluster and VPC itself. - - Name: BaseResources + - Name: Create-network Actions: - Name: Deploy ActionTypeId: @@ -275,10 +275,10 @@ Resources: - Name: BaseResources # And we deploy the application resources (Elasticache, DynamoDB, etc) - - Name: ChatResources + - Name: Build-dependencies-and-containers Actions: # Deploy the chat resources - - Name: Build architecture and containers + - Name: Queues-roles-and-tables ActionTypeId: Category: Deploy Owner: AWS @@ -300,7 +300,7 @@ Resources: - Name: ChatResources # Build the chat frontend docker image - - Name: Build + - Name: Build-chat-frontend ActionTypeId: Category: Build Owner: AWS @@ -315,7 +315,7 @@ Resources: RunOrder: 1 # Build the backend worker image - - Name: BuildMessageCards + - Name: Build-message-cards-service ActionTypeId: Category: Build Owner: AWS @@ -330,9 +330,9 @@ Resources: RunOrder: 1 # Finally we deploy the Fargate service to the cluster - - Name: Deploy containers + - Name: Deploy-containers Actions: - - Name: Deploy frontend container + - Name: Deploy-frontend-container ActionTypeId: Category: Deploy Owner: AWS @@ -355,6 +355,29 @@ Resources: - Name: Source - Name: FrontendBuildOutput + - Name: Deploy-message-card-container + ActionTypeId: + Category: Deploy + Owner: AWS + Version: 1 + Provider: CloudFormation + Configuration: + ActionMode: CREATE_UPDATE + RoleArn: !GetAtt CloudFormationDeployRole.Arn + StackName: !Sub ${EnvironmentName}-MessageCards + TemplatePath: Source::message-cards-service.yml + Capabilities: CAPABILITY_IAM + ParameterOverrides: !Sub | + { + "EnvironmentName": "${EnvironmentName}", + "ImageUrl": { + "Fn::GetParam" : ["BuildOutput", "build.json", "ImageUri"] + } + } + InputArtifacts: + - Name: Source + - Name: MessageCardsBuildOutput + Outputs: PipelineUrl: Value: !Sub https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${Pipeline} From db2fdaa24c9318ee057d53eb3ebcafb65fe96019 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 08:33:11 -0700 Subject: [PATCH 06/39] Removing an uneeded resource dependency in the message cards service template --- message-cards-service.yml | 3 --- pipeline.yml | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/message-cards-service.yml b/message-cards-service.yml index 67c6b7e..8674fd8 100644 --- a/message-cards-service.yml +++ b/message-cards-service.yml @@ -96,9 +96,6 @@ Resources: # as monitor the number of running tasks and replace any that have crashed Service: Type: AWS::ECS::Service - DependsOn: - - HTTPRule - - HTTPSRule Properties: ServiceName: !Ref 'ServiceName' Cluster: diff --git a/pipeline.yml b/pipeline.yml index 06b88fa..315df42 100644 --- a/pipeline.yml +++ b/pipeline.yml @@ -348,7 +348,7 @@ Resources: { "EnvironmentName": "${EnvironmentName}", "ImageUrl": { - "Fn::GetParam" : ["BuildOutput", "build.json", "ImageUri"] + "Fn::GetParam" : ["FrontendBuildOutput", "build.json", "ImageUri"] } } InputArtifacts: @@ -371,7 +371,7 @@ Resources: { "EnvironmentName": "${EnvironmentName}", "ImageUrl": { - "Fn::GetParam" : ["BuildOutput", "build.json", "ImageUri"] + "Fn::GetParam" : ["MessageCardsBuildOutput", "build.json", "ImageUri"] } } InputArtifacts: From 61d277476282828c78b00e195acc51ed6c1c9bb9 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 08:44:39 -0700 Subject: [PATCH 07/39] Adding some missing environment variables --- chat-service.yml | 2 ++ message-cards-service.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/chat-service.yml b/chat-service.yml index d9b727f..d55a94b 100644 --- a/chat-service.yml +++ b/chat-service.yml @@ -84,6 +84,8 @@ Resources: !Join [':', [!Ref 'EnvironmentName', 'RedisEndpoint']] - Name: DYNAMODB_ENDPOINT Value: !Join ['.', ['https://dynamodb', !Ref 'AWS::Region', 'amazonaws.com']] + - Name: SNS_ENDPOINT + Value: !Join ['.', ['https://sns', !Ref 'AWS::Region', 'amazonaws.com']] - Name: ENV_NAME Value: !Ref 'EnvironmentName' PortMappings: diff --git a/message-cards-service.yml b/message-cards-service.yml index 8674fd8..b049fb6 100644 --- a/message-cards-service.yml +++ b/message-cards-service.yml @@ -82,6 +82,8 @@ Resources: Value: Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'RedisEndpoint']] + - Name: SQS_ENDPOINT + Value: !Join ['.', ['https://sqs', !Ref 'AWS::Region', 'amazonaws.com']] - Name: ENV_NAME Value: !Ref 'EnvironmentName' LogConfiguration: From cec5994a720ed1c28f54a2ee35c4c4e8bd0b3952 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 09:06:32 -0700 Subject: [PATCH 08/39] Trying moving all the templates into a subfolder --- chat-service.yml => templates/chat-service.yml | 0 cluster.yml => templates/cluster.yml | 0 .../message-cards-service.yml | 0 pipeline.yml => templates/pipeline.yml | 8 ++++---- resources.yml => templates/resources.yml | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename chat-service.yml => templates/chat-service.yml (100%) rename cluster.yml => templates/cluster.yml (100%) rename message-cards-service.yml => templates/message-cards-service.yml (100%) rename pipeline.yml => templates/pipeline.yml (98%) rename resources.yml => templates/resources.yml (100%) diff --git a/chat-service.yml b/templates/chat-service.yml similarity index 100% rename from chat-service.yml rename to templates/chat-service.yml diff --git a/cluster.yml b/templates/cluster.yml similarity index 100% rename from cluster.yml rename to templates/cluster.yml diff --git a/message-cards-service.yml b/templates/message-cards-service.yml similarity index 100% rename from message-cards-service.yml rename to templates/message-cards-service.yml diff --git a/pipeline.yml b/templates/pipeline.yml similarity index 98% rename from pipeline.yml rename to templates/pipeline.yml index 315df42..d1bf640 100644 --- a/pipeline.yml +++ b/templates/pipeline.yml @@ -262,7 +262,7 @@ Resources: ActionMode: CREATE_UPDATE RoleArn: !GetAtt CloudFormationDeployRole.Arn StackName: !Sub ${EnvironmentName}-BaseResources - TemplatePath: Source::cluster.yml + TemplatePath: Source::templates/cluster.yml Capabilities: CAPABILITY_IAM ParameterOverrides: !Sub | { @@ -288,7 +288,7 @@ Resources: ActionMode: CREATE_UPDATE RoleArn: !GetAtt CloudFormationDeployRole.Arn StackName: !Sub ${EnvironmentName}-ChatResources - TemplatePath: Source::resources.yml + TemplatePath: Source::templates/resources.yml Capabilities: CAPABILITY_IAM ParameterOverrides: !Sub | { @@ -342,7 +342,7 @@ Resources: ActionMode: CREATE_UPDATE RoleArn: !GetAtt CloudFormationDeployRole.Arn StackName: !Sub ${EnvironmentName}-ChatService - TemplatePath: Source::chat-service.yml + TemplatePath: Source::templates/chat-service.yml Capabilities: CAPABILITY_IAM ParameterOverrides: !Sub | { @@ -365,7 +365,7 @@ Resources: ActionMode: CREATE_UPDATE RoleArn: !GetAtt CloudFormationDeployRole.Arn StackName: !Sub ${EnvironmentName}-MessageCards - TemplatePath: Source::message-cards-service.yml + TemplatePath: Source::templates/message-cards-service.yml Capabilities: CAPABILITY_IAM ParameterOverrides: !Sub | { diff --git a/resources.yml b/templates/resources.yml similarity index 100% rename from resources.yml rename to templates/resources.yml From 4e8b0ec58c3756d49f7a2be93651cdf99006814d Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 09:40:42 -0700 Subject: [PATCH 09/39] Fixing service name reference in message cards --- templates/message-cards-service.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/message-cards-service.yml b/templates/message-cards-service.yml index b049fb6..70f8c93 100644 --- a/templates/message-cards-service.yml +++ b/templates/message-cards-service.yml @@ -8,7 +8,7 @@ Parameters: Used to locate other resources in the same environment. ServiceName: Type: String - Default: chat + Default: message-cards Description: A name for the service ImageUrl: Type: String From b17fea6699edc07c5eb2ac91411be72a1827ccea Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 10:44:41 -0700 Subject: [PATCH 10/39] Adding SNS topic, SQS queue, subscription, and env variable plumbing --- templates/chat-service.yml | 16 ++++- templates/message-cards-service.yml | 6 +- templates/resources.yml | 93 ++++++++++++++++++++++++----- 3 files changed, 98 insertions(+), 17 deletions(-) diff --git a/templates/chat-service.yml b/templates/chat-service.yml index d55a94b..505f97c 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -43,6 +43,10 @@ Parameters: Type: Number Default: 2 Description: How many copies of the service task to run + DatadogApiKey: + Type: AWS::SSM::Parameter::Value + Default: DATADOG_API_KEY + Description: Import the Datadog API key from Systems Manager Parameter Store Resources: # A log group for storing the container logs for this service @@ -69,9 +73,14 @@ Resources: Fn::ImportValue: !Join [':', [!Ref 'EnvironmentName', 'ChatServiceRole']] ContainerDefinitions: + - Name: datadog-agent + Image: datadog/agent:latest + Environment: + - Name: DD_API_KEY + Value: !Ref DatadogApiKey + - Name: ECS_FARGATE + Value: true - Name: !Ref 'ServiceName' - Cpu: !Ref 'ContainerCpu' - Memory: !Ref 'ContainerMemory' Image: !Ref 'ImageUrl' Environment: - Name: REGION @@ -86,6 +95,9 @@ Resources: Value: !Join ['.', ['https://dynamodb', !Ref 'AWS::Region', 'amazonaws.com']] - Name: SNS_ENDPOINT Value: !Join ['.', ['https://sns', !Ref 'AWS::Region', 'amazonaws.com']] + - Name: MESSAGE_SENT_SNS_ARN + Value: + Fn::ImportValue: !Sub ${EnvironmentName}:MessageSentTopic - Name: ENV_NAME Value: !Ref 'EnvironmentName' PortMappings: diff --git a/templates/message-cards-service.yml b/templates/message-cards-service.yml index 70f8c93..3af519b 100644 --- a/templates/message-cards-service.yml +++ b/templates/message-cards-service.yml @@ -67,7 +67,7 @@ Resources: !Join [':', [!Ref 'EnvironmentName', 'ECSTaskExecutionRole']] TaskRoleArn: Fn::ImportValue: - !Join [':', [!Ref 'EnvironmentName', 'ChatServiceRole']] + !Join [':', [!Ref 'EnvironmentName', 'MessageCardsRole']] ContainerDefinitions: - Name: !Ref 'ServiceName' Cpu: !Ref 'ContainerCpu' @@ -84,6 +84,10 @@ Resources: !Join [':', [!Ref 'EnvironmentName', 'RedisEndpoint']] - Name: SQS_ENDPOINT Value: !Join ['.', ['https://sqs', !Ref 'AWS::Region', 'amazonaws.com']] + - Name: QUEUE_URL + Value: + Fn::ImportValue: + !Join [':', [!Ref 'EnvironmentName', 'MessageCardQueueUrl']] - Name: ENV_NAME Value: !Ref 'EnvironmentName' LogConfiguration: diff --git a/templates/resources.yml b/templates/resources.yml index d32d58d..d90fba0 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -96,20 +96,70 @@ Resources: Action: ['sts:AssumeRole'] Path: / Policies: - - PolicyName: users-dynamodb-table - PolicyDocument: - Statement: - - Effect: Allow - Action: - - "dynamodb:PutItem" - - "dynamodb:GetItem" - - "dynamodb:Query" - - "dynamodb:Scan" - - "dynamodb:UpdateItem" - - "dynamodb:DeleteItem" - Resource: - - !Join ['', ['arn:aws:dynamodb:*:*:table/', !Ref 'UsersTable']] - - !Join ['', ['arn:aws:dynamodb:*:*:table/', !Ref 'MessagesTable']] + - PolicyName: users-dynamodb-table + PolicyDocument: + Statement: + - Effect: Allow + Action: + - "dynamodb:PutItem" + - "dynamodb:GetItem" + - "dynamodb:Query" + - "dynamodb:Scan" + - "dynamodb:UpdateItem" + - "dynamodb:DeleteItem" + Resource: + - !Join ['', ['arn:aws:dynamodb:*:*:table/', !Ref 'UsersTable']] + - !Join ['', ['arn:aws:dynamodb:*:*:table/', !Ref 'MessagesTable']] + - PolicyName: publish-to-messagesent-topic + PolicyDocument: + Statement: + - Effect: Allow + Action: + - "sns:publish" + Resource: + - !Ref MessageSent + + # Role for the service that generates message cards + MessageCardsRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Effect: Allow + Principal: + Service: "ecs-tasks.amazonaws.com" + Action: ['sts:AssumeRole'] + Path: / + Policies: + - PolicyName: use-messages-queue + PolicyDocument: + Statement: + - Effect: Allow + Action: + - "sqs:*" + Resource: + - !Ref MessagesToCardify + + # An SNS topic for the chat service to publish to + MessageSentTopic: + Type: AWS::SNS::Topic + Properties: + DisplayName: A message has been sent + TopicName: !Sub ${EnvironmentName}-MessageSent + + # An SQS queue that holds sent messages + MessagesToCardifyQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: !Sub ${EnvironmentName}-MessagesToCardify + + # A subscription so that the MessagesToCardify queue receives messages sent to the MessageSent topic + MessagesSentToCardifySubscription: + Type: AWS::SNS::Subscription + Properties: + Endpoint: !Ref MessagesToCardifyQueue + Protocol: sqs + TopicArn: !Ref MessageSentTopic Outputs: RedisEndpoint: @@ -122,4 +172,19 @@ Outputs: Value: !GetAtt 'ChatServiceRole.Arn' Export: Name: !Join [ ':', [ !Ref 'EnvironmentName', 'ChatServiceRole' ] ] + MessageCardsRole: + Description: The role of the service that generates message cards + Value: !GetAtt 'MessageCardsRole.Arn' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'MessageCardsRole' ] ] + MessageSentTopicArn: + Description: The ARN of the topic that is published to when a message is sent + Value: !Ref 'MessageSentTopic' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'MessageSentTopic' ] ] + MessageCardQueueUrl: + Description: The URL of the queue that contains messages that need to be cardified + Value: !Ref 'MessagesToCardifyQueue' + Export: + Name: !Join [ ':', [ !Ref 'EnvironmentName', 'MessagesToCardifyQueue' ] ] From 58cac2f377e5c67aee10eb1c43cddcfc4b65a63d Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 10:59:44 -0700 Subject: [PATCH 11/39] Fixing references in the resources template --- templates/resources.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/resources.yml b/templates/resources.yml index d90fba0..4fd9e8a 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -117,7 +117,7 @@ Resources: Action: - "sns:publish" Resource: - - !Ref MessageSent + - !Ref MessageSentTopic # Role for the service that generates message cards MessageCardsRole: @@ -138,7 +138,7 @@ Resources: Action: - "sqs:*" Resource: - - !Ref MessagesToCardify + - !Ref MessagesToCardifyQueue # An SNS topic for the chat service to publish to MessageSentTopic: From cf820e8332cf8b14bf6c80ca7e1e7acd9aced46d Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 11:09:15 -0700 Subject: [PATCH 12/39] Fixing queue URL which should have been a queue ARN --- templates/pipeline.yml | 2 ++ templates/resources.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/pipeline.yml b/templates/pipeline.yml index d1bf640..c12d892 100644 --- a/templates/pipeline.yml +++ b/templates/pipeline.yml @@ -140,6 +140,8 @@ Resources: - "application-autoscaling:*" - "cloudwatch:*" - "route53:*" + - "sns:*" + - "sqs:*" Resource: "*" # While the build is in progress we need a place to store artifacts diff --git a/templates/resources.yml b/templates/resources.yml index 4fd9e8a..2324425 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -138,7 +138,7 @@ Resources: Action: - "sqs:*" Resource: - - !Ref MessagesToCardifyQueue + - !GetAtt MessagesToCardifyQueue.Arn # An SNS topic for the chat service to publish to MessageSentTopic: From 30ff748e41a1376f92d9b86e633041da477ab604 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 11:15:58 -0700 Subject: [PATCH 13/39] The SNS to SQS subscription also needs to be an ARN --- templates/resources.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/resources.yml b/templates/resources.yml index 2324425..4d9b54e 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -157,7 +157,7 @@ Resources: MessagesSentToCardifySubscription: Type: AWS::SNS::Subscription Properties: - Endpoint: !Ref MessagesToCardifyQueue + Endpoint: !GetAtt MessagesToCardifyQueue.Arn Protocol: sqs TopicArn: !Ref MessageSentTopic From 47f38aa294cbd46d60c95b72d076b5366c6c8412 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 11:26:32 -0700 Subject: [PATCH 14/39] Fixing reference to get the env variable value --- templates/message-cards-service.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/message-cards-service.yml b/templates/message-cards-service.yml index 3af519b..acdce69 100644 --- a/templates/message-cards-service.yml +++ b/templates/message-cards-service.yml @@ -64,10 +64,10 @@ Resources: - FARGATE ExecutionRoleArn: Fn::ImportValue: - !Join [':', [!Ref 'EnvironmentName', 'ECSTaskExecutionRole']] + !Sub '${EnvironmentName}:ECSTaskExecutionRole' TaskRoleArn: Fn::ImportValue: - !Join [':', [!Ref 'EnvironmentName', 'MessageCardsRole']] + !Sub '${EnvironmentName}:MessageCardsRole' ContainerDefinitions: - Name: !Ref 'ServiceName' Cpu: !Ref 'ContainerCpu' @@ -81,13 +81,13 @@ Resources: - Name: REDIS_ENDPOINT Value: Fn::ImportValue: - !Join [':', [!Ref 'EnvironmentName', 'RedisEndpoint']] + !Sub '${EnvironmentName}:RedisEndpoint' - Name: SQS_ENDPOINT - Value: !Join ['.', ['https://sqs', !Ref 'AWS::Region', 'amazonaws.com']] + Value: !Sub 'https://sqs.${AWS::Region}.amazonaws.com' - Name: QUEUE_URL Value: Fn::ImportValue: - !Join [':', [!Ref 'EnvironmentName', 'MessageCardQueueUrl']] + !Sub '${EnvironmentName}:MessagesToCardifyQueue' - Name: ENV_NAME Value: !Ref 'EnvironmentName' LogConfiguration: From a6069d01268c6d2407b257754b9fece4db956e42 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 11:36:35 -0700 Subject: [PATCH 15/39] Adding missing SSM IAM permission to the pipeline role --- templates/pipeline.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/pipeline.yml b/templates/pipeline.yml index c12d892..4ccb29c 100644 --- a/templates/pipeline.yml +++ b/templates/pipeline.yml @@ -142,6 +142,7 @@ Resources: - "route53:*" - "sns:*" - "sqs:*" + - "ssm:*" Resource: "*" # While the build is in progress we need a place to store artifacts From b7aaf5b80a6ec466ef7a906d217a63d8bcfec046 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 11:47:00 -0700 Subject: [PATCH 16/39] Fixing silly issue where both builds were sending images to the same ECR --- templates/pipeline.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/templates/pipeline.yml b/templates/pipeline.yml index 4ccb29c..4a53331 100644 --- a/templates/pipeline.yml +++ b/templates/pipeline.yml @@ -16,7 +16,11 @@ Parameters: Resources: # Create the ECR respository to hold built docker images - Repository: + ChatFrontendRepository: + Type: AWS::ECR::Repository + DeletionPolicy: Retain + + MessageCardsRepository: Type: AWS::ECR::Repository DeletionPolicy: Retain @@ -51,7 +55,17 @@ Resources: - s3:GetObject - s3:PutObject - s3:GetObjectVersion - - Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${Repository} + - Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ChatFrontendRepository} + Effect: Allow + Action: + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + - ecr:BatchCheckLayerAvailability + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${MessageCardsRepository} Effect: Allow Action: - ecr:GetDownloadUrlForLayer @@ -183,7 +197,7 @@ Resources: - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: REPOSITORY_URI - Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} + Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ChatFrontendRepository} Name: !Sub ${AWS::StackName}-chat-frontend ServiceRole: !Ref CodeBuildServiceRole @@ -220,7 +234,7 @@ Resources: - Name: AWS_DEFAULT_REGION Value: !Ref AWS::Region - Name: REPOSITORY_URI - Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository} + Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${MessageCardsRepository} Name: !Sub ${AWS::StackName}-message-cards ServiceRole: !Ref CodeBuildServiceRole From 270ba8f2569810c1bf5bb3fd8a179f5005c16e75 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 12:55:20 -0700 Subject: [PATCH 17/39] Updating the pipeline again --- templates/pipeline.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/pipeline.yml b/templates/pipeline.yml index 4a53331..d9b4c70 100644 --- a/templates/pipeline.yml +++ b/templates/pipeline.yml @@ -18,10 +18,12 @@ Resources: # Create the ECR respository to hold built docker images ChatFrontendRepository: Type: AWS::ECR::Repository + RepositoryName: !Sub ${EnvironmentName}-chat-web DeletionPolicy: Retain MessageCardsRepository: Type: AWS::ECR::Repository + RepositoryName: !Sub ${EnvironmentName}-message-cards DeletionPolicy: Retain # A role used to give CodeBuild permission to access code, From 42cd9071838e1d2065dd51f1495008f8b8fa162f Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 13:19:10 -0700 Subject: [PATCH 18/39] Fixing up the Datadog agent integration --- services/client/index.js | 5 +++++ services/client/package.json | 1 + templates/chat-service.yml | 12 ++++++++++++ templates/pipeline.yml | 6 ++++-- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/services/client/index.js b/services/client/index.js index 06f3dc3..4829bef 100644 --- a/services/client/index.js +++ b/services/client/index.js @@ -1,4 +1,9 @@ // Entrypoint + +// Initialize the Datadog APM tracer. +const tracer = require('dd-trace'); +tracer.init(); + var server = require('./server'); var config = require('./lib/config'); diff --git a/services/client/package.json b/services/client/package.json index f302f95..cff31c6 100644 --- a/services/client/package.json +++ b/services/client/package.json @@ -9,6 +9,7 @@ "aws-sdk": "2.262.1", "bcrypt": "2.0.1", "compression": "^1.7.2", + "dd-trace": "0.5.1", "express": "4.16.3", "express-sslify": "^1.2.0", "lodash": "^4.17.5", diff --git a/templates/chat-service.yml b/templates/chat-service.yml index 505f97c..7abe341 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -78,8 +78,12 @@ Resources: Environment: - Name: DD_API_KEY Value: !Ref DatadogApiKey + - Name: DD_APM_ENABLED + Value: true - Name: ECS_FARGATE Value: true + - Name: DD_RECEIVER_PORT + Value: 8126 - Name: !Ref 'ServiceName' Image: !Ref 'ImageUrl' Environment: @@ -100,6 +104,14 @@ Resources: Fn::ImportValue: !Sub ${EnvironmentName}:MessageSentTopic - Name: ENV_NAME Value: !Ref 'EnvironmentName' + - Name: DD_TRACE_AGENT_PORT + Value: 8126 + - Name: DD_TRACE_AGENT_HOSTNAME + Value: localhost + - Name: DD_SERVICE_NAME + Value: chat-frontend + - Name: DD_ENV + Value: !Ref EnvironmentName PortMappings: - ContainerPort: !Ref 'ContainerPort' LogConfiguration: diff --git a/templates/pipeline.yml b/templates/pipeline.yml index d9b4c70..8291603 100644 --- a/templates/pipeline.yml +++ b/templates/pipeline.yml @@ -18,13 +18,15 @@ Resources: # Create the ECR respository to hold built docker images ChatFrontendRepository: Type: AWS::ECR::Repository - RepositoryName: !Sub ${EnvironmentName}-chat-web DeletionPolicy: Retain + Properties: + RepositoryName: !Sub ${EnvironmentName}-chat-web MessageCardsRepository: Type: AWS::ECR::Repository - RepositoryName: !Sub ${EnvironmentName}-message-cards DeletionPolicy: Retain + Properties: + RepositoryName: !Sub ${EnvironmentName}-message-cards # A role used to give CodeBuild permission to access code, # build it, and upload the build results to ECR From 9dbc593811246ded07c0dd3044fd99ff92ed08ed Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 14:00:54 -0700 Subject: [PATCH 19/39] Test out adding some spans for custom tracing --- services/client/index.js | 4 +++- services/client/server.js | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/services/client/index.js b/services/client/index.js index 4829bef..4f077d1 100644 --- a/services/client/index.js +++ b/services/client/index.js @@ -4,8 +4,10 @@ const tracer = require('dd-trace'); tracer.init(); -var server = require('./server'); var config = require('./lib/config'); +config.tracer = tracer; + +var server = require('./server'); server.listen(config.PORT, function() { console.log('Server listening at port %d', config.PORT); diff --git a/services/client/server.js b/services/client/server.js index ef2c818..65d9e46 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -26,6 +26,8 @@ var Presence = require('./lib/presence'); var User = require('./lib/user'); var Message = require('./lib/message'); +var tracer = config.tracer; + // Lower the heartbeat timeout (helps us expire disconnected people faster) io.set('heartbeat timeout', config.HEARTBEAT_TIMEOUT); io.set('heartbeat interval', config.HEARTBEAT_INTERVAL); @@ -34,6 +36,8 @@ io.set('heartbeat interval', config.HEARTBEAT_INTERVAL); app.use(express.static(path.join(__dirname, 'public'))); io.on('connection', function(socket) { + const connection = tracer.startSpan('ws connection'); + // Initially the socket starts out as not authenticated socket.authenticated = false; @@ -46,6 +50,8 @@ io.on('connection', function(socket) { // when the client emits 'new message', this listens and executes socket.on('new message', async function(data, callback) { + const span = tracer.startSpan('ws new message'); + if (!socket.authenticated) { // Don't allow people not authenticated to send a message return callback('Can\'t send a message until you are authenticated'); @@ -74,6 +80,8 @@ io.on('connection', function(socket) { socket.broadcast.emit('new message', messageBody); + span.finish(); + return callback(null, messageBody); }); @@ -354,6 +362,8 @@ io.on('connection', function(socket) { }); } }); + + connection.finish(); }); module.exports = server; From 90eb40fd746a848088f06c09da961e98112391ad Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 14:29:38 -0700 Subject: [PATCH 20/39] Trying to fix up the custom tracing --- services/client/server.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/services/client/server.js b/services/client/server.js index 65d9e46..9e74aa7 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -36,7 +36,8 @@ io.set('heartbeat interval', config.HEARTBEAT_INTERVAL); app.use(express.static(path.join(__dirname, 'public'))); io.on('connection', function(socket) { - const connection = tracer.startSpan('ws connection'); + const connection = tracer.startSpan('web.request'); + connection.tag('http.url', '/connection'); // Initially the socket starts out as not authenticated socket.authenticated = false; @@ -50,7 +51,7 @@ io.on('connection', function(socket) { // when the client emits 'new message', this listens and executes socket.on('new message', async function(data, callback) { - const span = tracer.startSpan('ws new message'); + const span = tracer.startSpan('ws.connection'); if (!socket.authenticated) { // Don't allow people not authenticated to send a message @@ -65,6 +66,9 @@ io.on('connection', function(socket) { return callback('Must pass a parameter `message` which is a string'); } + span.setTag('room', data.room); + span.setTag('user', socket.username); + var messageBody = { room: data.room, time: Date.now(), From 7776a6c5c7b9ad062f1ee6cd980682ea1ecad786 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 14:41:01 -0700 Subject: [PATCH 21/39] Fixing span tagging --- services/client/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/client/server.js b/services/client/server.js index 9e74aa7..16db018 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -37,7 +37,7 @@ app.use(express.static(path.join(__dirname, 'public'))); io.on('connection', function(socket) { const connection = tracer.startSpan('web.request'); - connection.tag('http.url', '/connection'); + connection.setTag('http.url', '/connection'); // Initially the socket starts out as not authenticated socket.authenticated = false; From 28026b151c69fd89e6d82b57ed96b0bf65105d4d Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 15:05:37 -0700 Subject: [PATCH 22/39] Adding analyzed spans to the tracing agent --- templates/chat-service.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/chat-service.yml b/templates/chat-service.yml index 7abe341..e877318 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -110,6 +110,8 @@ Resources: Value: localhost - Name: DD_SERVICE_NAME Value: chat-frontend + - Name: DD_APM_ANALYZED_SPANS + Value: chat-frontend-http-client|http.request=1,chat-frontend|express.request=1,chat-frontend|ws.connection=1 - Name: DD_ENV Value: !Ref EnvironmentName PortMappings: From 025bb9a8ddfb959509b867eb6756d7fe048d6fff Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 15:33:29 -0700 Subject: [PATCH 23/39] Trying to fix the span details in Datadog --- services/client/server.js | 13 ++++++++----- templates/chat-service.yml | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/services/client/server.js b/services/client/server.js index 16db018..3a12551 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -36,8 +36,9 @@ io.set('heartbeat interval', config.HEARTBEAT_INTERVAL); app.use(express.static(path.join(__dirname, 'public'))); io.on('connection', function(socket) { - const connection = tracer.startSpan('web.request'); - connection.setTag('http.url', '/connection'); + const connection = tracer.startSpan('ws'); + connection.setTag('service.name', 'frontend-ws'); + connection.setTag('resource.name', 'connection'); // Initially the socket starts out as not authenticated socket.authenticated = false; @@ -47,11 +48,15 @@ io.on('connection', function(socket) { io.to(socket.id).emit('presence', { numUsers: users.length }); + + connection.finish(); }); // when the client emits 'new message', this listens and executes socket.on('new message', async function(data, callback) { - const span = tracer.startSpan('ws.connection'); + const span = tracer.startSpan('ws'); + connection.setTag('service.name', 'frontend-ws'); + connection.setTag('resource.name', 'new message'); if (!socket.authenticated) { // Don't allow people not authenticated to send a message @@ -366,8 +371,6 @@ io.on('connection', function(socket) { }); } }); - - connection.finish(); }); module.exports = server; diff --git a/templates/chat-service.yml b/templates/chat-service.yml index e877318..925859c 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -111,7 +111,7 @@ Resources: - Name: DD_SERVICE_NAME Value: chat-frontend - Name: DD_APM_ANALYZED_SPANS - Value: chat-frontend-http-client|http.request=1,chat-frontend|express.request=1,chat-frontend|ws.connection=1 + Value: frontend-ws|ws=1 - Name: DD_ENV Value: !Ref EnvironmentName PortMappings: From a26f4cb1a0576097359b8d500fa5ac64403f459a Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 15:34:20 -0700 Subject: [PATCH 24/39] Woops --- services/client/server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/client/server.js b/services/client/server.js index 3a12551..8fc19d4 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -55,8 +55,8 @@ io.on('connection', function(socket) { // when the client emits 'new message', this listens and executes socket.on('new message', async function(data, callback) { const span = tracer.startSpan('ws'); - connection.setTag('service.name', 'frontend-ws'); - connection.setTag('resource.name', 'new message'); + span.setTag('service.name', 'frontend-ws'); + span.setTag('resource.name', 'new message'); if (!socket.authenticated) { // Don't allow people not authenticated to send a message From b5e6a9767cfb5952d6e490a7f37ab9feb4c669ce Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Tue, 24 Jul 2018 15:50:48 -0700 Subject: [PATCH 25/39] Maybe I need to make use of the scope manager --- services/client/server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/client/server.js b/services/client/server.js index 8fc19d4..42e4d6d 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -39,6 +39,7 @@ io.on('connection', function(socket) { const connection = tracer.startSpan('ws'); connection.setTag('service.name', 'frontend-ws'); connection.setTag('resource.name', 'connection'); + tracer.scopeManager().activate(connection); // Initially the socket starts out as not authenticated socket.authenticated = false; @@ -57,6 +58,7 @@ io.on('connection', function(socket) { const span = tracer.startSpan('ws'); span.setTag('service.name', 'frontend-ws'); span.setTag('resource.name', 'new message'); + tracer.scopeManager().activate(span); if (!socket.authenticated) { // Don't allow people not authenticated to send a message From 93ec6d1a25a0b84559aad805b52e27039b841086 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 08:44:52 -0700 Subject: [PATCH 26/39] Adding tracking to all the client side handlers --- services/client/server.js | 72 +++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/services/client/server.js b/services/client/server.js index 42e4d6d..ed0af46 100644 --- a/services/client/server.js +++ b/services/client/server.js @@ -35,11 +35,18 @@ io.set('heartbeat interval', config.HEARTBEAT_INTERVAL); // Routing app.use(express.static(path.join(__dirname, 'public'))); +// This is a helper for creating a flame graph span to +// report to Datadog. +function trackSpan(spanName) { + const span = tracer.startSpan('ws'); + span.setTag('service.name', 'frontend-ws'); + span.setTag('resource.name', spanName); + tracer.scopeManager().activate(span); + return span; +} + io.on('connection', function(socket) { - const connection = tracer.startSpan('ws'); - connection.setTag('service.name', 'frontend-ws'); - connection.setTag('resource.name', 'connection'); - tracer.scopeManager().activate(connection); + const span = trackSpan('connection'); // Initially the socket starts out as not authenticated socket.authenticated = false; @@ -50,26 +57,26 @@ io.on('connection', function(socket) { numUsers: users.length }); - connection.finish(); + span.finish(); }); // when the client emits 'new message', this listens and executes socket.on('new message', async function(data, callback) { - const span = tracer.startSpan('ws'); - span.setTag('service.name', 'frontend-ws'); - span.setTag('resource.name', 'new message'); - tracer.scopeManager().activate(span); + const span = trackSpan('new message'); if (!socket.authenticated) { // Don't allow people not authenticated to send a message + span.finish(); return callback('Can\'t send a message until you are authenticated'); } if (!data.room || !_.isString(data.room)) { + span.finish(); return callback('Must pass a parameter `room` which is a string'); } if (!data.message || !_.isString(data.message)) { + span.finish(); return callback('Must pass a parameter `message` which is a string'); } @@ -92,43 +99,54 @@ io.on('connection', function(socket) { socket.broadcast.emit('new message', messageBody); span.finish(); - return callback(null, messageBody); }); socket.on('message list', async function(from, callback) { + const span = trackSpan('message list'); var messages; if (!from.room || !_.isString(from.room)) { + span.finish(); return callback('Must pass a parameter `from.room` which is a string'); } try { messages = await Message.listFromRoom(from); } catch (e) { + span.finish(); return callback(e); } + span.finish(); return callback(null, messages); }); socket.conn.on('heartbeat', function() { + const span = trackSpan('heartbeat'); if (!socket.authenticated) { // Don't start counting as present until they authenticate. + span.finish(); return; } Presence.upsert(socket.id, { username: socket.username }); + + span.finish(); }); // Client wants a list of rooms socket.on('room list', function(callback) { + const span = trackSpan('room list'); + if (!_.isFunction(callback)) { + span.finish(); return; } + span.finish(); return callback(null, [ { id: 'general', @@ -167,23 +185,30 @@ io.on('connection', function(socket) { // Client wants to create a new account socket.on('create user', async function(details, callback) { + const span = trackSpan('create user'); + if (!_.isFunction(callback)) { + span.finish(); return; } if (socket.authenticated) { + span.finish(); return callback('User already has a logged in identity'); } if (!details.username || !_.isString(details.username)) { + span.finish(); return callback('Must pass a parameter `username` which is a string'); } if (!details.email || !_.isString(details.email)) { + span.finish(); return callback('Must pass a parameter `email` which is a string'); } if (!details.password || !_.isString(details.password)) { + span.finish(); return callback('Must pass a parameter `password` which is a string'); } @@ -234,19 +259,25 @@ io.on('connection', function(socket) { // Client wants to authenticate a user socket.on('authenticate user', async function(details, callback) { + const span = trackSpan('authenticate user'); + if (!_.isFunction(callback)) { + span.finish(); return; } if (socket.authenticated) { + span.finish(); return callback('User already has a logged in identity'); } if (!details.username || !_.isString(details.username)) { + span.finish(); return callback('Must pass a parameter `username` which is a string'); } if (!details.password || !_.isString(details.password)) { + span.finish(); return callback('Must pass a parameter `password` which is a string'); } @@ -261,10 +292,12 @@ io.on('connection', function(socket) { password: details.password }); } catch (e) { + span.finish(); return callback(e.toString()); } if (!result) { + span.finish(); return callback('No matching account found'); } @@ -293,6 +326,7 @@ io.on('connection', function(socket) { }); }); + span.finish(); return callback(null, { username: socket.username, avatar: socket.avatar @@ -300,7 +334,10 @@ io.on('connection', function(socket) { }); socket.on('anonymous user', function(callback) { + const span = trackSpan('anonymous user'); + if (!_.isFunction(callback)) { + span.finish(); return; } @@ -327,6 +364,7 @@ io.on('connection', function(socket) { }); }); + span.finish(); return callback(null, { username: socket.username, avatar: socket.avatar @@ -335,7 +373,10 @@ io.on('connection', function(socket) { // when the client emits 'typing', we broadcast it to others socket.on('typing', function(room) { + const span = trackSpan('typing'); + if (!socket.authenticated) { + span.finish(); return; } @@ -344,11 +385,16 @@ io.on('connection', function(socket) { username: socket.username, avatar: socket.avatar }); + + span.finish(); }); // when the client emits 'stop typing', we broadcast it to others socket.on('stop typing', function(room) { + const span = trackSpan('stop typing'); + if (!socket.authenticated) { + span.finish(); return; } @@ -356,10 +402,14 @@ io.on('connection', function(socket) { room: room, username: socket.username }); + + span.finish(); }); // when the user disconnects.. perform this socket.on('disconnect', function() { + const span = trackSpan('disconnect'); + if (socket.authenticated) { Presence.remove(socket.id); @@ -372,6 +422,8 @@ io.on('connection', function(socket) { }); }); } + + span.finish(); }); }); From 6f2ead69e8ca6a8a6e3728522fdaf56355bfb2b6 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 09:47:04 -0700 Subject: [PATCH 27/39] Trying to turn on the analyzed spans --- templates/chat-service.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/chat-service.yml b/templates/chat-service.yml index 925859c..9b4084c 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -111,7 +111,7 @@ Resources: - Name: DD_SERVICE_NAME Value: chat-frontend - Name: DD_APM_ANALYZED_SPANS - Value: frontend-ws|ws=1 + Value: frontend-ws|connection=1,frontend-ws|ws=1 - Name: DD_ENV Value: !Ref EnvironmentName PortMappings: From 7b80948f83113369f0cad207f7efa603dfc00bb9 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 10:00:02 -0700 Subject: [PATCH 28/39] More attempts --- templates/chat-service.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/chat-service.yml b/templates/chat-service.yml index 9b4084c..50ca0bd 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -111,7 +111,7 @@ Resources: - Name: DD_SERVICE_NAME Value: chat-frontend - Name: DD_APM_ANALYZED_SPANS - Value: frontend-ws|connection=1,frontend-ws|ws=1 + Value: frontend-ws|ws.connection=1,frontend-ws|ws=1,frontend-ws|connection=1 - Name: DD_ENV Value: !Ref EnvironmentName PortMappings: From eb074cb578781a00dd0ff1157fe811bfc5bc6df1 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 13:03:48 -0700 Subject: [PATCH 29/39] Modifying the Datadog settings some more --- templates/chat-service.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/chat-service.yml b/templates/chat-service.yml index 50ca0bd..5e90825 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -109,9 +109,9 @@ Resources: - Name: DD_TRACE_AGENT_HOSTNAME Value: localhost - Name: DD_SERVICE_NAME - Value: chat-frontend + Value: frontend-ws - Name: DD_APM_ANALYZED_SPANS - Value: frontend-ws|ws.connection=1,frontend-ws|ws=1,frontend-ws|connection=1 + Value: frontend-ws|ws=1 - Name: DD_ENV Value: !Ref EnvironmentName PortMappings: From e5788498d6085dfcac5be2a2d41a6507d192cfe2 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 13:17:27 -0700 Subject: [PATCH 30/39] Modifying the environment variables and adding a debug line --- services/client/lib/message.js | 4 +++- templates/chat-service.yml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/services/client/lib/message.js b/services/client/lib/message.js index 289cd84..b446b05 100644 --- a/services/client/lib/message.js +++ b/services/client/lib/message.js @@ -60,7 +60,9 @@ Message.prototype.add = async function(message) { message: id }), TopicArn: config.MESSAGE_SENT_SNS_ARN - }).promise(); + }).promise().then(function() { + console.log(arguments); + }); } catch (e) { console.error(e); throw new Error('Failed to publish MessageSent notification'); diff --git a/templates/chat-service.yml b/templates/chat-service.yml index 5e90825..8e992b8 100644 --- a/templates/chat-service.yml +++ b/templates/chat-service.yml @@ -84,6 +84,8 @@ Resources: Value: true - Name: DD_RECEIVER_PORT Value: 8126 + - Name: DD_APM_ANALYZED_SPANS + Value: frontend-ws|ws=1 - Name: !Ref 'ServiceName' Image: !Ref 'ImageUrl' Environment: @@ -110,8 +112,6 @@ Resources: Value: localhost - Name: DD_SERVICE_NAME Value: frontend-ws - - Name: DD_APM_ANALYZED_SPANS - Value: frontend-ws|ws=1 - Name: DD_ENV Value: !Ref EnvironmentName PortMappings: From 093fd3b377aed7fae18f9aba7ef9e55a9fce8a84 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 13:39:29 -0700 Subject: [PATCH 31/39] Adding tracking to the background service as well --- services/message-cards/index.js | 15 ++++++++++++++- templates/message-cards-service.yml | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/services/message-cards/index.js b/services/message-cards/index.js index 936152b..ad38752 100644 --- a/services/message-cards/index.js +++ b/services/message-cards/index.js @@ -1,4 +1,10 @@ +// Initialize the Datadog APM tracer. +const tracer = require('dd-trace'); +tracer.init(); + var config = require('./lib/config'); +config.tracer = tracer; + var Squiss = require('squiss'); var poller = new Squiss({ @@ -16,8 +22,15 @@ console.log('Message card service up and ready to poll'); poller.start(); poller.on('message', function(msg) { - console.log(JSON.stringify(msg.body)); + const span = tracer.startSpan('message'); + span.setTag('service.name', 'message-cards'); + span.setTag('resource.name', 'message'); + tracer.scopeManager().activate(span); + + console.log(JSON.stringify(msg)); + msg.del(); + span.finish(); }); process.on('SIGTERM', function() { diff --git a/templates/message-cards-service.yml b/templates/message-cards-service.yml index acdce69..828255e 100644 --- a/templates/message-cards-service.yml +++ b/templates/message-cards-service.yml @@ -43,6 +43,10 @@ Parameters: Type: Number Default: 2 Description: How many copies of the service task to run + DatadogApiKey: + Type: AWS::SSM::Parameter::Value + Default: DATADOG_API_KEY + Description: Import the Datadog API key from Systems Manager Parameter Store Resources: # A log group for storing the container logs for this service @@ -69,6 +73,19 @@ Resources: Fn::ImportValue: !Sub '${EnvironmentName}:MessageCardsRole' ContainerDefinitions: + - Name: datadog-agent + Image: datadog/agent:latest + Environment: + - Name: DD_API_KEY + Value: !Ref DatadogApiKey + - Name: DD_APM_ENABLED + Value: true + - Name: ECS_FARGATE + Value: true + - Name: DD_RECEIVER_PORT + Value: 8126 + - Name: DD_APM_ANALYZED_SPANS + Value: message-cards|ws=1 - Name: !Ref 'ServiceName' Cpu: !Ref 'ContainerCpu' Memory: !Ref 'ContainerMemory' @@ -90,6 +107,14 @@ Resources: !Sub '${EnvironmentName}:MessagesToCardifyQueue' - Name: ENV_NAME Value: !Ref 'EnvironmentName' + - Name: DD_TRACE_AGENT_PORT + Value: 8126 + - Name: DD_TRACE_AGENT_HOSTNAME + Value: localhost + - Name: DD_SERVICE_NAME + Value: message-cards + - Name: DD_ENV + Value: !Ref EnvironmentName LogConfiguration: LogDriver: 'awslogs' Options: From 3cb5ee8dc493512ffd00cb0e6e4d3ea6feb26c63 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 13:52:11 -0700 Subject: [PATCH 32/39] Adding a policy document to allow SNS to publish to SQS --- templates/resources.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/templates/resources.yml b/templates/resources.yml index 4d9b54e..91e43de 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -153,6 +153,22 @@ Resources: Properties: QueueName: !Sub ${EnvironmentName}-MessagesToCardify + # Add a policy which allows SNS to send traffic to the SQS queue + MessageSentToMessageCardQueuePolicy: + Type: AWS::SQS::QueuePolicy + Properties: + Queues: + - !GetAtt MessagesToCardifyQueue.Arn + PolicyDocument: + Version: 2012-10-17 + Statement: + - Effect: Allow + Principal: * + Action: sqs:SendMessage + Resource: !GetAtt MessagesToCardifyQueue.Arn + Condition: + ArnEquals: !Ref MessageSentTopic + # A subscription so that the MessagesToCardify queue receives messages sent to the MessageSent topic MessagesSentToCardifySubscription: Type: AWS::SNS::Subscription From 2a4a5075454899f0d6ff25302f314420deb62c68 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 13:55:38 -0700 Subject: [PATCH 33/39] Trying again --- templates/resources.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/resources.yml b/templates/resources.yml index 91e43de..c2ad4cd 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -163,8 +163,8 @@ Resources: Version: 2012-10-17 Statement: - Effect: Allow - Principal: * - Action: sqs:SendMessage + Principal: "*" + Action: "sqs:SendMessage" Resource: !GetAtt MessagesToCardifyQueue.Arn Condition: ArnEquals: !Ref MessageSentTopic From 204aec035c1e181b4b8e49baa1014a93839afc85 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 14:18:27 -0700 Subject: [PATCH 34/39] Policy document needed the queue URL not the queue ARN --- templates/resources.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/resources.yml b/templates/resources.yml index c2ad4cd..b9a608a 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -158,7 +158,7 @@ Resources: Type: AWS::SQS::QueuePolicy Properties: Queues: - - !GetAtt MessagesToCardifyQueue.Arn + - !Ref MessagesToCardifyQueue PolicyDocument: Version: 2012-10-17 Statement: From 296b783874e886e59fa081b1387ac2503e3511ff Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 14:19:10 -0700 Subject: [PATCH 35/39] Removing the debug line --- services/client/lib/message.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/services/client/lib/message.js b/services/client/lib/message.js index b446b05..289cd84 100644 --- a/services/client/lib/message.js +++ b/services/client/lib/message.js @@ -60,9 +60,7 @@ Message.prototype.add = async function(message) { message: id }), TopicArn: config.MESSAGE_SENT_SNS_ARN - }).promise().then(function() { - console.log(arguments); - }); + }).promise(); } catch (e) { console.error(e); throw new Error('Failed to publish MessageSent notification'); From 40bfd0a2c4d1820e5efaac6df3b4930116367747 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 14:33:17 -0700 Subject: [PATCH 36/39] Fixing a malformed expression in the SQS policy document --- templates/resources.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/resources.yml b/templates/resources.yml index b9a608a..832c1c3 100644 --- a/templates/resources.yml +++ b/templates/resources.yml @@ -167,7 +167,8 @@ Resources: Action: "sqs:SendMessage" Resource: !GetAtt MessagesToCardifyQueue.Arn Condition: - ArnEquals: !Ref MessageSentTopic + ArnEquals: + aws:SourceArn: !Ref MessageSentTopic # A subscription so that the MessagesToCardify queue receives messages sent to the MessageSent topic MessagesSentToCardifySubscription: From c9d0ecb73228e58de19d1de566dba01f509169b5 Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 14:38:38 -0700 Subject: [PATCH 37/39] Woops, forgot the package.json dependency --- services/message-cards/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/services/message-cards/package.json b/services/message-cards/package.json index 03654a9..6daf2b7 100644 --- a/services/message-cards/package.json +++ b/services/message-cards/package.json @@ -7,6 +7,7 @@ "dependencies": { "async": "^2.6.0", "cheerio": "1.0.0-rc.2", + "dd-trace": "0.5.1", "redis": "2.8.0", "socket.io-redis": "5.2.0", "squiss": "2.2.1" From 8b6abee5f3257d544a85af6e44a79a23c352776c Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Wed, 25 Jul 2018 16:20:27 -0700 Subject: [PATCH 38/39] Fixing cicular stringify exception, and enabling APM trace analytics for the message card span --- services/message-cards/index.js | 2 +- templates/message-cards-service.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/message-cards/index.js b/services/message-cards/index.js index ad38752..3fc5ac6 100644 --- a/services/message-cards/index.js +++ b/services/message-cards/index.js @@ -27,7 +27,7 @@ poller.on('message', function(msg) { span.setTag('resource.name', 'message'); tracer.scopeManager().activate(span); - console.log(JSON.stringify(msg)); + console.log(JSON.stringify(msg.body)); msg.del(); span.finish(); diff --git a/templates/message-cards-service.yml b/templates/message-cards-service.yml index 828255e..85ed74a 100644 --- a/templates/message-cards-service.yml +++ b/templates/message-cards-service.yml @@ -85,7 +85,7 @@ Resources: - Name: DD_RECEIVER_PORT Value: 8126 - Name: DD_APM_ANALYZED_SPANS - Value: message-cards|ws=1 + Value: message-cards|message=1 - Name: !Ref 'ServiceName' Cpu: !Ref 'ContainerCpu' Memory: !Ref 'ContainerMemory' From cb98212ddbd46b4e22e8b10f67f653368443fcdb Mon Sep 17 00:00:00 2001 From: Nathan Peck Date: Thu, 26 Jul 2018 16:26:18 -0700 Subject: [PATCH 39/39] Adding a basic load test --- Makefile | 3 + deps/bootstrap-resources/wait-for-it.sh | 44 ++--- services/performance/Dockerfile | 10 + services/performance/README.md | 3 + services/performance/index.js | 145 +++++++++++++++ services/performance/package-lock.json | 237 ++++++++++++++++++++++++ services/performance/package.json | 18 ++ services/test-suite/package.json | 2 +- 8 files changed, 439 insertions(+), 23 deletions(-) create mode 100644 services/performance/Dockerfile create mode 100644 services/performance/README.md create mode 100644 services/performance/index.js create mode 100644 services/performance/package-lock.json create mode 100644 services/performance/package.json diff --git a/Makefile b/Makefile index 9f6608e..f397fae 100644 --- a/Makefile +++ b/Makefile @@ -20,3 +20,6 @@ cards: docker-compose build message-cards docker-compose up --no-deps -d message-cards docker-compose run --no-deps test + +clean: + docker system prune -a -f diff --git a/deps/bootstrap-resources/wait-for-it.sh b/deps/bootstrap-resources/wait-for-it.sh index 7ffc186..aeeaf82 100755 --- a/deps/bootstrap-resources/wait-for-it.sh +++ b/deps/bootstrap-resources/wait-for-it.sh @@ -1,32 +1,32 @@ #!/bin/sh echo "Waiting for $1 (This may take a few moments)"; -#clean=${1//http:\/\//} -#hostport=${clean//:/ } +clean=${1//http:\/\//} +hostport=${clean//:/ } -#set -f; IFS=' ' -#set -- $hostport -#second=$2; fourth=$4 -#set +f; unset IFS +set -f; IFS=' ' +set -- $hostport +second=$2; fourth=$4 +set +f; unset IFS -#HOST=$1 -#PORT=$2 +HOST=$1 +PORT=$2 -#while ! nc -w 1 -z $HOST $PORT; do sleep 1 && echo -n .; done; +while ! nc -w 1 -z $HOST $PORT; do sleep 1 && echo -n .; done; -while true -do - echo "pre poll"; - STATUS=$(curl -s -o /dev/null -w '%{http_code}' $1) - echo $STATUS - if [ $STATUS -eq 404 ]; then - echo "ready" - break - else - echo -n "." - fi - sleep 1 -done +#while true +#do +# echo "pre poll"; +# STATUS=$(curl -s -o /dev/null -w '%{http_code}' $1) +# echo $STATUS +# if [ $STATUS -eq 404 ]; then +# echo "ready" +# break +# else +# echo -n "." +# fi +# sleep 1 +#done #--output /dev/null --silent --head diff --git a/services/performance/Dockerfile b/services/performance/Dockerfile new file mode 100644 index 0000000..f3e4563 --- /dev/null +++ b/services/performance/Dockerfile @@ -0,0 +1,10 @@ +FROM node:9 AS build +WORKDIR /srv +ADD package.json . +RUN npm install + +FROM node:9-slim +COPY --from=build /srv . +ADD . . +EXPOSE 3000 +CMD ["node", "./node_modules/.bin/mocha", "-b", "--exit"] diff --git a/services/performance/README.md b/services/performance/README.md new file mode 100644 index 0000000..59d6968 --- /dev/null +++ b/services/performance/README.md @@ -0,0 +1,3 @@ +# Fargate chat performance test + +Just point it at an instance of the application, and let it go. This performance test is stateless. You can spawn as many copies across as many machines as you would like in order to stress test the application. diff --git a/services/performance/index.js b/services/performance/index.js new file mode 100644 index 0000000..9959e99 --- /dev/null +++ b/services/performance/index.js @@ -0,0 +1,145 @@ +var socketClient = require('socket.io-client'); +var faker = require('faker'); +var async = require('async'); +var config = { + APP_URL: process.env.APP_URL +}; + +var MAX_DURATION = parseInt(process.env.DURATION_MINS, 10) * 60 * 1000 || 30 * 60 * 1000; +var MAX_CONCURRENT = parseInt(process.env.MAX_CONCURRENT, 10) || 80; +var MIN_CONCURRENT = parseInt(process.env.MIN_CONCURRENT, 10) || 0; + +console.log(`Running for ${MAX_DURATION} starting from ` + + `${MIN_CONCURRENT} concurrent agents, and ramping up to ${MAX_CONCURRENT} ` + + 'concurrent agents'); + +var Agent = function(id) { + this.id = id; + this.authenticated = false; +}; + +Agent.prototype.typing = function(done) { + console.log(`Agent ${this.id} typing`); + this.socket.emit('typing', 'general'); + done(); +}; + +Agent.prototype.stopTyping = function(done) { + console.log(`Agent ${this.id} stop typing`); + this.socket.emit('stop typing', 'general'); + done(); +}; + +Agent.prototype.sendMessage = function(done) { + console.log(`Agent ${this.id} new message`); + this.socket.emit('new message', { + room: 'general', + message: faker.lorem.sentence() + }, done); +}; + +Agent.prototype.createAccount = function(done) { + this.socket.emit('create user', { + username: faker.internet.userName(), + email: faker.internet.email(), + password: faker.internet.password() + }, done); +}; + +Agent.prototype.authAnonymous = function(done) { + console.log(`Agent ${this.id} anonymous user`); + this.socket.emit('anonymous user', done); +}; + +Agent.prototype.connect = function(done) { + console.log(`Agent ${this.id} connect`); + this.socket = socketClient(config.APP_URL, { + transports: ['websocket'], + secure: true, + reconnect: true, + rejectUnauthorized: false + }); + this.socket.once('connect', done); + this.socket.on('connect', function() { + console.log(`Agent ${this.id} connect done`); + }); +}; + +Agent.prototype.disconnect = function(done) { + console.log(`Agent ${this.id} disconnect`); + this.socket.disconnect(); + done(); +}; + +Agent.prototype.pause = function(duration) { + return function(done) { + setTimeout(done, duration); + }; +}; + +Agent.prototype.spamMessage = function(number, done) { + console.log(`Agent ${this.id} sending a message`); + async.series([ + this.pause(1000).bind(this), + this.typing.bind(this), + this.pause(1000).bind(this), + this.sendMessage.bind(this), + this.stopTyping.bind(this) + ], done); +}; + +Agent.prototype.spamMessages = function(howMany) { + var self = this; + return function(doneSpamming) { + console.log(`Agent ${this.id} spamming messages`); + async.timesSeries(howMany, self.spamMessage.bind(self), doneSpamming); + }; +}; + +// Initiate an agent. +Agent.prototype.start = function(done) { + async.series([ + this.connect.bind(this), + this.pause(1000).bind(this), + this.authAnonymous.bind(this), + this.pause(1000).bind(this), + this.spamMessages(faker.random.number({ min: 5, max: 20 })).bind(this), + this.pause(1000).bind(this), + this.disconnect.bind(this) + ], done); +}; + +var concurrent = 0; +var start = Date.now(); +var agentId = 0; + +function scaleBetween(unscaledNum, minAllowed, maxAllowed, min, max) { + return (maxAllowed - minAllowed) * (unscaledNum - min) / (max - min) + minAllowed; +} + +function maybeLaunchAgent() { + var now = Date.now(); + var duration = now - start; + + if (duration > MAX_DURATION) { + process.exit(0); + } + + var targetConcurrent = scaleBetween(duration, MAX_CONCURRENT, MIN_CONCURRENT, MAX_DURATION, 0); + + if (concurrent < targetConcurrent) { + concurrent++; + + var agent = new Agent(agentId); + agentId++; + agent.start(function() { + concurrent--; + }); + + setImmediate(maybeLaunchAgent); + } else { + setTimeout(maybeLaunchAgent, 1000); + } +} + +maybeLaunchAgent(); diff --git a/services/performance/package-lock.json b/services/performance/package-lock.json new file mode 100644 index 0000000..2a2672f --- /dev/null +++ b/services/performance/package-lock.json @@ -0,0 +1,237 @@ +{ + "name": "tests", + "version": "0.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "requires": { + "lodash": "4.17.10" + } + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, + "blob": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz", + "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=" + }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "engine.io-client": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz", + "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==", + "requires": { + "component-emitter": "1.2.1", + "component-inherit": "0.0.3", + "debug": "3.1.0", + "engine.io-parser": "2.1.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "3.3.3", + "xmlhttprequest-ssl": "1.5.5", + "yeast": "0.1.2" + } + }, + "engine.io-parser": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz", + "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.4", + "has-binary2": "1.0.3" + } + }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" + }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "1.0.2" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "1.0.2" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "socket.io-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz", + "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "3.1.0", + "engine.io-client": "3.2.1", + "has-binary2": "1.0.3", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "3.2.0", + "to-array": "0.1.4" + } + }, + "socket.io-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz", + "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==", + "requires": { + "component-emitter": "1.2.1", + "debug": "3.1.0", + "isarray": "2.0.1" + } + }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, + "ws": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "1.0.0", + "safe-buffer": "5.1.2", + "ultron": "1.1.1" + } + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" + } + } +} diff --git a/services/performance/package.json b/services/performance/package.json new file mode 100644 index 0000000..d08c82e --- /dev/null +++ b/services/performance/package.json @@ -0,0 +1,18 @@ +{ + "name": "tests", + "version": "0.0.0", + "description": "The test suite for the application", + "author": "Nathan Peck", + "private": true, + "license": "BSD", + "dependencies": { + "async": "2.6.1", + "faker": "4.1.0", + "socket.io-client": "2.1.1" + }, + "eslintConfig": { + "globals": { + "Vue": true + } + } +} diff --git a/services/test-suite/package.json b/services/test-suite/package.json index 1c4d91b..79a2221 100644 --- a/services/test-suite/package.json +++ b/services/test-suite/package.json @@ -2,7 +2,7 @@ "name": "tests", "version": "0.0.0", "description": "The test suite for the application", - "author": "Grant Timmerman", + "author": "Nathan Peck", "private": true, "license": "BSD", "dependencies": {