Introduction
Learn how I built a secure cloud application by integrating Node.js with AWS Secrets Manager and automating the infrastructure deployment using Terraform. This project demonstrates modern cloud development practices and security implementation.
Table of contents
Project Overview
The Challenge
Solution Architecture
The Node Application
Infrastructure as Code with Terraform
Basic Setup
Setup The Provider
EC2 Instance Setup
IAM Roles and Policies
Security Groups Configuration
AWS Secrets Manager Setup
User Data Script Implementation
Deployment Process
Prerequisites
Step-by-Step Deployment Guide
Deployment Verification
Cost Considerations
Cleanup Process
Future Improvements
Conclusion
Project Overview
This project showcases a practical implementation of cloud security and infrastructure automation with Terraform. At its core, it combines a Node application that interacts with AWS Secrets Managerπ and a complete infrastructure setup automated through Terraform.
The project consist of two layers:
The application layer: A Node application that interacts directly with AWS Secrets Manager using the AWS SDK.
Infrastructure layer: The infrastructure is fully automated using Terraform, which provisions and configure the following components:
An EC2 instance to host the Node application
AWS Secrets Manager for secure secrets storage
IAM roles and policies following the principle of least privilege
Security groups with strictly controlled access
Instance profiles for secure EC2 AWS services communication
What makes this project particularly interesting is how it connects application development with infrastructure management π«°π«°π«°.
The Challenge
Last week, I was deploying a Node application on an EC2 instance and wondered how to provide environment variables from the .env file to my project. Here's where AWS Secrets Manager becomes useful.
I was curious about securely sharing secrets in my applications using Secrets Manager π€π©π½βπ», and I also wanted to learn Terraform for Infrastructure as Code (IaC) π. This was my first experience with Terraform and I really enjoyed it!
Solution Architecture
The diagram below shows the solution architecture I designed for this project.
This is a Node application that retrieves secrets from AWS Secrets Manager. This Node application is hosted on an EC2 instance with appropriate IAM roles and policies attached to it.
The Node Application
You can find the application in my GitHub Repository. This is a lightweight Node application with a simple UI that serves only to display the secrets, demonstrating the ability to access them from the Secrets Manager. The app uses the AWS SDK to connect with AWS.
Infrastructure as Code with Terraform
For the infrastructure layer of the application, I created this GitHub repository where you can find all my code.
Basic Setup β¨
For the setup, I first visited the Terraform Docs to learn how to install it on my local machine. Since I am working with AWS, I read the AWS Provider documentation to understand how to interact with various resources.
If you are using a different provider, you can find the registry here π: https://developer.hashicorp.com/terraform/language/providers#how-to-find-providers
Once you have this set up, make sure you are also logged in to the AWS CLI with the correct credentials.
About the code: For each Terraform configuration block, I created a file named after the block it references. Why? π€ Here are some of the reasons:
β This approach creates a modular and organized structure.
β Each file has a specific purpose.
β It makes it easier for team members to understand and navigate the codebase, as they can quickly find specific configurations.
.
βββ data.tf
βββ output.tf
βββ provider.tf
βββ resource.tf
βββ userdata.sh
βββ .gitignore
Setup The Provider βοΈ
To set up the AWS provider, I created a file named provider.tf
with the following content:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}
# Configure the AWS Provider
provider "aws" {
region = "us-west-2"
}
EC2 Instance Setup π©π½βπ»
For the EC2 Instance, I have to tackle different configurations.
First, I created a resource.tf
file with all the resources, one of the is the EC2 instance with the respective role to access the Secrets Manager and the Security groups.
This resource creates an EC2 instance with a Linux AMI, instance type t2.micro, an instance profile name, security groups for the instance, and user data to be injected during instance creation.
I also added a tag for billing tracking π.
resource "aws_instance" "app_server" {
ami = data.aws_ami.linux_ami.id
instance_type = "t2.micro"
iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name
key_name = "aws-terraform-challenge"
security_groups = [ aws_security_group.aws_terraform_challenge_security_group.name ]
user_data = file("userdata.sh")
tags = {
Name = "aws_terraform_challenge"
}
}
Keep in mind that the following lines in the code use values from data blocks that retrieve this information.
I'll explain each part for better understanding:
The AMI ID is retrieved from the following data block, where I set the most recent
argument to true
to ensure it brings the latest one. The owner is amazon
, and I filter by the Linux 2023 AMI
.
data "aws_ami" "linux_ami" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*"]
}
}
IAM Roles and Policies
The aws_iam_instance_profile
provides an IAM instance profile.
resource "aws_iam_instance_profile" "ec2_instance_profile" {
name = "terraform_challenge_instance_profile"
role = aws_iam_role.ec2_secrets_role.name
}
The Role:
resource "aws_iam_role" "ec2_secrets_role" {
name = "secretsrole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
tags = {
Name = "EC2SecretsManagerRole"
Purpose = "Allow EC2 to access Secrets Manager"
}
}
β¨ PRO TIP: Note that I added the tags argument for tracking billing purposes.
The policies:
resource "aws_iam_policy" "my_secrets_policy" {
name = "my_secrets_policy"
path = "/"
policy = data.aws_iam_policy_document.my_secrets_policy.json
}
In the code above, the policy argument refers to a reference to a JSON-formatted IAM policy that is generated using a data
block for an aws_iam_policy_document
. This policy is created following the Least Privilege principle.
data "aws_iam_policy_document" "my_secrets_policy" {
statement {
actions = [
"kms:DescribeKey",
"kms:ListAliases",
"kms:ListKeys"
]
resources = [
data.aws_kms_key.by_alias.arn,
]
}
statement {
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:ListSecretVersionIds"
]
resources = [
join("", [aws_secretsmanager_secret.secret_terraform_challenge.id, "*"])
]
}
}
The resources
argument in the code above refers to a data.aws_kms_key
piece of information. The AWS KMS Key data block is used to obtain detailed information about the specified KMS Key.
data "aws_kms_key" "by_alias" {
key_id = "alias/aws/secretsmanager"
}
Then create the resource that attaches a managed IAM Policy to an IAM role:
resource "aws_iam_role_policy_attachment" "secrets_policy" {
role = aws_iam_role.ec2_secrets_role.name
policy_arn = aws_iam_policy.my_secrets_policy.arn
}
Security Groups Configuration β¨
For the security groups, I need to do the following by creating a security group with the right rules:
β Create a security group.
β
Associate the security group with a specific VPC. As you can see, the vpc_id
:
β¨Allow all outbound rule.
β¨Set up inbound rules to:
Allow HTTP from my IP.
Allow SSH from my IP to connect to my EC2.
Allow traffic from my IP on port 3000.
resource "aws_security_group" "aws_terraform_challenge_security_group" {
name = "aws_terraform_challenge_security_group"
vpc_id = data.aws_vpc.default_vpc.id
tags = {
Name = "aws_terraform_challenge_security_group"
}
}
resource "aws_vpc_security_group_ingress_rule" "allow_http" {
security_group_id = aws_security_group.aws_terraform_challenge_security_group.id
cidr_ipv4 = "YOUR_IP_HERE"
from_port = 80
ip_protocol = "tcp"
to_port = 80
}
resource "aws_vpc_security_group_ingress_rule" "allow_ssh_from_my_ip" {
security_group_id = aws_security_group.aws_terraform_challenge_security_group.id
cidr_ipv4 = "YOUR_IP_HERE"
from_port = 22
ip_protocol = "tcp"
to_port = 22
}
resource "aws_vpc_security_group_ingress_rule" "allow_3000_traffic_from_my_ip" {
security_group_id = aws_security_group.aws_terraform_challenge_security_group.id
cidr_ipv4 = "YOUR_IP_HERE"
from_port = 3000
ip_protocol = "tcp"
to_port = 3000
}
resource "aws_vpc_security_group_egress_rule" "allow_all_egress_rule" {
security_group_id = aws_security_group.aws_terraform_challenge_security_group.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = -1
}
This security group is defined within aws_instance
.
The vpc_id
in the code above is obtained from the following data block, which gives details about a specific VPC, in this case, the default VPC.
data "aws_vpc" "default_vpc"{
default = true
}
AWS Secrets Manager Setupπ©π½βπ»
For the Secrets Manager, I created an aws_secretsmanager_secret
, which is a resource to manage AWS Secrets Manager secret metadata. I set the recovery window to 0 days. Why 0? Because setting it to 0 forces deletion without recovery (This is for the DEMO purposes only).
resource "aws_secretsmanager_secret" "secret_terraform_challenge" {
name = "secret_terraform_challenge"
recovery_window_in_days = 0
}
Then, set up the aws_secretsmanager_secret_version
to manage the version of the AWS Secrets Manager secret, including its secret value, which in this case is secret.
resource "aws_secretsmanager_secret_version" "secret_terraform_challenge_version" {
secret_id = aws_secretsmanager_secret.secret_terraform_challenge.id
secret_string = "secret"
}
User Data Script Implementation
To run the script when the EC2 instance is created, I created the following user-data bash script to run the application:
#!/bin/bash
yum update -y
curl -fsSL https://rpm.nodesource.com/setup_23.x | bash -
yum install -y nodejs git
mkdir -p /home/ec2-user/app && cd /home/ec2-user/app
git clone https://github.com/lalidiaz/terraform-aws-secrets-manager.git .
npm install
npm run start
π©΅ There's something really cool about Terraform π©΅: it has blocks called output
.
In this case, they will display the AWS Instance AMI and the Instance Public IP in the console. The public IP will be useful when we deploy the application, as you'll see later.
output "aws_instance_ami" {
value = aws_instance.app_server.ami
}
output "aws_instance_public_ip" {
value = aws_instance.app_server.public_ip
}
Deployment Process
Prerequisites
Make sure you have the following prerequisites before running the project:
An AWS Account
AWS CLI installed and configured with appropriate credentials
Terraform installed on your local machine
Your IPv4 address (to configure security group rules)
Step-by-Step Deployment Guide
To deploy the Node application, please clone my repository:
git clone https://github.com/lalidiaz/node-terraform-infrastructure/tree/main
cd node-terraform-infrastructure
Once you are inside the project's folder, let's deploy the app with Terraform and watch the magic happen β¨π!
Step 1
terraform init
command initializes a working directory containing Terraform configuration files.terraform init
Step 2
terraform plan
command creates an execution plan, which lets you preview the changes that Terraform plans to make to your infrastructure.By default, when Terraform creates a plan it:
Reads the current state of any already-existing remote objects to make sure that the Terraform state is up-to-date.
Compares the current configuration to the prior state and noting any differences.
Proposes a set of change actions that should, if applied, make the remote objects match the configuration.
terraform plan
Step 3
If everything looks correct, you can run the following command to apply the changes:
terraform apply
command executes the actions proposed in a Terraform plan.terraform apply
And that's it, the project is deployed successfully! β¨ππ
Deployment Verification
To verify the application, you will see the output in the console displaying the AWS EC2 Public IP. Just copy the IP and head over to the browser, paste the ip <aws_instance_public_ip>:3000
you should see the following screen:
Cost Considerations
Please note that we are creating an EC2 instance within the free tier, but the AWS Secrets Manager does have associated costs. Check the documentation pricing page for more details.
Cleanup Process
Finally, remember to always clean up so you donβt incur any changes.
terraform destroy
command is a convenient way to destroy all remote objects managed by a particular Terraform configuration.terraform destroy
Future Improvements
After talking with my mentor Mariano GonzΓ‘lez ,he suggested the following improvements for the app, which will be included in the next iterations:
Create a Terraform Backend block
Create a Module
Add CI/CD with GitHub Actions
Refactor the solution to enhance security
Add a local executor to calculate my IP on the fly instead of hardcoding it
Conclusion
While this was my first experience with Terraform, I really enjoyed it and took this challenge very seriously. I spent a few hours working on this solution, and I'm quite happy with the results.Thanks to my mentor Mariano GonzΓ‘lez π for guiding me and taking the time to explain concepts to me.
I must admit I was excited when I started working on this project. Although I know I need to keep studying, I learned a lot from this hands-on experience, which gave me the confidence to tackle future challenges!
Thanks for reading! See you in the next post! π