Recently I watched the recorded video of the Serverlesspresso team talk at 2022 Reinvent on the AWS Events Youtube Page. I got inspired and decided to take a cue from that video and other lessons from 2022 Reinvent and build an open-source project.
The result? FacePolls, a Serverless Facial Recognition Voting Application built entirely using AWS services, adheres to established best practices and uses the Event-Driven pattern.
This article describes how I built this project from scratch, the patterns I used and why, my key takeaways and learnings, and finally a call to action to build event-driven and serverless systems.
Let’s get right into it!
Prerequisite
Node.js 16
NPM
An AWS account
AWS CLI configured on your local machine with an AWS profile, would be used for deploying the backend section of the application, which was built using the Serverless Framework
Serverless installed globally, would be used to run the serverless deploy command
Deployment
First, you would need to clone the repo found here(Feel free to leave a star or fork it).
# Clone this repository
git clone https://github.com/chyke007/facial-vote.git
# Go into the repository
cd facial-vote
Frontend Application & Admin Frontend Application
Now inside the project folder, we can easily run both apps as they are built using Next.js. To run it locally, you would need to install the packages used, as well as create a .env file from the .env.example sample file provided, then provide the right values.
# Copy environment variable
$ cp facial-vote-admin/.env.example .env && cp facial-vote-frontend/.env.example facial-vote-frontend/.env
You should get the necessary values once the backend application is deployed, to get the necessary endpoints.
# Run Frontend (run from folder root)
$ cd facial-vote-frontend && npm i && npm run dev
# Run Admin (run from folder root)
$ cd facial-vote-admin && npm i && npm run dev
You could also deploy it to AWS Amplify, by connecting your fork of the repo, providing the necessary environment variables, and deploying the application.
Other providers like Vercel can also be used to quickly deploy the frontend and admin section.
Backend Application
Contains the backend/serverless part of the application built using the Serverless Framework. Simply create the .env file with the right values and deploy the app to your AWS account:
# Copy environment variable
$ cp facial-vote-backend/.env.example facial-vote-backend/.env
# Deploy backend (run from folder root)
$ npm i serverless -g
$ cd facial-vote-backend && serverless deploy
# Remove backend resources (run from folder root)
$ cd facial-vote-backend && sls remove
AWS Services used
AWS Amplify: Used in the frontend to manage users authentication and acts as a point of contact between the frontend and AWS services like S3, IoT, and Cognito
Amazon Cognito: Handled Authentication and management of users.
Amazon EventBridge: Acted as a link between services. Choreographed the movement of events between some AWS services.
Amazon Rekognition: Used to index, detect faces in the picture, and compare faces when users try voting, it was the heart of the facial voting feature.
AWS Lambda: Lambda function used to run server codes and application logic.
Amazon SES: Used to send OTP codes during the user account retrieval step.
AWS IoT: For real-time communication between the server and the frontend application.
AWS Step Functions: Acted as the orchestrator and brain of the application business logic.
Amazon DynamoDB: Used as the Database service owning to its Schemaless/Nosql design, Serverless nature, fast and highly scalable nature.
Amazon S3: Was used as storage for the uploaded faces.
Amazon API Gateway: Used for API management and endpoint creation.
AWS CloudFormation: Used to provision resources used by Serverless Framework and the application in general.
AWS STS: Used to generate temporal credentials.
Other Technology used
Next.js: Used to build the frontend application
Serverless Framework: Choose this over the AWS SAM, as I am more familiar with it, and seems easier to use.
How it works
The following below details how the application works, it is broken into 4 sections, which are:
A. Face Index Flow
Frontend was created using Next.js and hosted on Amplify (and Vercel). New users enter their email to retrieve their account(Admin has created users by adding users' email to the Cognito user pool).
Once the email is found, a custom challenge for authentication is fired, it sends an OTP using SES to users' email to verify they own the email they are claiming. Once OTP is correct, a new step is shown to upload their face to attach it to their retrieved email.
The user then uploads their face photo, it goes to a private folder in S3, which has an EventBridge integration with a lambda function, this function gets the object details and initializes the Index Face Step function.
Each Step function task gets the object details and does its work, the first task makes sure it is a face that was uploaded using Rekognition, and the second task checks if a face exists already in Rekognition collection and prevents users from proceeding if a face is found, the third task indexes the face to Rekognition and the last saves the new Face entry record to DynamoDB.
AWS IoT is used to relay real-time feedback to the user on how each stage goes.
B. Face Recognition / Vote Flow
This flow was the most complex of the 4 I had to deal with. As I needed to make it as secure as possible while still maintaining speed and efficiency.
Users use the application to upload their faces image to a public folder in the S3 bucket.
The user is subscribed to the custom image name generated before uploading to S3.
EventBridge trigger watches that folder and calls the lambda function integrated. This function then initializes the CompareFace Step Function with the object details.
The Step Function tasks do the following: validate it's a face that was uploaded, check if the face is found in the Amazon Rekognition collection, and if it's found, it calls the next task.
It fetches the user's email and sends an OTP, this serves as a 2FA to prevent a user from using another registered user's face to vote. Then it calls the next task.
This task is a lambda function that generates a 15mins(configurable from .env) STS credential that has permission to invoke API Gateway and is sent back to the user using AWS IoT.
AWS IoT publishes the STS credentials, encrypted user id, and encrypted Otp to the custom image name topic user is already subscribed to. This way only a specific user gets the message, the user who uploaded the image.
The frontend application gets the message using Amplify and then fetches all categories that can be voted in. (This API endpoint is only secured with an API key, and not AWS IAM, as it isn't a private endpoint)
The user then proceeds to call the vote endpoint to add their vote, this endpoint is secured using API Key and AWS IAM. The generated STS credential is then signed and used to authenticate this request
The add/validate vote lambda function validates the vote details and then adds the vote to DynamoDB.
DynamoDB Stream is configured for the DynamoDB table, hence it sends this new vote to the stream.
A Lambda function is listening to the stream INSERT method for PK starting with VOTES and processes the vote details.
Then it uses AWS IoT to publish the processed result to the VOTE_ADDED topic, which users on the Live Result page are subscribed to so they see the vote coming in real time!
C. Live Result
From the frontend application, the user navigates to the live result page, which subscribes the user to a VOTE_ADDED topic which is published once a vote is added to DynamoDB.
Selects the category they want to see the result, and then the result they requested is shown.
Once a new vote is added to DynamoDB, it uses DynamoDB streams to stream the new record to a Lambda function.
This function(same as in B above) then processes the record and publishes the result using AWS IoT to the VOTE_ADDED topic the user already subscribed to.
AWS IoT relays this information to Amplify and this results in an update to the UI in real time without the need to refresh the page!
D. Admin Flow
Creates users and voting categories using the Next.js application
Currently only secured using API key, but should implement more security
Architecture
Took me around 4–5 days to come up with the architecture below, I factored in ease of use, security, cost, scalability, and efficiency as I designed it:
Face Index Flow
Face Recognition/Voting Flow
Live Result Flow
Admin Flow
Step Functions
DynamoDB Schema
Architecting the DynamoDB schema was another exciting part of this project. I used the Single-Table Design approach and learned this from a previous role I worked in, and also from Alex Debries' write-ups on it.
For further reading on this concept, I recommend reading Alex Debrie’s article where he wrote a detailed write-up on this, as well as AWS ReInvent 2020 talk he gave here.
You can also check out an article I wrote here about this architectural pattern here.
I made use of a composite primary key(Partition and Sort key) and the table had 1 Global Secondary Index.
PK: Partition Key, SK: Sort Key, GS1PK: Global Secondary Index Partition key, GS1SK: Global Secondary Index Sort key
Voting/ Vote Category
PK: VOTING#status
SK: voting_id#voting_name
Votes
PK: VOTES#voting_id
SK: face_id#candidate_id
Face Entry
PK: FACE_ENTRY#user_email
SK: face_id#uploaded_image_key
The Face Entry also has a GSI, which is stored when creating a new face entry:
GS1PK: FACE_ENTRY#face_id,
GS1SK: user_email#uploaded_image_key
Lessons learned
Cost: Throughout the 2 weeks I spent developing the application (Immediately deployed the app using serverless from day 1 and updated it as I made changes) and 2 days of testing the full process with some of my friends, I noticed the cost was still kept at a record low. The total during this period was a record low of 1.37 USD spent!
Debugging: Required a lot, and was helpful. Helped me catch a particular issue when the DynamoDB stream kept retrying as the Lambda function wasn't fully set up to process it, caught it on the 50th retry using Lumigo and Cloudwatch, had I not, it could have retired 10000 times, Yikes! Another helpful service here was Step Functions, as I could see input and output on each step and also replay the function with new or the same old parameters
**Permission issues: **An example is the lambda function that calls the Amazon Rekognition method to index, compare or save a face, it needed to have permission to access S3. I had a challenging time figuring this out as I thought that since it was Rekognition, it should have all permissions and not necessarily the Lambda function that calls it.
Why I choose this approach
Realtime updates: I needed real-time updates as key events occurred, like a user voting, other viewers on the live result page needed to see this new input reflect immediately without the need to reload their pages. AWS IoT, DynamoDB, DynamoDB streams, and Lambda were the best combinations I could think of, and they delivered!
Cost-Effective: Lambda, API Gateway, SES.
Service orchestration: Step Functions it was!
Serverless and Event-Driven: S3, EventBridge, DynamoDB, and AWS IoT were the key services here.
Reduced engineering effort: Amazon Rekognition, AWS IoT, Cognito, and Amplify really shone in this part.
Easier management: API Gateway, CloudFormation, and Cognito were the key services here.
Security: STS and Cognito were the key services here.
Conclusion
Embarking on this project was a great opportunity to implement lots of best practices I had learned from work and also from preparing and getting multiple AWS certificates.
Completing it makes me more confident of my AWS and cloud skills and I look forward to creating more of these as time permits.
I encourage you to consider using the event-driven and serverless architecture in your next project as it makes development seamless and efficient.
Happy building!
Top comments (0)