@@ -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