Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
grabnerandi committed Jan 24, 2018
1 parent 7446a67 commit 424ddf3
Show file tree
Hide file tree
Showing 27 changed files with 1,735 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
JUSTFORANDI/*
932 changes: 932 additions & 0 deletions AWSDevOpsTutorialCloudFormationStack.json

Large diffs are not rendered by default.

50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,50 @@
# AWSDevOpsTutorial
Unbreakable DevOps Pipeline Tutorial with AWS CodeDeploy, AWS CodePipeline, AWS Lambda, EC2 and Dynatrace
Unbreakable DevOps Pipeline Tutorial with AWS CodeDeploy, AWS CodePipeline, AWS Lambda, EC2 and Dynatrace.

The goal of this tutorial is having a full end-to-end AWS DevOps Pipeline (Staging, Approval, Production) that is fully monitored with Dynatrace. With Dynatrace injected into the pipeline you get the following features
1. Monitor your Staging Environment
2. Automate Approve/Reject Promotion from Staging to Production based on Performance Data
3. Monitor your Production Environment
4. Automatic Deploy of previous revision in case Dynatrace detected problems in Production

Before we launch the CloudFormation stack which will create all required resources (EC2 Instances, Lambdas, CodeDeploy, CodePipeline, API Gateway) lets make sure we have all pre-requisits covered!

## Pre-Requisits
1. You need an AWS account. If you dont have one [get one here](https://aws.amazon.com/)
2. You need a Dynatrace Account. Get your [Free SaaS Trial here!](http://bit.ly/dtsaastrial)
3. You need to clone or copy the content of this GitHub repo to your local disk!

## Preparation
**Amazon**
As we are going to use AWS CodeDeploy, AWS CodePipeline, AWS Lambda, DynamoDB, API Gateway and EC2 make sure the AWS Region you select provides all these services. We have tested this cloud formation on US-West-2a (Oregon) and US-East-2b (Ohio). To be on the safe side we suggest you pick one of these regions!

1. Create an EC2 Key Pair for your AWS Region! Our CloudFormation Template needs an EC2 Key Pair!
1.1. To learn more about Key Pairs and how to connect to EC2 Instances for troubleshooting read [Connect to your Linux Instance](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstances.html)
2. Create a S3 Bucket with the naming scheme: <yourname>-dynatracedevops and enable versioning. See following screenshots for reference
![](./images/preparation_creates3bucket.png)
3. Copy the content from the folder "copytos3" to your newly created S3 bucket. This includes the application package, tests, monspec as well as all Lambda functions
![](./images/preparation_copytos3.png)

**Dynatrace**
We need a couple of things to launch the CloudFormation Template
1. Your *Dynatrace Tenant URL*: For SaaS that would be something like http://<yourtenant>.live.dynatrace.com. For Managed it would be http://<yourserver>/e/<your-env-id>
2. Your *Dynatrace OneAgent for Linux Download URL*: Go to Deploy Dynatrace -> Start Installation -> Linux and copy the URL within the quotes as shown below:
![](./images/preparation_dynatraceoneagenturl.png)
3. A *Dynatrace API Token*: Go to Settings -> Integration -> Dynatrace API and create a new Token
![](./images/preparation_dynatraceapitoken.png)

## Lets create the CloudFormation Stack
You can download the stack definition from [here](./AWSDevOpsTutorialCloudFormationStack.json) - or simply click on one of the following links which will directly get you to the CloudFormation Wizard for the respective region.

Region | Launch Template
------------ | -------------
**N. Virginia** (us-east-1) | [![Launch Dynatrace DevOps Stack into Virginia with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Ohio** (us-east-2) | [![Launch Dynatrace DevOps Stack into Ohio with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-2#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.jsonn)
**Oregon** (us-west-2) | [![Launch Dynatrace DevOps Stack into Oregon with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Ireland** (eu-west-1) | [![Launch Dynatrace DevOps Stack into Ireland with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Frankfurt** (eu-central-1) | [![Launch Dynatrace DevOps Stack into Frankfurt with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Tokyo** (ap-northeast-1) | [![Launch Dynatrace DevOps Stack into Tokyo with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Seoul** (ap-northeast-2) | [![Launch Dynatrace DevOps Stack into Seoul with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-2#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Singapore** (ap-southeast-1) | [![Launch Dynatrace DevOps Stack into Singapore with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-1#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)
**Sydney** (ap-southeast-2) | [![Launch Dynatrace DevOps Stack into Sydney with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-southeast-2#/stacks/new?stackName=dynatracedevopsstack&templateURL=https://github.com/Dynatrace/AWSDevOpsTutorial/AWSDevOpsTutorialCloudFormationStack.json)

249 changes: 249 additions & 0 deletions app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
var port = process.env.PORT || 80,
http = require('http'),
fs = require('fs'),
os = require('os'),
dttags = process.env.DT_TAGS || "<EMPTY>",
dtcustprops = process.env.DT_CUSTOM_PROP || "<EMPTY>",
dtclusterid = process.env.DT_CLUSTER_ID || "<EMPTY>",
html = fs.readFileSync('index.html').toString().replace("HOSTNAME", os.hostname() + " with DT_TAGS=" + dttags + "\nDT_CUSTOM_PROP=" + dtcustprops + "\nDT_CLUSTER_ID=" + dtclusterid);


// ======================================================================
// Here are some global config entries that change the behavior of the app
// ======================================================================
var buildNumber = 1;
var minSleep = 500;
var requestCount = 0;
var inProduction = false;
var invokeRequestCount = 0;
var failInvokeRequestPercentage = 0;

// ======================================================================
// does some init checks and sets variables!
// ======================================================================
var init = function(newBuildNumber) {
// CHECK IF WE ARE RUNNING "In Production"
inProduction = process.env.DEPLOYMENT_GROUP_NAME && process.env.DEPLOYMENT_GROUP_NAME.startsWith("Production");

if(inProduction) {
minSleep = 300; // we just simulate that production is a bit faster than staging, e.g: better hardware!
}

// here are some "problems" we simulate for different builds. Builds are identified via Env Variable BUILD_NUMBER;
// Build # | Problem
// 1 | no problem
// 2 | 50% of requests return HTTP 500 Status Code
// 3 | back to normal
// 4 | no problem in staging but problem in prod -> higher sleep time and 10% of requests fail
// X | any other build number will run like 1 & 3
if(newBuildNumber != null) {
buildNumber = parseInt(newBuildNumber);
}
else if(process.env.BUILD_NUMBER && process.env.BUILD_NUMBER != null) {
buildNumber = parseInt(process.env.BUILD_NUMBER);
}

switch(buildNumber) {
case 2:
failInvokeRequestPercentage = 2;
break;
case 4:
if(inProduction) {
minSleep = minSleep * 2;
failInvokeRequestPercentage = 10;
}
break;
default:
// everything normal here
failInvokeRequestPercentage = 0;
break;
}

console.log("Init: " + buildNumber + "/" + failInvokeRequestPercentage);
}

// ======================================================================
// Background colors for our app depending on the build
// ======================================================================
var backgroundColors = ["#EEA53E", "#73A53E", "#FF0000", "#FFFF00", "#777777"]
var getBackgroundColor = function() {
var buildNumberForBackgroundColor = buildNumber;
if(buildNumber == 0 || buildNumber > 4) buildNumberForBackgroundColor = 1;

return backgroundColors[buildNumberForBackgroundColor];
}


// ======================================================================
// This is for logging
// ======================================================================
var logstream = fs.createWriteStream('./serviceoutput.log');
var SEVERITY_DEBUG = "Debug";
var SEVERITY_INFO = "Info";
var SEVERITY_WARNING = "Warning";
var SEVERITY_ERROR = "Error";

var log = function(severity, entry) {
// console.log(entry);
if (severity === SEVERITY_DEBUG) {
// dont log debug
} else {
var logEntry = new Date().toISOString() + ' - ' + severity + " - " + entry + '\n';
// fs.appendFileSync('./serviceoutput.log', new Date().toISOString() + ' - ' + severity + " - " + entry + '\n');
logstream.write(logEntry);
}
};

// ======================================================================
// Very inefficient way to "sleep"
// ======================================================================
function sleep(time) {
if(time < minSleep) time = minSleep;
var stop = new Date().getTime();
while(new Date().getTime() < stop + time) {
;
}
}

// ======================================================================
// This is our main HttpServer Handler
// ======================================================================
var server = http.createServer(function (req, res) {
if (req.method === 'POST') {
var body = '';

req.on('data', function(chunk) {
body += chunk;
});

req.on('end', function() {
if (req.url === '/') {
log(SEVERITY_DEBUG, 'Received message: ' + body);
} else if (req.url = '/scheduled') {
log(SEVERITY_DEBUG, 'Received task ' + req.headers['x-aws-sqsd-taskname'] + ' scheduled at ' + req.headers['x-aws-sqsd-scheduled-at']);
}

res.writeHead(200, 'OK', {'Content-Type': 'text/plain'});
res.end();
});
} else if (req.url.startsWith("/api")) {
var url = require('url').parse(req.url, true);
var closeResponse = true;

// sleep a bit :-)
var sleeptime = parseInt(url.query["sleep"]);
if(sleeptime === 0) sleeptime = minSleep;
log(SEVERITY_DEBUG, "Sleeptime: " + sleeptime);
sleep(sleeptime);

// figure out which API call they want to execute
var status = "Unkown API Call";
if(url.pathname === "/api/sleeptime") {
// Usage: /api/sleeptime?min=1234
var sleepValue = parseInt(url.query["min"]);
if(sleepValue >= 0 && sleepValue <= 10000) minSleep = sleepValue;
status = "Minimum Sleep Time set to " + minSleep;
}
if(url.pathname === "/api/echo") {
// Usage: /api/echo?text=your text to be echoed!
status = "Thanks for saying: " + url.query["text"];
}
if(url.pathname === "/api/login") {
// Usage: /api/login?username=your user name
status = "Welcome " + url.query["username"];
}
if(url.pathname === "/api/invoke") {
// count the invokes for failed requests
var returnStatusCode = 200;
if(failInvokeRequestPercentage > 0) {
invokeRequestCount++;
var failRequest = (invokeRequestCount % failInvokeRequestPercentage);
if(failRequest == 0) {
returnStatusCode = 500;
invokeRequestCount = 0;
}
}

// Usage: /api/invoke?url=http://www.yourdomain.com
var urlRequest = url.query["url"];
status = "Trying to invoke remote call to: " + urlRequest;

var http = null;
if(urlRequest.startsWith("https")) http = require("https");
else http = require("http");
closeResponse = false;
var options = {
host: urlRequest,
path: '/'
};
var result = http.get(urlRequest, function(getResponse) {
log(SEVERITY_DEBUG, 'STATUS: ' + getResponse.statusCode);
log(SEVERITY_DEBUG, 'HEADERS: ' + JSON.stringify(getResponse.headers));

// Buffer the body entirely for processing as a whole.
var bodyChunks = [];
getResponse.on('data', function(chunk) {
bodyChunks.push(chunk);
}).on('end', function() {
var body = Buffer.concat(bodyChunks);
log(SEVERITY_DEBUG, 'BODY: ' + body);
status = "Request to '" + url.query["url"] + "' returned with HTTP Status: " + getResponse.statusCode + " and response body length: " + body.length;
res.writeHead(returnStatusCode, returnStatusCode == 200 ? 'OK' : 'ERROR', {'Content-Type': 'text/plain'});
res.write(status);
res.end();
}).on('error', function(error) {
status = "Request to '" + url.query["url"] + "' returned in an error: " + error;
res.writeHead(returnStatusCode, returnStatusCode == 200 ? 'OK' : 'ERROR', {'Content-Type': 'text/plain'});
res.write(status);
res.end();
log(SEVERITY_INFO, status);
})
});
}
if(url.pathname === "/api/version") {
if (url.query["newBuildNumber"] && url.query["newBuildNumber"] != null) {
init(url.query["newBuildNumber"]);
}

// usage: /api/version
// simply returns the build number as defined in BUILD_NUMBER env-variable which is specified
status = "Running build number: " + buildNumber;
}
if(url.pathname === "/api/causeerror") {
log(SEVERITY_ERROR, "somebody called /api/causeerror");
status = "We just caused an error log entry"
}

// only close response handler if we are done with work!
if(closeResponse) {
res.writeHead(200, 'OK', {'Content-Type': 'text/plain'});
res.write(status);
res.end();
}
}
else
{
res.writeHead(200, 'OK', {'Content-Type': 'text/html'});

// replace buildnumber and background color
var finalHtml = html.replace("BACKGROUND-COLOR", getBackgroundColor()).replace("BUILD_NUMBER", buildNumber);
res.write(finalHtml);
res.end();
}

requestCount++;
if(requestCount >= 100) {
log(SEVERITY_INFO, "Just served another 100 requests!");
requestCount = 0;
}
});

// first we initialize!
init(null);

// Listen on port 80, IP defaults to 127.0.0.1
server.listen(port);

// Put a friendly message on the terminal
console.log('Server running at http://127.0.0.1:' + port + '/');
log(SEVERITY_INFO, "Service is up and running - feed me with data!");
28 changes: 28 additions & 0 deletions app/appspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
version: 0.0
os: linux
files:
- source: index.html
destination: /home/ec2-user/
- source: app.js
destination: /home/ec2-user/
- source: package.json
destination: /home/ec2-user/
permissions:
- object: /home/ec2-user/
mode: 777
type:
- file
hooks:
BeforeInstall:
- location: scripts/stop_server.sh
timeout: 60
runas: root
ApplicationStart:
- location: scripts/start_server.sh
timeout: 60
runas: root
ApplicationStop:
- location: scripts/stop_server.sh
timeout: 60
runas: root

20 changes: 20 additions & 0 deletions app/buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 0.2

phases:
install:
commands:
- echo Installing Mocha...
pre_build:
commands:
- echo Installing source NPM dependencies...
build:
commands:
- echo Build started on `date`
post_build:
commands:
- echo Build completed on `date`
artifacts:
files:
- app.js
- index.html
- package.json
Loading

0 comments on commit 424ddf3

Please sign in to comment.