From 56bcf6366c997dd363208194034a9e6aa40b8579 Mon Sep 17 00:00:00 2001 From: Sam Alba Date: Wed, 3 Mar 2021 17:34:00 -0800 Subject: [PATCH] implemented http-monitoring example using AWS Cloudformation and AWS Cloudwatch Synthetics Signed-off-by: Sam Alba --- examples/aws-monitoring/http_monitor.cue | 269 +++++++++++++++++++++++ examples/aws-monitoring/main.cue | 29 +++ 2 files changed, 298 insertions(+) create mode 100644 examples/aws-monitoring/http_monitor.cue create mode 100644 examples/aws-monitoring/main.cue diff --git a/examples/aws-monitoring/http_monitor.cue b/examples/aws-monitoring/http_monitor.cue new file mode 100644 index 00000000..0840a230 --- /dev/null +++ b/examples/aws-monitoring/http_monitor.cue @@ -0,0 +1,269 @@ +package main + +import ( + "strings" + "regexp" + "encoding/json" + + "dagger.io/aws" + "dagger.io/aws/cloudformation" +) + +#Notification: { + protocol: string + endpoint: string +} + +#Canary: { + name: =~"^[0-9a-z_-]{1,21}$" + slug: strings.Join(regexp.FindAll("[0-9a-zA-Z]*", name, -1), "") + url: string + expectedHTTPCode: *200 | int + timeoutInSeconds: *30 | int + intervalExpression: *"1 minute" | string +} + +#HTTPMonitor: { + + // For sending notifications + notifications: [...#Notification] + // Canaries (tests) + canaries: [...#Canary] + // Name of the Cloudformation stack + cfnStackName: string + // AWS Config + awsConfig: aws.#Config + + cfnStack: cloudformation.#Stack & { + config: awsConfig + source: json.Marshal(#cfnTemplate) + stackName: cfnStackName + onFailure: "DO_NOTHING" + } + + // Function handler + #lambdaHandler: { + url: string + expectedHTTPCode: int + + script: #""" + var synthetics = require('Synthetics'); + const log = require('SyntheticsLogger'); + + const pageLoadBlueprint = async function () { + + // INSERT URL here + const URL = "\#(url)"; + + let page = await synthetics.getPage(); + const response = await page.goto(URL, {waitUntil: 'domcontentloaded', timeout: 30000}); + //Wait for page to render. + //Increase or decrease wait time based on endpoint being monitored. + await page.waitFor(15000); + // This will take a screenshot that will be included in test output artifacts + await synthetics.takeScreenshot('loaded', 'loaded'); + let pageTitle = await page.title(); + log.info('Page title: ' + pageTitle); + if (response.status() !== \#(expectedHTTPCode)) { + throw "Failed to load page!"; + } + }; + + exports.handler = async () => { + return await pageLoadBlueprint(); + }; + """# + } + + #cfnTemplate: { + AWSTemplateFormatVersion: "2010-09-09" + Description: "CloudWatch Synthetics website monitoring" + Resources: { + Topic: { + Type: "AWS::SNS::Topic" + Properties: Subscription: [ + for e in notifications { + Endpoint: e.endpoint + Protocol: e.protocol + }, + ] + } + TopicPolicy: { + Type: "AWS::SNS::TopicPolicy" + Properties: { + PolicyDocument: { + Id: "Id1" + Version: "2012-10-17" + Statement: [ + { + Sid: "Sid1" + Effect: "Allow" + Principal: AWS: "*" + Action: "sns:Publish" + Resource: Ref: "Topic" + Condition: StringEquals: "AWS:SourceOwner": Ref: "AWS::AccountId" + }, + ] + } + Topics: [ + { + Ref: "Topic" + }, + ] + } + } + CanaryBucket: { + Type: "AWS::S3::Bucket" + Properties: {} + } + CanaryRole: { + Type: "AWS::IAM::Role" + Properties: { + AssumeRolePolicyDocument: { + Version: "2012-10-17" + Statement: [ + { + Effect: "Allow" + Principal: Service: "lambda.amazonaws.com" + Action: "sts:AssumeRole" + }, + ] + } + Policies: [ + { + PolicyName: "execution" + PolicyDocument: { + Version: "2012-10-17" + Statement: [ + { + Effect: "Allow" + Action: "s3:ListAllMyBuckets" + Resource: "*" + }, + { + Effect: "Allow" + Action: "s3:PutObject" + Resource: "Fn::Sub": "${CanaryBucket.Arn}/*" + }, + { + Effect: "Allow" + Action: "s3:GetBucketLocation" + Resource: "Fn::GetAtt": [ + "CanaryBucket", + "Arn", + ] + }, + { + Effect: "Allow" + Action: "cloudwatch:PutMetricData" + Resource: "*" + Condition: StringEquals: "cloudwatch:namespace": "CloudWatchSynthetics" + }, + ] + } + }, + ] + } + } + CanaryLogGroup: { + Type: "AWS::Logs::LogGroup" + Properties: { + LogGroupName: "Fn::Sub": "/aws/lambda/cwsyn-\(cfnStackName)" + RetentionInDays: 14 + } + } + CanaryPolicy: { + Type: "AWS::IAM::Policy" + Properties: { + PolicyDocument: Statement: [ + { + Effect: "Allow" + Action: [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ] + Resource: "Fn::GetAtt": [ + "CanaryLogGroup", + "Arn", + ] + }, + ] + PolicyName: "logs" + Roles: [ + { + Ref: "CanaryRole" + }, + ] + } + } + for canary in canaries { + "Canary\(canary.slug)": { + Type: "AWS::Synthetics::Canary" + Properties: { + ArtifactS3Location: "Fn::Sub": "s3://${CanaryBucket}" + Code: { + #handler: #lambdaHandler & { + url: canary.url + expectedHTTPCode: canary.expectedHTTPCode + } + Handler: "index.handler" + Script: #handler.script + } + ExecutionRoleArn: "Fn::GetAtt": [ + "CanaryRole", + "Arn", + ] + FailureRetentionPeriod: 30 + Name: canary.name + RunConfig: TimeoutInSeconds: canary.timeoutInSeconds + RuntimeVersion: "syn-1.0" + Schedule: { + DurationInSeconds: "0" + Expression: "rate(\(canary.intervalExpression))" + } + StartCanaryAfterCreation: true + SuccessRetentionPeriod: 30 + } + } + "SuccessPercentAlarm\(canary.slug)": { + DependsOn: "TopicPolicy" + Type: "AWS::CloudWatch::Alarm" + Properties: { + AlarmActions: [ + { + Ref: "Topic" + }, + ] + AlarmDescription: "Canary is failing." + ComparisonOperator: "LessThanThreshold" + Dimensions: [ + { + Name: "CanaryName" + Value: Ref: "Canary\(canary.slug)" + }, + ] + EvaluationPeriods: 1 + MetricName: "SuccessPercent" + Namespace: "CloudWatchSynthetics" + OKActions: [ + { + Ref: "Topic" + }, + ] + Period: 300 + Statistic: "Minimum" + Threshold: 90 + TreatMissingData: "notBreaching" + } + } + } + } + Outputs: { + for canary in canaries { + "\(canary.slug)Canary": Value: Ref: "Canary\(canary.slug)" + "\(canary.slug)URL": Value: canary.url + } + NumberCanaries: Value: len(canaries) + } + } +} diff --git a/examples/aws-monitoring/main.cue b/examples/aws-monitoring/main.cue new file mode 100644 index 00000000..57819776 --- /dev/null +++ b/examples/aws-monitoring/main.cue @@ -0,0 +1,29 @@ +package main + +import ( + "dagger.io/aws" +) + +// Fill using: +// --input-string awsConfig.accessKey=XXX +// --input-string awsConfig.secretKey=XXX +awsConfig: aws.#Config & { + region: *"us-east-1" | string +} + +monitor: #HTTPMonitor & { + notifications: [ + #Notification & { + endpoint: "sam+test@blocklayerhq.com" + protocol: "email" + }, + ] + canaries: [ + #Canary & { + name: "website-test" + url: "https://www.google.com/" + }, + ] + cfnStackName: "my-monitor" + "awsConfig": awsConfig +}