DEV Community

Cansu Tekin
Cansu Tekin

Posted on • Originally published at cansu-tekin.hashnode.dev on

Building a Scalable Web App with AWS Elastic Beanstalk, DynamoDB, CloudFront, and Edge Location - with AWS Dashboard and EB CLI

In this real-world project, I was tasked with implementing an application capable of supporting a high volume of simultaneous users. This application was utilized during a large conference attended by over 10,000 people, both in-person and online, from around the globe. The event featured live broadcasts and the drawing of 10 vouchers for 3 Cloud certifications. At the peak moment, more than 10,000 audience members registered their emails to participate in the raffle.

We used AWS, Elastic Beanstalk services to deploy the web application, DynamoDB to store emails, and CloudFront to cache static and dynamic files in an Edge Location close to the user.

Solution Architecture

Part 1: Create a table in DynamoDB to store users’ email addresses and deploy the application using Elastic Beanstalk

Part 2: Create a CloudFront distribution

Part 3: Perform Load testing

Part 1: Create a table in DynamoDB to store users’ email addresses and deploy the application using Elastic Beanstalk

Create a table in DynamoDB to store users’ email addresses and deploy the application using Elastic Beanstalk, which will provision infrastructures such as EC2, Elastic Load Balancer, and Auto Scaling group.

  1. Create a table in DynamoDB to store users’ email addresses

Amazon DynamoDB is a fully managed, high-performance, and highly scalable NoSQL database service designed to handle large-scale data loads and ensure low-latency responses.

Search for DynamoDB in the AWS console and create a table “users”. Leave anything else as default.

A partition key is a unique attribute in a DynamoDB table that determines the partition in which the data is stored. Each item in the table must have a unique value for the partition key.

Table name: users

Partition key: email, type String

2. Create an Elastic Beanstalk Application

Amazon Elastic Beanstalk is an easy-to-use service for deploying and scaling web applications and services developed with Java, .NET, PHP, Node.js, Python, Ruby, Go, and Docker on familiar servers such as Apache, Nginx, Passenger, and IIS.

Many people will try accessing our application from a mobile or desktop to register. At this stage, the application needs to be robust and scalable to handle high traffic from many users. Elastic Beanstalk allows us to deploy and manage the web application in AWS Cloud without worrying about the infrastructure. It simplifies the process by provisioning and managing essential AWS resources like EC2 instances, Elastic Load Balancers, and Auto Scaling groups, ensuring the application remains responsive and available under varying loads. We will upload the application files, and Elastic Beanstalk will automatically manage capacity provisioning, load balancing, and scaling.

Elastic Beanstalk will use the provided application files to deploy the application. It is important to organize the application folders in a way that Elastic Beanstalk can understand. Check the Elastic Beanstalk documentation before uploading the application files. Each application requires its own folder structure. This project’s folders and files were designed specifically for the Python application.

application.py

from flask import Flask, render_template, flash, redirect, url_for, session, request, logging
from wtforms import Form, StringField, TextAreaField, PasswordField, validators
from wtforms.validators import InputRequired, Email
import boto3
import os
from urllib.parse import quote as url_quote

application = Flask(__name__)

#dynamodb = boto3.resource('dynamodb', endpoint_url="http://localhost:8000")
region = os.environ['AWS_REGION']
dynamodb = boto3.resource('dynamodb',region_name=region)

def put_user(email):
    table = dynamodb.Table('users')
    response = table.put_item(
       Item={
            'email': email
        }
    )
    return response

# Index
@application.route('/', methods=['GET', 'POST'])
def index():
    form = RegisterForm(request.form)
    if request.method == 'POST' and form.validate():
        user_resp = put_user(form.email.data)
        return render_template('obrigado.html')

    return render_template('index.html', form=form)

# Register Form Class
class RegisterForm(Form):
    email = StringField('Email', [InputRequired("Please enter your email.")])

if __name__ == '__main__':
    application.secret_key='secret123'
    application.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

requirements.txt

boto3==1.21.8
botocore==1.24.8
Flask==2.0.3
passlib==1.7.2
WTForms==2.3.3
jsons==1.6.1
itsdangerous==2.1.0
Werkzeug==2.0.3
Enter fullscreen mode Exit fullscreen mode

Proper organization of the application files helps Elastic Beanstalk deploy the application automatically. Structure your project, zip, and store the zip file in a S3 bucket or locally. Elastic Beanstalk will use it to deploy the application.

3. Configure environment

Search for Elastic Beanstalk in the AWS console and “Create Application”.

Application name: tcb-conference

Platform (runtime environment): Python 3.8

Application code: Upload the application file code

Presets: High availability

It launches an environment named tcb-conference-env with these AWS resources:

  • An Amazon Elastic Compute Cloud (Amazon EC2): An Amazon EC2 virtual machine configured to run web apps on the platform you choose.

  • An Amazon EC2 security group: An Amazon EC2 security group configured to allow incoming traffic on port 80. This resource lets HTTP traffic from the load balancer to the EC2 instance running your web app.

  • An Amazon Simple Storage Service (Amazon S3) bucket: A storage location for your source code, logs, and other artifacts that are created when you use Elastic Beanstalk.

  • Amazon CloudWatch alarms: Two CloudWatch alarms that monitor the load on the instances in your environment and are triggered if the load is too high or too low. When an alarm is triggered, your Auto Scaling group scales up or down in response.

  • An AWS CloudFormation stack: Elastic Beanstalk uses AWS CloudFormation to launch the resources in your environment and propagate configuration changes. The resources are defined in a template that you can view in the AWS CloudFormation console.

  • A domain name (autogenerated in our case): A domain name that routes to your web app in the form subdomain.region.elasticbeanstalk.com.

We do not need to specify the S3 bucket URL at this point. Once we move to the next step (from environment configuration to service access configuration) an S3 bucket will be automatically created without waiting to complete the EBS application creation process. We can use this bucket to store our application code or create a different one. I will use this one.

After designing the folder structure for the Python application based on AWS Elastic Beanstalk documentation, the application files will be uploaded to S3 bucked. That location is used to run the application. Go to S3 bucked and upload the application files.

Update your Public S3 URL with the bucket URL.

At this point, all public access to this bucket is allowed as default. I will keep it as it is but update it for later projects.

4. Configure service access

We need to set up the necessary permissions and roles that allow Elastic Beanstalk to interact with other AWS services securely.

  1. Service Role: Grants permissions to the Elastic Beanstalk service itself, allowing it to manage AWS resources on your behalf. This includes creating and managing EC2 instances, load balancers, and other resources necessary for running your application.

  2. EC2 Instance Profile: Grants permissions to the EC2 instances running our application. It allows the instances to interact with other AWS services (e.g., S3, DynamoDB) on your behalf.

When you click View permission details you will see recommended permission by AWS. We will add them to our IAM roles as well.

Service role permission recommendations:

  1. AWSElasticBeanstalkEnhancedHealth (this comes as default)

  2. AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy (we will attach it): This policy is for the AWS Elastic Beanstalk service role used to perform managed updates of Elastic Beanstalk environments. The policy grants broad permissions to create and manage resources across several AWS services including AutoScaling, EC2, ECS, Elastic Load Balancing, and CloudFormation.

EC2 instance profile recommendations:

  1. AWSElasticBeanstalkWebTier: Provide the instances in your web server environment access to upload log files to Amazon S3.

  2. AWSElasticBeanstalkWorkerTier: Provide the instances in your worker environment access to upload log files to Amazon S3, to use Amazon SQS to monitor your application’s job queue, to use Amazon DynamoDB to perform leader election, and to Amazon CloudWatch to publish metrics for health monitoring.

  3. AWSElasticBeanstalkMulticontainerDocker: Provide the instances in your multicontainer Docker environment access to use the Amazon EC2 Container Service to manage container deployment tasks.

First, we need to create an IAM role and attach it to the Elastic Beanstalk application. You can select Create and use a new service role if you do not have an existing one, or you want AWS to create one for you. This will create an IAM role with necessary permissions but you may need to add additional permissions based on your application needs. I will create a new one for this project following the steps below:

  1. Create a Service Role:

Go to IAM -> Roles -> Create role -> AWS service

When we specify the Use case as Elastic Beanstalk it comes with some default policies.

Named :elastic-beanstalk-service-role

AWSElasticBeanstalkEnhancedHealth: AWS Elastic Beanstalk Service policy for Health Monitoring system.

AWSElasticBeanstalkService: AWS Elastic Beanstalk Service role policy grants permissions to create & manage resources (i.e.: AutoScaling, EC2, S3, CloudFormation, ELB, etc.) on your behalf. This policy comes with the following permissions by default:

  • AllowCloudformationOperationsOnElasticBeanstalkStacks: Allows full CloudFormation operations.

  • AllowDeleteCloudwatchLogGroups: Allows Elastic Beanstalk to clean up log groups when environments are deleted.

  • AllowECSTagResource: Allows tagging of ECS resources

  • AllowS3OperationsOnElasticBeanstalkBuckets: Allows full S3 operations on Elastic Beanstalk-specific buckets. Grants permissions to manage Elastic Beanstalk application versions, environment configurations, and logs stored in S3 buckets.

  • AllowLaunchTemplateRunInstances: Enables Elastic Beanstalk to launch EC2 instances using predefined launch templates.

  • AllowOperations: Allows Elastic Beanstalk to fully manage instances, security groups, load balancers, scaling policies, and other resources necessary for the application environment. Includes permissions for Auto Scaling, EC2, ECS, Elastic Load Balancing, IAM, CloudWatch, RDS, S3, SNS, SQS, and CodeBuild.

Most of the necessary policies are attached by default. Create a role with given permissions first. If we need any additional permission we need to attach it to this role.

We will attach the recommended policy we mentioned earlier: AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy

AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy: Includes permissions needed specifically for updating instances and other resources.

AWSElasticBeanstalkService: Includes a wider range of permissions necessary for creating, updating, and deleting various AWS resources managed by Elastic Beanstalk. This policy is on a deprecation path. It comes as a default for now.

2. Create an EC2 Instance Profile

We will follow similar steps to create an EC2 Instance Profile.

We need to attach specific policies to give additional permission to the EC2 instance that our Python application will run inside it. In our case, our application needs to write data and read data from the DynamoDB table. We need a specific policy for that which is AmazonDynamoDBFullAccess.

Named :elastic-beanstalk-ec2-service-role

After we added AWS recommended policies, the final role comes with the following policies:

3. Create an SSH key:

To access the EC2 instance via SSH connection we will create an SSH key.

EC2 -> Key pairs -> Create key pair

Download and store the key.

We are all set for service access configuration.

You can Skip to review and edit configuration there or you can go step by step until the end.

5. Set up networking, database, and tags-optional

If you don’t configure a VPC, Elastic Beanstalk uses the default VPC. I will use a custom VPC I had before. You can use the default one. The configuration will be the same.

We will choose subnets in each AZ for the EC2 instances that run our application. To avoid exposing our application instances to the Internet, we can run them inside private subnets and load balancers in public subnets such that our application will be open the public access through NAT. If we choose private subnets for our EC2 instances, the VPC must have a NAT gateway in a public subnet that the EC2 instances can use to access the Internet. I will not create a NAT gateway and I will run both in public subnets for simplicity. To run the load balancer and instances in the same public subnets, we will assign public IP addresses to the instances.

I only picked public subnets and activated public IP addresses for EC2 instances. I am not going to use RDS, I will move to the next configuration.

6. Configure instance traffic and scaling *— optional*

We will set the Root volume type: General Purpose SSD — 8 GB

Capacity:Instances Min: 2 Instances Max: 4 Instance type: t2-micro

The Elastic Beanstalk will automatically create a Load Balancer for our application, with a minimum of 2 EC2 instances running when we first launch our application, and a maximum of 4 if triggered.

Configure the trigger that lets the auto-scaling group know when to scale up and add up more instances. if the load goes beyond 50% of CPU utilization, it is going to add more instances up to 4 to keep up with the workload.

Scaling triggers:

Load balancer network settings: We will use the same setting as we used for EC2 instances.

Listeners: By default, the load balancer is configured with a standard web server on port 80. I will use default settings.

If you wish you can configure Elastic Load Balancer to capture logs with detailed information about requests sent to your Load Balancer. Those logs will be stored in Amazon S3. I will not enable it for this project.

7. Configure updates, monitoring, and logging *— optional*

I will not touch settings here, if you want you can configure them based on your needs. It uses NGINX by default.

I will only set the AWS_REGION environment variable here. It will be passed to my application.

Submit to create the environment for the application run. It takes a few minutes.

S3 Bucket:

EC2 instances:

Security Groups:

Inbound-outbound rules of security groups:

Load Balancer:

The environment health turned to RED, I will look at the logs and figure it out before trying to run my application.

It did not work in my case. I ran this application without any problem with the same settings before. It should work. I do not know if AWS has updated any settings on its services since my last run. I got the following error for this time:

ERROR: ModuleNotFoundError: No module named 'application'

I applied the solutions below to solve it but I was not able to solve this error after too much debugging.

  • AWS calls the Flask instance as an application but Gunicorn calls it as an app. I updated a line, application = app = Flask(name), in my application.py file and then set WSGIPath to application:application.

I will use EB CLI after this point with the exact same settings. If you are also not able to run or debug by yourself you can move with me to use EB CLI.

Do not forget to destroy all the resources you created with Elastic Bean Stalk; tcb-conference-env and tcb-conference application. Keep the DynamoDB table.

Building a Scalable Web App with AWS Elastic Beanstalk, DynamoDB, and CloudFront — with EB CLI

Part 1: Deploy the application using Elastic Beanstalk — with EB CLI

  1. Install EB CLI

EB CLI is AWS Elastic Beanstalk Command Line Interface. First, we will install EB CLI. You can use this aws-elastic-beanstalk-cli-setup to install based on your operating system. I will follow the instructions for MacOS:

pip install awsebcli
# Verify the EB CLI Installation
eb --version
Enter fullscreen mode Exit fullscreen mode

2. Configure the EB CLI

Initialize an EB CLI project and select your region.

# Initialize 
eb init

#or
eb init -p python-3.8 tcb-conference --region us-east-1
Enter fullscreen mode Exit fullscreen mode

3. Setup IAM Credentials

We must provide our credentials first. We must provide a secret key and access key to authenticate (who we are) and authorize (what permission we do have) to allow EB CLI to access and manage AWS resources for us. Let’s go to the IAM console to create a secret and access key. The secret access key is available for download only when you create it, make sure you download it after you create it.

Go to IAM console -> Users -> Create User

I will give AdministratorAccess access, and I can update later if I wish.

Go and create access key.

Use case: Command Line Interface (CLI)

When you run EB CLI it will ask for these credentials. You can also configure the AWS CLI using environment variables

export AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID

export AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY

export AWS_DEFAULT_REGION=us-east-1

We are ready to configure the EBS environment, similar to what we did on the AWS console before:

4. Create an Elastic Beanstalk Environment

We will use the YAML file for configuration. The YAML file should be under .ebextentions. EBS will automatically detect and run this config file when it is provided. Otherwise, it will use default EBS settings to create an EBS environment.

mkdir .ebextensions 
touch .ebextensions/environment_configuration.config
Enter fullscreen mode Exit fullscreen mode

Our application files will be zipped and uploaded to the S3 bucket while creating the environment.

We will use the same configuration in our YAML file. This is actually easier and faster compared to using AWS Dashboard.

YAML config file:

option_settings:
  aws:elasticbeanstalk:environment:
    ServiceRole: "aws-elasticbeanstalk-service-role" # Set service role
  aws:autoscaling:launchconfiguration:
    InstanceType: t2.micro # Specify the instance type (adjust as needed)
    EC2KeyName: ebs-ssh-key # Set EC2 key pair
    IamInstanceProfile: aws-elasticbeanstalk-ec2-role # Set IAM instance profile
    RootVolumeType: gp2
    RootVolumeSize: "10"
    DisableIMDSv1: true # Deactivate IMDSv1
  aws:autoscaling:asg:
    MaxSize: 4 # Maximum number of instances
    MinSize: 2 # Minimum number of instances
  aws:ec2:vpc:
    VPCId: "vpc-03f8678fb9c5d5ea1" # Set the VPC ID
    ELBScheme: public
    Subnets:
      - "subnet-0449c3e40202e7665"
      - "subnet-01e26ba1a707f5b13"
      - "subnet-06a672c7f4aea1795"
      - "subnet-0461ac2c4cea08257"
    ELBSubnets:
      - "subnet-0449c3e40202e7665"
      - "subnet-01e26ba1a707f5b13"
      - "subnet-06a672c7f4aea1795"
      - "subnet-0461ac2c4cea08257"
    AssociatePublicIpAddress: true # Enable public IP addresses for instances
  aws:elasticbeanstalk:healthreporting:system:
    SystemType: "basic" # Enable enhanced health reporting
  aws:autoscaling:trigger:
    MeasureName: "CPUUtilization" # Use CPUUtilization as trigger measurement
    UpperThreshold: "50" # Upper threshold for CPU utilization
    LowerThreshold: "40" # Lower threshold for CPU utilization
    Unit: "Percent"
    Period: "1"
    UpperBreachScaleIncrement: "1" # Increase instance count by 1 on upper breach
    LowerBreachScaleIncrement: "-1" # Decrease instance count by 1 on lower breach
  aws:elasticbeanstalk:container:python:
    WSGIPath: "application:application" # Set WSGIPath to application:application
  aws:elasticbeanstalk:application:environment:
    AWS_REGION: "us-east-1" # Set AWS_REGION environment property
Enter fullscreen mode Exit fullscreen mode

You can use EBS documentation here while preparing your YAML file.

Ready to create an EBS environment:

eb create tcb-conference-env --region us-east-1
Enter fullscreen mode Exit fullscreen mode

We are now able to create our EBS environment without any problems at this time!

Health: OK

Click on the Domain name to open the application:

Our application files were zipped and uploaded to the S3 bucket. Remember we created an EBS application named tcb-conference at the beginning of the EB CLI initialization. Our files are zipped and placed in a directory named tcb-conference.

S3 bucket:

EC2 instances (Minimum 2):

Load Balancer:

Auto Scaling Group:

CloudFormation:

5. Validate the Application

The participants will need to enter their email address on the web page and the application will insert the email address into the DynamoDB.

Try to register:

Upps! We are not able to register. Our frontend is working but we are not allowed to register. Our email will not be written to DynamoDB. Let’s go and check our EC2 instance role.

We do not have permission to write data to DynamoDB. Permit your EC2 instance to write data on the DynamoDB table inside the IAM role. Add the permission AmazonDynamoDBFullAccess on the EC2 associate role in IAM.

Try again:

Check the users’ DynamoDB table:

Awesome!

Part 2: Create a CloudFront Distribution

The CloudFront is a Content Delivery Network. All the static and dynamic files (CSS, HTML, etc) coming from the application will be cached on the edge location closer to the user so that it can improve the application’s performance and provide the lowest latency. When a user requests content, the request is routed to the edge location.

Go to Console and search for CloudFront -> Create a CloudFront distribution

Our application origin is our Elastic Load Balancer.

  • Cache policy: CachingOptimized

  • Allowed HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE. We want to allow the POST method to insert the data inside DynamoDB.

  • Once CloudFront distribution is created, a domain name to access our application is already associated with it. This will give the Route 53 DNS entry that we use to access the application throughout CloudFront.

  • We could have a Custom SSL certificate — (optional), branded and customized domain name for our purpose associated with our CloudFront distribution and put the SSL certificate associated with that as well. We are going to use the default one created for us.

Copy the CloudFront domain name when it is ready. Let’s confirm if we can access the application using the CloudFront.

Here it is:

Part 3: Perform Load testing

We will basically simulate here what will happen if many users are accessing to EC2 instance at the same time and CPU utilization goes up.

We will induce load on the CPU. Copy the IP address from one of the EC2 instances. Open remote connectivity on your computer via SSH:

ssh -i "ebs-ssh-key.pem" ec2-user@ec2-52-201-156-146.compute-1.amazonaws.com

Install the stress tool to perform the load testing:

sudo amazon-linux-extras install epel -y
sudo yum install stress -y
stress -c 4
Enter fullscreen mode Exit fullscreen mode

This comment is going to bump up the CPU utilization inside the EC2 instance. Check Elastic Beanstalk status if it is turned “Warning”. Stress command bumping CPU utilization of this instance 100 %. We configured it as if CPU utilization is higher than 50%, the auto-scaling group will add one new instance so we can keep up with the workload.

Open a new terminal, connect remotely with SSH again, and use the “top” command that shows CPU utilization on the operating system.

New instances will be added shortly to scale up. Every single user who goes through the load balancer will be redirected to one of these instances.

Stop the loading process and end up stress command and check the process is not running anymore by running the command below:

Ctrl + C
ps -ef | grep stress
Enter fullscreen mode Exit fullscreen mode

The stress is running anymore so the health status will change to OK. The auto scale will scale down and remove 3rd instance added before, so we can save cost.

Once you finish exploring it, please remove the Elastic Beanstalk application, and Elastic Beanstalk environment, disable and delete the CloudFront distribution, and finally delete the DynamoDB users table.

CONGRATULATIONS!!!

REFERENCES

Elastic Beanstalk service role

AWS Elastic Beanstalk Developer Guide

AWS-Deploying a Flask application to Elastic Beanstalk

Deploying a Flask Application to Elastic Beanstalk

Elastic Beanstalk-supported platforms

No module named ‘application’ Error while deploying a simple web app to Elastic Beanstalk

WSGI configuration for Django Deployment using EB CLI

Create an application source bundle

What is AWS Elastic Beanstalk?

Deploy a sample web application using Elastic Beanstalk

Install the EB CLI

AWS security credentials

EBS configuration options for all environments

Top comments (0)