AWSTemplateFormatVersion: '2010-09-09'
Description: >
  aws-enforza-boilerplate-multi-az-landing-zone:
  Creates a VPC, public/private subnets across two AZs, security groups, EC2 instances (firewalls), and associated route tables.

Parameters:
  enforzaCfnVersion:
    Type: String
    Default: 2024.09.25/release/0.4
    Description: aws-enforza-boilerplate-multi-az-landing-zone version

  VPCCidr:
    Type: String
    Default: 10.1.0.0/16
    Description: Enter the VPC CIDR (Must be a valid /16 CIDR block)
    AllowedPattern: ^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/16)$
    ConstraintDescription: Must be a valid /16 IPv4 CIDR block.

  TrustedManagementIPs:
    Type: CommaDelimitedList
    Default: 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
    Description: Trusted management IP ranges for SSH, ICMP, and SNMP.

  LatestAmiId:
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: '/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id'
    Description: SSM AMI image for the Enforza Gateway (Ubuntu Jammy)

Resources:
  # VPC Creation
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: enforza-vpc

  # Public Subnets
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [0, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: enforza-public-subnet-1

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [1, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: enforza-public-subnet-2

  # Create 6 private subnets across two AZs (3 per AZ)
  PrivateSubnet1AZ1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [2, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: enforza-private-subnet-1-az1

  PrivateSubnet2AZ1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [3, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: enforza-private-subnet-2-az1

  PrivateSubnet3AZ1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [4, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Name
          Value: enforza-private-subnet-3-az1

  PrivateSubnet1AZ2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [5, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: enforza-private-subnet-1-az2

  PrivateSubnet2AZ2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [6, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: enforza-private-subnet-2-az2

  PrivateSubnet3AZ2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [7, !Cidr [!Ref VPCCidr, 8, 8]]
      AvailabilityZone: !Select [1, !GetAZs '']
      Tags:
        - Key: Name
          Value: enforza-private-subnet-3-az2

  # Internet Gateway and routing
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: enforza-igw

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: enforza-public-route-table

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteTableAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  PublicSubnetRouteTableAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  # Security Group for EC2s
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable traffic from trusted IPs and VPC
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          CidrIp: !Ref VPCCidr
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Select [0, !Ref TrustedManagementIPs]
        - IpProtocol: icmp
          FromPort: -1
          ToPort: -1
          CidrIp: !Select [0, !Ref TrustedManagementIPs]
        - IpProtocol: udp
          FromPort: 161
          ToPort: 161
          CidrIp: !Select [0, !Ref TrustedManagementIPs]
      Tags:
        - Key: Name
          Value: enforza-sg

  # Create an IAM Role and Instance Profile for EC2 instances
  EC2InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: AllowTagEC2Instance
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateTags
                Resource:
                  - !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*'

  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref EC2InstanceRole

  EnforzaFirewallInstanceAZ1NetworkInterface:
    Type: AWS::EC2::NetworkInterface
    Properties:
      SubnetId: !Ref PublicSubnet1
      Description: "eni for EnforzaFirewallInstanceAZ1"
      GroupSet:
        - !Ref InstanceSecurityGroup
      SourceDestCheck: false
      Tags:
        - Key: Name
          Value: enforza-eni-az1

  EnforzaFirewallInstanceAZ1:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      ImageId: !Ref LatestAmiId
      NetworkInterfaces:
        - NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface
          DeviceIndex: 0
      Tags:
        - Key: Name
          Value: enforza-firewall-instance-az1
      IamInstanceProfile: !Ref EC2InstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          set -x  # Enable shell debugging

          exec > >(tee /var/log/enforza-cfn-install.log|logger -t user-data -s 2>/dev/console) 2>&1

          # Install AWS CLI and jq
          sudo apt-get update
          sudo apt-get install -y unzip curl jq || echo "Error installing jq and curl" >> /var/log/enforza-cfn-install.log

          # Download and install AWS CLI
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" || echo "Error downloading AWS CLI" >> /var/log/enforza-cfn-install.log
          unzip awscliv2.zip
          sudo ./aws/install || echo "Error installing AWS CLI" >> /var/log/enforza-cfn-install.log
          aws --version || echo "AWS CLI version check failed" >> /var/log/enforza-cfn-install.log
          jq --version || echo "jq version check failed" >> /var/log/enforza-cfn-install.log

          # Fetch instance metadata token and instance ID
          TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
          INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)

          # Log the instance ID
          echo "Instance ID: $INSTANCE_ID" >> /var/log/enforza-cfn-install.log

          # Add efzadmin user
          sudo useradd -m -s /bin/bash efzadmin
          echo "efzadmin:$INSTANCE_ID" | sudo chpasswd
          sudo usermod -aG sudo efzadmin

          # Modify the sudoers file to allow passwordless sudo for efzadmin
          echo 'efzadmin ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/efzadmin
          sudo chmod 440 /etc/sudoers.d/efzadmin
          echo "Passwordless sudo enabled for efzadmin" >> /var/log/enforza-cfn-install.log

          # Run the Enforza install script
          curl -s -L https://efz.io/install | sudo bash || echo "Error running Enforza install script" >> /var/log/enforza-cfn-install.log

          # Wait for the Enforza installation
          while [ ! -f /opt/enforza/enforza.info ]; do
              echo "Waiting for /opt/enforza/enforza.info" >> /var/log/enforza-cfn-install.log
              sleep 10
          done

          CLAIM_KEY=$(jq -r '.claimKey0' /opt/enforza/enforza.info)

          echo "Claim Key: $CLAIM_KEY" >> /var/log/enforza-cfn-install.log

          # Tag the EC2 instance with the Claim Key
          aws ec2 create-tags \
            --resources $INSTANCE_ID \
            --tags Key=EnforzaClaimKey,Value=$CLAIM_KEY \
                  Key=EnforzaSerialConsoleUsername,Value=efzadmin \
                  Key=EnforzaSerialConsolePassword,Value=$INSTANCE_ID || echo "Error tagging instance with additional keys" >> /var/log/enforza-cfn-install.log
  EnforzaFirewallInstanceAZ2NetworkInterface:
    Type: AWS::EC2::NetworkInterface
    Properties:
      SubnetId: !Ref PublicSubnet2
      Description: "eni for EnforzaFirewallInstanceAZ2"
      GroupSet:
        - !Ref InstanceSecurityGroup
      SourceDestCheck: false
      Tags:
        - Key: Name
          Value: enforza-eni-az2

  EnforzaFirewallInstanceAZ2:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      ImageId: !Ref LatestAmiId
      NetworkInterfaces:
        - NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ2NetworkInterface
          DeviceIndex: 0
      Tags:
        - Key: Name
          Value: enforza-firewall-instance-az2
      IamInstanceProfile: !Ref EC2InstanceProfile
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          set -x  # Enable shell debugging

          exec > >(tee /var/log/enforza-cfn-install.log|logger -t user-data -s 2>/dev/console) 2>&1

          # Install AWS CLI and jq
          sudo apt-get update
          sudo apt-get install -y unzip curl jq || echo "Error installing jq and curl" >> /var/log/enforza-cfn-install.log

          # Download and install AWS CLI
          curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" || echo "Error downloading AWS CLI" >> /var/log/enforza-cfn-install.log
          unzip awscliv2.zip
          sudo ./aws/install || echo "Error installing AWS CLI" >> /var/log/enforza-cfn-install.log
          aws --version || echo "AWS CLI version check failed" >> /var/log/enforza-cfn-install.log
          jq --version || echo "jq version check failed" >> /var/log/enforza-cfn-install.log

          # Fetch instance metadata token and instance ID
          TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
          INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-id)

          # Log the instance ID
          echo "Instance ID: $INSTANCE_ID" >> /var/log/enforza-cfn-install.log

          # Add efzadmin user
          sudo useradd -m -s /bin/bash efzadmin
          echo "efzadmin:$INSTANCE_ID" | sudo chpasswd
          sudo usermod -aG sudo efzadmin

          # Modify the sudoers file to allow passwordless sudo for efzadmin
          echo 'efzadmin ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/efzadmin
          sudo chmod 440 /etc/sudoers.d/efzadmin
          echo "Passwordless sudo enabled for efzadmin" >> /var/log/enforza-cfn-install.log

          # Run the Enforza install script
          curl -s -L https://efz.io/install | sudo bash || echo "Error running Enforza install script" >> /var/log/enforza-cfn-install.log

          # Wait for the Enforza installation
          while [ ! -f /opt/enforza/enforza.info ]; do
              echo "Waiting for /opt/enforza/enforza.info" >> /var/log/enforza-cfn-install.log
              sleep 10
          done

          CLAIM_KEY=$(jq -r '.claimKey0' /opt/enforza/enforza.info)

          echo "Claim Key: $CLAIM_KEY" >> /var/log/enforza-cfn-install.log

          # Tag the EC2 instance with the Claim Key
          aws ec2 create-tags \
            --resources $INSTANCE_ID \
            --tags Key=EnforzaClaimKey,Value=$CLAIM_KEY \
                  Key=EnforzaSerialConsoleUsername,Value=efzadmin \
                  Key=EnforzaSerialConsolePassword,Value=$INSTANCE_ID || echo "Error tagging instance with additional keys" >> /var/log/enforza-cfn-install.log
  # Private Route Table
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: enforza-private-route-table

  PrivateRouteAZ1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

 # Associate Private Subnets with Private Route Table
  PrivateSubnetRouteTableAssociation1AZ1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1AZ1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetRouteTableAssociation2AZ1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2AZ1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetRouteTableAssociation3AZ1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet3AZ1
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetRouteTableAssociation1AZ2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1AZ2
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetRouteTableAssociation2AZ2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2AZ2
      RouteTableId: !Ref PrivateRouteTable

  PrivateSubnetRouteTableAssociation3AZ2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet3AZ2
      RouteTableId: !Ref PrivateRouteTable

 # Add Routes for Private Subnets via Firewall ENI (AZ1)
  PrivateRouteForPrivateSubnet1AZ1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: !GetAtt PrivateSubnet1AZ1.CidrBlock
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

  PrivateRouteForPrivateSubnet2AZ1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: !GetAtt PrivateSubnet2AZ1.CidrBlock
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

  PrivateRouteForPrivateSubnet3AZ1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: !GetAtt PrivateSubnet3AZ1.CidrBlock
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

  PrivateRouteForPrivateSubnet1AZ2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: !GetAtt PrivateSubnet1AZ2.CidrBlock
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

  PrivateRouteForPrivateSubnet2AZ2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: !GetAtt PrivateSubnet2AZ2.CidrBlock
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

  PrivateRouteForPrivateSubnet3AZ2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: !GetAtt PrivateSubnet3AZ2.CidrBlock
      NetworkInterfaceId: !Ref EnforzaFirewallInstanceAZ1NetworkInterface

###########################
  # IAM Role for Lambda functions to manage EC2 and invoke other Lambdas
  LambdaIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeRouteTables
                  - ec2:ReplaceRoute
                  - ec2:DeleteRoute
                  - ec2:CreateNetworkInterface
                  - ec2:DescribeNetworkInterfaces
                  - ec2:DeleteNetworkInterface
                  - lambda:InvokeFunction
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"

  # Master Lambda: EnforzaGatewayCheckerSupervisor (Not VPC-connected)
  EnforzaGatewayCheckerSupervisor:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: EnforzaGatewayCheckerSupervisor
      Runtime: python3.9
      Handler: index.lambda_handler
      Code:
        ZipFile: |
          import os
          import boto3
          import logging
          from botocore.exceptions import ClientError

          # Initialize logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          lambda_client = boto3.client('lambda')
          ec2_client = boto3.client('ec2')

          # Environment variables
          PRIVATE_ROUTE_TABLE_ID = os.environ['PRIVATE_ROUTE_TABLE_ID']
          ENI_AZ1 = os.environ['ENI_AZ1']
          ENI_AZ2 = os.environ['ENI_AZ2']
          SLAVE_LAMBDA_AZ1 = os.environ['SLAVE_LAMBDA_AZ1']
          PRIVATE_SUBNET_CIDRS = os.environ['PRIVATE_SUBNET_CIDRS'].split(',')

          def get_current_next_hop():
              """Gets the current next hop (active ENI) from the route table."""
              try:
                  route_tables = ec2_client.describe_route_tables(RouteTableIds=[PRIVATE_ROUTE_TABLE_ID])
                  for route in route_tables['RouteTables'][0]['Routes']:
                      if route['DestinationCidrBlock'] == '0.0.0.0/0':
                          return route.get('NetworkInterfaceId')
              except ClientError as e:
                  logger.error(f"Error retrieving next hop from route table: {e}")
                  return None

          def invoke_slave_lambda(slave_lambda_name):
              """Invoke a VPC-connected slave Lambda to check internet connectivity."""
              try:
                  logger.info(f"Invoking {slave_lambda_name}")
                  response = lambda_client.invoke(
                      FunctionName=slave_lambda_name,
                      InvocationType='RequestResponse'  # Synchronous call
                  )
                  payload = response['Payload'].read().decode('utf-8')
                  logger.info(f"{slave_lambda_name} response payload: {payload}")
                  
                  # Explicitly convert the payload string to a boolean
                  if payload.strip() == "true":
                      return True
                  elif payload.strip() == "false":
                      return False
                  else:
                      logger.error(f"Unexpected response from {slave_lambda_name}: {payload}")
                      return False  # Treat unexpected responses as failures

              except ClientError as e:
                  logger.error(f"Error invoking {slave_lambda_name}: {e}")
                  return False

          def update_route_table(eni_id, private_subnet_cidrs):
              """Update the next hop for both the default route and private subnet CIDRs in the route table."""
              try:
                  # Update the default route (0.0.0.0/0)
                  ec2_client.replace_route(
                      RouteTableId=PRIVATE_ROUTE_TABLE_ID,
                      DestinationCidrBlock='0.0.0.0/0',
                      NetworkInterfaceId=eni_id
                  )
                  logger.info(f"Default route updated to use ENI: {eni_id}")

                  # Update the private subnet CIDR range routes
                  for cidr in private_subnet_cidrs:
                      ec2_client.replace_route(
                          RouteTableId=PRIVATE_ROUTE_TABLE_ID,
                          DestinationCidrBlock=cidr,
                          NetworkInterfaceId=eni_id
                      )
                      logger.info(f"Route for {cidr} updated to use ENI: {eni_id}")

              except ClientError as e:
                  logger.error(f"Failed to update route: {e}")

          def lambda_handler(event, context):
              current_next_hop = get_current_next_hop()

              if current_next_hop == ENI_AZ1:
                  logger.info("AZ1 firewall is currently active. Checking connectivity...")
                  az1_status = invoke_slave_lambda(SLAVE_LAMBDA_AZ1)

                  if not az1_status:
                      # If AZ1 is down, flip to AZ2
                      logger.warning("AZ1 is down, flipping to AZ2.")
                      update_route_table(ENI_AZ2, PRIVATE_SUBNET_CIDRS)

              elif current_next_hop == ENI_AZ2:
                  logger.info("AZ2 firewall is currently active. Checking connectivity...")
                  az1_status = invoke_slave_lambda(SLAVE_LAMBDA_AZ1)

                  if not az1_status:
                      # If AZ2 is down, flip to AZ1
                      logger.warning("AZ2 is down, flipping to AZ1.")
                      update_route_table(ENI_AZ1, PRIVATE_SUBNET_CIDRS)

              else:
                  logger.error("No active firewall found in the route table.")
                  return

      Timeout: 30
      Role: !GetAtt LambdaIAMRole.Arn
      Environment:
        Variables:
          PRIVATE_ROUTE_TABLE_ID: !Ref PrivateRouteTable
          ENI_AZ1: !Ref EnforzaFirewallInstanceAZ1NetworkInterface
          ENI_AZ2: !Ref EnforzaFirewallInstanceAZ2NetworkInterface
          SLAVE_LAMBDA_AZ1: !Ref EnforzaGatewayCheckerAZWorkerAZ1
          PRIVATE_SUBNET_CIDRS: !Join [",", [
            !GetAtt PrivateSubnet1AZ1.CidrBlock,
            !GetAtt PrivateSubnet2AZ1.CidrBlock,
            !GetAtt PrivateSubnet3AZ1.CidrBlock,
            !GetAtt PrivateSubnet1AZ2.CidrBlock,
            !GetAtt PrivateSubnet2AZ2.CidrBlock,
            !GetAtt PrivateSubnet3AZ2.CidrBlock
          ]]
  # Slave Lambda for AZ1 (VPC-connected): EnforzaGatewayCheckerAZWorkerAZ1
  EnforzaGatewayCheckerAZWorkerAZ1:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: EnforzaGatewayCheckerAZWorkerAZ1
      Runtime: python3.9
      Handler: index.lambda_handler
      Code:
        ZipFile: |
          import socket
          import logging

          # Initialize logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)

          def build_dns_query(domain):
              """Builds a DNS query packet for the given domain."""
              # Transaction ID: 2 bytes (randomized)
              transaction_id = b'\xaa\xbb'

              # Flags: 2 bytes
              flags = b'\x01\x00'  # Standard query, recursion desired

              # Questions: 2 bytes (1 question)
              questions = b'\x00\x01'

              # Answer RRs, Authority RRs, Additional RRs: 2 bytes each (all 0)
              answer_rrs = b'\x00\x00'
              authority_rrs = b'\x00\x00'
              additional_rrs = b'\x00\x00'

              # Query section: domain name in DNS format, type A (IPv4), class IN (internet)
              query = b''.join([bytes([len(part)]) + part.encode() for part in domain.split('.')]) + b'\x00'  # End with null byte
              query_type = b'\x00\x01'  # Type A query (IPv4)
              query_class = b'\x00\x01'  # Class IN (internet)

              return transaction_id + flags + questions + answer_rrs + authority_rrs + additional_rrs + query + query_type + query_class

          def check_dns_lookup(domain):
              """Attempts to perform a DNS lookup for a domain using 8.8.8.8 (UDP)."""
              try:
                  # Set up a UDP socket
                  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                  sock.settimeout(3)  # 3-second timeout

                  # Google DNS server (8.8.8.8) on port 53
                  dns_server = ('8.8.8.8', 53)
                  
                  # Build the DNS query for the given domain
                  dns_query = build_dns_query(domain)
                  
                  # Send the DNS query to the server
                  logger.info(f"Sending DNS query for {domain} to 8.8.8.8...")
                  sock.sendto(dns_query, dns_server)
                  
                  # Wait for the response
                  data, addr = sock.recvfrom(512)  # Buffer size 512 bytes
                  
                  # If a response is received, log it and return success
                  logger.info(f"Received DNS response from {addr}")
                  sock.close()
                  return True
              
              except socket.timeout:
                  logger.error("DNS lookup timed out. No response from 8.8.8.8.")
                  return False
              
              except socket.error as e:
                  logger.error(f"Socket error occurred during DNS lookup: {e}")
                  return False
              
              except Exception as e:
                  logger.error(f"An unexpected error occurred: {e}")
                  return False

          def lambda_handler(event, context):
              # Perform a DNS lookup for www.google.com using 8.8.8.8
              return check_dns_lookup("www.google.com")

      Timeout: 30
      Role: !GetAtt LambdaIAMRole.Arn
      VpcConfig:
        SubnetIds:
          - !Ref PrivateSubnet1AZ1
        SecurityGroupIds:
          - !Ref InstanceSecurityGroup


  # Log Group for EnforzaGatewayCheckerSupervisor
  EnforzaGatewayCheckerSupervisorLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${EnforzaGatewayCheckerSupervisor}"
      RetentionInDays: 3

  # Log Group for EnforzaGatewayCheckerAZWorkerAZ1
  EnforzaGatewayCheckerAZWorkerAZ1LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${EnforzaGatewayCheckerAZWorkerAZ1}"
      RetentionInDays: 3

  # CloudWatch Event Rule to trigger EnforzaGatewayCheckerSupervisor every minute
  EnforzaGatewayCheckerSupervisorSchedule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: "rate(1 minute)"
      Targets:
        - Arn: !GetAtt EnforzaGatewayCheckerSupervisor.Arn
          Id: EnforzaGatewayCheckerSupervisor

  # Grant permission for CloudWatch Events to invoke the supervisor Lambda
  EnforzaGatewayCheckerSupervisorInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref EnforzaGatewayCheckerSupervisor
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EnforzaGatewayCheckerSupervisorSchedule.Arn

Outputs:
  VPCId:
    Description: The ID of the VPC
    Value: !Ref VPC
  PublicSubnet1Id:
    Description: The ID of the first public subnet
    Value: !Ref PublicSubnet1
  PublicSubnet2Id:
    Description: The ID of the second public subnet
    Value: !Ref PublicSubnet2
  PrivateSubnet1AZ1Id:
    Description: The ID of the first private subnet in AZ1
    Value: !Ref PrivateSubnet1AZ1
  PrivateSubnet1AZ2Id:
    Description: The ID of the first private subnet in AZ2
    Value: !Ref PrivateSubnet1AZ2
