CloudFormation nested stack with Cognito, ALB, EC2, Route53 for different environments
Published: 2023-09-07 06:00:00

**Introduction:**

AWS offers a large amount of services to create scalable and secure applications. One of the key challenges in this realm is crafting an infrastructure that not only facilitates seamless user access but also manages data security and authentication. In this blog post, we delve into architecting an AWS infrastructure using CloudFormation. We’ll walk through the process of setting up an environment with EC2 instance, Security Groups, Route53, Application Load Balancer, ALB Listeners and Target Groups, and Amazon Cognito User Pools.

**About the project:**

The main idea of the project is to separate and secure access to EC2 instance for different categories of users. We have 1 user for the development environment and 1 user for the testing environment and every user can sign in only to his environment. All infrastructure in AWS (except the S3 bucket) is created with CloudFormation. S3 bucket is necessary for storing nested stack templates, more information about nested stack is [here](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-nested-stacks.html). The EC2 instance has preinstalled and preconfigured IIS service under */development *and */testing *paths. We configured user data and public key material as Parameters and defined that during the process of creating the CloudFormation stack. Cognito user email addresses should be valid because temporary password will be sent on these emails, more information about Amazon Cognito is [here](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html). GitLab repository with all nested stack templates is [here](https://gitlab.com/Andr1500/cognito_alb_ec2.git).

In short, the process works as follows:

The development or testing user accesses Environment by pointing the browser to [https://instance.example.com/environment](https://instance.example.com/environment).
The ALB redirects the user to the login page provided by AWS Cognito User Pool.
The engineer authenticates with username, one-time-password, and provides a new password.
AWS Cognito User Pool redirects the engineer to [https://instance.example.com/environment](https://instance.example.com/environment).
The ALB verifies the authentication information and forwards the request to the necessary target group.

The project structure:

    
    ├── nestedALB.yaml
    ├── nestedCognito.yaml
    ├── nestedEC2.yaml
    ├── nestedNSG.yaml
    ├── nestedRoute53.yaml
    ├── root.yaml
    └── user_data.txt

root.yaml template:

    AWSTemplateFormatVersion: '2010-09-09'
    Description: Deploy infrastructure with EC2, ALB, Cognito, Route53
    
    Parameters:
      TemplateUrlBase: 
        Type: String
        Description: Base URL for CloudFormation templates
        Default: ''
      VpcId:
        Type: AWS::EC2::VPC::Id
        Description: ID of VPC
        Default: ''
      SubnetIds:
        Type: String
        Description: Comma-separated IDs of the subnets (minimum 2)
        Default: ''
      PublicDomainName:
        Type: String
        Description: The custom domain name
        Default: ''
      CertificateArn:
        Type: String
        Description: The ARN of a ACM certificate valid for the custom domain name
        Default: ''
      AmiID:
        Type: String
        Description: The ID of the AMI image
        Default: ''
      UserData:
        Type: String
        Description: User Data content for the ec2 instance
        Default: ''
      PublicKeyMaterial:
        Type: String
        Description: SSH Public Key content
        Default: ''
      CognitoUserEmailDev:
        Type: String
        Description: Email address for dev. Cognito user
        Default: ''
      CognitoUserEmailTest:
        Type: String
        Description: Email address for test Cognito user
        Default: ''
    
    Resources:
      nestedNSG:
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: !Sub '${TemplateUrlBase}/nestedNSG.yaml'
          Parameters:
            VpcId: !Ref VpcId
    
      nestedEC2:
        DependsOn:
          - nestedNSG
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: !Sub '${TemplateUrlBase}/nestedEC2.yaml'
          Parameters:
            UserData: !Ref UserData
            PublicKeyMaterial: !Ref PublicKeyMaterial
            SubnetIds: !Ref SubnetIds
            AmiID: !Ref AmiID
    
      nestedCognitoDev:
        DependsOn:
          - nestedEC2
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: !Sub '${TemplateUrlBase}/nestedCognito.yaml'
          Parameters:
            Environment: 'dev'
            PublicDomainName: !Ref PublicDomainName
            CognitoUserEmail: !Ref CognitoUserEmailDev
    
      nestedCognitoTest:
        DependsOn:
          - nestedEC2
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: !Sub '${TemplateUrlBase}/nestedCognito.yaml'
          Parameters:
            Environment: 'test'
            PublicDomainName: !Ref PublicDomainName
            CognitoUserEmail: !Ref CognitoUserEmailTest
    
      nestedALB:
        DependsOn:
          - nestedNSG
          - nestedEC2
          - nestedCognitoDev
          - nestedCognitoTest
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: !Sub '${TemplateUrlBase}/nestedALB.yaml'
          Parameters:
            VpcId: !Ref VpcId
            SubnetIds: !Ref SubnetIds
            CertificateArn: !Ref CertificateArn
    
      nestedRoute53:
        DependsOn:
          - nestedALB  
        Type: AWS::CloudFormation::Stack
        Properties:
          TemplateURL: !Sub '${TemplateUrlBase}/nestedRoute53.yaml'
          Parameters:
            PublicDomainName: !Ref PublicDomainName

nestedALB.yaml template:

    AWSTemplateFormatVersion: '2010-09-09'
    Description: ALB with Target Groups and Listeners
    
    Parameters:
      VpcId:
        Type: AWS::EC2::VPC::Id
      SubnetIds:
        Type: List<AWS::EC2::Subnet::Id>
      CertificateArn:
        Type: String
    
    Resources:
      # development TG for access to EC2
      TargetGroupDev:
        Type: AWS::ElasticLoadBalancingV2::TargetGroup
        Properties:
          HealthCheckEnabled: true
          HealthCheckIntervalSeconds: 30
          HealthCheckProtocol: HTTP
          HealthCheckPort: 80
          HealthCheckPath: "/development"
          Name: 'TG-access-to-ec2-dev'
          TargetType: instance
          Targets:
            - Id: !ImportValue WSWebAppInstanceID
          Protocol: HTTP
          Port: 80
          VpcId: !Ref VpcId
          Tags:
            - Key: Name
              Value: 'TG-access-to-ec2-dev'
    
      # testing TG for access to EC2
      TargetGroupTest:
        Type: AWS::ElasticLoadBalancingV2::TargetGroup
        Properties:
          HealthCheckEnabled: true
          HealthCheckIntervalSeconds: 30
          HealthCheckProtocol: HTTP
          HealthCheckPort: 80
          HealthCheckPath: "/testing"
          Name: 'TG-access-to-ec2-test'
          TargetType: instance
          Targets:
            - Id: !ImportValue WSWebAppInstanceID
          Protocol: HTTP
          Port: 80
          VpcId: !Ref VpcId
          Tags:
            - Key: Name
              Value: 'TG-access-to-ec2-test'
    
      # ALB
      ApplicationLoadBalancer:
        Type: AWS::ElasticLoadBalancingV2::LoadBalancer
        Properties:
          Name: ApplicationLoadBalancer
          Subnets: !Ref SubnetIds
          SecurityGroups:
            - !ImportValue  AlbSecurityGroupId
          Type: application
          Scheme: internet-facing
          LoadBalancerAttributes:
            - Key: idle_timeout.timeout_seconds
              Value: '300'
          Tags:
            - Key: Name
              Value: ApplicationLoadBalancer
    
    
      # ALB Listener redirects to HTTPS
      AlbListenerHTTP:
        Type: AWS::ElasticLoadBalancingV2::Listener
        Properties:
          DefaultActions:
            - Type: redirect 
              RedirectConfig:
                Host: "#{host}"
                Path: "/#{path}"
                Port: "443"
                Protocol: HTTPS
                Query: "#{query}"
                StatusCode: HTTP_301
          LoadBalancerArn: !Ref ApplicationLoadBalancer
          Port: 80
          Protocol: HTTP
    
      # ALB listener on HTTPS
      AlbListenerHTTPS:
        Type: AWS::ElasticLoadBalancingV2::Listener
        Properties:
          LoadBalancerArn: !Ref ApplicationLoadBalancer
          Port: 443
          Protocol: HTTPS
          SslPolicy: 'ELBSecurityPolicy-TLS13-1-2-2021-06'
          Certificates:
            - CertificateArn: !Ref CertificateArn
          DefaultActions:
            - Type: fixed-response
              FixedResponseConfig:
                ContentType: "text/plain"
                MessageBody: "Page not found"
                StatusCode: "404"
    
      # Listener rules for development access
      ListenerRuleDev:
        Type: AWS::ElasticLoadBalancingV2::ListenerRule
        Properties:
          ListenerArn: !Ref AlbListenerHTTPS
          Priority: 1
          Conditions:
            - Field: path-pattern
              Values:
                - /development
                - /development/*
          Actions:
            - Type: authenticate-cognito
              Order: 1
              AuthenticateCognitoConfig:
                UserPoolArn: !ImportValue dev-UserPoolArn
                UserPoolClientId: !ImportValue dev-UserPoolClientId
                UserPoolDomain: !ImportValue dev-UserPoolDomain
                SessionCookieName: devAppCookie
                OnUnauthenticatedRequest: authenticate
            - Type: forward
              Order: 2
              TargetGroupArn: !Ref TargetGroupDev
    
      # Listener rules for test access
      ListenerRuleTest:
        Type: AWS::ElasticLoadBalancingV2::ListenerRule
        Properties:
          ListenerArn: !Ref AlbListenerHTTPS
          Priority: 2
          Conditions:
            - Field: path-pattern
              Values:
                - /testing
                - /testing/*
          Actions:
            - Type: authenticate-cognito
              Order: 1
              AuthenticateCognitoConfig:
                UserPoolArn: !ImportValue test-UserPoolArn
                UserPoolClientId: !ImportValue test-UserPoolClientId
                UserPoolDomain: !ImportValue test-UserPoolDomain
                SessionCookieName: testAppCookie
                OnUnauthenticatedRequest: authenticate
            - Type: forward
              Order: 2
              TargetGroupArn: !Ref TargetGroupTest
    Outputs:
      LoadBalancerDNSName:
        Value: !GetAtt ApplicationLoadBalancer.DNSName
        Description: The DNS name of the load balancer
        Export:
          Name: LoadBalancerDNSName
      AlbCanonicalHostedZoneID:
        Value: !GetAtt ApplicationLoadBalancer.CanonicalHostedZoneID
        Description: The host ID of the ALB
        Export:
          Name: AlbCanonicalHostedZoneID

nestedCognito.yaml template:

    AWSTemplateFormatVersion: '2010-09-09'
    Description: AWS Cognito configuration
    
    Parameters:
      Environment:
        Type: String
      PublicDomainName:
        Type: String
      CognitoUserEmail:
        Type: String
    
    
    Resources:
      UserPool:
        Type: 'AWS::Cognito::UserPool'
        Properties:
          UserPoolName: !Sub '${Environment}-user-pool-ec2-web-access'
          AdminCreateUserConfig:
            AllowAdminCreateUserOnly: true
          AutoVerifiedAttributes:
          - email
          UsernameAttributes:
          - email
          Policies:
            PasswordPolicy:
              MinimumLength: 20
              RequireLowercase: true
              RequireNumbers: true
              RequireSymbols: true
              RequireUppercase: true
              TemporaryPasswordValidityDays: 10
    
      # Define Cognito User Pool Domain
      UserPoolDomain:
        Type: AWS::Cognito::UserPoolDomain
        Properties:
          Domain: !Sub
            - ${Environment}-${StackId}
            - StackId: !Select [2, !Split ['/', !Ref 'AWS::StackId']]
          UserPoolId: !Ref UserPool
    
      UserPoolClient:
        Type: 'AWS::Cognito::UserPoolClient'
        Properties:
          RefreshTokenValidity: 30
          IdTokenValidity: 30
          AccessTokenValidity: 30
          TokenValidityUnits: 
            AccessToken: minutes
            IdToken: minutes
            RefreshToken: days
          AllowedOAuthFlows:
            - code
          AllowedOAuthFlowsUserPoolClient: true
          AllowedOAuthScopes:
            - email
            - openid
            - profile
            - aws.cognito.signin.user.admin
          CallbackURLs:
            - !Sub https://instance.${PublicDomainName}/oauth2/idpresponse
          SupportedIdentityProviders:
            - COGNITO
          GenerateSecret: true
          UserPoolId: !Ref UserPool
    
      # create user 
      UserPoolUser:
        Type: 'AWS::Cognito::UserPoolUser'
        Properties:
          UserPoolId: !Ref UserPool
          Username: !Ref CognitoUserEmail
          DesiredDeliveryMediums:
            - EMAIL
          UserAttributes:
            - Name: email
              Value: !Ref CognitoUserEmail
            - Name: email_verified
              Value: "true"
    
    Outputs:
      UserPoolArn:
        Value: !GetAtt UserPool.Arn
        Description: ARN of the User pool
        Export:
          Name: !Sub '${Environment}-UserPoolArn'
      UserPoolClientId:
        Value: !Ref UserPoolClient
        Description: ID of  Userpool client
        Export:
          Name: !Sub '${Environment}-UserPoolClientId'
      UserPoolDomain:
        Value: !Ref UserPoolDomain
        Description: Domain of Userpool
        Export:
          Name: !Sub '${Environment}-UserPoolDomain'

The infrastructure schema:

![The infrastructure schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4tv4ygqljj7grvt3ww0.png)

**Prerequisites:**

Before you start, make sure the following requirements are met:

- An AWS account with permissions to create resources, a DNS domain in Route53, and a SSL/TLS certificate in Certificate Manager.

- AWS CLI installed on your local machine.

**Deployment:**

1. Clone the repository.
`git clone https://gitlab.com/Andr1500/cognito_alb_ec2.git`

2. Create an S3 bucket for nested stack templates.
`aws s3api create-bucket --bucket bucket-name --region YOUR_REGION \
--create-bucket-configuration LocationConstraint=YOUR_REGION`

3. Fill in all necessary Parameters in root.yaml file and send all nested stack files to the S3 bucket.
take AMI:
`aws ec2 describe-images \
--filters "Name=name,Values=Windows_Server-2022-English-Full-Base-*" "Name=architecture,Values=x86_64" "Name=root-device-type,Values=ebs" "Name=virtualization-type,Values=hvm" \
--query "Images | sort_by(@, &CreationDate) | [-1]" \
--output json`
send files to the S3 bucket:
`aws s3 cp . s3://bucket-name --recursive --exclude ".git/*" \
--exclude README.md --exclude user_data.txt --exclude ".images/*"`

4. Create ssh key and convert it into RSA format (if you don’t have it).
generation ssh key and converting the key into RSA format:
`ssh-keygen -t rsa -b 2048 -f ~/.ssh/ws-ec2-keypair
ssh-keygen -p -m PEM -f ~/.ssh/ws-ec2-keypair`

5. Create a CloudFormation stack.
create stack:
`aws cloudformation create-stack \
--stack-name cognito-alb-ec2 \
--template-body file://root.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=UserData,ParameterValue="$(base64 -i user_data.txt)" \
ParameterKey=PublicKeyMaterial,ParameterValue="$(cat ~/.ssh/ws-ec2-keypair.pub)" \
--capabilities CAPABILITY_NAMED_IAM \
--disable-rollback`
take the EC2 instance password for Admin access:
`aws ec2 get-password-data --instance-id  i-1234567890abcdef0 \
--priv-launch-key C:\Keys\ws-ec2.pem`

6. Browse to your custom domain name (e.g., instance.example.com) with the necessary path ( /development or /testing), and log in with the necessary user.

7. Delete the CloudFormation stack
`aws cloudformation delete-stack --stack-name cognito-alb-ec2`

8. Delete all files from the S3 bucket
`aws s3 rm s3://bucket-name--recursive`

9. delete the S3 bucket
`aws s3 rb s3://bucket-name --force`

**Conclusion:**

Usage of authentication on the load balancer add another layer of security to Web services handled on EC2 instances. Doing so reduced the attack surface to infrastructure. Implementing ALB authentication can be quite simple by using Cognito User Pools.

**If you found this post helpful and interesting, please click the reaction button below to show your support for the author. Feel free to use and share this post!
You can also support me with a virtual coffee [https://www.buymeacoffee.com/andrworld1500](https://www.buymeacoffee.com/andrworld1500) .**