From c2356ff1fcab060614827a9c854a2909e69f1050 Mon Sep 17 00:00:00 2001 From: Cole Michaels Date: Sun, 19 Apr 2026 22:15:16 +0900 Subject: [PATCH 1/2] added GSI to dynamodb, fixed polcies to actually use this index, fixed really annoying issue with sockets and presence. --- ChatInfra/modules/dynamodb/main.tf | 6 ++++++ ChatInfra/modules/dynamodb/outputs.tf | 1 + ChatInfra/modules/ecs/main.tf | 1 + 3 files changed, 8 insertions(+) diff --git a/ChatInfra/modules/dynamodb/main.tf b/ChatInfra/modules/dynamodb/main.tf index 1df1258..5867f2e 100644 --- a/ChatInfra/modules/dynamodb/main.tf +++ b/ChatInfra/modules/dynamodb/main.tf @@ -48,6 +48,12 @@ resource "aws_dynamodb_table" "conversation_members" { type = "S" } + global_secondary_index { + name = "userId-index" + hash_key = "userId" + projection_type = "KEYS_ONLY" + } + server_side_encryption { enabled = true } diff --git a/ChatInfra/modules/dynamodb/outputs.tf b/ChatInfra/modules/dynamodb/outputs.tf index 7a76a38..e266740 100644 --- a/ChatInfra/modules/dynamodb/outputs.tf +++ b/ChatInfra/modules/dynamodb/outputs.tf @@ -3,6 +3,7 @@ output "table_arns" { aws_dynamodb_table.server_registry.arn, aws_dynamodb_table.user_connections.arn, aws_dynamodb_table.conversation_members.arn, + "${aws_dynamodb_table.conversation_members.arn}/index/*", aws_dynamodb_table.messages.arn, aws_dynamodb_table.pending_messages.arn, ] diff --git a/ChatInfra/modules/ecs/main.tf b/ChatInfra/modules/ecs/main.tf index c72e6a5..e69ad7a 100644 --- a/ChatInfra/modules/ecs/main.tf +++ b/ChatInfra/modules/ecs/main.tf @@ -186,6 +186,7 @@ resource "aws_iam_role_policy" "chatserver_dynamodb" { Action = [ "dynamodb:DescribeTable", "dynamodb:GetItem", + "dynamodb:BatchGetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem", From d3ec168a104c556ac65eb73f28f10804c4d0f3d8 Mon Sep 17 00:00:00 2001 From: Cole Michaels Date: Sun, 19 Apr 2026 22:15:26 +0900 Subject: [PATCH 2/2] added GSI to dynamodb, fixed polcies to actually use this index, fixed really annoying issue with sockets and presence. --- .idea/workspace.xml | 51 +++++++------------ ChatServer/README.md | 23 ++++----- .../data/AbstractDynamoCompositeRegistry.kt | 23 ++++++--- .../data/registry/ConversationRegistry.kt | 43 ++++++++++++++++ .../main/kotlin/data/registry/UserRegistry.kt | 17 +++++++ ChatServer/src/main/kotlin/di/AppModule.kt | 2 +- ChatServer/src/main/kotlin/plugins/Sockets.kt | 30 ++++++----- .../main/kotlin/services/sqs/SqsConsumer.kt | 12 +++-- .../src/test/kotlin/plugins/SocketsTest.kt | 5 ++ .../kotlin/services/sqs/SqsConsumerTest.kt | 24 +++++---- diagram.svg | 2 +- 11 files changed, 149 insertions(+), 83 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index abe900a..d2358c7 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,28 +2,13 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + - { - "keyToString": { - "Gradle./home/cole/Projects/ChatSystem/ChatServer [ktlintFormat].executor": "Run", - "Gradle./home/cole/Projects/ChatSystem/ChatServer [test clean].executor": "Run", - "ModuleVcsDetector.initialDetectionPerformed": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", - "RunOnceActivity.git.unshallow": "true", - "git-widget-placeholder": "feature/soc2-compliant", - "kotlin-language-version-configured": "true", - "last_opened_file_path": "/home/cole/Projects/ChatSystem" + +}]]> diff --git a/ChatServer/README.md b/ChatServer/README.md index 492b832..ef1ee27 100644 --- a/ChatServer/README.md +++ b/ChatServer/README.md @@ -1,20 +1,17 @@ # ChatServer ## Overview -This ChatServer handles WebSocket connections and message delivery for the chat system. +ChatServer handles WebSocket connections and message delivery for the chat system. -A chat server will register itself and its SQS queue URL with DynamoDB. +A chat server registers itself and its SQS queue URL with DynamoDB on startup. -When a user joins a given chat server the user is associated with the server in DynamoDB. -Also, a mapping is kept of each user and their WebSocket session at runtime. +When a user connects, they are associated with this server in DynamoDB and a runtime mapping of userId to WebSocket session is maintained. A presence snapshot is sent to the connecting user showing which of their groupmates (users who share at least one conversation with them) are currently online. Presence is scoped using a GSI on `ConversationMembers` to avoid a full table scan. -When a user sends a message, the target user's server is found from DynamoDB and the associated SQS queue URL is used to route the message to that server for delivery. +When a user sends a message, the target user's server is found from DynamoDB and the associated SQS queue URL routes the message to that server for delivery. If the target user is offline the message is saved to a pending messages table in DynamoDB and delivered when the user reconnects. -Both 1-to-1 and group chat use the same conversation model. -A conversation has members stored in DynamoDB and a message sent to a conversation is fanned out to all members. Offline behavior is the same -- undelivered messages are saved as pending. +Both 1-to-1 and group chat use the same conversation model. A conversation has members stored in DynamoDB and a message sent to a conversation is fanned out to all members. Offline behavior is the same -- undelivered messages are saved as pending. Message history is persisted to DynamoDB on every send and can be fetched via the REST API. @@ -36,10 +33,12 @@ Message history is persisted to DynamoDB on every send and can be fetched via th | `serverId` | | String | Server the user is currently connected to | ### ConversationMembers -| Attribute | Key | Type | Description | -|------------------|-----|--------|--------------------------------| -| `conversationId` | PK | String | Unique conversation identifier | -| `userId` | SK | String | Member of the conversation | +| Attribute | Key | Type | Description | +|------------------|-------------|--------|--------------------------------| +| `conversationId` | PK | String | Unique conversation identifier | +| `userId` | SK / GSI PK | String | Member of the conversation | + +**GSI: `userId-index`** -- partition key `userId`, projection `KEYS_ONLY`. Used to look up all conversations a user belongs to, which enables scoped presence snapshots and targeted presence broadcast delivery. ### Messages | Attribute | Key | Type | Description | diff --git a/ChatServer/src/main/kotlin/data/AbstractDynamoCompositeRegistry.kt b/ChatServer/src/main/kotlin/data/AbstractDynamoCompositeRegistry.kt index c4270b9..fbf4027 100644 --- a/ChatServer/src/main/kotlin/data/AbstractDynamoCompositeRegistry.kt +++ b/ChatServer/src/main/kotlin/data/AbstractDynamoCompositeRegistry.kt @@ -5,6 +5,7 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbClient import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition import software.amazon.awssdk.services.dynamodb.model.BillingMode import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement import software.amazon.awssdk.services.dynamodb.model.KeyType import software.amazon.awssdk.services.dynamodb.model.ResourceInUseException @@ -16,6 +17,10 @@ abstract class AbstractDynamoCompositeRegistry(protected val dynamoClient: Dynam protected abstract val partitionKey: String protected abstract val sortKey: String + protected open fun gsiDefinitions(): List = emptyList() + + protected open fun gsiAttributeDefinitions(): List = emptyList() + private val logger = LoggerFactory.getLogger(AbstractDynamoCompositeRegistry::class.java) protected fun ensureTableExists() { @@ -24,20 +29,24 @@ abstract class AbstractDynamoCompositeRegistry(protected val dynamoClient: Dynam } catch (e: ResourceNotFoundException) { logger.info("$tableName table not found, creating...") try { - dynamoClient.createTable( + val attrDefs = + buildList { + add(AttributeDefinition.builder().attributeName(partitionKey).attributeType(ScalarAttributeType.S).build()) + add(AttributeDefinition.builder().attributeName(sortKey).attributeType(ScalarAttributeType.S).build()) + addAll(gsiAttributeDefinitions()) + } + val tableBuilder = CreateTableRequest.builder() .tableName(tableName) .billingMode(BillingMode.PAY_PER_REQUEST) - .attributeDefinitions( - AttributeDefinition.builder().attributeName(partitionKey).attributeType(ScalarAttributeType.S).build(), - AttributeDefinition.builder().attributeName(sortKey).attributeType(ScalarAttributeType.S).build(), - ) + .attributeDefinitions(attrDefs) .keySchema( KeySchemaElement.builder().attributeName(partitionKey).keyType(KeyType.HASH).build(), KeySchemaElement.builder().attributeName(sortKey).keyType(KeyType.RANGE).build(), ) - .build(), - ) + val gsis = gsiDefinitions() + if (gsis.isNotEmpty()) tableBuilder.globalSecondaryIndexes(gsis) + dynamoClient.createTable(tableBuilder.build()) dynamoClient.waiter().waitUntilTableExists { it.tableName(tableName) } logger.info("$tableName table created") } catch (e: ResourceInUseException) { diff --git a/ChatServer/src/main/kotlin/data/registry/ConversationRegistry.kt b/ChatServer/src/main/kotlin/data/registry/ConversationRegistry.kt index ad66a2d..1f849f3 100644 --- a/ChatServer/src/main/kotlin/data/registry/ConversationRegistry.kt +++ b/ChatServer/src/main/kotlin/data/registry/ConversationRegistry.kt @@ -2,8 +2,14 @@ package org.chatserver.data.registry import org.chatserver.data.AbstractDynamoCompositeRegistry import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition import software.amazon.awssdk.services.dynamodb.model.AttributeValue import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement +import software.amazon.awssdk.services.dynamodb.model.KeyType +import software.amazon.awssdk.services.dynamodb.model.Projection +import software.amazon.awssdk.services.dynamodb.model.ProjectionType import software.amazon.awssdk.services.dynamodb.model.PutItemRequest import software.amazon.awssdk.services.dynamodb.model.QueryRequest @@ -12,6 +18,22 @@ class ConversationRegistry(dynamoClient: DynamoDbClient) : AbstractDynamoComposi override val partitionKey = "conversationId" override val sortKey = "userId" + companion object { + private const val USER_ID_INDEX = "userId-index" + } + + // userId is already an attribute (table sort key), so no extra attribute definition needed. + override fun gsiDefinitions(): List = + listOf( + GlobalSecondaryIndex.builder() + .indexName(USER_ID_INDEX) + .keySchema(KeySchemaElement.builder().attributeName(sortKey).keyType(KeyType.HASH).build()) + .projection(Projection.builder().projectionType(ProjectionType.KEYS_ONLY).build()) + .build(), + ) + + override fun gsiAttributeDefinitions(): List = emptyList() + fun init() { ensureTableExists() } @@ -61,4 +83,25 @@ class ConversationRegistry(dynamoClient: DynamoDbClient) : AbstractDynamoComposi ) return response.items().mapNotNull { it[sortKey]?.s() } } + + fun getConversationsForUser(userId: String): List { + val response = + dynamoClient.query( + QueryRequest.builder() + .tableName(tableName) + .indexName(USER_ID_INDEX) + .keyConditionExpression("$sortKey = :uid") + .expressionAttributeValues(mapOf(":uid" to AttributeValue.fromS(userId))) + .build(), + ) + return response.items().mapNotNull { it[partitionKey]?.s() } + } + + fun getGroupmates(userId: String): Set { + val conversationIds = getConversationsForUser(userId) + return conversationIds + .flatMap { getMembers(it) } + .toSet() + .minus(userId) + } } diff --git a/ChatServer/src/main/kotlin/data/registry/UserRegistry.kt b/ChatServer/src/main/kotlin/data/registry/UserRegistry.kt index 7982706..71e7d2f 100644 --- a/ChatServer/src/main/kotlin/data/registry/UserRegistry.kt +++ b/ChatServer/src/main/kotlin/data/registry/UserRegistry.kt @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory import software.amazon.awssdk.services.dynamodb.DynamoDbClient import software.amazon.awssdk.services.dynamodb.model.AttributeValue import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest +import software.amazon.awssdk.services.dynamodb.model.KeysAndAttributes import software.amazon.awssdk.services.dynamodb.model.PutItemRequest import software.amazon.awssdk.services.dynamodb.model.ScanRequest @@ -56,6 +57,22 @@ class UserRegistry( return response.items().mapNotNull { it["userId"]?.s() } } + fun getConnectedUsersFrom(userIds: Set): Set { + if (userIds.isEmpty()) return emptySet() + val connected = mutableSetOf() + userIds.chunked(100).forEach { chunk -> + val keys = chunk.map { mapOf("userId" to AttributeValue.fromS(it)) } + val response = + dynamoClient.batchGetItem { + it.requestItems(mapOf(tableName to KeysAndAttributes.builder().keys(keys).build())) + } + response.responses()[tableName]?.forEach { item -> + item["userId"]?.s()?.let { id -> connected.add(id) } + } + } + return connected + } + fun deregister(userId: String) { dynamoClient.deleteItem( DeleteItemRequest.builder() diff --git a/ChatServer/src/main/kotlin/di/AppModule.kt b/ChatServer/src/main/kotlin/di/AppModule.kt index eeb2f0f..e9f4fe0 100644 --- a/ChatServer/src/main/kotlin/di/AppModule.kt +++ b/ChatServer/src/main/kotlin/di/AppModule.kt @@ -48,7 +48,7 @@ val appModule = single { PendingMessageRepository(get()) } single { ConversationRegistry(get()) } single { MessageRepository(get()) } - single { SqsConsumer(get(), get(), get()) } + single { SqsConsumer(get(), get(), get(), get()) } single { MessageRouter(get(), get(), get(), get(), get(), get()) } single { SessionStore() } single { diff --git a/ChatServer/src/main/kotlin/plugins/Sockets.kt b/ChatServer/src/main/kotlin/plugins/Sockets.kt index 9e00df3..b46efad 100644 --- a/ChatServer/src/main/kotlin/plugins/Sockets.kt +++ b/ChatServer/src/main/kotlin/plugins/Sockets.kt @@ -13,6 +13,7 @@ import io.ktor.websocket.close import io.ktor.websocket.readText import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.chatserver.data.registry.ConversationRegistry import org.chatserver.data.registry.ServerRegistry import org.chatserver.data.registry.UserRegistry import org.chatserver.data.repository.PendingMessageRepository @@ -31,6 +32,7 @@ fun Application.configureSockets() { install(WebSockets) val userRegistry by inject() + val conversationRegistry by inject() val sessionStore by inject() val messageRouter by inject() val pendingMessageRepository by inject() @@ -50,23 +52,23 @@ fun Application.configureSockets() { userRegistry.register(userId) sessionStore.add(userId, this) - // Send a cross-instance presence snapshot by reading UserConnections, - // which reflects all connected users regardless of which server they're on. - val alreadyOnline = userRegistry.getAllConnectedUserIds() - for (onlineUserId in alreadyOnline) { - send(Frame.Text(Json.encodeToString(PresenceEvent(userId = onlineUserId, online = true)))) - } + try { + val groupmates = conversationRegistry.getGroupmates(userId) + val onlineGroupmates = userRegistry.getConnectedUsersFrom(groupmates) + logger.info("Presence snapshot for $userId: ${groupmates.size} groupmate(s), ${onlineGroupmates.size} online") + send(Frame.Text(Json.encodeToString(PresenceEvent(userId = userId, online = true)))) + for (onlineUserId in onlineGroupmates) { + send(Frame.Text(Json.encodeToString(PresenceEvent(userId = onlineUserId, online = true)))) + } - // Notify all other servers that this user came online. - broadcastPresence(userId, online = true, serverRegistry, sqsClient) + broadcastPresence(userId, online = true, serverRegistry, sqsClient) - val pending = pendingMessageRepository.fetchAndClear(userId) - if (pending.isNotEmpty()) { - logger.info("Delivering ${pending.size} pending message(s) to $userId") - pending.forEach { send(Frame.Text(Json.encodeToString(it))) } - } + val pending = pendingMessageRepository.fetchAndClear(userId) + if (pending.isNotEmpty()) { + logger.info("Delivering ${pending.size} pending message(s) to $userId") + pending.forEach { send(Frame.Text(Json.encodeToString(it))) } + } - try { for (frame in incoming) { if (frame is Frame.Text) { try { diff --git a/ChatServer/src/main/kotlin/services/sqs/SqsConsumer.kt b/ChatServer/src/main/kotlin/services/sqs/SqsConsumer.kt index cf71591..c1a641c 100644 --- a/ChatServer/src/main/kotlin/services/sqs/SqsConsumer.kt +++ b/ChatServer/src/main/kotlin/services/sqs/SqsConsumer.kt @@ -3,6 +3,7 @@ package org.chatserver.services.sqs import io.ktor.websocket.Frame import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.chatserver.data.registry.ConversationRegistry import org.chatserver.data.repository.PendingMessageRepository import org.chatserver.models.ChatMessage import org.chatserver.models.PresenceEvent @@ -15,6 +16,7 @@ class SqsConsumer( private val sqsClient: SqsClient, private val sessionStore: SessionStore, private val pendingMessageRepository: PendingMessageRepository, + private val conversationRegistry: ConversationRegistry, ) { companion object { private const val MAX_MESSAGES = 10 @@ -62,9 +64,11 @@ class SqsConsumer( private suspend fun handlePresence(event: PresenceEvent) { val frame = Frame.Text(Json.encodeToString(event)) - sessionStore.getAll().forEach { userId -> - sessionStore.get(userId)?.send(frame) - } - logger.debug("Broadcast presence for ${event.userId} online=${event.online} to ${sessionStore.getAll().size} session(s)") + val groupmates = conversationRegistry.getGroupmates(event.userId) + val recipients = sessionStore.getAll().filter { it in groupmates } + recipients.forEach { userId -> sessionStore.get(userId)?.send(frame) } + logger.info( + "Presence for ${event.userId} online=${event.online}: ${groupmates.size} groupmate(s), ${recipients.size} local recipient(s)", + ) } } diff --git a/ChatServer/src/test/kotlin/plugins/SocketsTest.kt b/ChatServer/src/test/kotlin/plugins/SocketsTest.kt index c5a6af1..d5392e3 100644 --- a/ChatServer/src/test/kotlin/plugins/SocketsTest.kt +++ b/ChatServer/src/test/kotlin/plugins/SocketsTest.kt @@ -17,6 +17,7 @@ import io.mockk.verify import kotlinx.coroutines.withTimeout import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.chatserver.data.registry.ConversationRegistry import org.chatserver.data.registry.ServerRegistry import org.chatserver.data.registry.UserRegistry import org.chatserver.data.repository.PendingMessageRepository @@ -42,6 +43,7 @@ class SocketsTest { private val pendingRepo = mockk(relaxed = true) private val serverRegistry = mockk(relaxed = true) private val sqsClient = mockk(relaxed = true) + private val conversationRegistry = mockk(relaxed = true) private fun withChatApp(block: suspend ApplicationTestBuilder.(client: HttpClient) -> Unit) { testApplication { @@ -55,6 +57,7 @@ class SocketsTest { single { pendingRepo } single { serverRegistry } single { sqsClient } + single { conversationRegistry } }, ) } @@ -89,6 +92,8 @@ class SocketsTest { withChatApp { client -> client.webSocket("/ws?token=${token("alice")}") { withTimeout(2000.milliseconds) { + // First frame is the self-presence event; second is the pending message. + incoming.receive() val frame = incoming.receive() if (frame is Frame.Text) received.add(frame.readText()) } diff --git a/ChatServer/src/test/kotlin/services/sqs/SqsConsumerTest.kt b/ChatServer/src/test/kotlin/services/sqs/SqsConsumerTest.kt index 006e469..69abd5f 100644 --- a/ChatServer/src/test/kotlin/services/sqs/SqsConsumerTest.kt +++ b/ChatServer/src/test/kotlin/services/sqs/SqsConsumerTest.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import org.chatserver.data.registry.ConversationRegistry import org.chatserver.data.repository.PendingMessageRepository import org.chatserver.models.ChatMessage import org.chatserver.models.PresenceEvent @@ -30,7 +31,8 @@ class SqsConsumerTest { private val sqsClient = mockk(relaxed = true) private val sessionStore = mockk() private val pendingMessageRepository = mockk(relaxed = true) - private val consumer = SqsConsumer(sqsClient, sessionStore, pendingMessageRepository) + private val conversationRegistry = mockk() + private val consumer = SqsConsumer(sqsClient, sessionStore, pendingMessageRepository, conversationRegistry) private val chatMessage = ChatMessage(fromUserId = "alice", toUserId = "bob", conversationId = "conv-1", content = "hello") private val chatMessageJson = Json.encodeToString(chatMessage) @@ -163,12 +165,12 @@ class SqsConsumerTest { } @Test - fun `presence event is broadcast to all connected sessions`() = + fun `presence event is delivered only to groupmates of the sender`() = runTest { - val session1 = mockk() - val session2 = mockk() - coEvery { session1.send(any()) } returns Unit - coEvery { session2.send(any()) } returns Unit + val groupmateSession = mockk() + val nonGroupmateSession = mockk() + coEvery { groupmateSession.send(any()) } returns Unit + coEvery { nonGroupmateSession.send(any()) } returns Unit val presenceEvent = PresenceEvent(userId = "charlie", online = true) val presenceEnvelopeJson = @@ -179,15 +181,15 @@ class SqsConsumerTest { .messages(buildSqsMessage(presenceEnvelopeJson)) .build(), ) - every { sessionStore.getAll() } returns setOf("alice", "bob") - every { sessionStore.get("alice") } returns session1 - every { sessionStore.get("bob") } returns session2 + every { conversationRegistry.getGroupmates("charlie") } returns setOf("alice") + every { sessionStore.getAll() } returns setOf("alice", "dave") + every { sessionStore.get("alice") } returns groupmateSession val job = launch { consumer.start("https://sqs/queue") } job.join() - coVerify(exactly = 1) { session1.send(any()) } - coVerify(exactly = 1) { session2.send(any()) } + coVerify(exactly = 1) { groupmateSession.send(any()) } + coVerify(exactly = 0) { nonGroupmateSession.send(any()) } verify(exactly = 0) { pendingMessageRepository.save(any()) } } } diff --git a/diagram.svg b/diagram.svg index 3c56bd6..3ccdeb4 100644 --- a/diagram.svg +++ b/diagram.svg @@ -1,4 +1,4 @@ -
HTTPS :433
HTTPS :433
Internet
Internet
AWS Cloud
AWS Cloud
x
Public subnet
Public subnet
Private subnet
Private subnet
10.0.1.0/24 - 10.0.2.0/24
10.0.1.0/24 - 10.0.2.0/24
10.0.3.0/24 - 10.0.4.0/24
10.0.3.0/24 - 10.0.4.0/24
Elastic IP
Elastic IP
Elastic Container Service
Elastic Container Service
ChatServer

:8080
ChatServer...
PresenceServer

:8000
PresenceServer...
ApiServer

:8000
ApiServer...
RDS

:5432
RDS...
PresenceServer Internal ALB
PresenceServer Inter...
HTTP /
Websocket :8080
JWT - Websockets
x-internal - HTTP
HTTP /...
ChatServer NLB
ChatServer NLB
HTTP :8000
ecs-tasks-sg
JWT
HTTP :8000...
ApiServer ALB
ApiServer ALB
Redis

:6379
Redis...
Redis Serialization
Protocol
:6379
elasticache-sg
TLS
Redis Serialization...
PostgreSQL :5432
rds-sg
TLS
PostgreSQL :5432...
HTTP :80
internal-alb-sg
x-internal
HTTP :80...
HTTP :80
x-internal
HTTP :80...
SQS calls
DynamoDB calls
ECR calls

HTTPS :433
SQS calls...
HTTP :8000
ecs-tasks-sg
HTTP :8000...
KMS
KMS
KMS
KMS
NAT Gateway
NAT Gateway
ECR calls

HTTPS :433
ECR calls...
VPC
VPC
HTTP :80
alb-sg
HTTP :80...
TCP :80
TCP :80
Internet Gateway
Internet Gateway
Secrets Manager
Secrets Manager
SQS API Endpoint
SQS API Endpoint
SQS
SQS
DynamoDB API Endpoint
DynamoDB API Endpoint
DynamoDB
DynamoDB
ECR
ECR
S3 (ALB logs, CloudTrail, Client)

S3 (ALB logs, CloudT...
CloudFront
CloudFront
AES-256
AES-256
AWS Account
AWS Account
CloudTrail
CloudTrail
10.0.0.0/16
10.0.0.0/16
VPC Flow Logs
VPC Flow Logs
CloudWatch
CloudWatch
HTTPS :433
HTTPS :433
HTTPS :433
HTTPS :433
HTTPS :433
HTTPS :433

Network/Traffic Diagram

Network/Traffic Diagram

IAM/Permissions Diagram

IAM/Permissions Diagram
ECS TASK STARTUP (Execution Roles)
ECS TASK STARTUP (Execution Roles)
ApiServer
ApiServer
ChatServer
ChatServer
PresenceServer
PresenceServer
ecs-execution-api
ecs-execution-api
ecs-execution-chatserver
ecs-execution-chatse...
ecs-execution-presenceserver
ecs-execution-presen...
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
chatsystem/db
chatsystem/db
chatsystem/jwt_secret
chatsystem/jwt_secret
chatsystem/internal_api_key
chatsystem/internal_api_key
chatsystem/jwt_secret
chatsystem/jwt_secret
chatsystem/internal_api_key
chatsystem/internal_api_key
chatsystem/jwt_secret
chatsystem/jwt_secret
chatsystem/internal_api_key
chatsystem/internal_api_key
RUNTIME (Task Roles)
RUNTIME (Task Roles)
ChatServer
ChatServer
chatserver-task
chatserver-task
dynamodb: GetItem, PutItem, DeleteItem, UpdateItem, Query, DescribeTable, Scan
dynamodb: GetItem, PutItem, DeleteIt...
ServerRegistry
ServerRegistry
UserConnections
UserConnections
ConversationMembers
ConversationMembers
Messages
Messages
PendingMessages
PendingMessages
sqs: SendMessage, ReceiveMessage, DeleteMessage, GetQueue, CreateQueue, DeleteQueue
sqs: SendMessage, ReceiveMessage, De...
chat-server-* (wildcard)
chat-server-* (wildcard)
RESOURCE ACCESS POLICIES
RESOURCE ACCESS POLICIES
VPC Flow Logs
VPC Flow Logs
vpc-flow-logs
vpc-flow-logs
logs: CreateLogGroup, CreateLogStream, PutLogEvents, DescribeLogGroups, DescribeLogStream
logs: CreateLogGroup, CreateLogStrea...
*
*
client
client
cloudtrail
cloudtrail
alb-logs
alb-logs
S3 Bucket Policies
S3 Bucket Policies
cloudfront.amazonaws.com
cloudfront.amazonaws.com
cloudtrail.amazonaws.com
cloudtrail.amazonaws.com
ELB Service Account
ELB Service Account
delivery.logs.amazonaws.com
delivery.logs.amazonaws.com
s3: GetObject
s3: GetObject
bucket/*
bucket/*
s3: GetBucketAcl
s3: GetBucketAcl
s3: PutObject
s3: PutObject
bucket
bucket
bucket/AWSLogs/{account_id}/*
bucket/AWSLogs/{account_id}/*
s3: PutObject
s3: PutObject
s3: GetBucketAcl
s3: GetBucketAcl
s3: PutObject
s3: PutObject
bucket/*
bucket/*
bucket
bucket
bucket/*
bucket/*
Text is not SVG - cannot display
\ No newline at end of file +
HTTPS :433
HTTPS :433
Internet
Internet
AWS Cloud
AWS Cloud
x
Public subnet
Public subnet
Private subnet
Private subnet
10.0.1.0/24 - 10.0.2.0/24
10.0.1.0/24 - 10.0.2.0/24
10.0.3.0/24 - 10.0.4.0/24
10.0.3.0/24 - 10.0.4.0/24
Elastic IP
Elastic IP
Elastic Container Service
Elastic Container Service
ChatServer

:8080
ChatServer...
PresenceServer

:8000
PresenceServer...
ApiServer

:8000
ApiServer...
RDS

:5432
RDS...
PresenceServer Internal ALB
PresenceServer Inter...
HTTP /
Websocket :8080
JWT - Websockets
x-internal - HTTP
HTTP /...
ChatServer NLB
ChatServer NLB
HTTP :8000
ecs-tasks-sg
JWT
HTTP :8000...
ApiServer ALB
ApiServer ALB
Redis

:6379
Redis...
Redis Serialization
Protocol
:6379
elasticache-sg
TLS
Redis Serialization...
PostgreSQL :5432
rds-sg
TLS
PostgreSQL :5432...
HTTP :80
internal-alb-sg
x-internal
HTTP :80...
HTTP :80
x-internal
HTTP :80...
SQS calls
DynamoDB calls
ECR calls

HTTPS :433
SQS calls...
HTTP :8000
ecs-tasks-sg
HTTP :8000...
KMS
KMS
KMS
KMS
NAT Gateway
NAT Gateway
ECR calls

HTTPS :433
ECR calls...
VPC
VPC
HTTP :80
alb-sg
HTTP :80...
TCP :80
TCP :80
Internet Gateway
Internet Gateway
Secrets Manager
Secrets Manager
SQS API Endpoint
SQS API Endpoint
SQS
SQS
DynamoDB API Endpoint
DynamoDB API Endpoint
DynamoDB
DynamoDB
ECR
ECR
S3 (ALB logs, CloudTrail, Client)

S3 (ALB logs, CloudT...
CloudFront
CloudFront
AES-256
AES-256
AWS Account
AWS Account
CloudTrail
CloudTrail
10.0.0.0/16
10.0.0.0/16
VPC Flow Logs
VPC Flow Logs
CloudWatch
CloudWatch
HTTPS :433
HTTPS :433
HTTPS :433
HTTPS :433
HTTPS :433
HTTPS :433

Network/Traffic Diagram

Network/Traffic Diagram

IAM/Permissions Diagram

IAM/Permissions Diagram
ECS TASK STARTUP (Execution Roles)
ECS TASK STARTUP (Execution Roles)
ApiServer
ApiServer
ChatServer
ChatServer
PresenceServer
PresenceServer
ecs-execution-api
ecs-execution-api
ecs-execution-chatserver
ecs-execution-chatse...
ecs-execution-presenceserver
ecs-execution-presen...
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
secretsmanager:GetSecretValue
chatsystem/db
chatsystem/db
chatsystem/jwt_secret
chatsystem/jwt_secret
chatsystem/internal_api_key
chatsystem/internal_api_key
chatsystem/jwt_secret
chatsystem/jwt_secret
chatsystem/internal_api_key
chatsystem/internal_api_key
chatsystem/jwt_secret
chatsystem/jwt_secret
chatsystem/internal_api_key
chatsystem/internal_api_key
RUNTIME (Task Roles)
RUNTIME (Task Roles)
ChatServer
ChatServer
chatserver-task
chatserver-task
dynamodb: GetItem, PutItem, DeleteItem, UpdateItem, Query, DescribeTable, Scan
dynamodb: GetItem, PutItem, DeleteIt...
ServerRegistry
ServerRegistry
UserConnections
UserConnections
ConversationMembers
ConversationMembers
Messages
Messages
PendingMessages
PendingMessages
sqs: SendMessage, ReceiveMessage, DeleteMessage, GetQueue, CreateQueue, DeleteQueue
sqs: SendMessage, ReceiveMessage, De...
chat-server-* (wildcard)
chat-server-* (wildcard)
RESOURCE ACCESS POLICIES
RESOURCE ACCESS POLICIES
VPC Flow Logs
VPC Flow Logs
vpc-flow-logs
vpc-flow-logs
logs: CreateLogGroup, CreateLogStream, PutLogEvents, DescribeLogGroups, DescribeLogStream
logs: CreateLogGroup, CreateLogStrea...
*
*
client
client
cloudtrail
cloudtrail
alb-logs
alb-logs
S3 Bucket Policies
S3 Bucket Policies
cloudfront.amazonaws.com
cloudfront.amazonaws.com
cloudtrail.amazonaws.com
cloudtrail.amazonaws.com
ELB Service Account
ELB Service Account
delivery.logs.amazonaws.com
delivery.logs.amazonaws.com
s3: GetObject
s3: GetObject
bucket/*
bucket/*
s3: GetBucketAcl
s3: GetBucketAcl
s3: PutObject
s3: PutObject
bucket
bucket
bucket/AWSLogs/{account_id}/*
bucket/AWSLogs/{account_id}/*
s3: PutObject
s3: PutObject
s3: GetBucketAcl
s3: GetBucketAcl
s3: PutObject
s3: PutObject
bucket/*
bucket/*
bucket
bucket
bucket/*
bucket/*
Service
Service
Role
Role
Action
Action
Resource
Resource
Service
Service
Principal
Principal
Action
Action
Resource
Resource
Service
Service
Role
Role
Action
Action
Resource
Resource
ConversationMembers/index/*
ConversationMembers/index/*
Text is not SVG - cannot display
\ No newline at end of file