diff --git a/packages/cdk/bin/cdk.ts b/packages/cdk/bin/cdk.ts index 539aa30ab..ca83e628c 100644 --- a/packages/cdk/bin/cdk.ts +++ b/packages/cdk/bin/cdk.ts @@ -10,6 +10,7 @@ new ServiceCatalogue(app, 'ServiceCatalogue-PROD', { stack: 'deploy', stage: 'PROD', env: { region: 'eu-west-1' }, + steampipeDomainName: 'steampipe.gutools.co.uk', }); new ServiceCatalogue(app, 'ServiceCatalogue-CODE', { @@ -18,4 +19,5 @@ new ServiceCatalogue(app, 'ServiceCatalogue-CODE', { env: { region: 'eu-west-1' }, schedule: Schedule.rate(Duration.days(30)), rdsDeletionProtection: false, + steampipeDomainName: 'steampipe.code.dev-gutools.co.uk', }); diff --git a/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap b/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap index c26c86046..68e491a15 100644 --- a/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap +++ b/packages/cdk/lib/__snapshots__/service-catalogue.test.ts.snap @@ -8,6 +8,7 @@ exports[`The ServiceCatalogue stack matches the snapshot 1`] = ` "GuVpcParameter", "GuSecurityGroup", "GuSecurityGroup", + "GuSecurityGroup", "GuStringParameter", "GuLoggingStreamNameParameter", "GuAnghammaradTopicParameter", @@ -16,6 +17,8 @@ exports[`The ServiceCatalogue stack matches the snapshot 1`] = ` "GuScheduledLambda", "GuLambdaFunction", "GuScheduledLambda", + "GuSecurityGroup", + "GuCname", ], "gu:cdk:version": "TEST", }, @@ -18031,6 +18034,112 @@ spec: "Type": "AWS::SecretsManager::Secret", "UpdateReplacePolicy": "Delete", }, + "SteampipeDNS": { + "Properties": { + "Name": "steampipe.code.dev-gutools.co.uk", + "RecordType": "CNAME", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "steampipenlb8A25667C", + "DNSName", + ], + }, + ], + "Stage": "TEST", + "TTL": 3600, + }, + "Type": "Guardian::DNS::RecordSet", + }, + "SteampipeSecurityGroupServicecatalogue45A0E3D6": { + "Properties": { + "GroupDescription": "ServiceCatalogue/SteampipeSecurityGroupServicecatalogue", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1", + }, + ], + "SecurityGroupIngress": [ + { + "CidrIp": "10.0.0.4/22", + "Description": "Allow connection to Steampipe from the office network.", + "FromPort": 9193, + "IpProtocol": "tcp", + "ToPort": 9193, + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "service-catalogue", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "SteampipeSecurityGroupServicecataloguefromServiceCataloguePostgresAccessSecurityGroupServicecatalogue56F7252C919316EC7C95": { + "Properties": { + "Description": "from ServiceCataloguePostgresAccessSecurityGroupServicecatalogue56F7252C:9193", + "FromPort": 9193, + "GroupId": { + "Fn::GetAtt": [ + "SteampipeSecurityGroupServicecatalogue45A0E3D6", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "PostgresAccessSecurityGroupServicecatalogue03C78F14", + "GroupId", + ], + }, + "ToPort": 9193, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "SteampipeSecurityGroupServicecataloguefromServiceCataloguePostgresSecurityGroupServicecatalogue716624F091938A31DC0C": { + "Properties": { + "Description": "from ServiceCataloguePostgresSecurityGroupServicecatalogue716624F0:9193", + "FromPort": 9193, + "GroupId": { + "Fn::GetAtt": [ + "SteampipeSecurityGroupServicecatalogue45A0E3D6", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "PostgresSecurityGroupServicecatalogueD2F089D5", + "GroupId", + ], + }, + "ToPort": 9193, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, "TopicBFC7AF6E": { "Properties": { "Tags": [ @@ -19412,6 +19521,591 @@ spec: }, "Type": "AWS::SNS::Subscription", }, + "steampipeService2E9D07AE": { + "DependsOn": [ + "steampipenlbsteampipenlblistener4500F2D0", + "steampipenlbsteampipenlblistenersteampipenlbtargetGroup8BBC7369", + "steampipeTaskDefinitionTaskRoleDefaultPolicyE6E26240", + "steampipeTaskDefinitionTaskRole8DC44379", + ], + "Properties": { + "Cluster": { + "Ref": "servicecatalogueCluster5FC34DC5", + }, + "DeploymentConfiguration": { + "Alarms": { + "AlarmNames": [], + "Enable": false, + "Rollback": false, + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 50, + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "steampipeContainer", + "ContainerPort": 9193, + "TargetGroupArn": { + "Ref": "steampipenlbsteampipenlblistenersteampipenlbtargetGroup8BBC7369", + }, + }, + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "steampipesgServicecatalogue18CB8C1F", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "PrivateSubnets", + }, + }, + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + "TaskDefinition": { + "Ref": "steampipeTaskDefinition767BA166", + }, + }, + "Type": "AWS::ECS::Service", + }, + "steampipeTaskDefinition767BA166": { + "Properties": { + "ContainerDefinitions": [ + { + "Command": [ + "service", + "start", + "--foreground", + ], + "DockerLabels": { + "App": "service-catalogue", + "Stack": "deploy", + "Stage": "TEST", + }, + "Essential": true, + "Image": "ghcr.io/guardian/service-catalogue/steampipe:1", + "LogConfiguration": { + "LogDriver": "awsfirelens", + "Options": { + "Name": "kinesis_streams", + "region": { + "Ref": "AWS::Region", + }, + "retry_limit": "2", + "stream": { + "Ref": "LoggingStreamName", + }, + }, + }, + "Name": "steampipeContainer", + "PortMappings": [ + { + "ContainerPort": 9193, + "Name": "steampipe", + "Protocol": "tcp", + }, + ], + "Secrets": [ + { + "Name": "STEAMPIPE_DATABASE_PASSWORD", + "ValueFrom": { + "Fn::Join": [ + "", + [ + { + "Ref": "steampipecredentials98149F9E", + }, + ":steampipe-db-password::", + ], + ], + }, + }, + { + "Name": "GITHUB_TOKEN", + "ValueFrom": { + "Fn::Join": [ + "", + [ + { + "Ref": "steampipecredentials98149F9E", + }, + ":github-token::", + ], + ], + }, + }, + ], + }, + { + "Environment": [ + { + "Name": "STACK", + "Value": "deploy", + }, + { + "Name": "STAGE", + "Value": "TEST", + }, + { + "Name": "APP", + "Value": "service-catalogue", + }, + { + "Name": "GU_REPO", + "Value": "guardian/service-catalogue", + }, + ], + "Essential": true, + "FirelensConfiguration": { + "Type": "fluentbit", + }, + "Image": "ghcr.io/guardian/devx-logs:2", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "steampipeTaskDefinitionsteampipeFirelensLogGroup61FE1785", + }, + "awslogs-region": { + "Ref": "AWS::Region", + }, + "awslogs-stream-prefix": "deploy/TEST/service-catalogue", + }, + }, + "Name": "steampipeFirelens", + }, + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "steampipeTaskDefinitionExecutionRole36454CCE", + "Arn", + ], + }, + "Family": "ServiceCataloguesteampipeTaskDefinition7074901D", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE", + ], + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "steampipeTaskDefinitionTaskRole8DC44379", + "Arn", + ], + }, + }, + "Type": "AWS::ECS::TaskDefinition", + }, + "steampipeTaskDefinitionExecutionRole36454CCE": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "steampipeTaskDefinitionExecutionRoleDefaultPolicy901D37C5": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret", + ], + "Effect": "Allow", + "Resource": { + "Ref": "steampipecredentials98149F9E", + }, + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "steampipeTaskDefinitionsteampipeFirelensLogGroup61FE1785", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "steampipeTaskDefinitionExecutionRoleDefaultPolicy901D37C5", + "Roles": [ + { + "Ref": "steampipeTaskDefinitionExecutionRole36454CCE", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "steampipeTaskDefinitionTaskRole8DC44379": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "steampipeTaskDefinitionTaskRoleDefaultPolicyE6E26240": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:Describe*", + "kinesis:Put*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":kinesis:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":stream/", + { + "Ref": "LoggingStreamName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "steampipeTaskDefinitionTaskRoleDefaultPolicyE6E26240", + "Roles": [ + { + "Ref": "steampipeTaskDefinitionTaskRole8DC44379", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "steampipeTaskDefinitionsteampipeFirelensLogGroup61FE1785": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 1, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "steampipecredentials98149F9E": { + "DeletionPolicy": "Delete", + "Properties": { + "GenerateSecretString": {}, + "Name": "/TEST/deploy/service-catalogue/steampipe-credentials", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + }, + "Type": "AWS::SecretsManager::Secret", + "UpdateReplacePolicy": "Delete", + }, + "steampipenlb8A25667C": { + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false", + }, + ], + "Scheme": "internal", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "SteampipeSecurityGroupServicecatalogue45A0E3D6", + "GroupId", + ], + }, + { + "Fn::GetAtt": [ + "steampipesgServicecatalogue18CB8C1F", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "PrivateSubnets", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + "Type": "network", + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + }, + "steampipenlbsteampipenlblistener4500F2D0": { + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "steampipenlbsteampipenlblistenersteampipenlbtargetGroup8BBC7369", + }, + "Type": "forward", + }, + ], + "LoadBalancerArn": { + "Ref": "steampipenlb8A25667C", + }, + "Port": 9193, + "Protocol": "TCP", + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "steampipenlbsteampipenlblistenersteampipenlbtargetGroup8BBC7369": { + "Properties": { + "HealthCheckIntervalSeconds": 5, + "HealthCheckTimeoutSeconds": 2, + "HealthyThresholdCount": 2, + "Port": 9193, + "Protocol": "TCP", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + "TargetType": "ip", + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "steampipesgServicecatalogue18CB8C1F": { + "Properties": { + "GroupDescription": "ServiceCatalogue/steampipe-sgServicecatalogue", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1", + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "service-catalogue", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/service-catalogue", + }, + { + "Key": "Stack", + "Value": "deploy", + }, + { + "Key": "Stage", + "Value": "TEST", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "steampipesgServicecataloguefromServiceCataloguesteampipesgServicecatalogue96A883E79193F33A348B": { + "Properties": { + "Description": "Allow this SG to talk to other applications also using this SG (in this case NLB to ECS)", + "FromPort": 9193, + "GroupId": { + "Fn::GetAtt": [ + "steampipesgServicecatalogue18CB8C1F", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "steampipesgServicecatalogue18CB8C1F", + "GroupId", + ], + }, + "ToPort": 9193, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, }, } `; diff --git a/packages/cdk/lib/cloudquery/images.ts b/packages/cdk/lib/cloudquery/images.ts index 7c97dbe53..4434cd574 100644 --- a/packages/cdk/lib/cloudquery/images.ts +++ b/packages/cdk/lib/cloudquery/images.ts @@ -17,4 +17,7 @@ export const Images = { postgres: ContainerImage.fromRegistry( 'public.ecr.aws/docker/library/postgres:16-alpine', ), + steampipe: ContainerImage.fromRegistry( + 'ghcr.io/guardian/service-catalogue/steampipe:1', + ), }; diff --git a/packages/cdk/lib/cloudquery/index.ts b/packages/cdk/lib/cloudquery/index.ts index b7ad9775a..14fed588d 100644 --- a/packages/cdk/lib/cloudquery/index.ts +++ b/packages/cdk/lib/cloudquery/index.ts @@ -520,7 +520,7 @@ export function addCloudqueryEcsCluster( config: ns1SourceConfig(), }; - new CloudqueryCluster(scope, `${app}Cluster`, { + return new CloudqueryCluster(scope, `${app}Cluster`, { app, vpc, db, diff --git a/packages/cdk/lib/service-catalogue.test.ts b/packages/cdk/lib/service-catalogue.test.ts index 73de91ca8..8462e4a56 100644 --- a/packages/cdk/lib/service-catalogue.test.ts +++ b/packages/cdk/lib/service-catalogue.test.ts @@ -8,6 +8,7 @@ describe('The ServiceCatalogue stack', () => { const stack = new ServiceCatalogue(app, 'ServiceCatalogue', { stack: 'deploy', stage: 'TEST', + steampipeDomainName: 'steampipe.code.dev-gutools.co.uk', }); const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); diff --git a/packages/cdk/lib/service-catalogue.ts b/packages/cdk/lib/service-catalogue.ts index e6c54f2f7..fc429c79a 100644 --- a/packages/cdk/lib/service-catalogue.ts +++ b/packages/cdk/lib/service-catalogue.ts @@ -41,6 +41,7 @@ import { addCloudqueryEcsCluster } from './cloudquery'; import { addDataAuditLambda } from './data-audit'; import { InteractiveMonitor } from './interactive-monitor'; import { Repocop } from './repocop'; +import { STEAMPIPE_DB_PORT, SteampipeService } from './steampipe/service'; interface ServiceCatalogueProps extends GuStackProps { //TODO add fields for every kind of job to make schedule explicit at a glance. @@ -54,6 +55,13 @@ interface ServiceCatalogueProps extends GuStackProps { * @default true */ rdsDeletionProtection?: boolean; + + /** + * Domain to access Steampipe DB from + */ + steampipeDomainName: + | 'steampipe.code.dev-gutools.co.uk' + | 'steampipe.gutools.co.uk'; } export class ServiceCatalogue extends GuStack { @@ -63,7 +71,7 @@ export class ServiceCatalogue extends GuStack { const { stage, stack } = this; const app = props.app ?? 'service-catalogue'; - const { rdsDeletionProtection = true } = props; + const { rdsDeletionProtection = true, steampipeDomainName } = props; const nonProdSchedule = props.schedule; @@ -83,6 +91,15 @@ export class ServiceCatalogue extends GuStack { privateSubnetIds: privateSubnets.map((subnet) => subnet.subnetId), }); + const steampipeSecurityGroup = new GuSecurityGroup( + this, + 'SteampipeSecurityGroup', + { + app, + vpc, + }, + ); + const port = 5432; const dbSecurityGroup = new GuSecurityGroup(this, 'PostgresSecurityGroup', { @@ -123,11 +140,29 @@ export class ServiceCatalogue extends GuStack { 'Allow connection to Postgres from the office network.', ); + steampipeSecurityGroup.addIngressRule( + Peer.ipv4(GuardianPrivateNetworks.Engineering), + Port.tcp(STEAMPIPE_DB_PORT), + 'Allow connection to Steampipe from the office network.', + ); + dbSecurityGroup.connections.allowFrom( applicationToPostgresSecurityGroup, Port.tcp(port), ); + // Allow anything that can access the RDS DB to access Steampipe, so Grafana + steampipeSecurityGroup.connections.allowFrom( + applicationToPostgresSecurityGroup, + Port.tcp(STEAMPIPE_DB_PORT), + ); + + // Allow RDS DB to access Steampipe + steampipeSecurityGroup.connections.allowFrom( + dbSecurityGroup, + Port.tcp(9193), + ); + // Used by downstream services that read ServiceCatalogue data, namely Grafana. new StringParameter(this, 'PostgresAccessSecurityGroupParam', { parameterName: `/${stage}/${stack}/${app}/postgres-access-security-group`, @@ -144,7 +179,7 @@ export class ServiceCatalogue extends GuStack { dataType: ParameterDataType.TEXT, }); - addCloudqueryEcsCluster(this, { + const cluster = addCloudqueryEcsCluster(this, { nonProdSchedule, db, vpc, @@ -213,5 +248,16 @@ export class ServiceCatalogue extends GuStack { db, dbAccess: applicationToPostgresSecurityGroup, }); + + // This should ideally not be in the cloudquery folder, but unfortunately this is where we define our ECS cluster + // so here it stays for now! + new SteampipeService(this, 'steampipe', { + app, + cluster, + policies: [], + secrets: {}, + accessSecurityGroup: steampipeSecurityGroup, + domainName: steampipeDomainName, + }); } } diff --git a/packages/cdk/lib/steampipe/service.ts b/packages/cdk/lib/steampipe/service.ts new file mode 100644 index 000000000..50ab39a6d --- /dev/null +++ b/packages/cdk/lib/steampipe/service.ts @@ -0,0 +1,199 @@ +import { + type AppIdentity, + GuLoggingStreamNameParameter, + type GuStack, +} from '@guardian/cdk/lib/constructs/core'; +import { GuCname } from '@guardian/cdk/lib/constructs/dns'; +import { GuSecurityGroup } from '@guardian/cdk/lib/constructs/ec2'; +import { Duration } from 'aws-cdk-lib'; +import { Port } from 'aws-cdk-lib/aws-ec2'; +import type { ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; +import { + FargateService, + FargateTaskDefinition, + FireLensLogDriver, + FirelensLogRouterType, + LogDrivers, + Secret, +} from 'aws-cdk-lib/aws-ecs'; +import type { FargateServiceProps } from 'aws-cdk-lib/aws-ecs'; +import { + NetworkLoadBalancer, + Protocol, +} from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Secret as SecretsManager } from 'aws-cdk-lib/aws-secretsmanager'; +import { Images } from '../cloudquery/images'; + +export const STEAMPIPE_DB_PORT = 9193; + +export interface SteampipeServiceProps + extends AppIdentity, + Omit { + /** + * Any secrets to pass to the CloudQuery container. + * + * @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ecs.ContainerDefinitionOptions.html#secrets + * @see https://repost.aws/knowledge-center/ecs-data-security-container-task + */ + secrets?: Record; + + /** + * IAM policies to attach to the task. + */ + policies: PolicyStatement[]; + + /** + * Security group allowing access to Network Load Balancer + */ + accessSecurityGroup: ISecurityGroup; + + /** + * Domain to access Steampipe DB from + */ + domainName: 'steampipe.code.dev-gutools.co.uk' | 'steampipe.gutools.co.uk'; +} + +export class SteampipeService extends FargateService { + constructor(scope: GuStack, id: string, props: SteampipeServiceProps) { + const { policies, cluster, app, accessSecurityGroup, domainName } = props; + const { region, stack, stage } = scope; + const thisRepo = 'guardian/service-catalogue'; // TODO get this from GuStack + + const loggingStreamName = + GuLoggingStreamNameParameter.getInstance(scope).valueAsString; + const loggingStreamArn = scope.formatArn({ + service: 'kinesis', + resource: 'stream', + resourceName: loggingStreamName, + }); + + const logShippingPolicy = new PolicyStatement({ + actions: ['kinesis:Describe*', 'kinesis:Put*'], + effect: Effect.ALLOW, + resources: [loggingStreamArn], + }); + + const taskPolicies = [logShippingPolicy, ...policies]; + + const steampipeCredentials = new SecretsManager( + scope, + 'steampipe-credentials', + { + secretName: `/${stage}/${stack}/${app}/steampipe-credentials`, + }, + ); + + const steampipeSecurityGroup = new GuSecurityGroup(scope, `steampipe-sg`, { + app: app, + vpc: cluster.vpc, + }); + + // Anything with this SG can talk to anything else with this SG + // In this case the NLB can talk to the ECS Service + steampipeSecurityGroup.addIngressRule( + steampipeSecurityGroup, + Port.tcp(STEAMPIPE_DB_PORT), + 'Allow this SG to talk to other applications also using this SG (in this case NLB to ECS)', + ); + + const task = new FargateTaskDefinition(scope, `${id}TaskDefinition`, { + memoryLimitMiB: 512, + cpu: 256, + }); + + const fireLensLogDriver = new FireLensLogDriver({ + options: { + Name: `kinesis_streams`, + region, + stream: loggingStreamName, + retry_limit: '2', + }, + }); + + task.addContainer(`${id}Container`, { + image: Images.steampipe, + dockerLabels: { + Stack: stack, + Stage: stage, + App: app, + }, + secrets: { + STEAMPIPE_DATABASE_PASSWORD: Secret.fromSecretsManager( + steampipeCredentials, + 'steampipe-db-password', + ), + // Steampipe Github plugin currently only supports PAT tokens + GITHUB_TOKEN: Secret.fromSecretsManager( + steampipeCredentials, + 'github-token', + ), + }, + command: ['service', 'start', '--foreground'], + logging: fireLensLogDriver, + portMappings: [ + { + containerPort: STEAMPIPE_DB_PORT, + name: 'steampipe', + }, + ], + }); + + task.addFirelensLogRouter(`${id}Firelens`, { + image: Images.devxLogs, + logging: LogDrivers.awsLogs({ + streamPrefix: [stack, stage, app].join('/'), + logRetention: RetentionDays.ONE_DAY, + }), + environment: { + STACK: stack, + STAGE: stage, + APP: app, + GU_REPO: thisRepo, + }, + firelensConfig: { + type: FirelensLogRouterType.FLUENTBIT, + }, + }); + + taskPolicies.forEach((policy) => task.addToTaskRolePolicy(policy)); + + const nlb = new NetworkLoadBalancer(scope, `steampipe-nlb`, { + vpc: cluster.vpc, + securityGroups: [accessSecurityGroup, steampipeSecurityGroup], + }); + + const nlbListener = nlb.addListener(`steampipe-nlb-listener`, { + port: STEAMPIPE_DB_PORT, + protocol: Protocol.TCP, + }); + + new GuCname(scope, 'SteampipeDNS', { + app: app, + ttl: Duration.hours(1), + domainName, + resourceRecord: nlb.loadBalancerDnsName, + }); + + super(scope, id, { + cluster, + vpcSubnets: { subnets: cluster.vpc.privateSubnets }, + taskDefinition: task, + securityGroups: [steampipeSecurityGroup], + assignPublicIp: false, + desiredCount: 1, + }); + + nlbListener.addTargets(`steampipe-nlb-target`, { + port: STEAMPIPE_DB_PORT, + protocol: Protocol.TCP, + targets: [this], + healthCheck: { + healthyThresholdCount: 2, + interval: Duration.seconds(5), + timeout: Duration.seconds(2), + }, + }); + } +}