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 allDocumentation 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.
/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 withSigningBehavior: alwaysandSigningProtocol: sigv4. The bucket policy allowss3:GetObjectonly when the request’sAWS:SourceArnmatches the distribution ARN, so the bucket is never publicly reachable. - S3 origin path — CloudFront requests assets from the
/adminprefix of the bucket. - SPA router CloudFront Function — a lightweight
cloudfront-js-2.0function named{env}-{appName}-spa-routerintercepts every viewer request. If the last URL segment does not contain a.(i.e., it is an extensionless route like/dashboard), the function rewritesrequest.urito/index.html, enabling client-side routing without server redirects.
- Viewer protocol policy —
redirect-to-httpson the default cache behavior (S3 origin). - Price class —
PriceClass_100(North America and Europe edge locations only). - Default root object —
index.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.
| Resource | Value |
|---|---|
| Instance type | t4g.medium (default, configurable via Ec2InstanceType parameter) |
| Architecture | arm64 |
| AMI | Latest ECS-optimized Amazon Linux 2023 arm64 (resolved via SSM at deploy time) |
| ECS network mode | bridge |
| Launch type | EC2 |
| Container runtime | Docker (enabled and started via EC2 UserData) |
| Image registry | ECR repository {env}-{appName}-respository |
| Log retention | 7 days (/ecs/{env}-{appName}-backend) |
UserData script that writes the cluster name to /etc/ecs/ecs.config and starts the ECS agent:
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 usesNetworkMode: 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.
gsmgateway — Port 80 (Gateway)
gsmgateway — Port 80 (Gateway)
The public-facing entry point. CloudFront routes all
/api/* traffic to port 80 on the EC2 instance, where this container receives it.| Property | Value |
|---|---|
| Container name | gsmgateway |
| Image tag | gateway-latest |
| Host / container port | 80 |
| Health check | wget -qO- http://localhost:80/api/health |
| Health interval | 30 s, timeout 10 s, retries 3 |
| Secrets injected | JWT_SECRET (from SSM) |
| Env vars | ORIGINS (CloudFront domain), ENVIRONMENT |
| CPU | 256 units |
| Memory (hard) | TaskMemory parameter (default 512 MiB) |
| Memory (soft) | TaskMemoryReservation parameter (default 384 MiB) |
gmsauth — Port 8081 (Auth)
gmsauth — Port 8081 (Auth)
Handles authentication and JWT issuance. Receives traffic internally from the gateway over the Docker bridge network.
| Property | Value |
|---|---|
| Container name | gmsauth |
| Image tag | auth-latest |
| Host / container port | 8081 |
| Health check | wget -qO- http://localhost:8081/health |
| Health interval | 30 s, timeout 10 s, retries 3 |
| Secrets injected | JWT_SECRET, DB_MASTER_URL (from SSM) |
| Env vars | ENVIRONMENT |
| CPU | 256 units |
gsmapplication — Port 8082 (Application)
gsmapplication — Port 8082 (Application)
Core application business logic service.
| Property | Value |
|---|---|
| Container name | gsmapplication |
| Image tag | application-latest |
| Host / container port | 8082 |
| Health check | wget -qO- http://localhost:8082/health |
| Health interval | 30 s, timeout 10 s, retries 3 |
| Secrets injected | JWT_SECRET, DB_MASTER_URL (from SSM) |
| Env vars | ENVIRONMENT |
| CPU | 256 units |
gsmoperations — Port 8083 (Operations)
gsmoperations — Port 8083 (Operations)
Operations and administrative service.
| Property | Value |
|---|---|
| Container name | gsmoperations |
| Image tag | gsmoperations-latest |
| Host / container port | 8083 |
| Health check | wget -qO- http://localhost:8083/health |
| Health interval | 30 s, timeout 10 s, retries 3 |
| Secrets injected | JWT_SECRET, DB_MASTER_URL (from SSM) |
| Env vars | ENVIRONMENT |
| CPU | 256 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 container | Services that consume it |
|---|---|---|
dev/backend/JWT_SECRET | JWT_SECRET | Gateway, Auth, Application, Operations |
dev/backend/DB_MASTER_URL | DB_MASTER_URL | Auth, Application, Operations |
Secrets field, which references the full SSM parameter ARN:
{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:{env}-{appName}-ecs-security-group) enforces these rules:
Ingress:
| Protocol | Ports | Source | Purpose |
|---|---|---|---|
| TCP | 80 | pl-3b927c52 (CloudFront prefix list) | HTTP traffic from CloudFront edge nodes |
| TCP | 80 | VPC CIDR (e.g., 10.0.0.0/16) | Traffic from within the VPC |
| TCP | 8081–8083 | 172.17.0.0/16 | Docker bridge inter-container communication |
| Protocol | Ports | Destination | Purpose |
|---|---|---|---|
| TCP | 443 | 0.0.0.0/0 | HTTPS to SSM endpoints and ECR |
| UDP | 53 | 0.0.0.0/0 | DNS resolution |
| TCP | 8081–8083 | 172.17.0.0/16 | Docker bridge inter-container |
| TCP | DB port | SqlServerProviderIp | Outbound 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 — noAWS_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:
InfraExecutorRole trust policy allows sts:AssumeRoleWithWebIdentity when the OIDC token satisfies:
token.actions.githubusercontent.com:audequalssts.amazonaws.comtoken.actions.githubusercontent.com:submatches one of:repo:{GitHubOrg}/{GitHubRepo}:ref:refs/heads/{GitHubBranch}repo:{GitHubOrg}/{GitHubRepo}:environment:infra-*repo:{GitHubOrg}/{GitHubRepo}:environment:backend-*repo:{GitHubOrg}/{GitHubRepo}:environment:frontend-*
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 anAWS::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.
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 name | Expression (default) | Action |
|---|---|---|
{env}-{appName}-stop-lun-sab | cron(0 1 ? * MON-SAT *) | Scale all ECS services → 0, disassociate EIP, stop EC2 |
{env}-{appName}-start-lun-sab | cron(0 9 ? * MON-SAT *) | Start EC2, wait for running state, reassociate EIP, scale all ECS services → 1 |
- Scale all four ECS services to
desiredCount=0 - Sleep 10 seconds to let the ECS agent process the scale-down
- Disassociate the EIP (prevents charges for idle Elastic IPs)
- Stop the EC2 instance
- Start the EC2 instance
- Wait for the
instance_runningwaiter (polling every 10 s, up to 30 attempts) - Reassociate the EIP to the instance
- Sleep 30 seconds for the ECS agent to register with the cluster
- Scale all four ECS services to
desiredCount=1
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).