Production-ready CI/CD pipeline deploying Node.js applications to AWS EC2 with 13-second in-place updates. Built with GitHub Actions, Terraform, and AWS Systems Manager.
# 1. Clone repository
git clone https://github.com/engabelal/simple-nodejs-ec2-cicd.git
cd simple-nodejs-ec2-cicd
# 2. Configure AWS credentials in GitHub Secrets
# 3. Deploy: Actions → Run workflow → deploy
# 4. Push updates (automatic 13s deployment!)
git add .
git commit -m "Update app"
git push origin main| Feature | Description |
|---|---|
| ⚡ 13-Second Updates | In-place deployment without infrastructure recreation |
| 🤖 Fully Automated | Push to main → automatic build & deploy |
| 🏗️ Infrastructure as Code | Complete Terraform setup with remote state |
| 🔒 Production-Ready | Security best practices & IAM roles |
| 💰 Cost-Effective | ~$10/month on AWS (t3.micro) |
| Stage | Duration | Trigger |
|---|---|---|
| Build | 30s | Every push/deploy |
| Plan | 40s | Manual deploy only |
| Deploy (first time) | 3m 30s | Manual deploy |
| Update (in-place) | 13s ⚡ | Auto on push |
| Destroy | 1m 30s | Manual destroy |
| CI/CD | GitHub Actions |
| IaC | Terraform 1.10.4 |
| Cloud | AWS (EC2, S3, IAM, SSM, CloudWatch) |
| Runtime | Node.js 20.x |
| Framework | Express.js 4.x |
| Scripting | Bash (user-data.sh, deploy-update.sh) |
| Application | ABCloudOps Quote Generator (API integration, stats tracking) |
simple-nodejs-ec2-cicd/
├── .github/workflows/
│ └── deploy.yml # 4-stage CI/CD pipeline
├── app/
│ ├── server.js # Express.js REST API
│ ├── package.json # Dependencies
│ └── public/ # Frontend
│ ├── index.html # Quote Generator UI
│ ├── style.css # Glassmorphism design
│ └── app.js # Frontend logic
├── iac/
│ ├── main.tf # EC2, IAM, Security Groups
│ ├── variables.tf # Input variables
│ ├── terraform.tfvars # Configuration values
│ ├── outputs.tf # Outputs (IP, URL)
│ ├── backend.tf # S3 remote state
│ ├── providers.tf # AWS provider
│ └── scripts/
│ ├── user-data.sh # EC2 initialization
│ └── deploy-update.sh # In-place update via SSM
└── images/ # Screenshots
┌─────────────────────────────────────────────────────────┐
│ GitHub Actions │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Build │→ │ Plan │→ │ Deploy │ │ Update │ │
│ │ (30s) │ │ (40s) │ │ (2m20s) │ │ (13s) │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌──────────────────────┐
│ Amazon S3 │
│ • Artifacts (app.zip)│
│ • Terraform State │
└──────────────────────┘
↓
┌──────────────────────┐
│ Terraform │
│ (IaC Engine) │
└──────────────────────┘
↓
┌────────────────────────────────────────┐
│ AWS Infrastructure │
│ ┌──────────┐ ┌──────┐ ┌──────────┐ │
│ │ EC2 │ │ IAM │ │ Security │ │
│ │ Instance │ │ Role │ │ Group │ │
│ └──────────┘ └──────┘ └──────────┘ │
└────────────────────────────────────────┘
↓
┌──────────────────────┐
│ Node.js App │
│ Port: 3000 │
│ (Quote Generator) │
└──────────────────────┘
1. Build (30s) → Test, package, upload to S3
2. Plan (40s) → Terraform validate & plan
3. Deploy (2m20s) → Create EC2, IAM, SG, deploy app
1. Build (30s) → Test, package, upload to S3
2. Update (13s) → SSM sends commands to EC2
→ Download new app.zip
→ Restart application
Key Difference: Updates skip infrastructure provisioning!
- ✅ AWS Account
- ✅ GitHub Account
- ✅ Terraform ≥ 1.10.4 (optional, for local testing)
aws s3 mb s3://my-terraform-state-bucket --region eu-north-1
aws s3api put-bucket-versioning \
--bucket my-terraform-state-bucket \
--versioning-configuration Status=Enabledaws ec2 create-key-pair --key-name my-ec2-key \
--query 'KeyMaterial' --output text > ~/.ssh/my-ec2-key.pem
chmod 400 ~/.ssh/my-ec2-key.pemSettings → Secrets and variables → Actions → New repository secret
| Secret Name | Example Value | Description |
|---|---|---|
AWS_ACCESS_KEY_ID |
AKIA... |
AWS access key |
AWS_SECRET_ACCESS_KEY |
wJalr... |
AWS secret key |
AWS_KEY_PAIR_NAME |
my-ec2-key |
EC2 key pair name |
Edit iac/terraform.tfvars:
aws_region = "eu-north-1"
artifacts_bucket = "my-terraform-state-bucket"
instance_type = "t3.micro"
app_port = 3000
project_name = "nodejs-cicd"# Push code to GitHub
git add .
git commit -m "Initial deployment"
git push origin main
# Deploy via GitHub Actions UI
# Actions → Run workflow → Select "deploy" → RunResult: Infrastructure created + app deployed (3m 30s)
# Modify app code
vim app/server.js
# Push changes
git add app/
git commit -m "Update feature"
git push origin mainResult: Automatic 13-second update! No manual action needed.
# GitHub Actions → Run workflow → Select "update" → RunUse case: Force update without code changes
# GitHub Actions → Run workflow → Select "destroy" → RunResult: All AWS resources removed (1m 30s)
aws logs delete-log-group --log-group-name "/aws/lambda/nodejs-cicd"| Issue | Solution |
|---|---|
| Build fails | • Check Node.js version (20.x required) • Verify npm test passes locally |
| Terraform state locked | cd iac && terraform force-unlock <LOCK_ID> |
| EC2 not accessible | • Check security group allows port 3000 • Verify instance is running |
| App not responding | • SSH: ssh -i ~/.ssh/my-ec2-key.pem ec2-user@<ip>• Check: ps aux | grep node |
| Update fails | • Ensure instance exists (deploy first) • Check SSM agent is running |
🔍 CI/CD Pipeline Details
| Event | Trigger | Jobs | Use Case |
|---|---|---|---|
| Push to main | Automatic | Build → Update | Daily development |
| Manual: deploy | workflow_dispatch | Build → Plan → Deploy | First deployment |
| Manual: update | workflow_dispatch | Build → Update | Force update |
| Manual: destroy | workflow_dispatch | Destroy | Cleanup |
- Build - Test, package, upload artifact
- Plan - Terraform validate & plan (deploy only)
- Deploy - Provision infrastructure (first time)
- Update - In-place deployment (subsequent)
- Destroy - Remove all resources (manual)
⚡ In-Place Deployment Explained
- GitHub Actions builds
app.zipand uploads to S3 - Workflow finds EC2 instance by tag:
project-name-server - SSM sends commands to EC2:
pkill -f "node server.js" # Stop app aws s3 cp s3://bucket/artifacts/app.zip /tmp/ # Download unzip -o /tmp/app.zip -d /home/ec2-user/app/ # Extract nohup node server.js & # Restart
- Wait 30s for app to restart
- ✅ No infrastructure recreation
- ✅ Preserves IP address
- ✅ 13-second deployment
- ✅ Zero additional cost
- ✅ No downtime (quick restart)
🏗️ Infrastructure Components
- EC2 Instance - t3.micro, Amazon Linux 2023
- Security Group - Ports 22 (SSH), 3000 (App)
- IAM Role - S3 read, SSM managed instance
- Instance Profile - Attached to EC2
- S3 Bucket - Artifacts + Terraform state
main.tf- Resource definitionsvariables.tf- Input variablesterraform.tfvars- Configuration valuesoutputs.tf- Outputs (IP, URL)backend.tf- S3 remote stateproviders.tf- AWS provider config
🔒 Security Best Practices
- ✅ IAM roles (no hardcoded credentials on EC2)
- ✅ Least privilege permissions
- ✅ Security group rules (specific ports only)
- ✅ GitHub Secrets for AWS credentials
- ✅ S3 bucket versioning
- ✅ Terraform remote state locking
- 🔄 Enable AWS CloudTrail
- 🔄 Add CloudWatch alarms
- 🔄 Implement HTTPS (ALB + ACM)
- 🔄 Use AWS Secrets Manager
- 🔄 Enable VPC Flow Logs
- 🔄 Implement backup strategy
After completing this project, you'll understand:
✅ CI/CD Design - Multi-stage pipeline architecture
✅ Infrastructure as Code - Terraform best practices
✅ AWS Services - EC2, IAM, S3, SSM, CloudWatch
✅ GitHub Actions - Workflows, secrets, artifacts
✅ Deployment Strategies - In-place vs blue-green
✅ State Management - Remote state with S3
✅ Security - IAM roles, least privilege
✅ Automation - End-to-end DevOps workflow
MIT License - Free to use for learning and production!
Ahmed Belal - DevOps & Cloud Engineer
⭐ Star this repo if you found it helpful!
🐛 Found a bug? Open an issue
💡 Have suggestions? Create a pull request
📧 Questions? Reach out via email
Built with ❤️ by Ahmed Belal | ABCloudOps



