Skip to content

Commit 24f160e

Browse files
e9e4e5f0faefJonathan Alvarez Delgado
andauthored
Add stage EFS mount targets, NFS security group, and task definition volumes (#376)
* feat(pulumi): add stage EFS mount targets, NFS security group, and task definition volumes * fix(security): remove broker SG rule and add pre-flight isolation validator * feat(pulumi): add Amazon MQ RabbitMQ broker for stage worker isolation --------- Co-authored-by: Jonathan Alvarez Delgado <jonathan.adl@proton.me>
1 parent ddcc29a commit 24f160e

3 files changed

Lines changed: 1506 additions & 11 deletions

File tree

infra/pulumi/__main__.py

Lines changed: 235 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def main():
7575
vpc_resource = vpc.resources.get("vpc")
7676

7777
# -----------------------------------------------------------------
78-
# VPC Peering to default VPC (RDS, Redis, RabbitMQ, ES, EFS)
78+
# VPC Peering to default VPC (RDS, Redis, ES, EFS)
7979
# -----------------------------------------------------------------
8080
# We handle peering manually (not via MultiTierVpc config) because
8181
# MultiTierVpc places peering routes on vpc.default_route_table_id,
@@ -107,7 +107,7 @@ def main():
107107
)
108108

109109
# Add peering route to the PRIVATE route table (ECS tasks need
110-
# to reach RDS/Redis/RabbitMQ/ES/EFS in 172.31.0.0/16)
110+
# to reach RDS/Redis/ES/EFS in 172.31.0.0/16)
111111
# Extract route table ID from the route table associations that
112112
# MultiTierVpc exposes (the actual RouteTable is a local variable
113113
# inside the component and not directly accessible)
@@ -160,7 +160,7 @@ def main():
160160
# sg-d5539ea9 (amo-services-prod-tb):
161161
# Redis, Memcached, ES/OpenSearch, EFS
162162
# sg-5133b52c (default VPC SG):
163-
# RDS MySQL, RabbitMQ (and self-referencing for internal comms)
163+
# RDS MySQL (and self-referencing for internal comms)
164164
#
165165
# We add our VPC CIDR to both SGs for the relevant ports
166166

@@ -193,14 +193,18 @@ def main():
193193
opts=pulumi.ResourceOptions(depends_on=[default_vpc_peer]),
194194
)
195195

196-
# --- sg-5133b52c: default VPC SG (RDS, RabbitMQ) ---
196+
# --- sg-5133b52c: default VPC SG (RDS) ---
197+
# Note: RabbitMQ (5672) was removed after the broker isolation
198+
# incident (issue #375). The stage broker secret pointed elsewhere;
199+
# the SG rule gave ECS tasks a clean path to it
200+
# We should NOT re-add 5672 until a dedicated stage broker exists
201+
# and the secret is verified to point to it via the preflight check
197202
default_sg_ids = default_vpc_ingress_cfg.get(
198203
"default_sg_ids",
199204
["sg-5133b52c"],
200205
)
201206
default_sg_ports = {
202207
"mysql": 3306,
203-
"rabbitmq": 5672,
204208
}
205209
for sg_id in default_sg_ids:
206210
for svc_name, port in default_sg_ports.items():
@@ -439,6 +443,75 @@ def main():
439443
**sg_config,
440444
)
441445

446+
# =========================================================================
447+
# EFS Mount Targets (addons shared storage)
448+
# =========================================================================
449+
# The addons EFS filesystem hosts add-on files, uploads, and media
450+
# (legacy NFS share from the EC2 era). Mount targets in the ATN VPC
451+
# private subnets give Fargate tasks a local-VPC ENI for NFS so they
452+
# don't need to route through VPC peering for every file I/O
453+
#
454+
# The filesystem retains its existing mount targets in the default VPC
455+
# for the EC2 fleet; multi-VPC mount targets (Sep 2024) allow both
456+
# fleets to coexist during migration
457+
#
458+
# NFS SG: allows TCP 2049 inbound only from the container SGs that
459+
# actually need filesystem access (web + worker; versioncheck excluded
460+
# per existing Ansible config efs: false)
461+
efs_config = resources.get("aws:efs:MountTargets", {})
462+
efs_mount_targets = []
463+
efs_filesystem_id = None
464+
465+
if efs_config and private_subnets and vpc_resource:
466+
efs_secret_name = efs_config["efs_filesystem_id_secret_name"]
467+
efs_secret = aws.secretsmanager.get_secret_version(
468+
secret_id=efs_secret_name,
469+
)
470+
efs_filesystem_id = pulumi.Output.secret(efs_secret.secret_string)
471+
472+
# NFS security group for mount target ENIs
473+
efs_sg = aws.ec2.SecurityGroup(
474+
f"{project.name_prefix}-efs-mt-sg",
475+
name=f"{project.name_prefix}-efs-mt",
476+
description="NFS access to EFS mount targets from Fargate containers",
477+
vpc_id=vpc_resource.id,
478+
tags={
479+
**project.common_tags,
480+
"Name": f"{project.name_prefix}-efs-mt",
481+
},
482+
)
483+
484+
# Allow NFS (TCP 2049) from each container SG that needs EFS
485+
efs_ingress_services = efs_config.get(
486+
"ingress_from_services", ["web", "worker"]
487+
)
488+
for svc_name in efs_ingress_services:
489+
cont_sg = container_sgs.get(svc_name)
490+
if cont_sg:
491+
aws.ec2.SecurityGroupRule(
492+
f"{project.name_prefix}-efs-nfs-from-{svc_name}",
493+
type="ingress",
494+
security_group_id=efs_sg.id,
495+
from_port=2049,
496+
to_port=2049,
497+
protocol="tcp",
498+
source_security_group_id=cont_sg.resources["sg"].id,
499+
description=f"NFS from {svc_name} containers",
500+
)
501+
502+
# Mount target in each private subnet
503+
for i, subnet in enumerate(private_subnets):
504+
mt = aws.efs.MountTarget(
505+
f"{project.name_prefix}-efs-mt-{i}",
506+
file_system_id=efs_filesystem_id,
507+
subnet_id=subnet.id,
508+
security_groups=[efs_sg.id],
509+
opts=pulumi.ResourceOptions(depends_on=[efs_sg, subnet]),
510+
)
511+
efs_mount_targets.append(mt)
512+
513+
pulumi.export("efs_mount_target_ids", [mt.id for mt in efs_mount_targets])
514+
442515
# =========================================================================
443516
# Fargate App Task Role
444517
# =========================================================================
@@ -544,6 +617,15 @@ def main():
544617
if fargate_app_task_role and "task_role_arn" not in task_def:
545618
task_def["task_role_arn"] = fargate_app_task_role.arn
546619

620+
# Inject EFS filesystem ID from Secrets Manager into any
621+
# volume configs that declare an efs_volume_configuration
622+
# The YAML carries the volume structure
623+
if efs_filesystem_id is not None:
624+
for vol in task_def.get("volumes", []):
625+
efs_vol_cfg = vol.get("efs_volume_configuration")
626+
if efs_vol_cfg and "file_system_id" not in efs_vol_cfg:
627+
efs_vol_cfg["file_system_id"] = efs_filesystem_id
628+
547629
# Build depends_on list
548630
depends_on = [*subnets]
549631
if container_sg:
@@ -552,6 +634,11 @@ def main():
552634
depends_on.append(lb_sg.resources["sg"])
553635
if fargate_app_task_role:
554636
depends_on.append(fargate_app_task_role)
637+
# EFS mount targets must exist before tasks that mount them
638+
if efs_mount_targets and service_name in efs_config.get(
639+
"ingress_from_services", []
640+
):
641+
depends_on.extend(efs_mount_targets)
555642

556643
fargate_services[service_name] = (
557644
tb_pulumi.fargate.FargateClusterWithLogging(
@@ -658,6 +745,129 @@ def main():
658745
)
659746
)
660747

748+
# =========================================================================
749+
# Amazon MQ - RabbitMQ (stage-only Celery broker)
750+
# =========================================================================
751+
# Dedicated stage broker replacing the production EC2 RabbitMQ that
752+
# atn/stage/celery_broker previously pointed to (issue #375)
753+
mq_config = resources.get("aws:mq:RabbitMQBroker", {})
754+
755+
if mq_config and private_subnets and vpc_resource:
756+
mq_creds_secret_name = mq_config.get("credentials_secret_name")
757+
mq_creds_raw = aws.secretsmanager.get_secret_version(
758+
secret_id=mq_creds_secret_name,
759+
)
760+
mq_creds = json.loads(mq_creds_raw.secret_string)
761+
mq_username = mq_creds["username"]
762+
mq_password = pulumi.Output.secret(mq_creds["password"])
763+
764+
# SG for the broker: AMQPS (5671) from container SGs,
765+
# management API (15671) from VPC CIDR for post-deploy bootstrap
766+
mq_sg = aws.ec2.SecurityGroup(
767+
f"{project.name_prefix}-mq-sg",
768+
name=f"{project.name_prefix}-mq",
769+
description="Amazon MQ RabbitMQ broker - AMQPS from Fargate containers",
770+
vpc_id=vpc_resource.id,
771+
tags={
772+
**project.common_tags,
773+
"Name": f"{project.name_prefix}-mq",
774+
},
775+
)
776+
777+
mq_ingress_services = mq_config.get("ingress_from_services", ["web", "worker"])
778+
for svc_name in mq_ingress_services:
779+
cont_sg = container_sgs.get(svc_name)
780+
if cont_sg:
781+
aws.ec2.SecurityGroupRule(
782+
f"{project.name_prefix}-mq-amqps-from-{svc_name}",
783+
type="ingress",
784+
security_group_id=mq_sg.id,
785+
from_port=5671,
786+
to_port=5671,
787+
protocol="tcp",
788+
source_security_group_id=cont_sg.resources["sg"].id,
789+
description=f"AMQPS from {svc_name} containers",
790+
)
791+
792+
aws.ec2.SecurityGroupRule(
793+
f"{project.name_prefix}-mq-mgmt-from-vpc",
794+
type="ingress",
795+
security_group_id=mq_sg.id,
796+
from_port=15671,
797+
to_port=15671,
798+
protocol="tcp",
799+
cidr_blocks=[vpc_config.get("cidr_block", "10.100.0.0/16")],
800+
description="RabbitMQ management API from VPC (post-deploy bootstrap)",
801+
)
802+
803+
aws.ec2.SecurityGroupRule(
804+
f"{project.name_prefix}-mq-egress",
805+
type="egress",
806+
security_group_id=mq_sg.id,
807+
from_port=0,
808+
to_port=0,
809+
protocol="-1",
810+
cidr_blocks=["0.0.0.0/0"],
811+
description="Allow all outbound",
812+
)
813+
814+
mq_broker = aws.mq.Broker(
815+
f"{project.name_prefix}-mq-broker",
816+
broker_name=mq_config.get("broker_name", f"{project.name_prefix}-rabbitmq"),
817+
engine_type="RABBITMQ",
818+
engine_version=mq_config.get("engine_version", "3.13"),
819+
host_instance_type=mq_config.get("host_instance_type", "mq.t3.micro"),
820+
deployment_mode=mq_config.get("deployment_mode", "SINGLE_INSTANCE"),
821+
publicly_accessible=mq_config.get("publicly_accessible", False),
822+
auto_minor_version_upgrade=mq_config.get(
823+
"auto_minor_version_upgrade", True
824+
),
825+
security_groups=[mq_sg.id],
826+
subnet_ids=[private_subnets[0].id],
827+
maintenance_window_start_time=aws.mq.BrokerMaintenanceWindowStartTimeArgs(
828+
day_of_week=mq_config.get("maintenance_day", "SUNDAY"),
829+
time_of_day=mq_config.get("maintenance_hour", "06:00"),
830+
time_zone="UTC",
831+
),
832+
users=[
833+
aws.mq.BrokerUserArgs(
834+
username=mq_username,
835+
password=mq_password,
836+
console_access=True,
837+
),
838+
],
839+
tags={
840+
**project.common_tags,
841+
"Name": mq_config.get("broker_name", f"{project.name_prefix}-rabbitmq"),
842+
},
843+
opts=pulumi.ResourceOptions(depends_on=[mq_sg]),
844+
)
845+
846+
pulumi.export("mq_broker_id", mq_broker.id)
847+
pulumi.export("mq_broker_arn", mq_broker.arn)
848+
pulumi.export(
849+
"mq_broker_amqps_endpoints",
850+
mq_broker.instances.apply(
851+
lambda instances: [
852+
ep
853+
for inst in (instances or [])
854+
for ep in (inst.endpoints or [])
855+
if "amqps" in ep
856+
]
857+
),
858+
)
859+
pulumi.export(
860+
"mq_broker_console_url",
861+
mq_broker.instances.apply(
862+
lambda instances: [
863+
ep
864+
for inst in (instances or [])
865+
for ep in (inst.endpoints or [])
866+
if "https" in ep
867+
]
868+
),
869+
)
870+
661871
# =========================================================================
662872
# ECS Scheduled Tasks (Cron Jobs)
663873
# =========================================================================
@@ -772,6 +982,13 @@ def main():
772982
"manage",
773983
"help",
774984
], # Default; again overridden per schedule
985+
"mountPoints": [
986+
{
987+
"sourceVolume": "addons-efs",
988+
"containerPath": "/var/addons",
989+
"readOnly": False,
990+
}
991+
],
775992
"environment": [
776993
{
777994
"name": "DJANGO_SETTINGS_MODULE",
@@ -803,7 +1020,20 @@ def main():
8031020
execution_role_arn=cron_execution_role.arn,
8041021
task_role_arn=cron_task_role.arn,
8051022
container_definitions=cron_container_def,
1023+
volumes=[
1024+
aws.ecs.TaskDefinitionVolumeArgs(
1025+
name="addons-efs",
1026+
efs_volume_configuration=aws.ecs.TaskDefinitionVolumeEfsVolumeConfigurationArgs(
1027+
file_system_id=efs_filesystem_id,
1028+
root_directory="/",
1029+
transit_encryption="ENABLED",
1030+
),
1031+
)
1032+
],
8061033
tags=project.common_tags,
1034+
opts=pulumi.ResourceOptions(
1035+
depends_on=efs_mount_targets if efs_mount_targets else None,
1036+
),
8071037
)
8081038

8091039
# ---------------------------------------------------------------------

0 commit comments

Comments
 (0)