Moving applications to the cloud delivers scalability, operational flexibility, and service choice that on-premises infrastructure can’t match. The key to unlocking these advantages lies in selecting the right migration strategy. But the migration path matters – some strategies preserve existing code while others require complete rewrites, each with different resource requirements. Choose the right approach, and you’ll minimize development overhead while maximizing cloud benefits.
Migration Strategy Overview
Organizations moving workloads from on-premises to AWS follow one of four strategies:
– Lift and shift: Direct migration of virtual machines to Amazon EC2 instances without rewriting applications
– Modernize: Transition to managed services while keeping core application logic intact – enabling continuous delivery with cloud-native patterns
– Rearchitect: Redesign applications to leverage cloud resiliency, scalability, and flexibility
– Rebuild from scratch: Complete rewrite using containers, serverless, AI, and IoT technologies
AWS Elastic Beanstalk exemplifies the “modernize” approach – you keep your existing Node.js application code while AWS handles the underlying infrastructure, load balancing, auto-scaling, and monitoring.
In this post, I’ll show you how to deploy a Node.js to-do application to AWS Elastic Beanstalk using Terraform for infrastructure-as-code and GitHub Actions for automated deployments.
Solution Overview

This solution creates a scalable Node.js hosting environment using AWS Elastic Beanstalk within a secure VPC architecture. The Node.js application is packaged as a ZIP file and uploaded to Amazon S3, where Elastic Beanstalk retrieves it for deployment across multiple EC2 instances.
Elastic Beanstalk orchestrates the underlying infrastructure – provisioning an Application Load Balancer in public subnets to distribute traffic, EC2 instances in private subnets for security, and an Auto Scaling Group that automatically adjusts capacity based on CPU utilization. The service manages the entire application lifecycle, from deployment through monitoring.
The infrastructure spans two availability zones with dedicated security groups controlling traffic flow between the load balancer, EC2 instances, and the internet. IAM roles provide the necessary permissions for Elastic Beanstalk to manage resources and for EC2 instances to access CloudWatch for logging and monitoring.
Terraform provisions the infrastructure, while GitHub Actions automates the deployment pipeline – triggering infrastructure updates and application deployments when code changes are pushed to the repository.
If you are interested in following along with the code, check out my GitHub repository: kunduso/aws-beanstalk-nodejs-terraform.
This use case comprises 7 high-level steps, which I’ll explain in detail. These are:
1. Set up the Node.js to-do application
2. Create the network components to host the workload
3. Create IAM roles to enable the workload to access and manage AWS services
4. Create security groups to control network access
5. Create an Amazon S3 bucket for temporary storage
6. Create the Elastic Beanstalk application and environment
7. Automate the deployment using GitHub Actions
Prerequisites
Before we proceed with the implementation, please note that the deployment workflow is automated via GitHub Actions. This workflow requires an AWS IAM role with an OIDC trust policy that allows GitHub Actions to securely assume the role and create AWS cloud resources without long-lived credentials.
For detailed setup instructions, please see: Securely integrate AWS credentials with GitHub Actions using OpenID Connect. I followed the same approach and stored the role ARN as a GitHub Actions secret named IAM_ROLE, which is referenced in the GitHub Actions workflow file.
Implementation
Now let’s walk through each step to build and deploy the Node.js application to AWS Elastic Beanstalk.
Step 1: Set up the Node.js to-do application
The first step is to create a simple Node.js application that demonstrates basic task management operations. The application uses the Express.js framework with an in-memory data store for simplicity. The application listens on port 8080, which matches our Elastic Beanstalk configuration. The package.json file defines the dependencies and the start script.

The code is in the /app folder in the GitHub repository.
Step 2: Create the network components to host the workload
The networking foundation uses a custom VPC module that creates a secure, multi-AZ architecture. The terraform/network.tf file defines the VPC configuration.

This module provisions a VPC with four subnets — two private and two public — spread across two availability zones. It also creates an internet gateway and associates that with the public subnets, creates two NAT gateways across the two public subnets, and updates the private subnet route table to access the internet using the NAT gateways.
The multi-AZ design ensures high availability and fault tolerance across two availability zones in the us-west-2 region.
Step 3: Create IAM roles to enable the workload to access and manage AWS services
Elastic Beanstalk requires two distinct IAM roles with specific permissions. The terraform/iam.tf file creates these roles: (i) beanstalk_service, and (ii) beanstalk_ec2.
The service role uses the AWSElasticBeanstalkService managed policy, which grants permissions to create and manage load balancers, auto scaling groups, and EC2 instances. Additionally, the AWSElasticBeanstalkEnhancedHealth policy enables detailed health reporting and monitoring capabilities.

The EC2 role combines multiple managed policies: AWSElasticBeanstalkWebTier for web application hosting, AWSElasticBeanstalkWorkerTier for background processing capabilities, and CloudWatchLogsFullAccess for comprehensive logging to CloudWatch. These policies ensure instances can communicate with Elastic Beanstalk services, send logs and metrics to CloudWatch, and access necessary AWS services for application functionality.
Step 4: Create security groups to control network access
Security groups act as virtual firewalls controlling traffic flow between components. The terraform/security.tf file defines two security groups: (i) beanstalk_alb, and (ii) beanstalk_instances. This approach creates a secure traffic flow: Internet → ALB (port 80) → EC2 instances (port 8080). For inbound traffic, instances can receive traffic only from the load balancer, not directly from the internet. For outbound traffic, the instances have HTTPS (port 443) and HTTP (port 80) access to the internet through the NAT gateways for package downloads, updates, and external API calls.
Step 5: Create an Amazon S3 bucket for temporary storage
Elastic Beanstalk requires S3 storage for application versions. The terraform/app-storage.tf file creates a secure S3 bucket, enables versioning, adds server-side encryption and lifecycle configuration, and blocks public access.

Finally, it also uploads the zip file to the S3 bucket for the Elastic Beanstalk application to consume. The etag property uses the MD5 hash of the zip file to ensure Terraform uploads the object only when the application code changes, enabling efficient change detection and avoiding unnecessary uploads.
Step 6: Create the Elastic Beanstalk application and environment
The core Elastic Beanstalk configuration brings together all previous components. The terraform/beanstalk.tf file defines the solution stack, the application, version, and the environment to host the Beanstalk application.
Solution Stack Data Source: The aws_elastic_beanstalk_solution_stack data source dynamically retrieves the latest Node.js 20 platform running on Amazon Linux 2023.

This construct ensures the application always uses the most recent platform version with security patches and updates, eliminating the need to hardcode platform versions that become outdated.
Application Resource: The aws_elastic_beanstalk_application creates a logical container that groups related application versions and environments.

This container serves as the top-level organizational unit in Elastic Beanstalk’s hierarchy and provides a namespace for managing multiple versions of the same application across different environments (development, staging, production).
Application Version: The aws_elastic_beanstalk_application_version links the application code stored in S3 to the Beanstalk application. It references the S3 bucket and object created in Step 5, using the MD5 hash-based naming to ensure each code change creates a unique version.

This resource enables rollbacks, blue-green deployments, and version tracking throughout the application lifecycle.
Environment Configuration: The aws_elastic_beanstalk_environment is where all previous infrastructure components converge. It references the VPC and subnets from Step 2, applies the IAM roles from Step 3, uses the security groups from Step 4, and deploys the application version from S3. The environment settings configure the load balancer to use public subnets, place EC2 instances in private subnets, and apply custom auto-scaling triggers for responsive performance management.

The environment uses setting blocks to configure and customize various aspects of the Beanstalk deployment. Each setting uses a three-part structure: namespace (the AWS service or component), name (the specific configuration option), and value (the desired setting). This granular approach allows precise control over instance types, networking, scaling policies, and monitoring configurations. For a comprehensive list of available configuration options, refer to the AWS Elastic Beanstalk Configuration Options documentation.
Step 7: Automate the deployment using GitHub Actions
The final step includes a complete CI/CD pipeline using GitHub Actions. The .github/workflows/terraform.yml file orchestrates the entire deployment process using the OIDC authentication configured in the Prerequisites section, eliminating the need for long-lived AWS credentials.
This repository also includes a code-scanning pipeline using Checkov to ensure zero security violations by scanning Terraform configurations against hundreds of security best practices before deployment. For detailed implementation of Checkov with GitHub Actions, see: automate-terraform-configuration-scan-with-checkov-and-github-actions.
Deployment Process
After successful deployment, the AWS Elastic Beanstalk console displays the environment status and health information. The environment shows as “Ok” with a green health indicator when all components are functioning correctly.

The deployed application becomes accessible through the automatically generated Elastic Beanstalk URL. The to-do application loads successfully, indicating that all infrastructure components are functioning as expected.

⚠️ Production Security Note: For production deployments, consider integrating AWS Certificate Manager (ACM) to enable HTTPS encryption and Amazon Route 53 for custom domain management. These services can be easily configured with Elastic Beanstalk to replace the default HTTP URL with a secure, branded domain name that meets enterprise security requirements.
And that completes the implementation of a scalable Node.js deployment pipeline using AWS Elastic Beanstalk, Terraform, and GitHub Actions.
Conclusion
AWS Elastic Beanstalk provides an ideal balance between simplicity and control for teams migrating to the cloud. While containerized solutions require significant architectural changes, Beanstalk allows you to leverage cloud benefits with minimal application modifications, making it the perfect choice for a “modernize” migration strategy.
By following the steps outlined in this blog post, you can successfully deploy a Node.js application to AWS Elastic Beanstalk using Terraform and GitHub Actions. This approach not only simplifies deployment but also ensures your application is hosted in a secure, scalable environment.<