Skip to content

Commit 228810a

Browse files
junsjangKihoon Kownnaxxsterkaiz-io
authored
Add a python project about Route 53 Failover Record with Health Check that resolve #789 (#1053)
* route53 failover record * Reorganized stack structure * Adding alias record based health check for failover record * Changed container image of sample workload from personal sample app to managed sample app * Changed personal information to example information * Updated README file * Added instruction to use pre-made Route 53 HostedZone * Fixed the code to be deployed without previous resources. Extracted hosted zone and pass it as parameter to health check stacks example.com is reserved. Changes domain name to different name * Updated architecture diagram Move the email icon to outside of the region Added CloudWatch metrics that make alarm --------- Co-authored-by: Kihoon Kown <[email protected]> Co-authored-by: Junseong Jang <[email protected]> Co-authored-by: Michael Kaiser <[email protected]>
1 parent 370ac79 commit 228810a

File tree

11 files changed

+474
-0
lines changed

11 files changed

+474
-0
lines changed

python/route53-failover/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# CDK Failover DNS with Route 53 Health Check
2+
3+
This sample project demonstrates how to create a Failover DNS record in Route 53 using a CDK application. It leverages the Route 53 Health Check feature to monitor the status of endpoints and automatically fail over to a backup endpoint in case of failure. Additionally, it configures SNS notifications to alert administrators when a failover event occurs, ensuring prompt awareness and response.
4+
5+
## Architecture
6+
7+
The architecture of this solution is illustrated in the following diagram:
8+
9+
![Architecture Diagram](images/architecture.png)
10+
11+
The key components of the architecture are:
12+
13+
1. **Primary Endpoint**: The main endpoint that serves traffic under normal circumstances.
14+
2. **Secondary Endpoint**: The backup endpoint that takes over traffic when the primary endpoint fails.
15+
3. **Route 53 Hosted Zone**: The DNS hosted zone where the Failover DNS record is created.
16+
4. **Route 53 Health Checks**: Health checks that monitor the availability of the primary and secondary endpoints.
17+
5. **Route 53 Failover DNS Record**: The DNS record that routes traffic to the primary endpoint by default, but automatically switches to the secondary endpoint when the primary endpoint fails the health check.
18+
6. **SNS Topic**: The Simple Notification Service (SNS) topic that publishes notifications when a failover event occurs.
19+
7. **SNS Subscription**: The subscription to the SNS topic, which can be configured to deliver notifications via email or other channels.
20+
21+
## Getting Started
22+
23+
To get started with this project, follow these steps:
24+
25+
1. Clone the repository.
26+
2. Install the required dependencies using `npm install`.
27+
3. Configure the necessary AWS credentials and environment variables.
28+
4. Select a Route 53 HostedZone that can be used for this project.
29+
5. **Provide the required context values in the `cdk.json` file, including `domain` refering the HostedZone, `email` to get notifications, `primaryRegion`, and `secondaryRegion` to deploy the sample application.**
30+
6. Deploy the CDK application using `cdk deploy`.
31+
32+
Refer to the project's documentation for more detailed instructions and configuration options.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from constructs import Construct
2+
from aws_cdk import (
3+
Stack,
4+
aws_route53 as route53,
5+
aws_elasticloadbalancingv2 as elbv2,
6+
aws_route53_targets as route53_targets,
7+
)
8+
9+
class AliasHealthcheckRecordStack(Stack):
10+
def __init__(self, scope: Construct, construct_id: str, zone: route53.HostedZone, primaryLoadBalancer: elbv2.ILoadBalancerV2, secondaryLoadBalancer: elbv2.ILoadBalancerV2, **kwargs) -> None:
11+
super().__init__(scope, construct_id, **kwargs)
12+
13+
# primary record
14+
primary = route53.ARecord(self, "PrimaryRecordSet",
15+
zone = zone,
16+
record_name="alias",
17+
target = route53.RecordTarget.from_alias(route53_targets.LoadBalancerTarget(primaryLoadBalancer)),
18+
)
19+
primaryRecordSet = primary.node.default_child
20+
primaryRecordSet.failover = "PRIMARY"
21+
primaryRecordSet.add_property_override('AliasTarget.EvaluateTargetHealth', True)
22+
primaryRecordSet.set_identifier = "Primary"
23+
24+
# secondary record
25+
secondary = route53.ARecord(self, "SecondaryRecordSet",
26+
zone = zone,
27+
record_name="alias",
28+
target= route53.RecordTarget.from_alias(route53_targets.LoadBalancerTarget(secondaryLoadBalancer)),
29+
)
30+
secondaryRecordSet = secondary.node.default_child
31+
secondaryRecordSet.failover = "SECONDARY"
32+
secondaryRecordSet.add_property_override('AliasTarget.EvaluateTargetHealth', True)
33+
secondaryRecordSet.set_identifier = "Secondary"
34+
35+
# # cloudwatch metric & alarm to SNS
36+
# snsTopic = sns.Topic(self, "AlarmNotificationTopic")
37+
# snsTopic.add_subscription(
38+
# EmailSubscription(email_address=email)
39+
# )
40+
41+
# healthCheckMetric = cloudwatch.Metric(
42+
# metric_name="HealthCheckStatus",
43+
# namespace="AWS/Route53",
44+
# statistic="Minimum",
45+
# period=Duration.minutes(1),
46+
# region="us-east-1",
47+
# dimensions_map={
48+
# "HealthCheckId": primaryHealthCheck.attr_health_check_id
49+
# }
50+
# )
51+
# healthCheckAlarm = healthCheckMetric.create_alarm(self, 'HealthCheckFailureAlarm',
52+
# evaluation_periods=1,
53+
# threshold=1,
54+
# comparison_operator=cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD
55+
# )
56+
57+
# healthCheckAlarm.add_alarm_action(SnsAction(snsTopic))

python/route53-failover/app.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env python3
2+
import os
3+
4+
import aws_cdk as cdk
5+
6+
from fargate_app_stack import FargateAppStack
7+
from hosted_zone_stack import HostedZoneStack
8+
from healthcheck_alarm_stack import HealthcheckAlarmStack
9+
from alias_healthcheck_record_stack import AliasHealthcheckRecordStack
10+
11+
app = cdk.App()
12+
13+
domain=app.node.try_get_context('domain')
14+
email=app.node.try_get_context('email')
15+
primaryRegion=app.node.try_get_context('primaryRegion')
16+
secondaryRegion=app.node.try_get_context('secondaryRegion')
17+
account=os.getenv('CDK_DEFAULT_ACCOUNT')
18+
region=os.getenv('CDK_DEFAULT_REGION')
19+
20+
# Sample app 1
21+
app1 = FargateAppStack(app, "PrimaryFargateApp", env=cdk.Environment(
22+
account=account,
23+
region=primaryRegion
24+
))
25+
26+
# Sample app 2
27+
app2 = FargateAppStack(app, "SecondaryFargateApp", env=cdk.Environment(
28+
account=account,
29+
region=secondaryRegion
30+
))
31+
32+
# Hosted Zone
33+
hostedZoneStack = HostedZoneStack(app, "HostedZone", domain=domain, env=cdk.Environment(
34+
account=account,
35+
region="us-east-1"
36+
))
37+
38+
HealthcheckAlarmStack(
39+
app, "HealthCheckAlarm",
40+
zone=hostedZoneStack.zone,
41+
primaryLoadBalancer=app1.fargate_service.load_balancer,
42+
secondaryLoadBalancer=app2.fargate_service.load_balancer,
43+
email=email,
44+
env=cdk.Environment(
45+
account=account,
46+
region="us-east-1"
47+
),
48+
cross_region_references=True
49+
)
50+
51+
AliasHealthcheckRecordStack(
52+
app, "AliasHealthCheckRecord",
53+
zone=hostedZoneStack.zone,
54+
primaryLoadBalancer=app1.fargate_service.load_balancer,
55+
secondaryLoadBalancer=app2.fargate_service.load_balancer,
56+
env=cdk.Environment(
57+
account=account,
58+
region="us-east-1"
59+
),
60+
cross_region_references=True
61+
)
62+
63+
app.synth()

python/route53-failover/cdk.json

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{
2+
"app": "python3 app.py",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"requirements*.txt",
11+
"source.bat",
12+
"**/__init__.py",
13+
"**/__pycache__",
14+
"tests"
15+
]
16+
},
17+
"context": {
18+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
19+
"@aws-cdk/core:checkSecretUsage": true,
20+
"@aws-cdk/core:target-partitions": [
21+
"aws",
22+
"aws-cn"
23+
],
24+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
27+
"@aws-cdk/aws-iam:minimizePolicies": true,
28+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
31+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
32+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
33+
"@aws-cdk/core:enablePartitionLiterals": true,
34+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
35+
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
36+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
37+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
38+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
39+
"@aws-cdk/aws-route53-patters:useCertificate": true,
40+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
41+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
42+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
43+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
44+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
45+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
46+
"@aws-cdk/aws-redshift:columnId": true,
47+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
48+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
49+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
50+
"@aws-cdk/aws-kms:aliasNameRef": true,
51+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
52+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
53+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
54+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
55+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
56+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
57+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
58+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
59+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
60+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
61+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
62+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
63+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
64+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
65+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
66+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
67+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
68+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
69+
"domain": "aws-cdk-samples.com",
70+
"email": "[email protected]",
71+
"primaryRegion": "us-east-1",
72+
"secondaryRegion": "us-west-2"
73+
}
74+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from aws_cdk import (
2+
aws_ec2 as ec2,
3+
aws_ecs as ecs,
4+
aws_ecs_patterns as ecs_patterns,
5+
CfnOutput, Stack
6+
)
7+
from constructs import Construct
8+
9+
# Need to Change different app
10+
class FargateAppStack(Stack):
11+
12+
def __init__(self, scope: Construct, id: str, **kwargs) -> None:
13+
super().__init__(scope, id, **kwargs)
14+
15+
# Create VPC and Fargate Cluster
16+
# NOTE: Limit AZs to avoid reaching resource quotas
17+
vpc = ec2.Vpc(
18+
self, "MyVpc",
19+
max_azs=2
20+
)
21+
22+
cluster = ecs.Cluster(
23+
self, 'Ec2Cluster',
24+
vpc=vpc
25+
)
26+
27+
self.fargate_service = ecs_patterns.NetworkLoadBalancedFargateService(
28+
self, "FargateService",
29+
cluster=cluster,
30+
task_image_options=ecs_patterns.NetworkLoadBalancedTaskImageOptions(
31+
image=ecs.ContainerImage.from_registry("amazon/amazon-ecs-sample")
32+
)
33+
)
34+
35+
self.fargate_service.service.connections.security_groups[0].add_ingress_rule(
36+
peer = ec2.Peer.ipv4(vpc.vpc_cidr_block),
37+
connection = ec2.Port.tcp(80),
38+
description="Allow http inbound from VPC"
39+
)
40+
41+
CfnOutput(
42+
self, "LoadBalancerDNS",
43+
value=self.fargate_service.load_balancer.load_balancer_dns_name
44+
)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from constructs import Construct
2+
from aws_cdk import (
3+
Stack,
4+
Duration,
5+
aws_route53 as route53,
6+
aws_elasticloadbalancingv2 as elbv2,
7+
aws_route53_targets as route53_targets,
8+
aws_cloudwatch as cloudwatch,
9+
aws_sns as sns
10+
)
11+
from aws_cdk.aws_cloudwatch_actions import SnsAction
12+
from aws_cdk.aws_sns_subscriptions import EmailSubscription
13+
14+
class HealthcheckAlarmStack(Stack):
15+
def __init__(self, scope: Construct, construct_id: str, zone: route53.HostedZone, primaryLoadBalancer: elbv2.ILoadBalancerV2, secondaryLoadBalancer: elbv2.ILoadBalancerV2, email: str, **kwargs) -> None:
16+
super().__init__(scope, construct_id, **kwargs)
17+
18+
# primary record
19+
primaryHealthCheck = route53.CfnHealthCheck(self, "DNSPrimaryHealthCheck", health_check_config=route53.CfnHealthCheck.HealthCheckConfigProperty(
20+
fully_qualified_domain_name=primaryLoadBalancer.load_balancer_dns_name,
21+
type="HTTP",
22+
port=80
23+
))
24+
primary = route53.ARecord(self, "PrimaryRecordSet",
25+
zone = zone,
26+
record_name="failover",
27+
target = route53.RecordTarget.from_alias(route53_targets.LoadBalancerTarget(primaryLoadBalancer)),
28+
)
29+
primaryRecordSet = primary.node.default_child
30+
primaryRecordSet.failover = "PRIMARY"
31+
primaryRecordSet.health_check_id = primaryHealthCheck.attr_health_check_id
32+
primaryRecordSet.set_identifier = "Primary"
33+
34+
# secondary record
35+
secondaryHealthCheck = route53.CfnHealthCheck(self, "DNSSecondaryHealthCheck", health_check_config=route53.CfnHealthCheck.HealthCheckConfigProperty(
36+
fully_qualified_domain_name=secondaryLoadBalancer.load_balancer_dns_name,
37+
type="HTTP",
38+
port=80,
39+
))
40+
secondary = route53.ARecord(self, "SecondaryRecordSet",
41+
zone = zone,
42+
record_name="failover",
43+
target= route53.RecordTarget.from_alias(route53_targets.LoadBalancerTarget(secondaryLoadBalancer)),
44+
)
45+
secondaryRecordSet = secondary.node.default_child
46+
secondaryRecordSet.failover = "SECONDARY"
47+
secondaryRecordSet.health_check_id = secondaryHealthCheck.attr_health_check_id
48+
secondaryRecordSet.set_identifier = "Secondary"
49+
50+
# cloudwatch metric & alarm to SNS
51+
snsTopic = sns.Topic(self, "AlarmNotificationTopic")
52+
snsTopic.add_subscription(
53+
EmailSubscription(email_address=email)
54+
)
55+
56+
healthCheckMetric = cloudwatch.Metric(
57+
metric_name="HealthCheckStatus",
58+
namespace="AWS/Route53",
59+
statistic="Minimum",
60+
period=Duration.minutes(1),
61+
region="us-east-1",
62+
dimensions_map={
63+
"HealthCheckId": primaryHealthCheck.attr_health_check_id
64+
}
65+
)
66+
healthCheckAlarm = healthCheckMetric.create_alarm(self, 'HealthCheckFailureAlarm',
67+
evaluation_periods=1,
68+
threshold=1,
69+
comparison_operator=cloudwatch.ComparisonOperator.LESS_THAN_THRESHOLD
70+
)
71+
72+
healthCheckAlarm.add_alarm_action(SnsAction(snsTopic))
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from constructs import Construct
2+
from aws_cdk import (
3+
Stack,
4+
aws_route53 as route53,
5+
)
6+
7+
class HostedZoneStack(Stack):
8+
def __init__(self, scope: Construct, construct_id: str, domain: str, **kwargs) -> None:
9+
super().__init__(scope, construct_id, **kwargs)
10+
11+
# Test Env
12+
self.zone = route53.PublicHostedZone(self, "HostedZone", zone_name=domain)
13+
# use below code to use already created hosted zone
14+
# self.hostzone = route53.HostedZone.from_lookup(self, "HostedZone", domain_name=domain)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.drawio.bkp

0 commit comments

Comments
 (0)