├── .dockerignore ├── deploy ├── 02-ecr.tf ├── data.tf ├── local.tf ├── 05-s3.tf ├── 12-cloudwatchlogs.tf ├── 01-main.tf ├── 00-variables.tf ├── terraform.tfvars ├── 10-loadbalancer.tf ├── 06-codebuild.tf ├── task-definitions │ └── service.json.tpl ├── 04-codepipeline-role.tf ├── 11-task-role.tf ├── 03-codepipeline.tf ├── .terraform.lock.hcl ├── 09-network.tf ├── 08-ecs.tf └── 07-codebuild-role.tf ├── img └── aws-node-ecs2.JPG ├── Dockerfile ├── models └── User.js ├── .gitignore ├── server.js ├── package.json ├── config └── db.js ├── middleware └── auth.js ├── routes └── api │ ├── users.js │ └── auth.js └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /deploy/02-ecr.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "node_app" { 2 | name = var.app_name 3 | } -------------------------------------------------------------------------------- /deploy/data.tf: -------------------------------------------------------------------------------- 1 | data "aws_secretsmanager_secret" "mongo_password_secret" { 2 | name = "MongoPassword2" 3 | } -------------------------------------------------------------------------------- /img/aws-node-ecs2.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatthewCYLau/node-aws-fargate-terraform/HEAD/img/aws-node-ecs2.JPG -------------------------------------------------------------------------------- /deploy/local.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | vpc_cidr = "10.0.0.0/16" 3 | availability_zones = ["us-east-1a", "us-east-1b"] 4 | } 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8-alpine 2 | RUN mkdir -p /usr/src/app 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | RUN npm install 6 | EXPOSE 3000 7 | CMD ["npm","start"] -------------------------------------------------------------------------------- /deploy/05-s3.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "node_app" { 2 | bucket = var.app_name 3 | acl = "private" 4 | force_destroy = true 5 | 6 | } -------------------------------------------------------------------------------- /deploy/12-cloudwatchlogs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudwatch_log_group" "node-aws-fargate-app" { 2 | name = "awslogs-node-aws-fargate-app" 3 | 4 | tags = { 5 | Environment = var.environment 6 | Application = var.app_name 7 | } 8 | } -------------------------------------------------------------------------------- /deploy/01-main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "us-east-1" 3 | } 4 | 5 | terraform { 6 | backend "s3" { 7 | bucket = "node-app-tf-state" 8 | key = "terraform.tfstate" 9 | region = "us-east-1" 10 | } 11 | } 12 | 13 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /deploy/00-variables.tf: -------------------------------------------------------------------------------- 1 | variable "github_token" {} 2 | 3 | variable "github_username" {} 4 | 5 | variable "github_project_name" {} 6 | 7 | variable "app_name" {} 8 | 9 | variable "environment" {} 10 | 11 | variable "default_region" {} 12 | 13 | variable "docker_username" {} 14 | 15 | variable "mongo_username" {} 16 | 17 | variable "mongo_host" {} 18 | 19 | variable "mongo_database_name" {} 20 | -------------------------------------------------------------------------------- /deploy/terraform.tfvars: -------------------------------------------------------------------------------- 1 | default_region = "us-east-1" 2 | docker_username = "matlau" 3 | github_username = "MatthewCYLau" 4 | github_project_name = "node-aws-fargate-terraform" 5 | app_name = "node-aws-fargate-app" 6 | environment = "production" 7 | mongo_username = "admin-matlau" 8 | mongo_host = "mattewcylau-5ltcp.mongodb.net" 9 | mongo_database_name = "node-aws-fargate-app" -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true 7 | }, 8 | email: { 9 | type: String, 10 | required: true, 11 | unique: true 12 | }, 13 | password: { 14 | type: String, 15 | required: true 16 | }, 17 | date: { 18 | type: Date, 19 | default: Date.now 20 | } 21 | }); 22 | 23 | module.exports = User = mongoose.model("user", UserSchema); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | ### Terraform ### 27 | # Local .terraform directories 28 | **/.terraform/* 29 | 30 | # .tfstate files 31 | *.tfstate 32 | *.tfstate.* 33 | 34 | *secret.tfvars -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const connectDB = require("./config/db"); 3 | 4 | const app = express(); 5 | 6 | // Connect Database 7 | connectDB(); 8 | 9 | // Init Middleware 10 | app.use(express.json({ extended: false })); 11 | 12 | // Define Routes 13 | app.use("/api/users", require("./routes/api/users")); 14 | app.use("/api/auth", require("./routes/api/auth")); 15 | 16 | const PORT = process.env.PORT || 3000; 17 | 18 | app.get("/ping", (req, res) => { 19 | res.send("Pong!"); 20 | }); 21 | 22 | app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-aws-fargate-terraform-app", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon server" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcryptjs": "^2.4.3", 14 | "concurrently": "^5.3.0", 15 | "config": "^3.3.3", 16 | "express": "^4.17.1", 17 | "express-validator": "^6.9.2", 18 | "gravatar": "^1.8.1", 19 | "jsonwebtoken": "^8.5.1", 20 | "mongoose": "^5.11.16", 21 | "nodemon": "^2.0.7", 22 | "request": "^2.88.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const mongoConnectionString = `mongodb+srv://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@${process.env.MONGO_HOST}/${process.env.MONGO_DB_NAME}?retryWrites=true`; 4 | 5 | const connectDB = async () => { 6 | try { 7 | await mongoose.connect(mongoConnectionString, { 8 | useNewUrlParser: true, 9 | useCreateIndex: true, 10 | useFindAndModify: false, 11 | useUnifiedTopology: true 12 | }); 13 | 14 | console.log("MongoDB Connected..."); 15 | } catch (err) { 16 | console.error(err.message); 17 | // Exit process with failure 18 | process.exit(1); 19 | } 20 | }; 21 | 22 | module.exports = connectDB; 23 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | module.exports = async function(req, res, next) { 4 | // Get token from header 5 | const token = req.header("x-auth-token"); 6 | 7 | // Check if not token 8 | if (!token) { 9 | return res.status(401).json({ msg: "No token, authorization denied" }); 10 | } 11 | 12 | // Verify token 13 | try { 14 | await jwt.verify(token, process.env.JWT_SECRET, (error, decoded) => { 15 | if (error) { 16 | res.status(401).json({ msg: "Token is not valid" }); 17 | } else { 18 | req.user = decoded.user; 19 | next(); 20 | } 21 | }); 22 | } catch (err) { 23 | console.error("something wrong with auth middleware"); 24 | res.status(500).json({ msg: "Server Error" }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /deploy/10-loadbalancer.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "this" { 2 | name = "alb" 3 | subnets = aws_subnet.public_subnets[*].id 4 | load_balancer_type = "application" 5 | security_groups = [aws_security_group.lb.id] 6 | 7 | tags = { 8 | Environment = var.environment 9 | Application = var.app_name 10 | } 11 | } 12 | 13 | output "aws_lb_dns_name" { 14 | value = aws_lb.this.dns_name 15 | } 16 | 17 | resource "aws_lb_listener" "https_forward" { 18 | load_balancer_arn = aws_lb.this.arn 19 | port = 80 20 | protocol = "HTTP" 21 | 22 | default_action { 23 | type = "forward" 24 | target_group_arn = aws_lb_target_group.this.arn 25 | } 26 | } 27 | 28 | resource "aws_lb_target_group" "this" { 29 | name = "${var.app_name}-alb-tg" 30 | port = 80 31 | protocol = "HTTP" 32 | vpc_id = aws_vpc.vpc.id 33 | target_type = "ip" 34 | 35 | health_check { 36 | healthy_threshold = "3" 37 | interval = "90" 38 | protocol = "HTTP" 39 | matcher = "200-299" 40 | timeout = "20" 41 | path = "/" 42 | unhealthy_threshold = "2" 43 | } 44 | } -------------------------------------------------------------------------------- /deploy/06-codebuild.tf: -------------------------------------------------------------------------------- 1 | resource "aws_codebuild_project" "node_aws_fargate_app" { 2 | name = "node_aws_fargate_app" 3 | description = "Node AWS Fargate App" 4 | build_timeout = "5" 5 | service_role = aws_iam_role.node_express_ecs_codebuild_role.arn 6 | 7 | artifacts { 8 | type = "CODEPIPELINE" 9 | } 10 | 11 | environment { 12 | compute_type = "BUILD_GENERAL1_SMALL" 13 | image = "aws/codebuild/standard:4.0" 14 | type = "LINUX_CONTAINER" 15 | image_pull_credentials_type = "CODEBUILD" 16 | privileged_mode = true 17 | 18 | environment_variable { 19 | name = "AWS_DEFAULT_REGION" 20 | value = var.default_region 21 | } 22 | 23 | environment_variable { 24 | name = "DOCKER_USERNAME" 25 | value = var.docker_username 26 | } 27 | 28 | environment_variable { 29 | name = "AWS_ACCOUNT_ID" 30 | value = data.aws_caller_identity.current.account_id 31 | } 32 | 33 | environment_variable { 34 | name = "IMAGE_NAME" 35 | value = var.app_name 36 | } 37 | 38 | environment_variable { 39 | name = "CONTAINER_NAME" 40 | value = var.app_name 41 | } 42 | 43 | environment_variable { 44 | name = "ECR_REPO_URL" 45 | value = aws_ecr_repository.node_app.repository_url 46 | } 47 | } 48 | 49 | source { 50 | type = "CODEPIPELINE" 51 | } 52 | } -------------------------------------------------------------------------------- /deploy/task-definitions/service.json.tpl: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "${container_name}", 4 | "image": "${aws_ecr_repository}:${tag}", 5 | "essential": true, 6 | "logConfiguration": { 7 | "logDriver": "awslogs", 8 | "options": { 9 | "awslogs-region": "us-east-1", 10 | "awslogs-stream-prefix": "${aws_cloudwatch_log_group_name}-service", 11 | "awslogs-group": "${aws_cloudwatch_log_group_name}" 12 | } 13 | }, 14 | "portMappings": [ 15 | { 16 | "containerPort": 3000, 17 | "hostPort": 3000, 18 | "protocol": "tcp" 19 | } 20 | ], 21 | "cpu": 1, 22 | "environment": [ 23 | { 24 | "name": "NODE_ENV", 25 | "value": "production" 26 | }, 27 | { 28 | "name": "JWT_SECRET", 29 | "value": "superscretwhichshouldnotbehere" 30 | }, 31 | { 32 | "name": "MONGO_USERNAME", 33 | "value": "${mongo_username}" 34 | }, 35 | { 36 | "name": "MONGO_HOST", 37 | "value": "${mongo_host}" 38 | }, 39 | { 40 | "name": "MONGO_DB_NAME", 41 | "value": "${mongo_database_name}" 42 | } 43 | ], 44 | "secrets": [{ 45 | "name": "MONGO_PASSWORD", 46 | "valueFrom": "${mongo_password_secret_manager_secret}" 47 | }], 48 | "ulimits": [ 49 | { 50 | "name": "nofile", 51 | "softLimit": 65536, 52 | "hardLimit": 65536 53 | } 54 | ], 55 | "mountPoints": [], 56 | "memory": 2048, 57 | "volumesFrom": [] 58 | } 59 | ] -------------------------------------------------------------------------------- /deploy/04-codepipeline-role.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "node_express_ecs_codepipeline_role" { 2 | name = "node_express_ecs_codepipeline_role" 3 | 4 | assume_role_policy = < { 26 | const errors = validationResult(req); 27 | if (!errors.isEmpty()) { 28 | return res.status(400).json({ errors: errors.array() }); 29 | } 30 | 31 | const { name, email, password } = req.body; 32 | 33 | try { 34 | let user = await User.findOne({ email }); 35 | 36 | if (user) { 37 | return res 38 | .status(400) 39 | .json({ errors: [{ msg: "User already exists" }] }); 40 | } 41 | 42 | const avatar = gravatar.url(email, { 43 | s: "200", 44 | r: "pg", 45 | d: "mm" 46 | }); 47 | 48 | user = new User({ 49 | name, 50 | email, 51 | avatar, 52 | password 53 | }); 54 | 55 | const salt = await bcrypt.genSalt(10); 56 | 57 | user.password = await bcrypt.hash(password, salt); 58 | 59 | await user.save(); 60 | 61 | const payload = { 62 | user: { 63 | id: user.id 64 | } 65 | }; 66 | 67 | jwt.sign( 68 | payload, 69 | process.env.JWT_SECRET, 70 | { expiresIn: 360000 }, 71 | (err, token) => { 72 | if (err) throw err; 73 | res.json({ token }); 74 | } 75 | ); 76 | } catch (err) { 77 | console.error(err.message); 78 | res.status(500).send("Server error"); 79 | } 80 | } 81 | ); 82 | 83 | module.exports = router; 84 | -------------------------------------------------------------------------------- /routes/api/auth.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const bcrypt = require("bcryptjs"); 4 | const auth = require("../../middleware/auth"); 5 | const jwt = require("jsonwebtoken"); 6 | const { check, validationResult } = require("express-validator"); 7 | 8 | const User = require("../../models/User"); 9 | 10 | // @route GET api/auth 11 | // @desc Test route 12 | // @access Public 13 | router.get("/", auth, async (req, res) => { 14 | try { 15 | const user = await User.findById(req.user.id).select("-password"); 16 | res.json(user); 17 | } catch (err) { 18 | console.error(err.message); 19 | res.status(500).send("Server Error"); 20 | } 21 | }); 22 | 23 | // @route POST api/auth 24 | // @desc Authenticate user & get token 25 | // @access Public 26 | router.post( 27 | "/", 28 | [ 29 | check("email", "Please include a valid email").isEmail(), 30 | check("password", "Password is required").exists() 31 | ], 32 | async (req, res) => { 33 | const errors = validationResult(req); 34 | if (!errors.isEmpty()) { 35 | return res.status(400).json({ errors: errors.array() }); 36 | } 37 | 38 | const { email, password } = req.body; 39 | 40 | try { 41 | let user = await User.findOne({ email }); 42 | 43 | if (!user) { 44 | return res 45 | .status(400) 46 | .json({ errors: [{ msg: "Invalid Credentials" }] }); 47 | } 48 | 49 | const isMatch = await bcrypt.compare(password, user.password); 50 | 51 | if (!isMatch) { 52 | return res 53 | .status(400) 54 | .json({ errors: [{ msg: "Invalid Credentials" }] }); 55 | } 56 | 57 | const payload = { 58 | user: { 59 | id: user.id 60 | } 61 | }; 62 | 63 | jwt.sign( 64 | payload, 65 | process.env.JWT_SECRET, 66 | { expiresIn: 360000 }, 67 | (err, token) => { 68 | if (err) throw err; 69 | res.json({ token }); 70 | } 71 | ); 72 | } catch (err) { 73 | console.error(err.message); 74 | res.status(500).send("Server error"); 75 | } 76 | } 77 | ); 78 | 79 | module.exports = router; 80 | -------------------------------------------------------------------------------- /deploy/08-ecs.tf: -------------------------------------------------------------------------------- 1 | data "template_file" "node_app" { 2 | template = file("task-definitions/service.json.tpl") 3 | vars = { 4 | aws_ecr_repository = aws_ecr_repository.node_app.repository_url 5 | tag = "latest" 6 | container_name = var.app_name 7 | aws_cloudwatch_log_group_name = aws_cloudwatch_log_group.node-aws-fargate-app.name 8 | mongo_password_secret_manager_secret = "${data.aws_secretsmanager_secret.mongo_password_secret.id}:MONGO_PASSWORD::" 9 | mongo_username = var.mongo_username 10 | mongo_host = var.mongo_host 11 | mongo_database_name = var.mongo_database_name 12 | } 13 | } 14 | 15 | resource "aws_ecs_task_definition" "service" { 16 | family = "${var.app_name}-${var.environment}" 17 | network_mode = "awsvpc" 18 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 19 | cpu = 256 20 | memory = 2048 21 | requires_compatibilities = ["FARGATE"] 22 | container_definitions = data.template_file.node_app.rendered 23 | tags = { 24 | Environment = var.environment 25 | Application = var.app_name 26 | } 27 | } 28 | 29 | resource "aws_ecs_service" "production" { 30 | name = var.environment 31 | cluster = aws_ecs_cluster.this.id 32 | task_definition = aws_ecs_task_definition.service.arn 33 | desired_count = 1 34 | deployment_maximum_percent = 250 35 | launch_type = "FARGATE" 36 | 37 | network_configuration { 38 | security_groups = [aws_security_group.ecs_tasks.id] 39 | subnets = aws_subnet.public_subnets[*].id 40 | assign_public_ip = true 41 | } 42 | 43 | load_balancer { 44 | target_group_arn = aws_lb_target_group.this.arn 45 | container_name = var.app_name 46 | container_port = 3000 47 | } 48 | 49 | depends_on = [aws_lb_listener.https_forward, aws_iam_role_policy.ecs_task_execution_role] 50 | 51 | tags = { 52 | Environment = var.environment 53 | Application = var.app_name 54 | } 55 | } 56 | 57 | resource "aws_ecs_cluster" "this" { 58 | name = "${var.app_name}-cluster" 59 | } -------------------------------------------------------------------------------- /deploy/07-codebuild-role.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "node_express_ecs_codebuild_role" { 2 | name = "node_express_ecs_codebuild_role" 3 | 4 | assume_role_policy = <Buy Me A Coffee 80 | 81 | ## License 82 | 83 | [MIT](https://choosealicense.com/licenses/mit/) 84 | --------------------------------------------------------------------------------