Shared Templates Refactoring Plan
Identified ~1,000 lines of duplication across mpac-smartpos/ecs-stack.yaml and mpac-pgw/ecs-stack.yaml (and partially mpac-obs/). Extracting shared nested stacks would reduce maintenance burden — e.g., the PG version upgrade and DependsOn cleanup required touching identical code in multiple files.
1. VPC Stack (~180 lines saved)
File: shared/vpc-stack.yaml
Duplicated resources (18 total):
- VPC, InternetGateway, InternetGatewayAttachment
- PublicSubnetA/B, PrivateSubnetA/B, DatabaseSubnetA/B
- NatGatewayEIP, NatGateway
- PublicRouteTable, PublicRoute, 2 public route table associations
- PrivateRouteTable, PrivateRoute, 2 private route table associations
- DatabaseRouteTable, 2 database route table associations
Parameters: SystemName, Environment, VpcCidr, subnet CIDR mapping
Outputs: VpcId, PublicSubnetIds, PrivateSubnetIds, DatabaseSubnetIds
Used by: mpac-smartpos, mpac-pgw
2. Bastion Host Stack (~140 lines saved)
File: shared/bastion-stack.yaml
Duplicated resources:
- BastionSecurityGroup
- BastionKeyPair
- BastionInstance (t3.micro, AMI via SSM dynamic reference)
- BastionEIP
- RDSBastionIngress
- RedisBastionIngress
Parameters: SystemName, Environment, VpcId, PublicSubnetId, RDSSecurityGroupId, RedisSecurityGroupId, AllowedIP
Used by: mpac-smartpos, mpac-pgw
3. Security Groups Stack (~140 lines saved)
File: shared/security-groups-stack.yaml
Duplicated resources:
- ALBSecurityGroup (HTTP/HTTPS ingress)
- ECSSecurityGroup (ingress from ALB)
- RDSSecurityGroup (PostgreSQL 5432 from ECS)
- RedisSecurityGroup (Redis 6379 from ECS)
Parameters: SystemName, Environment, VpcId, ECSIngressPorts (list — smartpos uses 8000+8080, pgw uses 8080 only)
Used by: mpac-smartpos, mpac-pgw
4. RDS PostgreSQL Stack (~100 lines saved)
File: shared/rds-postgres-stack.yaml
Duplicated resources:
- DBSubnetGroup
- DBParameterGroup
- RDSInstance (gp3, encrypted, backup 7 days)
- DBSecret
- DBSecretAttachment
Parameters: SystemName, Environment, DBName, PostgresFamily (e.g. postgres17), EngineVersion, InstanceClass, AllocatedStorage, DatabaseSubnetIds, SecurityGroupId
Outputs: DBEndpoint, DBSecretArn
Used by: mpac-smartpos, mpac-pgw
5. ElastiCache Redis Stack (~80 lines saved)
File: shared/redis-stack.yaml
Duplicated resources:
- RedisSubnetGroup
- RedisParameterGroup
- RedisCluster (engine 7.0, single-node)
- RedisSecret
Parameters: SystemName, Environment, DatabaseSubnetIds, SecurityGroupId
Outputs: RedisEndpoint, RedisPort, RedisSecretArn
Used by: mpac-smartpos, mpac-pgw
6. ECS IAM Roles (~120 lines saved)
File: shared/ecs-iam-roles-stack.yaml
Duplicated resources:
- ECSTaskExecutionRole (ecs-tasks.amazonaws.com, AmazonECSTaskExecutionRolePolicy, secrets/logs access)
- Base ECSTaskRole (ecs-tasks.amazonaws.com, ssmmessages for ECS Exec)
Parameters: SystemName, Environment, SecretArns (list of secrets the execution role can read)
Outputs: ExecutionRoleArn, TaskRoleArn
Used by: mpac-smartpos, mpac-pgw, mpac-obs/ecs-stack, mpac-obs/observability-stack
Note: Task roles have service-specific policies (IoT, S3, etc.) — those remain in the parent stack and attach to the role via inline policies or managed policy ARNs.
7. ECR Repository (~25 lines saved per repo)
File: shared/ecr-repo-stack.yaml
Duplicated pattern: ECR repository with "keep last 10 images" lifecycle policy. Used 3 times (1 in mpac-pgw, 2 in mpac-smartpos).
Parameters: RepositoryName
8. Alloy Config Dedup in mpac-obs (~80 lines saved)
mpac-obs/ecs-stack.yaml and mpac-obs/observability-stack.yaml contain nearly identical inline Alloy (OTLP collector) configurations. The only difference is bearer token auth in ecs-stack.
Approach: Extract to an S3-hosted config file with environment variable substitution, or use a shared config snippet referenced by both templates.
9. mpac-obs Template Consolidation (~200+ lines saved)
mpac-obs/ecs-stack.yaml and mpac-obs/observability-stack.yaml duplicate:
- ObservabilityCluster
- ECSTaskExecutionRole / ECSTaskRole
- GrafanaLogGroup / AlloyLogGroup
- ServiceDiscoveryNamespace / AlloyServiceDiscovery
- GrafanaALB, GrafanaTargetGroup, GrafanaListener
- GrafanaTaskDefinition, GrafanaService, AlloyTaskDefinition, AlloyService
These appear to be alternative deployment modes (EC2-hybrid vs pure-Fargate). Consider sharing common resources via cross-stack references or a base nested stack.
Summary
| Shared Module | Saves | Used By |
|---|---|---|
| shared/vpc-stack.yaml | ~180 lines | smartpos, pgw |
| shared/bastion-stack.yaml | ~140 lines | smartpos, pgw |
| shared/security-groups-stack.yaml | ~140 lines | smartpos, pgw |
| shared/rds-postgres-stack.yaml | ~100 lines | smartpos, pgw |
| shared/redis-stack.yaml | ~80 lines | smartpos, pgw |
| shared/ecs-iam-roles-stack.yaml | ~120 lines | smartpos, pgw, obs x2 |
| shared/ecr-repo-stack.yaml | ~75 lines | smartpos, pgw |
| Alloy config dedup | ~80 lines | obs x2 |
| obs template consolidation | ~200+ lines | obs x2 |
| Total | ~1,100+ lines |
Migration Strategy
Follow the two-phase migration convention:
- Phase 1: Create shared nested stacks, update one system (e.g. mpac-pgw) to use them. Validate in dev.
- Phase 2: Migrate remaining systems (mpac-smartpos, mpac-obs) to use shared stacks. Remove duplicated resources from parent stacks.
Start with VPC and RDS stacks — these had the most painful cross-template changes in this lint fix round.