Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ffefa93
feat: correct sticker-award to handle errors according to API spec
scottgerring Jun 17, 2025
a0b9565
feat: add AWS implementation and IaC
jeastham1993 Jun 17, 2025
f92f770
Update user-management/infra/aws/lib/network.ts
jeastham1993 Jun 17, 2025
03e01a1
feat: update connection string
jeastham1993 Jun 17, 2025
e02a6e2
Merge branch 'feat/infra-as-code' of github.com:DataDog/stickerlandia…
jeastham1993 Jun 17, 2025
5dc72ca
Merge pull request #34 from DataDog/feat/return-structured-errors
scottgerring Jun 18, 2025
9fd53b1
chore: add labels to docker image
jeastham1993 Jun 18, 2025
c784466
chore: remove ASPNET Lambda hosting
jeastham1993 Jun 18, 2025
ef2c47a
chore: add todos for future shared infra
jeastham1993 Jun 18, 2025
46378bf
feat: enable static analysis (#44)
jeastham1993 Jun 18, 2025
4e9c392
Merge pull request #45 from DataDog/feat/add-datadog-to-docker
jeastham1993 Jun 18, 2025
630ded4
feat(user-management): update docs (#37)
jeastham1993 Jun 18, 2025
a0d6c50
feat: add AWS implementation and IaC
jeastham1993 Jun 17, 2025
70df2cf
feat: update connection string
jeastham1993 Jun 17, 2025
728f589
Update user-management/infra/aws/lib/network.ts
jeastham1993 Jun 17, 2025
3f8db04
chore: add labels to docker image
jeastham1993 Jun 18, 2025
4dc35d0
chore: remove ASPNET Lambda hosting
jeastham1993 Jun 18, 2025
c94f5d1
chore: add todos for future shared infra
jeastham1993 Jun 18, 2025
da4a502
Merge branch 'feat/infra-as-code' of github.com:DataDog/stickerlandia…
jeastham1993 Jun 18, 2025
22dfdd6
chore: fix static analysis for AWS impls
jeastham1993 Jun 18, 2025
afea477
feat: add arch diagram and README for AWS infra
jeastham1993 Jun 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion user-management/Stickerlandia.UserManagement.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{FC9DDF76-8
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Agnostic", "src\Stickerlandia.UserManagement.Agnostic\Stickerlandia.UserManagement.Agnostic.csproj", "{295E8315-2CD2-4CD8-B7F4-21EF587B74A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Lambda", "src\Stickerlandia.UserManagement.Lambda\src\Stickerlandia.UserManagement.Lambda\Stickerlandia.UserManagement.Lambda.csproj", "{5C770F92-B6E7-4270-A041-A32F385DA6C4}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stickerlandia.UserManagement.Lambda", "src\Stickerlandia.UserManagement.Lambda\Stickerlandia.UserManagement.Lambda.csproj", "{5C770F92-B6E7-4270-A041-A32F385DA6C4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Driving", "Driving", "{924749FC-98D5-40A6-8F4B-BA9BFDEDDEC3}"
EndProject
Expand Down
8 changes: 8 additions & 0 deletions user-management/infra/aws/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions user-management/infra/aws/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
96 changes: 96 additions & 0 deletions user-management/infra/aws/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"app": "npx ts-node --prefer-ts-exts bin/user-service.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
"@aws-cdk/core:enableAdditionalMetadataCollection": true,
"@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false,
"@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
"@aws-cdk/aws-events:requireEventBusPolicySid": true,
"@aws-cdk/core:aspectPrioritiesMutating": true,
"@aws-cdk/aws-dynamodb:retainTableReplica": true,
"@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true,
"@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true,
"@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true,
"@aws-cdk/aws-s3:publicAccessBlockedByDefault": true
}
}
8 changes: 8 additions & 0 deletions user-management/infra/aws/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
}
};
203 changes: 203 additions & 0 deletions user-management/infra/aws/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { Vpc, SecurityGroup, Port } from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs";
import { SharedProps } from "./constructs/shared-props";
import {
Cluster,
ContainerImage,
CpuArchitecture,
FargateService,
FargateTaskDefinition,
FirelensLogRouterType,
LogDriver,
LogDrivers,
OperatingSystemFamily,
Protocol,
} from "aws-cdk-lib/aws-ecs";
import { Repository } from "aws-cdk-lib/aws-ecr";
import { Topic } from "aws-cdk-lib/aws-sns";
import { Queue } from "aws-cdk-lib/aws-sqs";
import {
ApplicationLoadBalancer,
ApplicationTargetGroup,
TargetType,
ApplicationProtocol,
HealthCheck,
Protocol as HealthCheckProtocol,
} from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Duration, CfnOutput } from "aws-cdk-lib";
import { ApplicationLoadBalancedFargateService } from "aws-cdk-lib/aws-ecs-patterns";

export class ApiProps {
sharedProps: SharedProps;
vpc: Vpc;
cluster: Cluster;
}

export class Api extends Construct {
stickerClaimedQueue: Queue;
stickerClaimedDLQ: Queue;
userRegisteredTopic: Topic;
constructor(scope: Construct, id: string, props: ApiProps) {
super(scope, id);

const ecrRepository = Repository.fromRepositoryName(
this,
"UserServiceRepo",
"stickerlandia-user-management"
);

this.userRegisteredTopic = new Topic(this, "UserRegisteredTopic", {
topicName: `${props.sharedProps.serviceName}-${props.sharedProps.environment}-user-registered`,
});
this.stickerClaimedDLQ = new Queue(this, "StickerClaimedDLQ", {
queueName: `${props.sharedProps.serviceName}-${props.sharedProps.environment}-sticker-claimed-dlq`,
});

//TODO: Add EventBridge rule mapping to subscribe to sticker claimed events published to the shared EventBus.
this.stickerClaimedQueue = new Queue(this, "StickerClaimedQueue", {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to ask how we're doing queue/topic ownership given the stacks are going to be broken down by service!

So the queue exists here - and when we add the topic on the award-service side, we'll add a subscription here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the point of integration between services should be Amazon EventBridge. So we'd have a shared event bus deployed as part of some shared infra, with the name/ARN stored in SSM. In the respective CDK stacks we can then pull down a reference to that event bus to define rules. Left it out for the moment, can add when we come to add the proper AWS deployments

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we likely don't need SNS, eh?

queueName: `${props.sharedProps.serviceName}-${props.sharedProps.environment}-sticker-claimed`,
deadLetterQueue: {
queue: this.stickerClaimedDLQ,
maxReceiveCount: 5, // Messages will be sent to DLQ after 5 failed attempts
},
});

const applicationTaskDef = new FargateTaskDefinition(
this,
"UserServiceTaskDef",
{
runtimePlatform: {
operatingSystemFamily: OperatingSystemFamily.LINUX,
cpuArchitecture: CpuArchitecture.ARM64,
},
cpu: 256,
memoryLimitMiB: 512,
}
);

const containerPort = 8080;

const container = applicationTaskDef.addContainer("UserServiceContainer", {
image: ContainerImage.fromEcrRepository(
ecrRepository,
props.sharedProps.version
),
portMappings: [
{
containerPort: containerPort,
protocol: Protocol.TCP,
},
],
environment: {
ConnectionStrings__messaging: "",
ConnectionStrings__database: props.sharedProps.connectionString,
Aws__UserRegisteredTopicArn: this.userRegisteredTopic.topicArn,
Aws__StickerClaimedQueueUrl: this.stickerClaimedQueue.queueUrl,
Aws__StickerClaimedDLQUrl: this.stickerClaimedDLQ.queueUrl,
DRIVING: "ASPNET",
DRIVEN: "AWS",
DISABLE_SSL: "true",
},
logging: LogDrivers.firelens({
options: {
Name: "datadog",
Host: "http-intake.logs.datadoghq.eu",
TLS: "on",
dd_service: props.sharedProps.serviceName,
dd_source: "aspnet",
dd_message_key: "log",
dd_tags: `project:${props.sharedProps.serviceName}`,
provider: "ecs",
apikey: props.sharedProps.datadog.apiKey,
},
}),
});
container.addDockerLabel(
Comment thread
jeastham1993 marked this conversation as resolved.
"com.datadoghq.tags.env",
props.sharedProps.environment
);
container.addDockerLabel(
"com.datadoghq.tags.service",
props.sharedProps.serviceName
);
container.addDockerLabel(
"com.datadoghq.tags.version",
props.sharedProps.version
);

container.addPortMappings({
containerPort: containerPort,
protocol: Protocol.TCP,
});

applicationTaskDef.addContainer("datadog-agent", {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's going to be interesting here is, once we deploy more than one service to ECS-fargate we're likely going to want a single Daemon style instance of the agent in the cluster, I suspect

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, the only deployment method for Fargate is per task. The team are working on that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why you couldn't just deploy the agent ordinarily and point at it, the same as in any other containerised environment? Does some part of the auto-instrumentation or magical environment discovery not work?

image: ContainerImage.fromRegistry("public.ecr.aws/datadog/agent:latest"),
portMappings: [
{
containerPort: 8125,
protocol: Protocol.UDP, // Dogstatsd port
},
{
containerPort: 8126,
protocol: Protocol.TCP, // APM port
},
],
containerName: "datadog-agent",
environment: {
DD_API_KEY: props.sharedProps.datadog.apiKey,
DD_SITE: props.sharedProps.datadog.site,
DD_APM_ENABLED: "true",
DD_LOGS_ENABLED: "true",
ECS_FARGATE: "true",
DD_APM_NON_LOCAL_TRAFFIC: "true",
DD_DOGSTATSD_NON_LOCAL_TRAFFIC: "true",
DD_APM_IGNORE_RESOURCES: `(GET) /api/users/v1/health`,
},
});
applicationTaskDef.addFirelensLogRouter("firelens-router", {
essential: true,
image: ContainerImage.fromRegistry("amazon/aws-for-fluent-bit:stable"),
firelensConfig: {
type: FirelensLogRouterType.FLUENTBIT,
options: {
enableECSLogMetadata: true,
},
},
});

this.userRegisteredTopic.grantPublish(applicationTaskDef.taskRole);
this.stickerClaimedQueue.grantSendMessages(applicationTaskDef.taskRole);
this.stickerClaimedDLQ.grantSendMessages(applicationTaskDef.taskRole);
this.stickerClaimedQueue.grantConsumeMessages(applicationTaskDef.taskRole);
this.stickerClaimedDLQ.grantConsumeMessages(applicationTaskDef.taskRole);

// TODO: move this to a shared infra project to allow one ALB across multiple services
const service = new ApplicationLoadBalancedFargateService(
Comment thread
jeastham1993 marked this conversation as resolved.
this,
"UserServiceFargateService",
{
cluster: props.cluster,
taskDefinition: applicationTaskDef,
assignPublicIp: false,
publicLoadBalancer: true,
desiredCount: 1,
loadBalancerName: `${props.sharedProps.serviceName}-${props.sharedProps.environment}-user-service-lb`,
serviceName: `${props.sharedProps.serviceName}-${props.sharedProps.environment}-user-service`,
listenerPort: 80,
protocol: ApplicationProtocol.HTTP,
healthCheckGracePeriod: Duration.minutes(2),
circuitBreaker: {
rollback: true,
enable: true,
},
}
);
service.targetGroup.configureHealthCheck({
path: "/api/users/v1/health",
interval: Duration.seconds(60),
timeout: Duration.seconds(5),
enabled: true,
healthyHttpCodes: "200-499",
});
}
}
Loading
Loading