Deploy a Node.js Service to a remote server using GitHub Actions
├── .github
│ └── workflows
│ └── deploy_service.yml # Github Actions CI/CD workflow
├── ansible # Ansible playbooks and inventory
├── node_app # Node.js application code
└── terraform # Terraform configuration files
- GitHub account
- AWS account
- SSH key pair generated.
Clone the repository:
git clone https://github.com/MGhaith/nodejs-service-deployment.git
cd nodejs-service-deployment
You need this repository to store the project code, trigger the deployment workflow, and store secrets.
- Create a new repository on GitHub for the project.
- Generate an SSH key pair, if you don't one already.
ssh-keygen -t rsa -P "" -f ~/.ssh/id_rsa - Create a new secret in the repository settings (Settings > Secrets and variables > Actions > New repository secret).
- Name:
SSH_PRIVATE_KEY - Value: Your private SSH key (contents of
~/.ssh/id_rsa) - Name:
SSH_PUBLIC_KEY - Value: Your public SSH key (contents of
~/.ssh/id_rsa.pub)
- Name:
- Log in to the AWS Management Console.
- Navigate to the S3 service and create a new S3 bucket.
- Bucket name:
node-app-terraform-state-<Your AWS Account ID>(Replace<Your AWS Account ID>with your AWS Account ID) - Region:
us-east-1 - Enable versioning
- Enable public access block
- Create bucket
- Bucket name:
- Navigate to the S3 service and create a new DynamoDB table for state locking
- Table name:
node-app-terraform-locks - Partition key:
LockID(String) - Create table
- Table name:
- update
terraform\backend.tfwith your bucket name and DynamoDB table name.terraform { required_version = ">= 1.13.0" backend "s3" { bucket = "node-app-terraform-state-<Your AWS Account ID>" # Change this key = "global/terraform.tfstate" region = "us-east-1" dynamodb_table = "node-app-terraform-locks" # And this encrypt = true } }
-
Log in to the AWS Management Console.
-
Navigate to the IAM service.
-
Create a new
Web identityrole- Trusted entity type:
Web identity - Identity provider:
token.actions.githubusercontent.com - Audience:
sts.amazonaws.com - GitHub organization:
Your Github UsernameorYour Github Organization - GitHub repository:
Your repository name
- Trusted entity type:
-
In the new role you created add the following inline policy:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "EC2FullAccess", "Effect": "Allow", "Action": "ec2:*", "Resource": "*" }, { "Sid": "STSGetCallerIdentity", "Effect": "Allow", "Action": "sts:GetCallerIdentity", "Resource": "*" }, { "Sid": "TerraformS3Backend", "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket" ], "Resource": [ "<Your S3 Bucket Name ARN>", "<Your S3 Bucket Name ARN>/*" ] }, { "Sid": "TerraformDynamoDBLock", "Effect": "Allow", "Action": [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem", "dynamodb:UpdateItem" ], "Resource": "<Your DynamoDB Table ARN>" } ] }Note: Replace
<Your S3 Bucket Name ARN>and<Your DynamoDB Table ARN>with your State Bucket ARN and DynamoDB Table ARN created for Terraform remote state (./terraform/backend.tf). -
Copy the Role ARN, we will need it later.
-
In
.github\workflows\deploy_service.yml, change therole-to-assumevalue to your role ARN.- name: Configure AWS credentials via OIDC (Terraform) uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::<Account-ID>:role/<Role-Name> # Change this to your role ARN aws-region: us-east-1
-
In
ansible\roles\app\tasks\main.yml, update the repo URL to point to your new repository.- name: Clone repo into tmp git: # Change this to your repository URL repo: "https://github.com/yourusername/yourrepository.git" dest: /tmp/node-service-repo version: main
- Commit and push your changes to the
mainbranch of the repository you created.git add . git commit -m "Deploy Node.js service" git push origin main - Check the Actions tab in your repository to monitor the deployment progress.
To destroy the infrastructure created by this project, follow these steps:
- Navigate to the project directory.
- Ensure you have Terraform installed and configured with AWS credentials.
- Run the following command to destroy the resources:
cd terraform terraform destroy - Enter you public key content from
~/.ssh/id_rsa.puband confirm the destruction when prompted.
The Node.js service is a minimal Express app with one endpoint:
GET /
Response: "Hello, world!"- For local testing:
cd node_app npm install node index.js
Note: You can modify the Node.js application code according to your preferences. However, when making significant changes to the application structure or dependencies, make sure to review and update the
ansible\roles\app\tasks\main.ymlfile accordingly to ensure proper deployment.
The GitHub Actions workflow (deploy_service.yml) automates the deployment process through three main jobs:
- Triggered on push to main branch (excluding README and git files)
- Sets up Terraform and AWS credentials via OIDC
- Deploys EC2 instance using Terraform configurations
- Outputs the server IP for use in subsequent jobs
- Runs after successful Terraform deployment
- Configures SSH access using repository secrets
- Generates Ansible inventory with server IP and private key secret
- Runs Ansible playbook to install and configure the Node.js service
- Only runs if previous jobs fail
- Destroys AWS resources using Terraform
- Ensures no orphaned resources remain in case of deployment failure
The workflow uses repository secrets (SSH_PRIVATE_KEY and SSH_PUBLIC_KEY) for secure access and AWS OIDC for authentication. It follows IaC principles by using Terraform for infrastructure provisioning and Ansible for configuration management.
- The SSH private key and public key must be kept as Github secret, and never commit it to the repo.
- The
ANSIBLE_HOST_KEY_CHECKING=Falsein.github\workflows\deploy_service.ymlis used in CI for ephemeral runners for theansiblejob.
This project is licensed under the MIT License - see the LICENSE file for details.