Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ti-infinite/GSMInfrastructure/llms.txt

Use this file to discover all available pages before exploring further.

GSM Infrastructure composes a set of purpose-built AWS services into a single cohesive platform. A CloudFront distribution acts as the unified entry point: static SPA assets are served from a private S3 bucket, while all /api/* requests are proxied over HTTP to a single EC2 arm64 instance that hosts four containerized ECS microservices running in Docker bridge mode. Secrets are never baked into images — they are pulled at container startup from SSM Parameter Store using KMS-encrypted parameters. GitHub Actions workflows assume a short-lived IAM role via GitHub OIDC to deploy the three CloudFormation stacks, and an EventBridge Scheduler + Lambda pair automatically starts and stops the EC2 instance on a weekday schedule to keep costs low.

Frontend layer

The static frontend is hosted in an S3 bucket named {env}-{appName}-frontend. The bucket has all public access blocked, uses AES-256 server-side encryption, and enforces HTTPS-only access through a bucket policy that denies any request where aws:SecureTransport is false. A CloudFront distribution sits in front of the bucket and the EC2 backend. Key configuration choices extracted directly from the infrastructure template:
  • Origin Access Control (OAC) — named {env}-{appName}-oac, configured with SigningBehavior: always and SigningProtocol: sigv4. The bucket policy allows s3:GetObject only when the request’s AWS:SourceArn matches the distribution ARN, so the bucket is never publicly reachable.
  • S3 origin path — CloudFront requests assets from the /admin prefix of the bucket.
  • SPA router CloudFront Function — a lightweight cloudfront-js-2.0 function named {env}-{appName}-spa-router intercepts every viewer request. If the last URL segment does not contain a . (i.e., it is an extensionless route like /dashboard), the function rewrites request.uri to /index.html, enabling client-side routing without server redirects.
function handler(event) {
    var request = event.request;
    var lastSegment = request.uri.split('/').pop();
    if (!lastSegment.includes('.')) {
        request.uri = '/index.html';
    }
    return request;
}
  • Viewer protocol policyredirect-to-https on the default cache behavior (S3 origin).
  • Price classPriceClass_100 (North America and Europe edge locations only).
  • Default root objectindex.html.
CloudFront forwards a custom X-CloudFront-Origin header to the EC2 origin on every /api/* request. The header value is injected at deploy time via the CloudFrontHeader parameter, letting backend services verify that requests arrived through CloudFront and not directly to the instance.

Backend layer

The backend runs on a single EC2 instance registered with an ECS cluster named {env}-{appName}-cluster.
ResourceValue
Instance typet4g.medium (default, configurable via Ec2InstanceType parameter)
Architecturearm64
AMILatest ECS-optimized Amazon Linux 2023 arm64 (resolved via SSM at deploy time)
ECS network modebridge
Launch typeEC2
Container runtimeDocker (enabled and started via EC2 UserData)
Image registryECR repository {env}-{appName}-respository
Log retention7 days (/ecs/{env}-{appName}-backend)
The EC2 instance registers itself with the cluster automatically on boot via a UserData script that writes the cluster name to /etc/ecs/ecs.config and starts the ECS agent:
#!/bin/bash
mkdir -p /etc/ecs
echo ECS_CLUSTER=${Environment}-${AppName}-cluster > /etc/ecs/ecs.config

systemctl enable docker
systemctl start docker
systemctl enable ecs
systemctl daemon-reload
systemctl start ecs
An Elastic IP is allocated and associated with the instance at stack creation time. The CloudFront distribution uses this EIP’s public DNS name as its EC2 origin, ensuring the origin address survives instance stop/start cycles managed by the scheduler.
ECS task definitions use MaximumPercent: 100 and MinimumHealthyPercent: 0 in their deployment configuration. This allows the scheduler Lambda to scale services to desiredCount=0 (stopping all tasks) before shutting down the instance, avoiding task eviction errors.

Microservices

All four services share the same ECR repository, differentiated by image tag. Each task definition uses NetworkMode: bridge and maps a fixed host port, so the EC2 instance effectively acts as the port-to-service router. Every container has a StartPeriod: 120 seconds in its health check to allow for JVM or framework warm-up time.
The public-facing entry point. CloudFront routes all /api/* traffic to port 80 on the EC2 instance, where this container receives it.
PropertyValue
Container namegsmgateway
Image taggateway-latest
Host / container port80
Health checkwget -qO- http://localhost:80/api/health
Health interval30 s, timeout 10 s, retries 3
Secrets injectedJWT_SECRET (from SSM)
Env varsORIGINS (CloudFront domain), ENVIRONMENT
CPU256 units
Memory (hard)TaskMemory parameter (default 512 MiB)
Memory (soft)TaskMemoryReservation parameter (default 384 MiB)
Handles authentication and JWT issuance. Receives traffic internally from the gateway over the Docker bridge network.
PropertyValue
Container namegmsauth
Image tagauth-latest
Host / container port8081
Health checkwget -qO- http://localhost:8081/health
Health interval30 s, timeout 10 s, retries 3
Secrets injectedJWT_SECRET, DB_MASTER_URL (from SSM)
Env varsENVIRONMENT
CPU256 units
Core application business logic service.
PropertyValue
Container namegsmapplication
Image tagapplication-latest
Host / container port8082
Health checkwget -qO- http://localhost:8082/health
Health interval30 s, timeout 10 s, retries 3
Secrets injectedJWT_SECRET, DB_MASTER_URL (from SSM)
Env varsENVIRONMENT
CPU256 units
Operations and administrative service.
PropertyValue
Container namegsmoperations
Image taggsmoperations-latest
Host / container port8083
Health checkwget -qO- http://localhost:8083/health
Health interval30 s, timeout 10 s, retries 3
Secrets injectedJWT_SECRET, DB_MASTER_URL (from SSM)
Env varsENVIRONMENT
CPU256 units

Secrets management

Sensitive runtime configuration is stored in AWS Systems Manager Parameter Store and is never embedded in container images or CloudFormation template bodies. Two parameters are used across the services:
Parameter name (default)Secret name in containerServices that consume it
dev/backend/JWT_SECRETJWT_SECRETGateway, Auth, Application, Operations
dev/backend/DB_MASTER_URLDB_MASTER_URLAuth, Application, Operations
Parameters are injected at container startup via the ECS Secrets field, which references the full SSM parameter ARN:
Secrets:
  - Name: JWT_SECRET
    ValueFrom: !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${JWTSecretParameterName}'
  - Name: DB_MASTER_URL
    ValueFrom: !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${DBMasterUrlParameterName}'
Both the ECS execution role ({env}-{appName}-ecs-role) and the ECS task role ({env}-{appName}-ecs-task-role) carry a shared managed policy ({env}-{appName}-ecs-ssm-read) that grants ssm:GetParameter / ssm:GetParameters on the /{env}/* parameter path and kms:Decrypt for KMS-encrypted values. The EC2 instance role carries an equivalent inline policy for the ECS agent itself.
Parameter names are environment-scoped by convention (e.g., dev/backend/JWT_SECRET, qa/backend/JWT_SECRET) and controlled via the JWTSecretParameterName and DBMasterUrlParameterName CloudFormation parameters, which are passed in at deploy time from GitHub Actions repository variables.

Networking

Traffic flows through two distinct paths inside the CloudFront distribution, controlled by its cache behavior configuration:
Browser

  ├─ GET /api/*  ──▶ CloudFront cache behavior (AllowedMethods: all, TTL: 0)
  │                        │
  │                        └─▶ EC2 origin (HTTP port 80) ──▶ gsmgateway container

  └─ GET /*      ──▶ CloudFront default behavior (GET/HEAD/OPTIONS, redirect-to-https)

                           └─▶ S3 origin (OAC, /admin path)
The ECS security group ({env}-{appName}-ecs-security-group) enforces these rules: Ingress:
ProtocolPortsSourcePurpose
TCP80pl-3b927c52 (CloudFront prefix list)HTTP traffic from CloudFront edge nodes
TCP80VPC CIDR (e.g., 10.0.0.0/16)Traffic from within the VPC
TCP8081–8083172.17.0.0/16Docker bridge inter-container communication
Egress:
ProtocolPortsDestinationPurpose
TCP4430.0.0.0/0HTTPS to SSM endpoints and ECR
UDP530.0.0.0/0DNS resolution
TCP8081–8083172.17.0.0/16Docker bridge inter-container
TCPDB portSqlServerProviderIpOutbound to SQL Server database
The /api/* cache behavior forwards the Authorization, Content-Type, Accept, Origin, and Referer headers along with all cookies to the EC2 origin. MinTTL, DefaultTTL, and MaxTTL are all set to 0, so API responses are never cached at the edge.

CI/CD authentication

GSM Infrastructure uses GitHub OIDC to achieve keyless AWS authentication — no AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY is stored anywhere. The base stack creates an AWS::IAM::OIDCProvider for https://token.actions.githubusercontent.com with two thumbprints for certificate chain validation:
ThumbprintList:
  - 6938fd4d98bab03faadb97b34396831e3780aea1
  - 1c58a3a8518e8759bf075b76b750d4f2df264fcd
The InfraExecutorRole trust policy allows sts:AssumeRoleWithWebIdentity when the OIDC token satisfies:
  • token.actions.githubusercontent.com:aud equals sts.amazonaws.com
  • token.actions.githubusercontent.com:sub matches one of:
    • repo:{GitHubOrg}/{GitHubRepo}:ref:refs/heads/{GitHubBranch}
    • repo:{GitHubOrg}/{GitHubRepo}:environment:infra-*
    • repo:{GitHubOrg}/{GitHubRepo}:environment:backend-*
    • repo:{GitHubOrg}/{GitHubRepo}:environment:frontend-*
In the GitHub Actions workflows, credentials are configured as follows:
permissions:
  id-token: write   # required to request the OIDC JWT
  contents: read

steps:
  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: ${{ secrets.AWS_INFRA_ROLE_ARN }}
      aws-region: us-east-1
Both the infrastructure workflow (triggered by changes to devops/infrastructure/template.yml) and the scheduler workflow (triggered by changes to devops/scheduler/template.yml) share the same InfraExecutorRole. Each workflow is enabled independently via repository variables: WORKFLOW_INFRASTRUCTURE_ENABLED and WORKFLOW_SCHEDULER_ENABLED.

Cost controls

Two independent mechanisms keep AWS spend in check.

AWS Budget

The infrastructure stack creates an AWS::Budgets::Budget resource with a configurable monthly USD limit (BudgetLimitUSD, default $30). When actual spend exceeds 100% of the threshold, an SNS notification is published to the {env}-{appName}-notification-alerts topic, which emails the configured AlertEmail address.
NotificationsWithSubscribers:
  - Notification:
      NotificationType: ACTUAL
      ComparisonOperator: GREATER_THAN
      Threshold: 100
      ThresholdType: PERCENTAGE
    Subscribers:
      - SubscriptionType: SNS
        Address: !Ref NotificationSNSTopic

EventBridge Scheduler + Lambda

The scheduler stack deploys two Python 3.12 arm64 Lambda functions and two EventBridge schedules in a dedicated schedule group ({env}-{appName}-ec2-schedules):
Schedule nameExpression (default)Action
{env}-{appName}-stop-lun-sabcron(0 1 ? * MON-SAT *)Scale all ECS services → 0, disassociate EIP, stop EC2
{env}-{appName}-start-lun-sabcron(0 9 ? * MON-SAT *)Start EC2, wait for running state, reassociate EIP, scale all ECS services → 1
The stop sequence inside the Lambda:
  1. Scale all four ECS services to desiredCount=0
  2. Sleep 10 seconds to let the ECS agent process the scale-down
  3. Disassociate the EIP (prevents charges for idle Elastic IPs)
  4. Stop the EC2 instance
The start sequence inside the Lambda:
  1. Start the EC2 instance
  2. Wait for the instance_running waiter (polling every 10 s, up to 30 attempts)
  3. Reassociate the EIP to the instance
  4. Sleep 30 seconds for the ECS agent to register with the cluster
  5. Scale all four ECS services to desiredCount=1
Each schedule has a retry policy of MaximumRetryAttempts: 2 with a MaximumEventAgeInSeconds: 3600. Lambda log groups retain output for 14 days.
The scheduler stack takes the EC2 instance ID and EIP allocation ID as parameters, which means it must be deployed after the infrastructure stack outputs are available. Both values are passed in via GitHub Actions repository variables (EC2_INSTANCE_ID and EC2_ELASTIC_IP_ID).

Build docs developers (and LLMs) love