diff --git a/spire/templates/apps-200A.yml b/spire/templates/apps-200A.yml index 2a4bae57..c384a8f8 100644 --- a/spire/templates/apps-200A.yml +++ b/spire/templates/apps-200A.yml @@ -87,6 +87,8 @@ Parameters: FeederSharedAlbListenerRulePriorityPrefix: { Type: String } + InsightsSharedAlbListenerRulePriorityPrefix: { Type: String } + NetworksSharedAlbListenerRulePriorityPrefix: { Type: String } RemixSharedAlbListenerRulePriorityPrefix: { Type: String } @@ -299,6 +301,39 @@ Resources: TemplateURL: !Sub ${TemplateUrlPrefix}/feeder.yml TimeoutInMinutes: 20 + InsightsStack: + Type: AWS::CloudFormation::Stack + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + Parameters: + NestedChangeSetScrubbingResourcesState: !Ref NestedChangeSetScrubbingResourcesState + AlbFullName: !Ref AlbFullName + AlbHttpsListenerArn: !Ref AlbHttpsListenerArn + EcsClusterArn: !Ref EcsClusterArn + VpcId: !Ref VpcId + EcrImageTag: !Sub /prx/${EnvironmentTypeAbbreviation}/Spire/Dovetail-Insights/pkg/docker-image-tag + NewRelicApiKeyPrxLite: !Ref NewRelicApiKeyPrxLite + AlbListenerRulePriorityPrefix: !Ref InsightsSharedAlbListenerRulePriorityPrefix + EnvironmentType: !Ref EnvironmentType + EnvironmentTypeAbbreviation: !Ref EnvironmentTypeAbbreviation + EnvironmentTypeLowercase: !Ref EnvironmentTypeLowercase + RegionMode: !Ref RegionMode + RootStackName: !Ref RootStackName + RootStackId: !Ref RootStackId + IdHostname: !Ref IdHostname + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + TemplateURL: !Sub ${TemplateUrlPrefix}/dovetail-insights.yml + TimeoutInMinutes: 20 + NetworksStack: Type: AWS::CloudFormation::Stack DeletionPolicy: Delete @@ -426,6 +461,9 @@ Outputs: FeederWebTargetGroupFullName: Value: !GetAtt FeederStack.Outputs.WebTargetGroupFullName + InsightsWebTargetGroupFullName: + Value: !GetAtt InsightsStack.Outputs.WebTargetGroupFullName + NetworksPublicWebTargetGroupFullName: Value: !GetAtt NetworksStack.Outputs.PublicWebTargetGroupFullName diff --git a/spire/templates/apps/dovetail-insights.yml b/spire/templates/apps/dovetail-insights.yml new file mode 100644 index 00000000..fe3ad2dd --- /dev/null +++ b/spire/templates/apps/dovetail-insights.yml @@ -0,0 +1,271 @@ +# stacks/apps/dovetail-insights.yml +# 200A +# +# The names of the SQS queues created by this template are intended to +# implicitly match some configuration that exists within the CMS application. +# The only part of the queue names that is passed to the application is the +# prefix; if the stems change in other the template or the app config, things +# will not function as expected. +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Description: >- + Creates a dedicated load balancer and the ECS service for the public Insights + web server. + +Parameters: + kWebContainerName: + Type: String + Default: insights-web + kWebApplicationPort: + Type: Number + Default: 3000 + ####### + NestedChangeSetScrubbingResourcesState: { Type: String } + AlbFullName: { Type: String } + AlbHttpsListenerArn: { Type: String } + EcsClusterArn: { Type: String } + EnvironmentType: { Type: String } + EnvironmentTypeAbbreviation: { Type: String } + EnvironmentTypeLowercase: { Type: String } + RegionMode: { Type: String } + RootStackName: { Type: String } + RootStackId: { Type: String } + VpcId: { Type: AWS::EC2::VPC::Id } + NewRelicApiKeyPrxLite: { Type: String } + EcrImageTag: { Type: AWS::SSM::Parameter::Value } + AlbListenerRulePriorityPrefix: { Type: String } + IdHostname: { Type: String } + +Conditions: + IsProduction: !Equals [!Ref EnvironmentType, Production] + IsPrimaryRegion: !Equals [!Ref RegionMode, Primary] + EnableNestedChangeSetScrubbingResources: !Equals [!Ref NestedChangeSetScrubbingResourcesState, Enabled] + +Resources: + NestedChangeSetScrubber: { Type: AWS::SNS::Topic, Condition: EnableNestedChangeSetScrubbingResources } + + HostHeaderListenerRule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - TargetGroupArn: !Ref WebTargetGroup + Type: forward + Conditions: + - Field: host-header + Values: + - insights.dovetail.* + ListenerArn: !Ref AlbHttpsListenerArn + Priority: !Join ["", [!Ref AlbListenerRulePriorityPrefix, "01"]] + + ExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Version: "2012-10-17" + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy + Policies: + - PolicyDocument: + Statement: + - Action: ssm:GetParameters + Effect: Allow + Resource: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/prx/${EnvironmentTypeAbbreviation}/Spire/Dovetail-Insights/* + Sid: AllowAppParameterRead + Version: "2012-10-17" + PolicyName: ContainerSecrets + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + TaskRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: ecs-tasks.amazonaws.com + Version: "2012-10-17" + Policies: + - PolicyDocument: + Statement: + - Action: events:PutEvents + Effect: Allow + Resource: !Sub arn:${AWS::Partition}:events:${AWS::Region}:${AWS::AccountId}:event-bus/default + Sid: AllowDefaultEventBusPut + Version: "2012-10-17" + PolicyName: DefaultEventBus + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + + WebTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + HealthCheckIntervalSeconds: 15 + HealthCheckPath: /api/v1 + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + Port: 80 + Protocol: HTTP + TargetGroupAttributes: + - Key: deregistration_delay.timeout_seconds + Value: "30" + Tags: + - { Key: Name, Value: !Sub "${RootStackName}_insights" } + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + UnhealthyThresholdCount: 3 + VpcId: !Ref VpcId + WebTargetGroupHttp5xxAlarm: + Type: AWS::CloudWatch::Alarm + Condition: IsProduction + Properties: + AlarmName: !Sub ERROR [Insights] Web server <${EnvironmentTypeAbbreviation}> RETURNING 5XX ERRORS (${RootStackName}) + AlarmDescription: !Sub >- + ${EnvironmentType} Insights' Rails server is returning 5XX errors from + the ECS service to the load balancer. + ComparisonOperator: GreaterThanThreshold + Dimensions: + - Name: LoadBalancer + Value: !Ref AlbFullName + - Name: TargetGroup + Value: !GetAtt WebTargetGroup.TargetGroupFullName + EvaluationPeriods: 1 + MetricName: HTTPCode_Target_5XX_Count + Namespace: AWS/ApplicationELB + Period: 60 + Statistic: Sum + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:ops:cloudwatch-log-group-name, Value: !Ref WebTaskLogGroup } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + Threshold: 0 + TreatMissingData: notBreaching + + WebEcsService: + Type: AWS::ECS::Service + # Condition: HasAuroraEndpoint # See README + Properties: + Cluster: !Ref EcsClusterArn + DeploymentConfiguration: + MaximumPercent: 200 + MinimumHealthyPercent: 50 + DesiredCount: !If [IsPrimaryRegion, !If [IsProduction, 0, 0], 0] + EnableECSManagedTags: true + LoadBalancers: + - ContainerName: !Ref kWebContainerName + ContainerPort: !Ref kWebApplicationPort + TargetGroupArn: !Ref WebTargetGroup + PlacementConstraints: + - Type: memberOf + Expression: attribute:ecs.cpu-architecture == ARM64 + PropagateTags: TASK_DEFINITION + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + TaskDefinition: !Ref WebTaskDefinition + WebTaskLogGroup: + Type: AWS::Logs::LogGroup + DeletionPolicy: Delete + UpdateReplacePolicy: Delete + Properties: + RetentionInDays: 14 + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + WebTaskDefinition: + Type: AWS::ECS::TaskDefinition + Properties: + ContainerDefinitions: + - Command: + - web + Cpu: !If [IsProduction, 200, 128] + Environment: + - Name: ID_HOST + Value: !Ref IdHostname + - Name: NEW_RELIC_KEY + Value: !Ref NewRelicApiKeyPrxLite + - Name: NEW_RELIC_NAME + Value: !If [IsProduction, Insights Production, Insights Staging] + - Name: PORT + Value: !Ref kWebApplicationPort + - Name: RAILS_ENV + Value: !Ref EnvironmentTypeLowercase + Essential: true + Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrImageTag} + LinuxParameters: + InitProcessEnabled: true + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: !Ref WebTaskLogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: ecs + Memory: !If [IsProduction, 2000, 1000] + MemoryReservation: !If [IsProduction, 1000, 500] + Name: !Ref kWebContainerName + PortMappings: + - ContainerPort: !Ref kWebApplicationPort + HostPort: 0 + # Secrets: + # - Name: API_ADMIN_TOKENS + # ValueFrom: !Sub /prx/${EnvironmentTypeAbbreviation}/Spire/Dovetail-Feeder/api-admin-tokens + ExecutionRoleArn: !GetAtt ExecutionRole.Arn + NetworkMode: bridge + Tags: + - { Key: prx:meta:tagging-version, Value: "2021-04-07" } + - { Key: prx:cloudformation:stack-name, Value: !Ref AWS::StackName } + - { Key: prx:cloudformation:stack-id, Value: !Ref AWS::StackId } + - { Key: prx:cloudformation:root-stack-name, Value: !Ref RootStackName } + - { Key: prx:cloudformation:root-stack-id, Value: !Ref RootStackId } + - { Key: prx:ops:environment, Value: !Ref EnvironmentType } + - { Key: prx:dev:family, Value: Dovetail } + - { Key: prx:dev:application, Value: Insights } + TaskRoleArn: !GetAtt TaskRole.Arn + +Outputs: + WebTargetGroupFullName: + Value: !GetAtt WebTargetGroup.TargetGroupFullName diff --git a/spire/templates/dashboards.yml b/spire/templates/dashboards.yml index dcdb2f0c..359b8406 100644 --- a/spire/templates/dashboards.yml +++ b/spire/templates/dashboards.yml @@ -67,6 +67,8 @@ Parameters: IframelyTargetGroupFullName: { Type: String } + InsightsWebTargetGroupFullName: { Type: String } + MetricsTargetGroupFullName: { Type: String } NetworksPublicWebTargetGroupFullName: { Type: String } @@ -174,6 +176,7 @@ Resources: [ "AWS/ApiGateway", "Count", "ApiId", "${FeederAuthProxyApiId}", { "label": "Feeder Auth Proxy" } ], [ "AWS/ApplicationELB", "RequestCount", "TargetGroup", "${IdTargetGroupFullName}", "LoadBalancer", "${SharedAlbFullName}", { "label": "ID" } ], [ "AWS/ApplicationELB", "RequestCount", "TargetGroup", "${IframelyTargetGroupFullName}", "LoadBalancer", "${SharedAlbFullName}", { "label": "Iframely" } ], + [ "AWS/ApplicationELB", "RequestCount", "TargetGroup", "${InsightsWebTargetGroupFullName}", "LoadBalancer", "${SharedAlbFullName}", { "label": "Dovetail Insights" } ], [ "AWS/ApplicationELB", "RequestCount", "TargetGroup", "${MetricsTargetGroupFullName}", "LoadBalancer", "${SharedAlbFullName}", { "label": "Metrics" } ], [ "AWS/ApplicationELB", "RequestCount", "TargetGroup", "${NetworksPublicWebTargetGroupFullName}", "LoadBalancer", "${SharedAlbFullName}", { "label": "Networks" } ], [ "AWS/ApplicationELB", "RequestCount", "TargetGroup", "${PlayWebTargetGroupFullName}", "LoadBalancer", "${SharedAlbFullName}", { "label": "Play::Web" } ], diff --git a/spire/templates/root.yml b/spire/templates/root.yml index 601837fb..5fae618d 100644 --- a/spire/templates/root.yml +++ b/spire/templates/root.yml @@ -71,6 +71,7 @@ Mappings: Networks: { prefix: 28 } Remix: { prefix: 29 } Metrics: { prefix: 36 } + Insights: { prefix: 37 } TheCount: { prefix: 42 } TheCastle: { prefix: 50 } # Legacy Castle Styleguide: { prefix: 65 } @@ -825,6 +826,8 @@ Resources: FeederSharedAlbListenerRulePriorityPrefix: !FindInMap [SharedAlbListenerRulePriorityMap, Feeder, prefix] + InsightsSharedAlbListenerRulePriorityPrefix: !FindInMap [SharedAlbListenerRulePriorityMap, Insights, prefix] + NetworksSharedAlbListenerRulePriorityPrefix: !FindInMap [SharedAlbListenerRulePriorityMap, Networks, prefix] RemixSharedAlbListenerRulePriorityPrefix: !FindInMap [SharedAlbListenerRulePriorityMap, Remix, prefix] @@ -1084,6 +1087,8 @@ Resources: IframelyTargetGroupFullName: !GetAtt Apps100AStack.Outputs.IframelyTargetGroupFullName + InsightsWebTargetGroupFullName: !GetAtt Apps200AStack.Outputs.InsightsWebTargetGroupFullName + MetricsTargetGroupFullName: !GetAtt Apps300AStack.Outputs.MetricsTargetGroupFullName NetworksPublicWebTargetGroupFullName: !GetAtt Apps200AStack.Outputs.NetworksPublicWebTargetGroupFullName