├── backend ├── terraform.tfstate ├── terraform.tfvars ├── images │ └── terraform_state_backend.jpg ├── outputs.tf ├── variables.tf ├── .terraform.lock.hcl ├── main.tf ├── Readme.md └── terraform.tfstate.backup ├── ToDo ├── gitlab_cicd ├── requirements.txt ├── Dockerfile ├── app.py └── .gitlab-ci.yml ├── app ├── requirements.txt ├── Readme.md ├── Dockerfile └── app.py ├── ecs ├── docker_dev │ ├── requirements.txt │ ├── Readme.md │ ├── Dockerfile │ └── app.py ├── docker_prod │ ├── requirements.txt │ ├── Readme.md │ ├── Dockerfile │ └── app.py ├── images │ ├── branches.png │ ├── git_pull.png │ ├── variables.png │ ├── gitlab_cicd.png │ ├── new_project.png │ ├── gitlabcicdyaml.png │ ├── protect_branches.png │ └── three_variables.png ├── locals.tf ├── terraform.tfvars ├── logs.tf ├── route53.tf ├── container_definitions │ ├── web_app_dev.json │ └── web_app_prod.json ├── variables.tf ├── acm.tf ├── provider.tf ├── outputs.tf ├── ecr.tf ├── sg.tf ├── .terraform.lock.hcl ├── alb.tf ├── ecs.tf ├── iam.tf └── Readme.md ├── images ├── image.png ├── branches.png ├── git_pull.png ├── gitlab_cicd.png ├── new_project.png ├── variables.png ├── gitlab_cicd_01.png ├── gitlabcicdyaml.png ├── parameters_store.png ├── protect_branches.png ├── terraform_plan.png ├── three_variables.png ├── architecture_diagram.png ├── cicd_pipeline_stages.jpg ├── terraform_state_backend.jpg └── README.md ├── networking ├── image.png ├── terraform.tfvars ├── terraform_plan.png ├── variables.tf ├── outputs.tf ├── main.tf ├── provider.tf ├── Readme.md └── .terraform.lock.hcl └── README.md /backend/terraform.tfstate: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ToDo: -------------------------------------------------------------------------------- 1 | Finish Gitlab CICD instruction -------------------------------------------------------------------------------- /gitlab_cicd/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | Pillow==10.0.1 -------------------------------------------------------------------------------- /ecs/docker_dev/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | Pillow==10.0.1 -------------------------------------------------------------------------------- /ecs/docker_prod/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.3 2 | Pillow==10.0.1 -------------------------------------------------------------------------------- /images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/image.png -------------------------------------------------------------------------------- /images/branches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/branches.png -------------------------------------------------------------------------------- /images/git_pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/git_pull.png -------------------------------------------------------------------------------- /backend/terraform.tfvars: -------------------------------------------------------------------------------- 1 | region = "us-east-1" 2 | project_name = "my-project" 3 | environment = "prod" 4 | -------------------------------------------------------------------------------- /images/gitlab_cicd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/gitlab_cicd.png -------------------------------------------------------------------------------- /images/new_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/new_project.png -------------------------------------------------------------------------------- /images/variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/variables.png -------------------------------------------------------------------------------- /networking/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/networking/image.png -------------------------------------------------------------------------------- /ecs/images/branches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/branches.png -------------------------------------------------------------------------------- /ecs/images/git_pull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/git_pull.png -------------------------------------------------------------------------------- /ecs/images/variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/variables.png -------------------------------------------------------------------------------- /networking/terraform.tfvars: -------------------------------------------------------------------------------- 1 | region = "us-east-1" 2 | project_name = "my-project" 3 | environment = "dev" 4 | -------------------------------------------------------------------------------- /ecs/images/gitlab_cicd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/gitlab_cicd.png -------------------------------------------------------------------------------- /ecs/images/new_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/new_project.png -------------------------------------------------------------------------------- /images/gitlab_cicd_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/gitlab_cicd_01.png -------------------------------------------------------------------------------- /images/gitlabcicdyaml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/gitlabcicdyaml.png -------------------------------------------------------------------------------- /images/parameters_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/parameters_store.png -------------------------------------------------------------------------------- /images/protect_branches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/protect_branches.png -------------------------------------------------------------------------------- /images/terraform_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/terraform_plan.png -------------------------------------------------------------------------------- /images/three_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/three_variables.png -------------------------------------------------------------------------------- /ecs/images/gitlabcicdyaml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/gitlabcicdyaml.png -------------------------------------------------------------------------------- /networking/terraform_plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/networking/terraform_plan.png -------------------------------------------------------------------------------- /ecs/images/protect_branches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/protect_branches.png -------------------------------------------------------------------------------- /ecs/images/three_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/ecs/images/three_variables.png -------------------------------------------------------------------------------- /images/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/architecture_diagram.png -------------------------------------------------------------------------------- /images/cicd_pipeline_stages.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/cicd_pipeline_stages.jpg -------------------------------------------------------------------------------- /images/terraform_state_backend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/images/terraform_state_backend.jpg -------------------------------------------------------------------------------- /backend/images/terraform_state_backend.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sahibgasimov/ecs-gitlab-terraform/HEAD/backend/images/terraform_state_backend.jpg -------------------------------------------------------------------------------- /backend/outputs.tf: -------------------------------------------------------------------------------- 1 | output "s3_bucket_debug" { 2 | value = module.s3_bucket 3 | } 4 | 5 | output "dynamodb_table_debug" { 6 | value = module.dynamodb_table 7 | } 8 | -------------------------------------------------------------------------------- /ecs/locals.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | vpc = { 3 | vpc_id = data.terraform_remote_state.vpc.outputs.vpc_id 4 | } 5 | 6 | public_subnets = data.terraform_remote_state.vpc.outputs.public_subnets 7 | private_subnets = data.terraform_remote_state.vpc.outputs.private_subnets 8 | } 9 | -------------------------------------------------------------------------------- /backend/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | description = "AWS region" 4 | } 5 | 6 | variable "project_name" { 7 | type = string 8 | description = "Project name" 9 | } 10 | 11 | variable "environment" { 12 | type = string 13 | description = "Environment (e.g., dev, prod)" 14 | } 15 | -------------------------------------------------------------------------------- /networking/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | description = "AWS region" 4 | } 5 | 6 | variable "project_name" { 7 | type = string 8 | description = "Project name" 9 | } 10 | 11 | variable "environment" { 12 | type = string 13 | description = "Environment (e.g., dev, prod)" 14 | } 15 | -------------------------------------------------------------------------------- /app/Readme.md: -------------------------------------------------------------------------------- 1 | aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 452303021915.dkr.ecr.us-east-1.amazonaws.com 2 | docker build -t web-app . 3 | 4 | docker tag web-app:latest 654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:main-latest 5 | 6 | docker push 654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:main-latest -------------------------------------------------------------------------------- /gitlab_cicd/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image d 2 | FROM python:3.9-slim 3 | 4 | # Set working directory 5 | WORKDIR /usr/src/app 6 | 7 | # Copy application code 8 | COPY . . 9 | 10 | # Install dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Expose application port 14 | EXPOSE 8080 15 | 16 | # Start application 17 | CMD ["python", "app.py"] 18 | 19 | -------------------------------------------------------------------------------- /ecs/docker_prod/Readme.md: -------------------------------------------------------------------------------- 1 | aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 452303021915.dkr.ecr.us-east-1.amazonaws.com 2 | docker build -t web-app . 3 | 4 | docker tag web-app:latest 654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:main-latest 5 | 6 | docker push 654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:main-latest -------------------------------------------------------------------------------- /ecs/docker_dev/Readme.md: -------------------------------------------------------------------------------- 1 | aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 654654507397.dkr.ecr.us-east-1.amazonaws.com 2 | 3 | docker build -t web-app . 4 | 5 | docker tag web-app:latest 654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:dev-latest 6 | 7 | docker push 654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:dev-latest 8 | -------------------------------------------------------------------------------- /networking/outputs.tf: -------------------------------------------------------------------------------- 1 | output "private_subnets" { 2 | value = module.vpc.private_subnets 3 | description = "IDs of the private subnets" 4 | } 5 | 6 | output "public_subnets" { 7 | value = module.vpc.public_subnets 8 | description = "IDs of the public subnets" 9 | } 10 | 11 | output "vpc_id" { 12 | value = module.vpc.vpc_id 13 | description = "ID of the VPC" 14 | } 15 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as the base image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the application code and requirements into the container 8 | COPY . /app 9 | 10 | # Install dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Expose port 8080 for the Flask application 14 | EXPOSE 8080 15 | 16 | # Run the Flask application 17 | CMD ["python", "app.py"] 18 | -------------------------------------------------------------------------------- /ecs/docker_dev/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as the base image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the application code and requirements into the container 8 | COPY . /app 9 | 10 | # Install dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Expose port 8080 for the Flask application 14 | EXPOSE 8080 15 | 16 | # Run the Flask application 17 | CMD ["python", "app.py"] 18 | -------------------------------------------------------------------------------- /ecs/docker_prod/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as the base image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy the application code and requirements into the container 8 | COPY . /app 9 | 10 | # Install dependencies 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | # Expose port 8080 for the Flask application 14 | EXPOSE 8080 15 | 16 | # Run the Flask application 17 | CMD ["python", "app.py"] 18 | -------------------------------------------------------------------------------- /gitlab_cicd/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify 2 | 3 | app = Flask(__name__) 4 | 5 | # Health check endpoint 6 | @app.route('/api/health', methods=['GET']) 7 | def health_check(): 8 | return jsonify(status="UP", message="Service is healthy"), 200 9 | 10 | # Sample application endpoint 11 | @app.route('/', methods=['GET']) 12 | def home(): 13 | return "Hello from the Python PROD app v1!" 14 | 15 | if __name__ == '__main__': 16 | import os 17 | port = int(os.getenv("PORT", 8080)) 18 | app.run(host='0.0.0.0', port=port) 19 | 20 | -------------------------------------------------------------------------------- /ecs/terraform.tfvars: -------------------------------------------------------------------------------- 1 | # Environment and Region 2 | env_name_prod = "prod" 3 | env_name_dev = "dev" # Change to "prod" for production 4 | region = "us-east-1" # AWS region 5 | 6 | # Application Details 7 | app_name = "web-app" # Your application name 8 | 9 | # AWS Account Details 10 | account_id = "452303021915" # Your AWS account ID 11 | 12 | # ALB Configuration 13 | alb_hostname_dev = "web-app-dev.654654507397.realhandsonlabs.net" 14 | alb_hostname_prod = "web-app-prod.654654507397.realhandsonlabs.net" 15 | 16 | # Subnet and VPC IDs 17 | 18 | route53_zone_id = "Z07843433K5AR756RSU8Z" 19 | 20 | -------------------------------------------------------------------------------- /ecs/logs.tf: -------------------------------------------------------------------------------- 1 | # CloudWatch Log Group for dev environment 2 | resource "aws_cloudwatch_log_group" "dev_log_group" { 3 | name = "/ecs/web-app-dev" 4 | retention_in_days = 180 # 6 months retention 5 | 6 | tags = { 7 | Environment = var.env_name_dev 8 | Project = var.app_name 9 | } 10 | } 11 | 12 | # CloudWatch Log Group for prod environment 13 | resource "aws_cloudwatch_log_group" "prod_log_group" { 14 | name = "/ecs/web-app-prod" 15 | retention_in_days = 180 # 6 months retention 16 | 17 | tags = { 18 | Environment = var.env_name_prod 19 | Project = var.app_name 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /networking/main.tf: -------------------------------------------------------------------------------- 1 | module "vpc" { 2 | source = "terraform-aws-modules/vpc/aws" 3 | version = "~> 4.0" 4 | 5 | name = "${var.project_name}-vpc" 6 | 7 | cidr = "10.0.0.0/16" 8 | 9 | azs = data.aws_availability_zones.available.names 10 | private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 11 | public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] 12 | 13 | enable_nat_gateway = true 14 | 15 | tags = { 16 | Environment = var.environment 17 | Project = var.project_name 18 | } 19 | } 20 | 21 | data "aws_availability_zones" "available" { 22 | state = "available" 23 | } 24 | -------------------------------------------------------------------------------- /ecs/route53.tf: -------------------------------------------------------------------------------- 1 | #############ROUTE53############## 2 | 3 | resource "aws_route53_record" "dev_record" { 4 | zone_id = var.route53_zone_id 5 | name = var.alb_hostname_dev 6 | type = "A" 7 | 8 | alias { 9 | name = aws_lb.app_alb.dns_name 10 | zone_id = aws_lb.app_alb.zone_id 11 | evaluate_target_health = true 12 | } 13 | } 14 | 15 | resource "aws_route53_record" "prod_record" { 16 | zone_id = var.route53_zone_id 17 | name = var.alb_hostname_prod 18 | type = "A" 19 | 20 | alias { 21 | name = aws_lb.app_alb.dns_name 22 | zone_id = aws_lb.app_alb.zone_id 23 | evaluate_target_health = true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /networking/provider.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | terraform { 5 | required_version = ">= 1.6.0, < 2.0.0" 6 | 7 | required_providers { 8 | aws = { 9 | source = "hashicorp/aws" 10 | version = "~> 5.35.0" 11 | } 12 | } 13 | } 14 | 15 | terraform { 16 | backend "s3" { 17 | bucket = "my-project-terraform-state-prod" # Replace with your actual bucket name 18 | key = "networking/terraform.tfstate" # Unique state file key for networking module 19 | region = "us-east-1" # Replace with your AWS region 20 | dynamodb_table = "my-project-terraform-lock" # Replace with your actual DynamoDB table name 21 | encrypt = true 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /ecs/container_definitions/web_app_dev.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dev-container", 4 | "image": "654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:dev-latest", 5 | "cpu": 256, 6 | "memory": 512, 7 | "essential": true, 8 | "portMappings": [ 9 | { 10 | "containerPort": 8080, 11 | "hostPort": 8080, 12 | "protocol": "tcp" 13 | } 14 | ], 15 | "healthCheck": { 16 | "command": ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"], 17 | "interval": 30, 18 | "timeout": 5, 19 | "retries": 3, 20 | "startPeriod": 10 21 | }, 22 | "logConfiguration": { 23 | "logDriver": "awslogs", 24 | "options": { 25 | "awslogs-group": "/ecs/web-app-dev", 26 | "awslogs-region": "us-east-1", 27 | "awslogs-stream-prefix": "dev" 28 | } 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /ecs/container_definitions/web_app_prod.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "prod-container", 4 | "image": "654654507397.dkr.ecr.us-east-1.amazonaws.com/web-app-repository:main-latest", 5 | "cpu": 512, 6 | "memory": 1024, 7 | "essential": true, 8 | "portMappings": [ 9 | { 10 | "containerPort": 8080, 11 | "hostPort": 8080, 12 | "protocol": "tcp" 13 | } 14 | ], 15 | "healthCheck": { 16 | "command": ["CMD-SHELL", "curl -f http://localhost:8080/api/health || exit 1"], 17 | "interval": 30, 18 | "timeout": 5, 19 | "retries": 3, 20 | "startPeriod": 10 21 | }, 22 | "logConfiguration": { 23 | "logDriver": "awslogs", 24 | "options": { 25 | "awslogs-group": "/ecs/web-app-prod", 26 | "awslogs-region": "us-east-1", 27 | "awslogs-stream-prefix": "prod" 28 | } 29 | } 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /ecs/variables.tf: -------------------------------------------------------------------------------- 1 | variable "route53_zone_id" { 2 | type = string 3 | description = "The ID of the Route53 hosted zone used for ACM validation" 4 | } 5 | 6 | variable "region" { 7 | type = string 8 | description = "Region to deploy resources" 9 | } 10 | 11 | variable "env_name_dev" { 12 | type = string 13 | description = "Name of the environment to deploy" 14 | } 15 | 16 | variable "env_name_prod" { 17 | type = string 18 | description = "Name of the environment to deploy" 19 | } 20 | 21 | variable "account_id" { 22 | type = string 23 | description = "Account ID for target account" 24 | } 25 | 26 | variable "alb_hostname_dev" { 27 | type = string 28 | description = "Hostname for the dev environment" 29 | } 30 | 31 | variable "alb_hostname_prod" { 32 | type = string 33 | description = "Hostname for the prod environment" 34 | } 35 | 36 | variable "app_name" { 37 | type = string 38 | description = "Application name" 39 | default = "web-app" 40 | } 41 | -------------------------------------------------------------------------------- /ecs/acm.tf: -------------------------------------------------------------------------------- 1 | resource "aws_acm_certificate" "app_certificate" { 2 | domain_name = var.alb_hostname_prod # Use prod domain for ACM validation 3 | validation_method = "DNS" 4 | 5 | subject_alternative_names = [ 6 | var.alb_hostname_dev # Add dev domain as SAN 7 | ] 8 | 9 | tags = { 10 | Environment = var.env_name_prod 11 | Project = var.app_name 12 | } 13 | } 14 | 15 | resource "aws_route53_record" "acm_validation" { 16 | for_each = { 17 | for dvo in aws_acm_certificate.app_certificate.domain_validation_options : 18 | dvo.domain_name => { 19 | name = dvo.resource_record_name 20 | type = dvo.resource_record_type 21 | value = dvo.resource_record_value 22 | } 23 | } 24 | 25 | zone_id = var.route53_zone_id 26 | name = each.value.name 27 | type = each.value.type 28 | records = [each.value.value] 29 | ttl = 300 30 | } 31 | 32 | resource "aws_acm_certificate_validation" "app_certificate_validation" { 33 | certificate_arn = aws_acm_certificate.app_certificate.arn 34 | validation_record_fqdns = [for record in aws_route53_record.acm_validation : record.fqdn] 35 | } -------------------------------------------------------------------------------- /ecs/provider.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.73.0" 8 | } 9 | 10 | random = { 11 | source = "hashicorp/random" 12 | version = "~> 3.6.0" 13 | } 14 | } 15 | 16 | backend "s3" { 17 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 18 | key = "ecs/terraform.tfstate" # Unique key for ECS state file 19 | region = "us-east-1" # Replace with your AWS region 20 | dynamodb_table = "my-project-terraform-lock" # Replace with your DynamoDB table name 21 | encrypt = true 22 | } 23 | } 24 | 25 | provider "aws" { 26 | region = var.region 27 | 28 | # Optional: Default tags that will be applied to all resources 29 | } 30 | 31 | data "terraform_remote_state" "vpc" { 32 | backend = "s3" 33 | config = { 34 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 35 | key = "networking/terraform.tfstate" # Path to the networking state file 36 | region = "us-east-1" # Replace with your AWS region 37 | dynamodb_table = "my-project-terraform-lock" # Replace with your DynamoDB table name 38 | encrypt = true 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /networking/Readme.md: -------------------------------------------------------------------------------- 1 | # VPC Module for ECS Cluster 2 | 3 | This module configures a Virtual Private Cloud (VPC) for the ECS cluster, including private and public subnets, NAT gateway, and necessary configurations. 4 | 5 | --- 6 | 7 | ![alt text](image.png) 8 | 9 | ## Features 10 | 11 | - **VPC Creation**: 12 | - CIDR block: `10.0.0.0/16` 13 | - Public and private subnets across availability zones. 14 | - **NAT Gateway**: 15 | - Enabled for outbound internet access from private subnets. 16 | - **Tagging**: 17 | - Tags for environment and project are applied. 18 | 19 | --- 20 | 21 | ## Prerequisites 22 | 23 | - Ensure the backend S3 bucket and DynamoDB table for Terraform state are already configured. 24 | - AWS credentials must be set up for the specified region. 25 | 26 | --- 27 | 28 | ## File Structure 29 | 30 | ```plaintext 31 | . 32 | ├── main.tf # Defines the VPC module and its configurations 33 | ├── provider.tf # Configures the AWS provider and backend 34 | ├── variables.tf # Declares input variables 35 | ├── terraform.tfvars # Defines variable values 36 | ├── outputs.tf # Exports outputs such as subnet and VPC IDs 37 | 38 | ``` 39 | ### Usage 40 | 41 | ``` 42 | terraform init 43 | terraform apply 44 | ``` 45 | 46 | ![alt text](terraform_plan.png) 47 | 48 | ### Notes 49 | - The terraform-aws-modules/vpc module is used to simplify VPC creation. 50 | - Ensure availability zones in your region support the configuration. -------------------------------------------------------------------------------- /ecs/outputs.tf: -------------------------------------------------------------------------------- 1 | output "alb_dns_name" { 2 | value = aws_lb.app_alb.dns_name 3 | description = "The DNS name of the ALB" 4 | } 5 | 6 | output "acm_certificate_arn" { 7 | value = aws_acm_certificate_validation.app_certificate_validation.certificate_arn 8 | description = "The ARN of the validated ACM certificate" 9 | } 10 | 11 | output "dev_target_group_arn" { 12 | value = aws_lb_target_group.dev_target_group.arn 13 | description = "The ARN of the dev target group" 14 | } 15 | 16 | output "prod_target_group_arn" { 17 | value = aws_lb_target_group.prod_target_group.arn 18 | description = "The ARN of the prod target group" 19 | } 20 | 21 | 22 | # output "ecr_repository_url" { 23 | # value = aws_ecr_repository.app_repository.repository_url 24 | # description = "The URL of the ECR repository" 25 | # } 26 | 27 | output "ecs_task_execution_role_arn" { 28 | description = "ARN of the ECS Task Execution Role" 29 | value = aws_iam_role.ecs_task_execution_role.arn 30 | } 31 | 32 | output "ecs_task_role_arn" { 33 | description = "ARN of the ECS Task Role" 34 | value = aws_iam_role.ecs_task_role.arn 35 | } 36 | 37 | output "dev_route53_record" { 38 | value = aws_route53_record.dev_record.fqdn 39 | description = "The FQDN of the dev Route 53 record" 40 | } 41 | 42 | output "prod_route53_record" { 43 | value = aws_route53_record.prod_record.fqdn 44 | description = "The FQDN of the prod Route 53 record" 45 | } 46 | 47 | -------------------------------------------------------------------------------- /networking/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.35.0" 6 | constraints = ">= 4.35.0, ~> 5.35.0" 7 | hashes = [ 8 | "h1:fggCACmhwwn6NOo3D6xY6WDyZfBSbMIb47X/MOC+zqE=", 9 | "zh:3a2a6f40db82d30ea8c5e3e251ca5e16b08e520570336e7e342be823df67e945", 10 | "zh:420a23b69b412438a15b8b2e2c9aac2cf2e4976f990f117e4bf8f630692d3949", 11 | "zh:4d8b887f6a71b38cff77ad14af9279528433e279eed702d96b81ea48e16e779c", 12 | "zh:4edd41f8e1c7d29931608a7b01a7ae3d89d6f95ef5502cf8200f228a27917c40", 13 | "zh:6337544e2ded5cf37b55a70aa6ce81c07fd444a2644ff3c5aad1d34680051bdc", 14 | "zh:668faa3faaf2e0758bf319ea40d2304340f4a2dc2cd24460ddfa6ab66f71b802", 15 | "zh:79ddc6d7c90e59fdf4a51e6ea822ba9495b1873d6a9d70daf2eeaf6fc4eb6ff3", 16 | "zh:885822027faf1aa57787f980ead7c26e7d0e55b4040d926b65709b764f804513", 17 | "zh:8c50a8f397b871388ff2e048f5eb280af107faa2e8926694f1ffd9f32a7a7cdf", 18 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 19 | "zh:a2f5d2553df5573a060641f18ee7585587047c25ba73fd80617f59b5893d22b4", 20 | "zh:c43833ae2a152213ee92eb5be7653f9493779eddbe0ce403ea49b5f1d87fd766", 21 | "zh:dab01527a3a55b4f0f958af6f46313d775e27f9ad9d10bedbbfea4a35a06dc5f", 22 | "zh:ed49c65620ec42718d681a7fc00c166c295ff2795db6cede2c690b83f9fb3e65", 23 | "zh:f0a358c0ae1087c466d0fbcc3b4da886f33f881a145c3836ec43149878b86a1a", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /backend/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.35.0" 6 | constraints = ">= 3.69.0, >= 4.9.0, ~> 5.35.0" 7 | hashes = [ 8 | "h1:fggCACmhwwn6NOo3D6xY6WDyZfBSbMIb47X/MOC+zqE=", 9 | "zh:3a2a6f40db82d30ea8c5e3e251ca5e16b08e520570336e7e342be823df67e945", 10 | "zh:420a23b69b412438a15b8b2e2c9aac2cf2e4976f990f117e4bf8f630692d3949", 11 | "zh:4d8b887f6a71b38cff77ad14af9279528433e279eed702d96b81ea48e16e779c", 12 | "zh:4edd41f8e1c7d29931608a7b01a7ae3d89d6f95ef5502cf8200f228a27917c40", 13 | "zh:6337544e2ded5cf37b55a70aa6ce81c07fd444a2644ff3c5aad1d34680051bdc", 14 | "zh:668faa3faaf2e0758bf319ea40d2304340f4a2dc2cd24460ddfa6ab66f71b802", 15 | "zh:79ddc6d7c90e59fdf4a51e6ea822ba9495b1873d6a9d70daf2eeaf6fc4eb6ff3", 16 | "zh:885822027faf1aa57787f980ead7c26e7d0e55b4040d926b65709b764f804513", 17 | "zh:8c50a8f397b871388ff2e048f5eb280af107faa2e8926694f1ffd9f32a7a7cdf", 18 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 19 | "zh:a2f5d2553df5573a060641f18ee7585587047c25ba73fd80617f59b5893d22b4", 20 | "zh:c43833ae2a152213ee92eb5be7653f9493779eddbe0ce403ea49b5f1d87fd766", 21 | "zh:dab01527a3a55b4f0f958af6f46313d775e27f9ad9d10bedbbfea4a35a06dc5f", 22 | "zh:ed49c65620ec42718d681a7fc00c166c295ff2795db6cede2c690b83f9fb3e65", 23 | "zh:f0a358c0ae1087c466d0fbcc3b4da886f33f881a145c3836ec43149878b86a1a", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /ecs/ecr.tf: -------------------------------------------------------------------------------- 1 | # resource "aws_ecr_repository" "app_repository" { 2 | # name = "${var.app_name}-repository" 3 | 4 | # tags = { 5 | # Project = var.app_name 6 | # } 7 | # } 8 | 9 | # resource "aws_ecr_lifecycle_policy" "app_repository_policy" { 10 | # repository = aws_ecr_repository.app_repository.name 11 | 12 | # policy = jsonencode({ 13 | # rules = [ 14 | # { 15 | # rulePriority = 1 16 | # description = "Expire untagged images" 17 | # selection = { 18 | # tagStatus = "untagged" 19 | # countType = "imageCountMoreThan" 20 | # countNumber = 10 21 | # } 22 | # action = { 23 | # type = "expire" 24 | # } 25 | # }, 26 | # { 27 | # rulePriority = 2 28 | # description = "Retain only the last 5 dev images" 29 | # selection = { 30 | # tagStatus = "tagged" 31 | # tagPrefixList = ["dev"] 32 | # countType = "imageCountMoreThan" 33 | # countNumber = 5 34 | # } 35 | # action = { 36 | # type = "expire" 37 | # } 38 | # }, 39 | # { 40 | # rulePriority = 3 41 | # description = "Retain only the last 5 prod images" 42 | # selection = { 43 | # tagStatus = "tagged" 44 | # tagPrefixList = ["prod"] 45 | # countType = "imageCountMoreThan" 46 | # countNumber = 5 47 | # } 48 | # action = { 49 | # type = "expire" 50 | # } 51 | # } 52 | # ] 53 | # }) 54 | # } 55 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, send_file, jsonify 2 | from PIL import Image 3 | import os 4 | 5 | app = Flask(__name__) 6 | UPLOAD_FOLDER = 'uploads' 7 | OUTPUT_FOLDER = 'output' 8 | os.makedirs(UPLOAD_FOLDER, exist_ok=True) 9 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 10 | 11 | @app.route('/') 12 | def home(): 13 | return ''' 14 | 15 | Image to PDF Converter 16 |

Upload an image to convert to PDF

17 |
18 | 19 | 20 |
21 | ''' 22 | 23 | @app.route('/convert', methods=['POST']) 24 | def convert_to_pdf(): 25 | if 'image' not in request.files: 26 | return "No file uploaded", 400 27 | 28 | file = request.files['image'] 29 | if file.filename == '': 30 | return "No selected file", 400 31 | 32 | try: 33 | # Save the uploaded file 34 | image_path = os.path.join(UPLOAD_FOLDER, file.filename) 35 | file.save(image_path) 36 | 37 | # Convert to PDF 38 | image = Image.open(image_path) 39 | if image.mode != 'RGB': 40 | image = image.convert('RGB') 41 | pdf_path = os.path.join(OUTPUT_FOLDER, f"{os.path.splitext(file.filename)[0]}.pdf") 42 | image.save(pdf_path, "PDF") 43 | 44 | # Send the PDF file as a response 45 | return send_file(pdf_path, as_attachment=True) 46 | except Exception as e: 47 | return f"An error occurred: {e}", 500 48 | 49 | # Health check endpoint for ALB 50 | @app.route('/api/health', methods=['GET']) 51 | def health_check(): 52 | return jsonify({"status": "healthy"}), 200 53 | 54 | if __name__ == '__main__': 55 | app.run(host='0.0.0.0', port=8080, debug=True) 56 | -------------------------------------------------------------------------------- /ecs/docker_dev/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, send_file, jsonify 2 | from PIL import Image 3 | import os 4 | 5 | app = Flask(__name__) 6 | UPLOAD_FOLDER = 'uploads' 7 | OUTPUT_FOLDER = 'output' 8 | os.makedirs(UPLOAD_FOLDER, exist_ok=True) 9 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 10 | 11 | @app.route('/') 12 | def home(): 13 | return ''' 14 | 15 | Image to PDF Converter 16 |

Upload an image to convert to PDF

17 |
18 | 19 | 20 |
21 | ''' 22 | 23 | @app.route('/convert', methods=['POST']) 24 | def convert_to_pdf(): 25 | if 'image' not in request.files: 26 | return "No file uploaded", 400 27 | 28 | file = request.files['image'] 29 | if file.filename == '': 30 | return "No selected file", 400 31 | 32 | try: 33 | # Save the uploaded file 34 | image_path = os.path.join(UPLOAD_FOLDER, file.filename) 35 | file.save(image_path) 36 | 37 | # Convert to PDF 38 | image = Image.open(image_path) 39 | if image.mode != 'RGB': 40 | image = image.convert('RGB') 41 | pdf_path = os.path.join(OUTPUT_FOLDER, f"{os.path.splitext(file.filename)[0]}.pdf") 42 | image.save(pdf_path, "PDF") 43 | 44 | # Send the PDF file as a response 45 | return send_file(pdf_path, as_attachment=True) 46 | except Exception as e: 47 | return f"An error occurred: {e}", 500 48 | 49 | # Health check endpoint for ALB 50 | @app.route('/api/health', methods=['GET']) 51 | def health_check(): 52 | return jsonify({"status": "healthy"}), 200 53 | 54 | if __name__ == '__main__': 55 | app.run(host='0.0.0.0', port=8080, debug=True) 56 | -------------------------------------------------------------------------------- /ecs/docker_prod/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, send_file, jsonify 2 | from PIL import Image 3 | import os 4 | 5 | app = Flask(__name__) 6 | UPLOAD_FOLDER = 'uploads' 7 | OUTPUT_FOLDER = 'output' 8 | os.makedirs(UPLOAD_FOLDER, exist_ok=True) 9 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 10 | 11 | @app.route('/') 12 | def home(): 13 | return ''' 14 | 15 | Image to PDF Converter 16 |

Upload an image to convert to PDF

17 |
18 | 19 | 20 |
21 | ''' 22 | 23 | @app.route('/convert', methods=['POST']) 24 | def convert_to_pdf(): 25 | if 'image' not in request.files: 26 | return "No file uploaded", 400 27 | 28 | file = request.files['image'] 29 | if file.filename == '': 30 | return "No selected file", 400 31 | 32 | try: 33 | # Save the uploaded file 34 | image_path = os.path.join(UPLOAD_FOLDER, file.filename) 35 | file.save(image_path) 36 | 37 | # Convert to PDF 38 | image = Image.open(image_path) 39 | if image.mode != 'RGB': 40 | image = image.convert('RGB') 41 | pdf_path = os.path.join(OUTPUT_FOLDER, f"{os.path.splitext(file.filename)[0]}.pdf") 42 | image.save(pdf_path, "PDF") 43 | 44 | # Send the PDF file as a response 45 | return send_file(pdf_path, as_attachment=True) 46 | except Exception as e: 47 | return f"An error occurred: {e}", 500 48 | 49 | # Health check endpoint for ALB 50 | @app.route('/api/health', methods=['GET']) 51 | def health_check(): 52 | return jsonify({"status": "healthy"}), 200 53 | 54 | if __name__ == '__main__': 55 | app.run(host='0.0.0.0', port=8080, debug=True) 56 | -------------------------------------------------------------------------------- /backend/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6.0, < 2.0.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.35.0" 8 | } 9 | } 10 | } 11 | 12 | provider "aws" { 13 | region = var.region 14 | } 15 | 16 | module "s3_bucket" { 17 | source = "terraform-aws-modules/s3-bucket/aws" 18 | version = "~> 3.0" 19 | 20 | bucket = "${var.project_name}-terraform-state-${var.environment}" 21 | acl = null 22 | force_destroy = true 23 | 24 | versioning = { 25 | enabled = true 26 | } 27 | 28 | tags = { 29 | Environment = var.environment 30 | Project = var.project_name 31 | } 32 | } 33 | 34 | module "dynamodb_table" { 35 | source = "terraform-aws-modules/dynamodb-table/aws" 36 | version = "~> 2.0" 37 | 38 | name = "${var.project_name}-terraform-lock" 39 | hash_key = "LockID" 40 | billing_mode = "PAY_PER_REQUEST" 41 | 42 | attributes = [ 43 | { 44 | name = "LockID" 45 | type = "S" 46 | } 47 | ] 48 | 49 | tags = { 50 | Environment = var.environment 51 | Project = var.project_name 52 | } 53 | } 54 | 55 | #First run tf apply to create with local backend then uncomment and move your local backend to the s3 56 | terraform { 57 | backend "s3" { 58 | bucket = "my-project-terraform-state-prod" # Replace with created S3 bucket name 59 | key = "backend/terraform.tfstate" # Unique key for this module's state file 60 | region = "us-east-1" # Replace with your AWS region 61 | dynamodb_table = "my-project-terraform-lock" # Replace with created DynamoDB table name 62 | encrypt = true 63 | } 64 | } 65 | 66 | # resource "aws_s3_bucket" "my-project-terraform-state-prod" { 67 | # bucket = "my-project-terraform-state-prod" 68 | 69 | # tags = { 70 | # Name = "MyBucket" 71 | # Environment = "Dev" 72 | # } 73 | # } 74 | 75 | # Optional: Add versioning 76 | # resource "aws_s3_bucket_versioning" "my-project-terraform-state-prod" { 77 | # bucket = aws_s3_bucket.my-project-terraform-state-prod.id 78 | 79 | # versioning_configuration { 80 | # status = "Enabled" 81 | # } 82 | # } -------------------------------------------------------------------------------- /ecs/sg.tf: -------------------------------------------------------------------------------- 1 | # Security Group for ALB 2 | module "sg_alb" { 3 | source = "terraform-aws-modules/security-group/aws" 4 | version = "~> 5.0" 5 | 6 | name = "${var.app_name}-alb" 7 | description = "Security group for ALB" 8 | vpc_id = local.vpc.vpc_id 9 | 10 | ingress_with_cidr_blocks = [ 11 | { 12 | from_port = 80 13 | to_port = 80 14 | protocol = "tcp" 15 | cidr_blocks = "0.0.0.0/0" 16 | }, 17 | { 18 | from_port = 443 19 | to_port = 443 20 | protocol = "tcp" 21 | cidr_blocks = "0.0.0.0/0" 22 | } 23 | ] 24 | 25 | egress_with_cidr_blocks = [ 26 | { 27 | from_port = 0 28 | to_port = 0 29 | protocol = "-1" 30 | cidr_blocks = "0.0.0.0/0" 31 | } 32 | ] 33 | 34 | tags = { 35 | Name = "${var.app_name}-alb" 36 | } 37 | } 38 | 39 | # Security Group for ECS Tasks (Dev) 40 | module "sg_ecs_dev" { 41 | source = "terraform-aws-modules/security-group/aws" 42 | version = "~> 5.0" 43 | 44 | name = "${var.app_name}-dev-ecs" 45 | description = "Security group for ECS tasks (Dev)" 46 | vpc_id = local.vpc.vpc_id 47 | 48 | ingress_with_source_security_group_id = [ 49 | { 50 | from_port = 8080 51 | to_port = 8080 52 | protocol = "tcp" 53 | source_security_group_id = module.sg_alb.security_group_id 54 | } 55 | ] 56 | 57 | egress_with_cidr_blocks = [ 58 | { 59 | from_port = 0 60 | to_port = 0 61 | protocol = "-1" 62 | cidr_blocks = "0.0.0.0/0" 63 | } 64 | ] 65 | 66 | tags = { 67 | Name = "${var.app_name}-dev-ecs" 68 | } 69 | } 70 | 71 | # Security Group for ECS Tasks (Prod) 72 | module "sg_ecs_prod" { 73 | source = "terraform-aws-modules/security-group/aws" 74 | version = "~> 5.0" 75 | 76 | name = "${var.app_name}-prod-ecs" 77 | description = "Security group for ECS tasks (Prod)" 78 | vpc_id = local.vpc.vpc_id 79 | 80 | ingress_with_source_security_group_id = [ 81 | { 82 | from_port = 8080 83 | to_port = 8080 84 | protocol = "tcp" 85 | source_security_group_id = module.sg_alb.security_group_id 86 | } 87 | ] 88 | 89 | egress_with_cidr_blocks = [ 90 | { 91 | from_port = 0 92 | to_port = 0 93 | protocol = "-1" 94 | cidr_blocks = "0.0.0.0/0" 95 | } 96 | ] 97 | 98 | tags = { 99 | Name = "${var.app_name}-prod-ecs" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ecs/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.73.0" 6 | constraints = ">= 3.29.0, ~> 5.73.0" 7 | hashes = [ 8 | "h1:j7FKP03yef+XO3EqgVs67emtFJZaxWEVYBepJdKOnLA=", 9 | "zh:0d24edc51ab6600f56d759831658a9d7a8f69b53900546b75038fc8e3f312406", 10 | "zh:1f8b8414f710a8c5a8777cb1ef1cad1cb4293bc035deb804734a8ec698b0850d", 11 | "zh:2cf76b03564051ee86ef5fbdaea1949e3af549f8836e56371fe94335cf795e1c", 12 | "zh:2ffe05c62b4ae6292dda66cd3a3cbe3e290a1a04369f3e6f74812e885cf3f2f0", 13 | "zh:3564069d9bc918e5bded252d65b6a8758d08b309e1ac54bf7c8e5947a94cdadc", 14 | "zh:4eb5395d52cfcb3c78e86c4ca3759bf9736e0e8dfa6955b0e1a59d9a7f41d805", 15 | "zh:6cd14cbabbcf8b1c15fa73f9ebba4d4df41215ef92bf8d14a3780a7cb571e5c4", 16 | "zh:6f7dc212dee1be2edb4620d352d9b0ea759744b5be08b84012a7621efa262052", 17 | "zh:7468a490d6df04a401f49422c86b46ef91eba00878cc9a5ec3ee4a12fe9447d0", 18 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 19 | "zh:b440ff1be9fc62235b2dcb522dd922cefe751065ba4a601415130462e79fb68e", 20 | "zh:d53dfd7311d8f130f0ce3184ed50461c34086d3490913a0d80d63574dac104a6", 21 | "zh:de9a130dd684aed5b89edc7ce44aef37fa38eca06549035cf387cde9d3937432", 22 | "zh:e0922d81fbed02062a74ea126d3cc6830fa0c8eac92108825d1120a262980831", 23 | "zh:fdd6cdabcf5e9bedb3a419ac18bd12b5b02d8371ba0fb2a6123420937354c8e1", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/random" { 28 | version = "3.6.3" 29 | constraints = "~> 3.6.0" 30 | hashes = [ 31 | "h1:Fnaec9vA8sZ8BXVlN3Xn9Jz3zghSETIKg7ch8oXhxno=", 32 | "zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451", 33 | "zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8", 34 | "zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe", 35 | "zh:4fa45c44c0de582c2edb8a2e054f55124520c16a39b2dfc0355929063b6395b1", 36 | "zh:588508280501a06259e023b0695f6a18149a3816d259655c424d068982cbdd36", 37 | "zh:737c4d99a87d2a4d1ac0a54a73d2cb62974ccb2edbd234f333abd079a32ebc9e", 38 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 39 | "zh:a357ab512e5ebc6d1fda1382503109766e21bbfdfaa9ccda43d313c122069b30", 40 | "zh:c51bfb15e7d52cc1a2eaec2a903ac2aff15d162c172b1b4c17675190e8147615", 41 | "zh:e0951ee6fa9df90433728b96381fb867e3db98f66f735e0c3e24f8f16903f0ad", 42 | "zh:e3cdcb4e73740621dabd82ee6a37d6cfce7fee2a03d8074df65086760f5cf556", 43 | "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0", 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /backend/Readme.md: -------------------------------------------------------------------------------- 1 | # Terraform S3 Backend Configuration 2 | 3 | This repository contains a Terraform configuration to manage state files and locking using AWS S3 and DynamoDB. It sets up an S3 bucket for state file storage and a DynamoDB table for state locking, ensuring a safe and reliable state management process. 4 | 5 | --- 6 | 7 | ## Features 8 | 9 | - **S3 Bucket**: Stores the Terraform state file. 10 | - Versioning is enabled for safety. 11 | - Configured to allow state storage with high reliability. 12 | - **DynamoDB Table**: Provides state locking to prevent race conditions in multi-user environments. 13 | 14 | --- 15 | 16 | ## How It Works 17 | 18 | 1. **Manual S3 Bucket Creation**: 19 | - Before running Terraform, create the S3 bucket manually to ensure the bucket is not accidentally destroyed by Terraform. 20 | 21 | 2. **Local to Remote State Migration**: 22 | - Initially, the state is stored locally. After running Terraform to create required resources (S3 and DynamoDB), the backend is updated to use the S3 bucket for remote state storage. 23 | 24 | 3. **Modules**: 25 | - Uses the `terraform-aws-modules/s3-bucket` module to configure the S3 bucket. 26 | - Uses the `terraform-aws-modules/dynamodb-table` module to configure the DynamoDB table. 27 | 28 | --- 29 | 30 | ## Prerequisites 31 | 32 | - AWS credentials should be configured for the specified region. 33 | - Create an S3 bucket manually before applying the Terraform configuration. 34 | 35 | --- 36 | 37 | ## Installation and Usage 38 | 39 | ### Step 1: Initialize and Apply Configuration with Local Backend 40 | 41 | 1. Clone the repository: 42 | ```bash 43 | git clone 44 | cd 45 | Initialize Terraform: 46 | 47 | ``` 48 | terraform init 49 | terraform apply 50 | ``` 51 | 52 | ### Step 2: Configure Remote Backend with S3 53 | Uncomment the following block in main.tf and update the placeholders: 54 | 55 | ``` 56 | terraform { 57 | backend "s3" { 58 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 59 | key = "backend/terraform.tfstate" 60 | region = "us-east-1" 61 | dynamodb_table = "my-project-terraform-lock" 62 | encrypt = true 63 | } 64 | } 65 | ``` 66 | 67 | Migrate the state file to the S3 backend: 68 | 69 | ``` 70 | terraform init -migrate-state 71 | ``` 72 | 73 | ![alt text](images/terraform_state_backend.jpg) 74 | 75 | #### File Structure 76 | ``` 77 | 78 | ├── main.tf # Main configuration file 79 | ├── variables.tf # Input variables 80 | ├── terraform.tfvars # Variable values 81 | ├── outputs.tf # Outputs for debug and reference 82 | ├── README.md # Project documentation 83 | ``` 84 | 85 | 86 | #### Notes 87 | - Create S3 remote state bucket manually to prevent accidental deletion. 88 | - The backend configuration uses versioning for safety and state locking for consistency. 89 | - Dynamodb table locks Terraform state files to prevent concurrent modifications. 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /gitlab_cicd/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - validate_environment 3 | - build_and_publish 4 | - deploy_to_dev 5 | - deploy_to_production 6 | - finalize_pipeline 7 | 8 | workflow: # Trigger pipeline only for specific branches 9 | rules: 10 | - if: '$CI_COMMIT_BRANCH == "main"' 11 | - if: '$CI_COMMIT_BRANCH == "dev"' 12 | 13 | image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest 14 | 15 | variables: 16 | AWS_ACCOUNT_NUMBER : "654654507397" 17 | REGION : "us-east-1" 18 | IMAGE_REPOSITORY : "web-app-repository" 19 | CLUSTER_NAME : "web-app-cluster" 20 | 21 | DEV_SERVICE_NAME : "web-app-dev" 22 | DEV_TASK_NAME : "web-app-dev" 23 | 24 | PROD_SERVICE_NAME : "web-app-prod" 25 | PROD_TASK_NAME : "web-app-prod" 26 | 27 | test_environment: 28 | stage: validate_environment 29 | script: 30 | - echo "Validating environment setup..." 31 | - aws --version 32 | - docker --version 33 | - jq --version 34 | - aws sts get-caller-identity 35 | 36 | build_and_push: 37 | stage: build_and_publish 38 | services: 39 | - docker:dind 40 | variables: 41 | DOCKER_HOST: tcp://docker:2375 42 | before_script: 43 | - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com 44 | 45 | script: 46 | - echo "Building the Docker image..." 47 | - docker build -t $IMAGE_REPOSITORY . 48 | - echo "Tagging the image..." 49 | - docker tag $IMAGE_REPOSITORY:latest $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-latest 50 | - docker tag $IMAGE_REPOSITORY:latest $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA 51 | - echo "Pushing the image to ECR..." 52 | - docker push $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-latest 53 | - docker push $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA 54 | 55 | deploy_to_dev: 56 | stage: deploy_to_dev 57 | rules: 58 | - if: '$CI_COMMIT_BRANCH == "dev"' 59 | script: 60 | - echo "Deploying to development environment..." 61 | - | 62 | aws ecs update-service \ 63 | --cluster $CLUSTER_NAME \ 64 | --service $DEV_SERVICE_NAME \ 65 | --task-definition $DEV_TASK_NAME \ 66 | --force-new-deployment 67 | 68 | deploy_to_production: 69 | stage: deploy_to_production 70 | when: manual # Require manual confirmation for production 71 | manual_confirmation: 'Proceed with production deployment?' 72 | allow_failure: false # Must succeed to continue 73 | rules: 74 | - if: '$CI_COMMIT_BRANCH == "main"' 75 | script: 76 | - echo "Deploying to production environment..." 77 | - | 78 | aws ecs update-service \ 79 | --cluster $CLUSTER_NAME \ 80 | --service $PROD_SERVICE_NAME \ 81 | --task-definition $PROD_TASK_NAME \ 82 | --force-new-deployment 83 | 84 | finalize_pipeline: 85 | stage: finalize_pipeline 86 | script: 87 | - echo "CI/CD Pipeline completed successfully!" 88 | -------------------------------------------------------------------------------- /ecs/alb.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "app_alb" { 2 | name = "${var.app_name}-alb" 3 | internal = false 4 | load_balancer_type = "application" 5 | security_groups = [module.sg_alb.security_group_id] 6 | subnets = local.public_subnets 7 | 8 | enable_deletion_protection = false 9 | 10 | tags = { 11 | Project = var.app_name 12 | } 13 | } 14 | 15 | resource "aws_lb_listener" "https" { 16 | load_balancer_arn = aws_lb.app_alb.arn 17 | port = 443 18 | protocol = "HTTPS" 19 | ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" 20 | certificate_arn = aws_acm_certificate_validation.app_certificate_validation.certificate_arn 21 | 22 | default_action { 23 | type = "fixed-response" 24 | fixed_response { 25 | content_type = "text/plain" 26 | message_body = "Not Found" 27 | status_code = "404" 28 | } 29 | } 30 | } 31 | 32 | resource "aws_lb_listener" "http" { 33 | load_balancer_arn = aws_lb.app_alb.arn 34 | port = 80 35 | protocol = "HTTP" 36 | 37 | default_action { 38 | type = "redirect" 39 | redirect { 40 | port = "443" 41 | protocol = "HTTPS" 42 | status_code = "HTTP_301" 43 | } 44 | } 45 | } 46 | 47 | resource "aws_lb_listener_rule" "dev_rule" { 48 | listener_arn = aws_lb_listener.https.arn 49 | priority = 100 50 | 51 | condition { 52 | host_header { 53 | values = [var.alb_hostname_dev] 54 | } 55 | } 56 | 57 | action { 58 | type = "forward" 59 | target_group_arn = aws_lb_target_group.dev_target_group.arn 60 | } 61 | } 62 | 63 | resource "aws_lb_listener_rule" "prod_rule" { 64 | listener_arn = aws_lb_listener.https.arn 65 | priority = 200 66 | 67 | condition { 68 | host_header { 69 | values = [var.alb_hostname_prod] 70 | } 71 | } 72 | 73 | action { 74 | type = "forward" 75 | target_group_arn = aws_lb_target_group.prod_target_group.arn 76 | } 77 | } 78 | 79 | 80 | resource "aws_lb_target_group" "dev_target_group" { 81 | name = "${var.app_name}-dev-tg" 82 | port = 8080 83 | protocol = "HTTP" 84 | target_type = "ip" 85 | vpc_id = local.vpc.vpc_id 86 | 87 | health_check { 88 | path = "/api/health" 89 | interval = 30 90 | timeout = 5 91 | healthy_threshold = 2 92 | unhealthy_threshold = 2 93 | matcher = "200" 94 | } 95 | 96 | tags = { 97 | Environment = var.env_name_dev 98 | Project = var.app_name 99 | } 100 | } 101 | 102 | resource "aws_lb_target_group" "prod_target_group" { 103 | name = "${var.app_name}-prod-tg" 104 | port = 8080 105 | protocol = "HTTP" 106 | target_type = "ip" 107 | vpc_id = local.vpc.vpc_id 108 | 109 | health_check { 110 | path = "/api/health" 111 | interval = 30 112 | timeout = 5 113 | healthy_threshold = 2 114 | unhealthy_threshold = 2 115 | matcher = "200" 116 | } 117 | 118 | tags = { 119 | Environment = var.env_name_prod 120 | Project = var.app_name 121 | } 122 | } 123 | 124 | -------------------------------------------------------------------------------- /ecs/ecs.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecs_cluster" "app_cluster" { 2 | name = "${var.app_name}-cluster" 3 | 4 | setting { 5 | name = "containerInsights" 6 | value = "enabled" 7 | } 8 | 9 | tags = { 10 | Name = "${var.app_name}-cluster" 11 | } 12 | 13 | lifecycle { 14 | ignore_changes = [setting] 15 | } 16 | } 17 | 18 | resource "aws_ecs_cluster_capacity_providers" "app_cluster_providers" { 19 | cluster_name = aws_ecs_cluster.app_cluster.name 20 | 21 | capacity_providers = ["FARGATE", "FARGATE_SPOT"] 22 | 23 | default_capacity_provider_strategy { 24 | base = 1 25 | weight = 100 26 | capacity_provider = "FARGATE" 27 | } 28 | } 29 | 30 | ########ECS SERVICE########## 31 | # ECS Service for Dev 32 | resource "aws_ecs_service" "dev" { 33 | name = "${var.app_name}-dev" 34 | cluster = aws_ecs_cluster.app_cluster.id 35 | task_definition = aws_ecs_task_definition.dev.arn 36 | desired_count = 1 37 | launch_type = "FARGATE" 38 | 39 | network_configuration { 40 | assign_public_ip = false 41 | security_groups = [module.sg_ecs_dev.security_group_id] 42 | subnets = local.private_subnets 43 | } 44 | 45 | load_balancer { 46 | target_group_arn = aws_lb_target_group.dev_target_group.arn 47 | container_name = "dev-container" 48 | container_port = 8080 49 | } 50 | } 51 | 52 | # ECS Service for Prod 53 | resource "aws_ecs_service" "prod" { 54 | name = "${var.app_name}-prod" 55 | cluster = aws_ecs_cluster.app_cluster.id 56 | task_definition = aws_ecs_task_definition.prod.arn 57 | desired_count = 1 58 | launch_type = "FARGATE" 59 | 60 | network_configuration { 61 | assign_public_ip = false 62 | security_groups = [module.sg_ecs_prod.security_group_id] 63 | subnets = local.private_subnets 64 | } 65 | 66 | load_balancer { 67 | target_group_arn = aws_lb_target_group.prod_target_group.arn 68 | container_name = "prod-container" 69 | container_port = 8080 70 | } 71 | } 72 | 73 | 74 | ######### ECS TASK DEFINITION########### 75 | 76 | resource "aws_ecs_task_definition" "dev" { 77 | family = "${var.app_name}-dev" 78 | container_definitions = file("${path.module}/container_definitions/web_app_dev.json") 79 | cpu = "256" 80 | memory = "512" 81 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 82 | task_role_arn = aws_iam_role.ecs_task_role.arn 83 | network_mode = "awsvpc" 84 | requires_compatibilities = ["FARGATE"] 85 | 86 | tags = { 87 | Name = "${var.app_name}-dev" 88 | } 89 | } 90 | 91 | resource "aws_ecs_task_definition" "prod" { 92 | family = "${var.app_name}-prod" 93 | container_definitions = file("${path.module}/container_definitions/web_app_prod.json") 94 | cpu = "512" 95 | memory = "1024" 96 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 97 | task_role_arn = aws_iam_role.ecs_task_role.arn 98 | network_mode = "awsvpc" 99 | requires_compatibilities = ["FARGATE"] 100 | 101 | tags = { 102 | Name = "${var.app_name}-prod" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ecs/iam.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "ecs_task_execution_role" { 2 | name = "${var.app_name}-task-execution" 3 | description = "Allows ECS tasks to call AWS services on your behalf" 4 | assume_role_policy = jsonencode({ 5 | Version = "2012-10-17", 6 | Statement = [ 7 | { 8 | Effect = "Allow", 9 | Principal = { Service = "ecs-tasks.amazonaws.com" }, 10 | Action = "sts:AssumeRole" 11 | } 12 | ] 13 | }) 14 | 15 | tags = { 16 | Name = "${var.app_name}-task-execution" 17 | } 18 | } 19 | 20 | resource "aws_iam_role_policy" "ecs_task_execution_policy" { 21 | name = "${var.app_name}-execution-policy" 22 | role = aws_iam_role.ecs_task_execution_role.name 23 | 24 | policy = jsonencode({ 25 | Version = "2012-10-17", 26 | Statement = [ 27 | { 28 | Effect = "Allow", 29 | Action = [ 30 | "ecr:GetDownloadUrlForLayer", 31 | "ecr:BatchGetImage", 32 | "ecr:BatchCheckLayerAvailability", 33 | "logs:PutLogEvents", 34 | "logs:CreateLogStream", 35 | "ssm:GetParameters", 36 | "ssm:GetParameter", 37 | "ssm:GetParameterHistory", 38 | "ecr:GetAuthorizationToken" 39 | 40 | ], 41 | Resource = "*" 42 | } 43 | ] 44 | }) 45 | } 46 | 47 | resource "aws_iam_role" "ecs_task_role" { 48 | name = "${var.app_name}-task" 49 | description = "Allows ECS tasks to assume this role" 50 | 51 | assume_role_policy = jsonencode({ 52 | Version = "2012-10-17", 53 | Statement = [ 54 | { 55 | Effect = "Allow", 56 | Principal = { Service = "ecs-tasks.amazonaws.com" }, 57 | Action = "sts:AssumeRole" 58 | } 59 | ] 60 | }) 61 | 62 | tags = { 63 | Name = "${var.app_name}-task" 64 | } 65 | } 66 | 67 | resource "aws_iam_role_policy" "ecs_task_custom_policy" { 68 | name = "${var.app_name}-custom-task-policy" 69 | role = aws_iam_role.ecs_task_role.name 70 | 71 | policy = jsonencode({ 72 | Version = "2012-10-17", 73 | Statement = [ 74 | { 75 | Effect = "Allow", 76 | Action = [ 77 | "s3:ListBucket", 78 | "s3:GetObject", 79 | "secretsmanager:GetSecretValue", 80 | "logs:PutLogEvents", 81 | "logs:CreateLogStream" 82 | ], 83 | Resource = "*" 84 | } 85 | ] 86 | }) 87 | } 88 | 89 | ##### GitLab CICD user ##### 90 | 91 | 92 | 93 | # Create the IAM user 94 | resource "aws_iam_user" "gitlab_cicd" { 95 | name = "gitlab-cicd" 96 | } 97 | 98 | # Attach AmazonEC2ContainerRegistryPowerUser policy 99 | resource "aws_iam_user_policy_attachment" "ecr_power_user" { 100 | user = aws_iam_user.gitlab_cicd.name 101 | policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser" 102 | } 103 | 104 | # Attach AmazonECS_FullAccess policy 105 | resource "aws_iam_user_policy_attachment" "ecs_full_access" { 106 | user = aws_iam_user.gitlab_cicd.name 107 | policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess" 108 | } 109 | 110 | # Create access keys for the user 111 | resource "aws_iam_access_key" "gitlab_cicd_key" { 112 | user = aws_iam_user.gitlab_cicd.name 113 | } 114 | 115 | # Store Access Key ID in SSM Parameter Store 116 | resource "aws_ssm_parameter" "access_key_id" { 117 | name = "/gitlab-cicd/access_key_id" 118 | description = "Access Key ID for gitlab-cicd user" 119 | type = "String" 120 | value = aws_iam_access_key.gitlab_cicd_key.id 121 | 122 | } 123 | 124 | # Store Secret Access Key in SSM Parameter Store 125 | resource "aws_ssm_parameter" "secret_access_key" { 126 | name = "/gitlab-cicd/secret_access_key" 127 | description = "Secret Access Key for gitlab-cicd user" 128 | type = "SecureString" 129 | value = aws_iam_access_key.gitlab_cicd_key.secret 130 | 131 | } 132 | -------------------------------------------------------------------------------- /images/README.md: -------------------------------------------------------------------------------- 1 | # ECS Fargate Deployment with Terraform and GitLab CI/CD 2 | 3 | ## Table of Contents 4 | 1. [Overview](#overview) 5 | 2. [Project Architecture](#project-architecture) 6 | 3. [Getting Started](#getting-started) 7 | 4. [Modules](#modules) 8 | - [Terraform Backend](#terraform-backend) 9 | - [Networking](#networking) 10 | - [ECS Infrastructure](#ecs-infrastructure) 11 | 5. [GitLab CI/CD Setup](#gitlab-cicd-setup) 12 | 6. [Application Details](#application-details) 13 | 7. [File Structure](#file-structure) 14 | 8. [Additional Notes](#additional-notes) 15 | 16 | --- 17 | 18 | ## Overview 19 | This project automates the deployment of a containerized Python application on **AWS ECS Fargate** using **Terraform** for infrastructure provisioning and **GitLab CI/CD** for continuous integration and delivery. 20 | 21 | ### Key Features 22 | - **Modular Terraform Setup**: Backend, networking, and ECS modules for scalability. 23 | - **CI/CD Pipeline**: Automates Docker image build, push to ECR, and deployment to ECS. 24 | - **Dev and Prod Environments**: Isolated environments with dedicated ALB configurations. 25 | - **TLS Support**: Secure HTTPS setup using ACM and Route 53. 26 | 27 | --- 28 | 29 | ## Project Architecture 30 | ![Project Architecture](images/architecture_diagram.png) 31 | 32 | --- 33 | 34 | ## Getting Started 35 | 36 | ### Prerequisites 37 | - **AWS CLI** installed and configured. 38 | - **Terraform** (>= 1.6.0). 39 | - **GitLab Account** with necessary permissions. 40 | - **Docker** installed locally for testing builds. 41 | 42 | ### Steps 43 | 1. Clone the repository: 44 | ```bash 45 | git clone https://github.com/sahibgasimov/ecs-gitlab-terraform.git 46 | 47 | cd ecs-gitlab-terraform 48 | ``` 49 | 50 | ### Modules 51 | 52 | ```tree 53 | ├── backend/ # Terraform backend module 54 | ├── networking/ # VPC and networking module 55 | ├── ecs/ # ECS cluster and services module 56 | ├── images/ # Architecture and CI/CD images 57 | ├── app/ # Python application code 58 | ├── .gitlab-ci.yml # GitLab CI/CD pipeline 59 | └── README.md # Project documentation 60 | ``` 61 | 62 | ### Terraform Backend 63 | 64 | This repository contains a Terraform configuration to manage state files and locking using AWS S3 and DynamoDB. It sets up an S3 bucket for state file storage and a DynamoDB table for state locking. 65 | 66 | --- 67 | 68 | #### Features 69 | 70 | - **S3 Bucket**: Stores the Terraform state file. 71 | - Versioning is enabled for safety. 72 | - Configured to allow state storage with high reliability. 73 | - **DynamoDB Table**: Provides state locking to prevent race conditions in multi-user environments. 74 | 75 | --- 76 | 77 | ### How It Works 78 | 79 | 1. **Manual S3 Bucket Creation**: 80 | - Before running Terraform, create the S3 bucket manually to ensure the bucket is not accidentally destroyed by Terraform. 81 | 82 | 2. **Local to Remote State Migration**: 83 | - Initially, the state is stored locally. After running Terraform to create required resources (S3 and DynamoDB), the backend is updated to use the S3 bucket for remote state storage. 84 | 85 | ## Installation and Usage 86 | 87 | ### Step 1: Initialize and Apply Configuration with Local Backend 88 | 89 | 1. Clone the repository: 90 | ```git 91 | git clone https://github.com/sahibgasimov/ecs-gitlab-terraform.git 92 | cd backend 93 | ``` 94 | Initialize Terraform: 95 | 96 | ```hcl 97 | terraform init 98 | terraform apply 99 | ``` 100 | 101 | ### Step 2: Configure Remote Backend with S3 102 | Uncomment the following block in main.tf and run terraform apply to create backend module. Once created update your existing s3 bucket to migrate state file to s3 bucket. 103 | 104 | ``` 105 | terraform { 106 | backend "s3" { 107 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 108 | key = "backend/terraform.tfstate" 109 | region = "us-east-1" 110 | dynamodb_table = "my-project-terraform-lock" 111 | encrypt = true 112 | } 113 | } 114 | ``` 115 | 116 | 117 | Migrate the state file to the S3 backend: 118 | 119 | ```hcl 120 | terraform init -migrate-state 121 | ``` 122 | 123 | ![alt text](images/terraform_state_backend.jpg) 124 | 125 | #### File Structure 126 | ``` 127 | 128 | ├── main.tf # Main configuration file 129 | ├── variables.tf # Input variables 130 | ├── terraform.tfvars # Variable values 131 | ├── outputs.tf # Outputs for debug and reference 132 | ├── README.md # Project documentation 133 | ``` 134 | 135 | 136 | #### Notes 137 | - Create S3 remote state bucket manually to prevent accidental deletion. 138 | - The backend configuration uses versioning for safety and state locking for consistency. 139 | - Dynamodb table locks Terraform state files to prevent concurrent modifications. 140 | 141 | ### Networking 142 | 143 | # VPC Module for ECS Cluster 144 | 145 | This module configures a Virtual Private Cloud (VPC) for the ECS cluster, including private and public subnets, NAT gateway, and necessary configurations. 146 | 147 | --- 148 | 149 | ![alt text](image.png) 150 | 151 | ## Features 152 | 153 | - **VPC Creation**: 154 | - CIDR block: `10.0.0.0/16` 155 | - Public and private subnets across availability zones. 156 | - **NAT Gateway**: 157 | - Enabled for outbound internet access from private subnets. 158 | - **Tagging**: 159 | - Tags for environment and project are applied. 160 | 161 | --- 162 | 163 | ## Prerequisites 164 | 165 | - Ensure the backend S3 bucket and DynamoDB table for Terraform state are already configured. 166 | - AWS credentials must be set up for the specified region. 167 | 168 | --- 169 | 170 | ## File Structure 171 | 172 | ```plaintext 173 | . 174 | ├── main.tf # Defines the VPC module and its configurations 175 | ├── provider.tf # Configures the AWS provider and backend 176 | ├── variables.tf # Declares input variables 177 | ├── terraform.tfvars # Defines variable values 178 | ├── outputs.tf # Exports outputs such as subnet and VPC IDs 179 | 180 | ``` 181 | ### Usage 182 | 183 | ``` 184 | terraform init 185 | terraform apply 186 | ``` 187 | 188 | ![alt text](terraform_plan.png) 189 | 190 | ### Notes 191 | - The terraform-aws-modules/vpc module is used to simplify VPC creation. 192 | - Ensure availability zones in your region support the configuration. -------------------------------------------------------------------------------- /backend/terraform.tfstate.backup: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "terraform_version": "1.6.0", 4 | "serial": 7, 5 | "lineage": "9a68cde6-9d32-6c6d-faad-6800240122a2", 6 | "outputs": { 7 | "dynamodb_table_debug": { 8 | "value": { 9 | "dynamodb_table_arn": "arn:aws:dynamodb:us-east-1:654654507397:table/my-project-terraform-lock", 10 | "dynamodb_table_id": "my-project-terraform-lock", 11 | "dynamodb_table_stream_arn": null, 12 | "dynamodb_table_stream_label": null 13 | }, 14 | "type": [ 15 | "object", 16 | { 17 | "dynamodb_table_arn": "string", 18 | "dynamodb_table_id": "string", 19 | "dynamodb_table_stream_arn": "dynamic", 20 | "dynamodb_table_stream_label": "dynamic" 21 | } 22 | ] 23 | }, 24 | "s3_bucket_debug": { 25 | "value": { 26 | "s3_bucket_arn": "arn:aws:s3:::my-project-terraform-state-prod", 27 | "s3_bucket_bucket_domain_name": "my-project-terraform-state-prod.s3.amazonaws.com", 28 | "s3_bucket_bucket_regional_domain_name": "my-project-terraform-state-prod.s3.us-east-1.amazonaws.com", 29 | "s3_bucket_hosted_zone_id": "Z3AQBSTGFYJSTF", 30 | "s3_bucket_id": "my-project-terraform-state-prod", 31 | "s3_bucket_lifecycle_configuration_rules": "", 32 | "s3_bucket_policy": "", 33 | "s3_bucket_region": "us-east-1", 34 | "s3_bucket_website_domain": "", 35 | "s3_bucket_website_endpoint": "" 36 | }, 37 | "type": [ 38 | "object", 39 | { 40 | "s3_bucket_arn": "string", 41 | "s3_bucket_bucket_domain_name": "string", 42 | "s3_bucket_bucket_regional_domain_name": "string", 43 | "s3_bucket_hosted_zone_id": "string", 44 | "s3_bucket_id": "string", 45 | "s3_bucket_lifecycle_configuration_rules": "string", 46 | "s3_bucket_policy": "string", 47 | "s3_bucket_region": "string", 48 | "s3_bucket_website_domain": "string", 49 | "s3_bucket_website_endpoint": "string" 50 | } 51 | ] 52 | } 53 | }, 54 | "resources": [ 55 | { 56 | "mode": "managed", 57 | "type": "aws_s3_bucket", 58 | "name": "my-project-terraform-state-prod", 59 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 60 | "instances": [ 61 | { 62 | "schema_version": 0, 63 | "attributes": { 64 | "acceleration_status": "", 65 | "acl": null, 66 | "arn": "arn:aws:s3:::my-project-terraform-state-prod", 67 | "bucket": "my-project-terraform-state-prod", 68 | "bucket_domain_name": "my-project-terraform-state-prod.s3.amazonaws.com", 69 | "bucket_prefix": "", 70 | "bucket_regional_domain_name": "my-project-terraform-state-prod.s3.us-east-1.amazonaws.com", 71 | "cors_rule": [], 72 | "force_destroy": false, 73 | "grant": [ 74 | { 75 | "id": "2bc01bfd0e9ff0bf48ada93f05d011e2467399385e0dda1c8bcf83f2569b68d9", 76 | "permissions": [ 77 | "FULL_CONTROL" 78 | ], 79 | "type": "CanonicalUser", 80 | "uri": "" 81 | } 82 | ], 83 | "hosted_zone_id": "Z3AQBSTGFYJSTF", 84 | "id": "my-project-terraform-state-prod", 85 | "lifecycle_rule": [], 86 | "logging": [], 87 | "object_lock_configuration": [], 88 | "object_lock_enabled": false, 89 | "policy": "", 90 | "region": "us-east-1", 91 | "replication_configuration": [], 92 | "request_payer": "BucketOwner", 93 | "server_side_encryption_configuration": [ 94 | { 95 | "rule": [ 96 | { 97 | "apply_server_side_encryption_by_default": [ 98 | { 99 | "kms_master_key_id": "", 100 | "sse_algorithm": "AES256" 101 | } 102 | ], 103 | "bucket_key_enabled": false 104 | } 105 | ] 106 | } 107 | ], 108 | "tags": { 109 | "Environment": "Dev", 110 | "Name": "MyBucket" 111 | }, 112 | "tags_all": { 113 | "Environment": "Dev", 114 | "Name": "MyBucket" 115 | }, 116 | "timeouts": null, 117 | "versioning": [ 118 | { 119 | "enabled": false, 120 | "mfa_delete": false 121 | } 122 | ], 123 | "website": [], 124 | "website_domain": null, 125 | "website_endpoint": null 126 | }, 127 | "sensitive_attributes": [], 128 | "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjM2MDAwMDAwMDAwMDAsInJlYWQiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19" 129 | } 130 | ] 131 | }, 132 | { 133 | "mode": "managed", 134 | "type": "aws_s3_bucket_versioning", 135 | "name": "my-project-terraform-state-prod", 136 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 137 | "instances": [ 138 | { 139 | "schema_version": 0, 140 | "attributes": { 141 | "bucket": "my-project-terraform-state-prod", 142 | "expected_bucket_owner": "", 143 | "id": "my-project-terraform-state-prod", 144 | "mfa": null, 145 | "versioning_configuration": [ 146 | { 147 | "mfa_delete": "", 148 | "status": "Enabled" 149 | } 150 | ] 151 | }, 152 | "sensitive_attributes": [], 153 | "private": "bnVsbA==", 154 | "dependencies": [ 155 | "aws_s3_bucket.my-project-terraform-state-prod" 156 | ] 157 | } 158 | ] 159 | }, 160 | { 161 | "module": "module.dynamodb_table", 162 | "mode": "managed", 163 | "type": "aws_dynamodb_table", 164 | "name": "this", 165 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 166 | "instances": [ 167 | { 168 | "index_key": 0, 169 | "schema_version": 1, 170 | "attributes": { 171 | "arn": "arn:aws:dynamodb:us-east-1:654654507397:table/my-project-terraform-lock", 172 | "attribute": [ 173 | { 174 | "name": "LockID", 175 | "type": "S" 176 | } 177 | ], 178 | "billing_mode": "PAY_PER_REQUEST", 179 | "deletion_protection_enabled": false, 180 | "global_secondary_index": [], 181 | "hash_key": "LockID", 182 | "id": "my-project-terraform-lock", 183 | "import_table": [], 184 | "local_secondary_index": [], 185 | "name": "my-project-terraform-lock", 186 | "point_in_time_recovery": [ 187 | { 188 | "enabled": false 189 | } 190 | ], 191 | "range_key": null, 192 | "read_capacity": 0, 193 | "replica": [], 194 | "restore_date_time": null, 195 | "restore_source_name": null, 196 | "restore_to_latest_time": null, 197 | "server_side_encryption": [], 198 | "stream_arn": "", 199 | "stream_enabled": false, 200 | "stream_label": "", 201 | "stream_view_type": "", 202 | "table_class": "STANDARD", 203 | "tags": { 204 | "Environment": "prod", 205 | "Name": "my-project-terraform-lock", 206 | "Project": "my-project" 207 | }, 208 | "tags_all": { 209 | "Environment": "prod", 210 | "Name": "my-project-terraform-lock", 211 | "Project": "my-project" 212 | }, 213 | "timeouts": { 214 | "create": "10m", 215 | "delete": "10m", 216 | "update": "60m" 217 | }, 218 | "ttl": [ 219 | { 220 | "attribute_name": "", 221 | "enabled": false 222 | } 223 | ], 224 | "write_capacity": 0 225 | }, 226 | "sensitive_attributes": [], 227 | "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwLCJ1cGRhdGUiOjM2MDAwMDAwMDAwMDB9LCJzY2hlbWFfdmVyc2lvbiI6IjEifQ==" 228 | } 229 | ] 230 | }, 231 | { 232 | "module": "module.s3_bucket", 233 | "mode": "data", 234 | "type": "aws_caller_identity", 235 | "name": "current", 236 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 237 | "instances": [ 238 | { 239 | "schema_version": 0, 240 | "attributes": { 241 | "account_id": "654654507397", 242 | "arn": "arn:aws:iam::654654507397:user/cloud_user", 243 | "id": "654654507397", 244 | "user_id": "AIDAZQ3DTBGC4FOIAIIRI" 245 | }, 246 | "sensitive_attributes": [] 247 | } 248 | ] 249 | }, 250 | { 251 | "module": "module.s3_bucket", 252 | "mode": "data", 253 | "type": "aws_partition", 254 | "name": "current", 255 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 256 | "instances": [ 257 | { 258 | "schema_version": 0, 259 | "attributes": { 260 | "dns_suffix": "amazonaws.com", 261 | "id": "aws", 262 | "partition": "aws", 263 | "reverse_dns_prefix": "com.amazonaws" 264 | }, 265 | "sensitive_attributes": [] 266 | } 267 | ] 268 | }, 269 | { 270 | "module": "module.s3_bucket", 271 | "mode": "data", 272 | "type": "aws_region", 273 | "name": "current", 274 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 275 | "instances": [ 276 | { 277 | "schema_version": 0, 278 | "attributes": { 279 | "description": "US East (N. Virginia)", 280 | "endpoint": "ec2.us-east-1.amazonaws.com", 281 | "id": "us-east-1", 282 | "name": "us-east-1" 283 | }, 284 | "sensitive_attributes": [] 285 | } 286 | ] 287 | }, 288 | { 289 | "module": "module.s3_bucket", 290 | "mode": "managed", 291 | "type": "aws_s3_bucket", 292 | "name": "this", 293 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 294 | "instances": [ 295 | { 296 | "index_key": 0, 297 | "schema_version": 0, 298 | "attributes": { 299 | "acceleration_status": "", 300 | "acl": null, 301 | "arn": "arn:aws:s3:::my-project-terraform-state-prod", 302 | "bucket": "my-project-terraform-state-prod", 303 | "bucket_domain_name": "my-project-terraform-state-prod.s3.amazonaws.com", 304 | "bucket_prefix": "", 305 | "bucket_regional_domain_name": "my-project-terraform-state-prod.s3.us-east-1.amazonaws.com", 306 | "cors_rule": [], 307 | "force_destroy": true, 308 | "grant": [ 309 | { 310 | "id": "2bc01bfd0e9ff0bf48ada93f05d011e2467399385e0dda1c8bcf83f2569b68d9", 311 | "permissions": [ 312 | "FULL_CONTROL" 313 | ], 314 | "type": "CanonicalUser", 315 | "uri": "" 316 | } 317 | ], 318 | "hosted_zone_id": "Z3AQBSTGFYJSTF", 319 | "id": "my-project-terraform-state-prod", 320 | "lifecycle_rule": [], 321 | "logging": [], 322 | "object_lock_configuration": [], 323 | "object_lock_enabled": false, 324 | "policy": "", 325 | "region": "us-east-1", 326 | "replication_configuration": [], 327 | "request_payer": "BucketOwner", 328 | "server_side_encryption_configuration": [ 329 | { 330 | "rule": [ 331 | { 332 | "apply_server_side_encryption_by_default": [ 333 | { 334 | "kms_master_key_id": "", 335 | "sse_algorithm": "AES256" 336 | } 337 | ], 338 | "bucket_key_enabled": false 339 | } 340 | ] 341 | } 342 | ], 343 | "tags": { 344 | "Environment": "prod", 345 | "Name": "MyBucket", 346 | "Project": "my-project" 347 | }, 348 | "tags_all": { 349 | "Environment": "prod", 350 | "Name": "MyBucket", 351 | "Project": "my-project" 352 | }, 353 | "timeouts": null, 354 | "versioning": [ 355 | { 356 | "enabled": true, 357 | "mfa_delete": false 358 | } 359 | ], 360 | "website": [], 361 | "website_domain": null, 362 | "website_endpoint": null 363 | }, 364 | "sensitive_attributes": [], 365 | "private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxMjAwMDAwMDAwMDAwLCJkZWxldGUiOjM2MDAwMDAwMDAwMDAsInJlYWQiOjEyMDAwMDAwMDAwMDAsInVwZGF0ZSI6MTIwMDAwMDAwMDAwMH19" 366 | } 367 | ] 368 | }, 369 | { 370 | "module": "module.s3_bucket", 371 | "mode": "managed", 372 | "type": "aws_s3_bucket_public_access_block", 373 | "name": "this", 374 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 375 | "instances": [ 376 | { 377 | "index_key": 0, 378 | "schema_version": 0, 379 | "attributes": { 380 | "block_public_acls": true, 381 | "block_public_policy": true, 382 | "bucket": "my-project-terraform-state-prod", 383 | "id": "my-project-terraform-state-prod", 384 | "ignore_public_acls": true, 385 | "restrict_public_buckets": true 386 | }, 387 | "sensitive_attributes": [], 388 | "private": "bnVsbA==", 389 | "dependencies": [ 390 | "module.s3_bucket.aws_s3_bucket.this" 391 | ] 392 | } 393 | ] 394 | }, 395 | { 396 | "module": "module.s3_bucket", 397 | "mode": "managed", 398 | "type": "aws_s3_bucket_versioning", 399 | "name": "this", 400 | "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]", 401 | "instances": [ 402 | { 403 | "index_key": 0, 404 | "schema_version": 0, 405 | "attributes": { 406 | "bucket": "my-project-terraform-state-prod", 407 | "expected_bucket_owner": "", 408 | "id": "my-project-terraform-state-prod", 409 | "mfa": null, 410 | "versioning_configuration": [ 411 | { 412 | "mfa_delete": "", 413 | "status": "Enabled" 414 | } 415 | ] 416 | }, 417 | "sensitive_attributes": [], 418 | "private": "bnVsbA==", 419 | "dependencies": [ 420 | "module.s3_bucket.aws_s3_bucket.this" 421 | ] 422 | } 423 | ] 424 | } 425 | ], 426 | "check_results": null 427 | } 428 | -------------------------------------------------------------------------------- /ecs/Readme.md: -------------------------------------------------------------------------------- 1 | # GitLab CI/CD with AWS ECS and Terraform 2 | 3 | ## Table of Contents 4 | 1. [Overview](#overview) 5 | 2. [Infrastructure Setup](#infrastructure-setup) 6 | - [ECS Configuration](#ecs-configuration) 7 | - [ALB Configuration](#alb-configuration) 8 | - [ECR Configuration](#ecr-configuration) 9 | - [ACM Configuration](#acm-configuration) 10 | - [Route53 Configuration](#route53-configuration) 11 | - [IAM Permissions](#iam-roles-and-policies-for-ecs-tasks) 12 | - [Security Groups](#security-groups-configuration) 13 | - [Provider and Backend Configuration](#provider-and-backend-configuration) 14 | - [Terraform.Tfvars](#terraform-tfvars) 15 | 16 | 3. [GitLab CI/CD Configuration Overview](#gitlab-cicd-configuration-overview) 17 | - [Create an IAM user account for GitLab CI/CD](#gitlab-iam-user) 18 | - [Set up CI/CD Pipelines](#pipelines-setup) 19 | - [Python application](#python-application) 20 | - [Dockerfile](#dockerfile) 21 | 22 | --- 23 | 24 | ## Overview 25 | This project demonstrates setting up a CI/CD pipeline using GitLab for deploying a containerized web application on AWS ECS Fargate. The infrastructure is provisioned using Terraform with a focus on security, scalability, and maintainability. 26 | 27 | --- 28 | 29 | ## Infrastructure Setup 30 | 31 | ### ECS Configuration 32 | 33 | ECS Cluster has two ECS services with two task definitions for dev and prod. Application is listening on port 8080. 34 | 35 | 36 | ```hcl 37 | resource "aws_ecs_cluster" "app_cluster" { 38 | name = "${var.app_name}-cluster" 39 | 40 | setting { 41 | name = "containerInsights" 42 | value = "enabled" 43 | } 44 | 45 | tags = { 46 | Name = "${var.app_name}-cluster" 47 | } 48 | 49 | lifecycle { 50 | ignore_changes = [setting] 51 | } 52 | } 53 | 54 | resource "aws_ecs_cluster_capacity_providers" "app_cluster_providers" { 55 | cluster_name = aws_ecs_cluster.app_cluster.name 56 | 57 | capacity_providers = ["FARGATE", "FARGATE_SPOT"] 58 | 59 | default_capacity_provider_strategy { 60 | base = 1 61 | weight = 100 62 | capacity_provider = "FARGATE" 63 | } 64 | } 65 | 66 | ######## ECS SERVICE ########## 67 | # ECS Service for Dev 68 | resource "aws_ecs_service" "dev" { 69 | name = "${var.app_name}-dev" 70 | cluster = aws_ecs_cluster.app_cluster.id 71 | task_definition = aws_ecs_task_definition.dev.arn 72 | desired_count = 1 73 | launch_type = "FARGATE" 74 | 75 | network_configuration { 76 | assign_public_ip = false 77 | security_groups = [module.sg_ecs_dev.security_group_id] 78 | subnets = local.private_subnets 79 | } 80 | 81 | load_balancer { 82 | target_group_arn = aws_lb_target_group.dev_target_group.arn 83 | container_name = "dev-container" 84 | container_port = 8080 85 | } 86 | } 87 | 88 | # ECS Service for Prod 89 | resource "aws_ecs_service" "prod" { 90 | name = "${var.app_name}-prod" 91 | cluster = aws_ecs_cluster.app_cluster.id 92 | task_definition = aws_ecs_task_definition.prod.arn 93 | desired_count = 1 94 | launch_type = "FARGATE" 95 | 96 | network_configuration { 97 | assign_public_ip = false 98 | security_groups = [module.sg_ecs_prod.security_group_id] 99 | subnets = local.private_subnets 100 | } 101 | 102 | load_balancer { 103 | target_group_arn = aws_lb_target_group.prod_target_group.arn 104 | container_name = "prod-container" 105 | container_port = 8080 106 | } 107 | } 108 | 109 | 110 | ######### ECS TASK DEFINITION ########### 111 | 112 | resource "aws_ecs_task_definition" "dev" { 113 | family = "${var.app_name}-dev" 114 | container_definitions = file("${path.module}/container_definitions/web_app_dev.json") 115 | cpu = "256" 116 | memory = "512" 117 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 118 | task_role_arn = aws_iam_role.ecs_task_role.arn 119 | network_mode = "awsvpc" 120 | requires_compatibilities = ["FARGATE"] 121 | 122 | tags = { 123 | Name = "${var.app_name}-dev" 124 | } 125 | } 126 | 127 | resource "aws_ecs_task_definition" "prod" { 128 | family = "${var.app_name}-prod" 129 | container_definitions = file("${path.module}/container_definitions/web_app_prod.json") 130 | cpu = "512" 131 | memory = "1024" 132 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 133 | task_role_arn = aws_iam_role.ecs_task_role.arn 134 | network_mode = "awsvpc" 135 | requires_compatibilities = ["FARGATE"] 136 | 137 | tags = { 138 | Name = "${var.app_name}-prod" 139 | } 140 | } 141 | 142 | 143 | ``` 144 | 145 | ### ALB Configuration 146 | 147 | The Application Load Balancer (ALB) manages traffic routing to ECS services based on the environment (dev or prod). There are two target groups dev and prod, listening on ecs task definition containers port 8080. There are two listeners port 443/https and 80/http permanent redirection to https. 148 | 149 | ```hcl 150 | resource "aws_lb" "app_alb" { 151 | name = "${var.app_name}-alb" 152 | internal = false 153 | load_balancer_type = "application" 154 | security_groups = [module.sg_alb.security_group_id] 155 | subnets = local.public_subnets 156 | 157 | enable_deletion_protection = false 158 | 159 | tags = { 160 | Project = var.app_name 161 | } 162 | } 163 | 164 | resource "aws_lb_listener" "https" { 165 | load_balancer_arn = aws_lb.app_alb.arn 166 | port = 443 167 | protocol = "HTTPS" 168 | ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" 169 | certificate_arn = aws_acm_certificate_validation.app_certificate_validation.certificate_arn 170 | 171 | default_action { 172 | type = "fixed-response" 173 | fixed_response { 174 | content_type = "text/plain" 175 | message_body = "Not Found" 176 | status_code = "404" 177 | } 178 | } 179 | } 180 | 181 | resource "aws_lb_listener" "http" { 182 | load_balancer_arn = aws_lb.app_alb.arn 183 | port = 80 184 | protocol = "HTTP" 185 | 186 | default_action { 187 | type = "redirect" 188 | redirect { 189 | port = "443" 190 | protocol = "HTTPS" 191 | status_code = "HTTP_301" 192 | } 193 | } 194 | } 195 | 196 | resource "aws_lb_listener_rule" "dev_rule" { 197 | listener_arn = aws_lb_listener.https.arn 198 | priority = 100 199 | 200 | condition { 201 | host_header { 202 | values = [var.alb_hostname_dev] 203 | } 204 | } 205 | 206 | action { 207 | type = "forward" 208 | target_group_arn = aws_lb_target_group.dev_target_group.arn 209 | } 210 | } 211 | 212 | resource "aws_lb_listener_rule" "prod_rule" { 213 | listener_arn = aws_lb_listener.https.arn 214 | priority = 200 215 | 216 | condition { 217 | host_header { 218 | values = [var.alb_hostname_prod] 219 | } 220 | } 221 | 222 | action { 223 | type = "forward" 224 | target_group_arn = aws_lb_target_group.prod_target_group.arn 225 | } 226 | } 227 | 228 | 229 | resource "aws_lb_target_group" "dev_target_group" { 230 | name = "${var.app_name}-dev-tg" 231 | port = 8080 232 | protocol = "HTTP" 233 | target_type = "ip" 234 | vpc_id = local.vpc.vpc_id 235 | 236 | health_check { 237 | path = "/api/health" 238 | interval = 30 239 | timeout = 5 240 | healthy_threshold = 2 241 | unhealthy_threshold = 2 242 | matcher = "200" 243 | } 244 | 245 | tags = { 246 | Environment = var.env_name_dev 247 | Project = var.app_name 248 | } 249 | } 250 | 251 | resource "aws_lb_target_group" "prod_target_group" { 252 | name = "${var.app_name}-prod-tg" 253 | port = 8080 254 | protocol = "HTTP" 255 | target_type = "ip" 256 | vpc_id = local.vpc.vpc_id 257 | 258 | health_check { 259 | path = "/api/health" 260 | interval = 30 261 | timeout = 5 262 | healthy_threshold = 2 263 | unhealthy_threshold = 2 264 | matcher = "200" 265 | } 266 | 267 | tags = { 268 | Environment = var.env_name_prod 269 | Project = var.app_name 270 | } 271 | } 272 | ``` 273 | 274 | ### ECR Configuration 275 | 276 | For this demo tutorial I have to create ECR manually since we have to push image to ECR before we do task definition deployment. I do however included ECR configuration in the ecr.tf file. 277 | 278 | ### ACM Configuration 279 | 280 | ACM certificate is needed for ALB validation with Route53 domain for https connection. Terraform will automatically create necessary CNAME records for ACM validation. 281 | 282 | ```hcl 283 | resource "aws_acm_certificate" "app_certificate" { 284 | domain_name = var.alb_hostname_prod # Use prod domain for ACM validation 285 | validation_method = "DNS" 286 | 287 | subject_alternative_names = [ 288 | var.alb_hostname_dev # Add dev domain as SAN 289 | ] 290 | 291 | tags = { 292 | Environment = var.env_name_prod 293 | Project = var.app_name 294 | } 295 | } 296 | 297 | resource "aws_route53_record" "acm_validation" { 298 | for_each = { 299 | for dvo in aws_acm_certificate.app_certificate.domain_validation_options : 300 | dvo.domain_name => { 301 | name = dvo.resource_record_name 302 | type = dvo.resource_record_type 303 | value = dvo.resource_record_value 304 | } 305 | } 306 | 307 | zone_id = var.route53_zone_id 308 | name = each.value.name 309 | type = each.value.type 310 | records = [each.value.value] 311 | ttl = 300 312 | } 313 | 314 | resource "aws_acm_certificate_validation" "app_certificate_validation" { 315 | certificate_arn = aws_acm_certificate.app_certificate.arn 316 | validation_record_fqdns = [for record in aws_route53_record.acm_validation : record.fqdn] 317 | } 318 | 319 | ``` 320 | 321 | ### Route53 Configuration 322 | 323 | Two records will be created one for dev and one for prod domain. You can specify domain names and hostedzone in terraform.tfvars file. 324 | 325 | ```hcl 326 | 327 | resource "aws_route53_record" "dev_record" { 328 | zone_id = var.route53_zone_id 329 | name = var.alb_hostname_dev 330 | type = "A" 331 | 332 | alias { 333 | name = aws_lb.app_alb.dns_name 334 | zone_id = aws_lb.app_alb.zone_id 335 | evaluate_target_health = true 336 | } 337 | } 338 | 339 | resource "aws_route53_record" "prod_record" { 340 | zone_id = var.route53_zone_id 341 | name = var.alb_hostname_prod 342 | type = "A" 343 | 344 | alias { 345 | name = aws_lb.app_alb.dns_name 346 | zone_id = aws_lb.app_alb.zone_id 347 | evaluate_target_health = true 348 | } 349 | } 350 | 351 | ``` 352 | 353 | 354 | # IAM Roles and Policies for ECS Tasks 355 | 356 | ## 1. Execution Role 357 | #### Role: `aws_iam_role.ecs_task_execution_role` 358 | 359 | #### Permissions 360 | 1. **Pull Docker images from ECR** 361 | - `ecr:GetDownloadUrlForLayer` 362 | - `ecr:BatchGetImage` 363 | - `ecr:BatchCheckLayerAvailability` 364 | 2. **Write logs to CloudWatch Logs** 365 | - `logs:PutLogEvents` 366 | - `logs:CreateLogStream` 367 | 3. **Access SSM Parameters** 368 | - `ssm:GetParameters` 369 | - `ssm:GetParameter` 370 | - `ssm:GetParameterHistory` 371 | 372 | #### Purpose 373 | - Pull container images from ECR. 374 | - Enable centralized monitoring via CloudWatch Logs. 375 | - Fetch configuration parameters from SSM Parameter Store. 376 | 377 | ### Policy: `ecs_task_execution_policy` 378 | Combines minimal permissions required for: 379 | - ECR image pulls. 380 | - CloudWatch Logs writing. 381 | - SSM Parameter access. 382 | 383 | --- 384 | 385 | ### 2. Task Role: `aws_iam_role.ecs_task_role` 386 | This role allows ECS tasks to perform application-specific operations. 387 | 388 | #### Permissions 389 | 1. **Access S3 Buckets** 390 | - `s3:ListBucket` 391 | - `s3:GetObject` 392 | 2. **Fetch secrets from Secrets Manager** 393 | - `secretsmanager:GetSecretValue` 394 | 3. **Write logs to CloudWatch Logs** 395 | - `logs:PutLogEvents` 396 | - `logs:CreateLogStream` 397 | 398 | #### Purpose 399 | - Access S3 resources for fetching required files. 400 | - Retrieve sensitive application credentials securely from AWS Secrets Manager (if required). 401 | - Log application activity to CloudWatch Logs. 402 | 403 | ### Policy: `ecs_task_custom_policy` 404 | Custom policy with fine-grained permissions for: 405 | - S3 bucket operations. 406 | - Secrets Manager access. 407 | - Application-level logging to CloudWatch Logs. 408 | 409 | --- 410 | 411 | ### Security Groups Configuration 412 | 413 | This Terraform configuration sets up security groups for an Application Load Balancer (ALB) and ECS tasks in both development and production environments using the `terraform-aws-modules/security-group` module. 414 | 415 | ### Overview 416 | 417 | - **ALB Security Group**: 418 | - Allows inbound HTTP (port 80) and HTTPS (port 443) traffic from anywhere. 419 | - Allows all outbound traffic. 420 | 421 | - **ECS Tasks (Dev and Prod) Security Groups**: 422 | - Allow inbound traffic on port 8080 **from the ALB security group**. 423 | - Allow all outbound traffic. 424 | - Separate security groups for development and production services. 425 | 426 | ### 427 | 428 | ```hcl 429 | # Security Group for ALB 430 | module "sg_alb" { 431 | source = "terraform-aws-modules/security-group/aws" 432 | version = "~> 5.0" 433 | 434 | name = "${var.app_name}-alb" 435 | description = "Security group for ALB" 436 | vpc_id = local.vpc.vpc_id 437 | 438 | ingress_with_cidr_blocks = [ 439 | { 440 | from_port = 80 441 | to_port = 80 442 | protocol = "tcp" 443 | cidr_blocks = "0.0.0.0/0" 444 | }, 445 | { 446 | from_port = 443 447 | to_port = 443 448 | protocol = "tcp" 449 | cidr_blocks = "0.0.0.0/0" 450 | } 451 | ] 452 | 453 | egress_with_cidr_blocks = [ 454 | { 455 | from_port = 0 456 | to_port = 0 457 | protocol = "-1" 458 | cidr_blocks = "0.0.0.0/0" 459 | } 460 | ] 461 | 462 | tags = { 463 | Name = "${var.app_name}-alb" 464 | } 465 | } 466 | 467 | # Security Group for ECS Tasks (Dev) 468 | module "sg_ecs_dev" { 469 | source = "terraform-aws-modules/security-group/aws" 470 | version = "~> 5.0" 471 | 472 | name = "${var.app_name}-dev-ecs" 473 | description = "Security group for ECS tasks (Dev)" 474 | vpc_id = local.vpc.vpc_id 475 | 476 | ingress_with_source_security_group_id = [ 477 | { 478 | from_port = 8080 479 | to_port = 8080 480 | protocol = "tcp" 481 | source_security_group_id = module.sg_alb.security_group_id 482 | } 483 | ] 484 | 485 | egress_with_cidr_blocks = [ 486 | { 487 | from_port = 0 488 | to_port = 0 489 | protocol = "-1" 490 | cidr_blocks = "0.0.0.0/0" 491 | } 492 | ] 493 | 494 | tags = { 495 | Name = "${var.app_name}-dev-ecs" 496 | } 497 | } 498 | 499 | # Security Group for ECS Tasks (Prod) 500 | module "sg_ecs_prod" { 501 | source = "terraform-aws-modules/security-group/aws" 502 | version = "~> 5.0" 503 | 504 | name = "${var.app_name}-prod-ecs" 505 | description = "Security group for ECS tasks (Prod)" 506 | vpc_id = local.vpc.vpc_id 507 | 508 | ingress_with_source_security_group_id = [ 509 | { 510 | from_port = 8080 511 | to_port = 8080 512 | protocol = "tcp" 513 | source_security_group_id = module.sg_alb.security_group_id 514 | } 515 | ] 516 | 517 | egress_with_cidr_blocks = [ 518 | { 519 | from_port = 0 520 | to_port = 0 521 | protocol = "-1" 522 | cidr_blocks = "0.0.0.0/0" 523 | } 524 | ] 525 | 526 | tags = { 527 | Name = "${var.app_name}-prod-ecs" 528 | } 529 | } 530 | ``` 531 | 532 | ### Provider and Backend Configuration 533 | 534 | This Terraform configuration sets up the foundational settings for managing AWS infrastructure, including backend state management and required providers. 535 | 536 | 537 | 538 | ### Required Version 539 | - **Terraform**: `>= 1.6.0` 540 | 541 | ### Providers 542 | - **AWS**: `~> 5.73.0` 543 | - **Random**: `~> 3.6.0` 544 | 545 | ### Backend Configuration 546 | The state file is stored in an S3 bucket for secure and centralized management. State locking is enabled using DynamoDB. 547 | 548 | - **S3 Bucket**: `my-project-terraform-state-prod` (replace with your bucket name) 549 | - **State File Key**: `ecs/terraform.tfstate` 550 | - **Region**: `us-east-1` (replace with your AWS region) 551 | - **DynamoDB Table**: `my-project-terraform-lock` (replace with your table name) 552 | - **Encryption**: Enabled 553 | 554 | ## AWS Provider Configuration 555 | - **Region**: Configured dynamically using `var.region`. 556 | - **Default Tags**: Optional feature to apply tags to all resources (to be configured as needed). 557 | 558 | ## Remote State Data Source 559 | - Pulls the networking state from a remote S3 bucket. 560 | - **Bucket**: `my-project-terraform-state-prod` (replace with your bucket name) 561 | - **State File Key**: `networking/terraform.tfstate` 562 | - **Region**: `us-east-1` 563 | - **DynamoDB Table**: `my-project-terraform-lock` 564 | - **Encryption**: Enabled 565 | 566 | ## Key Highlights 567 | - **Remote State Management**: Ensures infrastructure consistency across teams by using a centralized state file. 568 | - **State Locking**: Prevents concurrent modifications using DynamoDB. 569 | - **Modular Design**: Enables seamless integration with other infrastructure components via remote state. 570 | 571 | Replace placeholders with your project-specific details for production use. 572 | 573 | 574 | ```hcl 575 | terraform { 576 | required_version = ">= 1.6.0" 577 | 578 | required_providers { 579 | aws = { 580 | source = "hashicorp/aws" 581 | version = "~> 5.73.0" 582 | } 583 | 584 | random = { 585 | source = "hashicorp/random" 586 | version = "~> 3.6.0" 587 | } 588 | } 589 | 590 | backend "s3" { 591 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 592 | key = "ecs/terraform.tfstate" # Unique key for ECS state file 593 | region = "us-east-1" # Replace with your AWS region 594 | dynamodb_table = "my-project-terraform-lock" # Replace with your DynamoDB table name 595 | encrypt = true 596 | } 597 | } 598 | 599 | provider "aws" { 600 | region = var.region 601 | 602 | } 603 | 604 | data "terraform_remote_state" "vpc" { 605 | backend = "s3" 606 | config = { 607 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 608 | key = "networking/terraform.tfstate" # Path to the networking state file 609 | region = "us-east-1" # Replace with your AWS region 610 | dynamodb_table = "my-project-terraform-lock" # Replace with your DynamoDB table name 611 | encrypt = true 612 | } 613 | } 614 | 615 | ``` 616 | 617 | ### Terraform.Tfvars 618 | 619 | #### Environment and Region Configuration 620 | 621 | 622 | - **Environment Names**: 623 | - Development: `dev` 624 | - Production: `prod` 625 | - **AWS Region**: `us-east-1` (update if using a different region) 626 | 627 | ### Application Details 628 | - **Application Name**: `web-app` 629 | 630 | ### AWS Account Details 631 | - **Account ID**: `123456789` (update with your AWS account ID) 632 | 633 | ### ALB Configuration 634 | - **Development ALB Hostname**: `web-app-dev.123456789.example.net` 635 | - **Production ALB Hostname**: `web-app-prod.123456789.example.net` 636 | 637 | ### DNS 638 | - **Route 53 Hosted Zone ID**: `ZSDFSDF3FDSSDFSDFDF` (update as per your hosted zone) 639 | 640 | ### 641 | ```hcl 642 | # Environment and Region 643 | env_name_prod = "prod" 644 | env_name_dev = "dev" # Change to "prod" for production 645 | region = "us-east-1" # AWS region 646 | 647 | # Application Details 648 | app_name = "web-app" # Your application name 649 | 650 | # AWS Account Details 651 | account_id = "452303021915" # Your AWS account ID 652 | 653 | # ALB Configuration 654 | alb_hostname_dev = "web-app-dev.452303021915.realhandsonlabs.net" 655 | alb_hostname_prod = "web-app-prod.452303021915.realhandsonlabs.net" 656 | 657 | # Subnet and VPC IDs 658 | 659 | route53_zone_id = "Z10075723METSXT8WC4VB" 660 | ``` 661 | 662 | ## Gitlab CI/CD Configuration Overview 663 | 664 | We will build a GitLab CI/CD pipeline from scratch to automate deployments to AWS ECS Fargate utilizing Docker containers, AWS ECR for image storage, and GitLab YAML for configuration. 665 | 666 | Pipeline Structure: Two branches (dev and prod) dynamically define the target ECS service/task based on the environment name and deploy the application. 667 | Stages: Automate validation, build, push, and deployment through defined CI/CD stages. 668 | 669 | 670 | ### GitLab IAM user 671 | 672 | Create an IAM user 'gitlab-cicd' give the below permissions. This user doesn't need programmatic console access only. Generate Access and Secret keys for this user. 673 | 674 | ![alt text](images/gitlab_cicd.png) 675 | 676 | 677 | Log in to your Gitlab account and create a new project 678 | 679 | ![alt text](images/new_project.png) 680 | 681 | Log in to your linux server and git initialize new repository 682 | 683 | ![alt text](images/git_pull.png) 684 | 685 | You will need to have two branches 'dev' and 'main': 686 | ![alt text](images/branches.png) 687 | 688 | Protect both of your branch from deletion as well as push without merge 689 | 690 | ![alt text](images/protect_branches.png) 691 | 692 | Set 3 variables for your pipeline 693 | ![alt text](images/variables.png) 694 | 695 | ![alt text](images/three_variables.png) 696 | 697 | ### Pipelines setup 698 | ## 699 | 700 | ![alt text](images/gitlabcicdyaml.png) 701 | 702 | ## .gitlab-ci.yml 703 | 704 | ```yaml 705 | stages: 706 | - validate_environment 707 | - build_and_publish 708 | - deploy_to_dev 709 | - deploy_to_production 710 | - finalize_pipeline 711 | 712 | workflow: # Trigger pipeline only for specific branches 713 | rules: 714 | - if: '$CI_COMMIT_BRANCH == "main"' 715 | - if: '$CI_COMMIT_BRANCH == "dev"' 716 | 717 | image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest 718 | 719 | variables: 720 | AWS_ACCOUNT_NUMBER : "452303021915" 721 | REGION : "us-east-1" 722 | IMAGE_REPOSITORY : "web-app-repository" 723 | CLUSTER_NAME : "web-app-cluster" 724 | 725 | DEV_SERVICE_NAME : "web-app-dev" 726 | DEV_TASK_NAME : "web-app-dev" 727 | 728 | PROD_SERVICE_NAME : "web-app-prod" 729 | PROD_TASK_NAME : "web-app-prod" 730 | 731 | test_environment: 732 | stage: validate_environment 733 | script: 734 | - echo "Validating environment setup..." 735 | - aws --version 736 | - docker --version 737 | - jq --version 738 | - aws sts get-caller-identity 739 | 740 | build_and_push: 741 | stage: build_and_publish 742 | services: 743 | - docker:dind 744 | variables: 745 | DOCKER_HOST: tcp://docker:2375 746 | before_script: 747 | - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com 748 | 749 | script: 750 | - echo "Building the Docker image..." 751 | - docker build -t $IMAGE_REPOSITORY . 752 | - echo "Tagging the image..." 753 | - docker tag $IMAGE_REPOSITORY:latest $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-latest 754 | - docker tag $IMAGE_REPOSITORY:latest $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA 755 | - echo "Pushing the image to ECR..." 756 | - docker push $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-latest 757 | - docker push $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA 758 | 759 | deploy_to_dev: 760 | stage: deploy_to_dev 761 | rules: 762 | - if: '$CI_COMMIT_BRANCH == "dev"' 763 | script: 764 | - echo "Deploying to development environment..." 765 | - | 766 | aws ecs update-service \ 767 | --cluster $CLUSTER_NAME \ 768 | --service $DEV_SERVICE_NAME \ 769 | --task-definition $DEV_TASK_NAME \ 770 | --force-new-deployment 771 | 772 | deploy_to_production: 773 | stage: deploy_to_production 774 | when: manual # Require manual confirmation for production 775 | manual_confirmation: 'Proceed with production deployment?' 776 | allow_failure: false # Must succeed to continue 777 | rules: 778 | - if: '$CI_COMMIT_BRANCH == "main"' 779 | script: 780 | - echo "Deploying to production environment..." 781 | - | 782 | aws ecs update-service \ 783 | --cluster $CLUSTER_NAME \ 784 | --service $PROD_SERVICE_NAME \ 785 | --task-definition $PROD_TASK_NAME \ 786 | --force-new-deployment 787 | 788 | finalize_pipeline: 789 | stage: finalize_pipeline 790 | script: 791 | - echo "CI/CD Pipeline completed successfully!" 792 | ``` 793 | 794 | 795 | ## Python application 796 | 797 | Small Python application for converting image to pdf. 798 | 799 | app.py 800 | 801 | ```python 802 | from flask import Flask, jsonify 803 | 804 | app = Flask(__name__) 805 | 806 | # Health check endpoint 807 | @app.route('/api/health', methods=['GET']) 808 | def health_check(): 809 | return jsonify(status="UP", message="Service is healthy"), 200 810 | 811 | # Sample application endpoint 812 | @app.route('/', methods=['GET']) 813 | def home(): 814 | return "Hello from the Python PROD app v1!" 815 | 816 | if __name__ == '__main__': 817 | import os 818 | port = int(os.getenv("PORT", 8080)) 819 | app.run(host='0.0.0.0', port=port) 820 | 821 | ``` 822 | 823 | ### Dockerfile 824 | 825 | 826 | 827 | ```Dockerfile 828 | # Base image 829 | FROM python:3.9-slim 830 | 831 | # Set working directory 832 | WORKDIR /usr/src/app 833 | 834 | # Copy application code 835 | COPY . . 836 | 837 | # Install dependencies 838 | RUN pip install --no-cache-dir -r requirements.txt 839 | 840 | # Expose application port 841 | EXPOSE 8080 842 | 843 | # Start application 844 | CMD ["python", "app.py"] 845 | 846 | ``` 847 | 848 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS ECS Fargate Deployments with Terraform and GitLab CI/CD using Python application. 2 | 3 | ## Table of Contents 4 | 1. [Overview](#overview) 5 | 2. [Project Architecture](#project-architecture) 6 | 3. [Getting Started](#getting-started) 7 | 4. [Modules](#modules) 8 | - [Terraform Backend](#terraform-backend) 9 | - [Networking](#networking) 10 | - [ECS Infrastructure Overview](#ecs-infrastructure) 11 | - [ECS Configuration](#ecs-configuration) 12 | - [ALB Configuration](#alb-configuration) 13 | - [ECR Configuration](#ecr-configuration) (must to create manually to push the docker image first) 14 | - [ACM Configuration](#acm-configuration) 15 | - [Route53 Configuration](#route53-configuration) 16 | - [IAM Permissions](#iam-roles-and-policies-for-ecs-tasks) 17 | - [Security Groups](#security-groups-configuration) 18 | - [Provider and Backend Configuration](#provider-and-backend-configuration) 19 | - [terraform.tfvars](#terraform-tfvars) 20 | 5. [GitLab CI/CD Setup](#gitlab-cicd-setup) 21 | - [Create an IAM user account for GitLab CI/CD](#gitlab-iam-user) 22 | - [Set up CI/CD Pipelines](#pipelines-setup) 23 | 24 | 25 | 6. [Application Details](#application-details) 26 | - [Python application](#python-application) 27 | - [Dockerfile](#dockerfile) 28 | 7. [Additional Notes](#additional-notes) 29 | 30 | --- 31 | 32 | ## Overview 33 | This project automates the deployment of a containerized Python application on **AWS ECS Fargate** using **Terraform** for infrastructure provisioning and **GitLab CI/CD** for continuous integration and delivery. 34 | 35 | ### Key Features 36 | - **Modular Terraform Setup**: Backend, networking, and ECS modules for scalability. 37 | - **CI/CD Pipeline**: Automates Docker image build, push to ECR, and deployment to ECS. 38 | - **Dev and Prod Environments**: Isolated environments with dedicated ALB configurations. 39 | - **TLS Support**: Secure HTTPS setup using ACM and Route 53. 40 | 41 | --- 42 | 43 | ## Project Architecture 44 | ![Project Architecture](images/architecture_diagram.png) 45 | 46 | --- 47 | 48 | ## Getting Started 49 | 50 | ### Prerequisites 51 | - **AWS CLI** installed and configured. 52 | - **Terraform** (>= 1.6.0). 53 | - **GitLab Account** with necessary permissions. 54 | - **Docker** installed locally for testing builds. 55 | 56 | ### Steps 57 | 1. Clone the repository: 58 | ```bash 59 | git clone https://github.com/sahibgasimov/ecs-gitlab-terraform.git 60 | 61 | cd ecs-gitlab-terraform 62 | ``` 63 | 64 | ### Modules 65 | 66 | ```tree 67 | ├── backend/ # Terraform backend module 68 | ├── networking/ # VPC and networking module 69 | ├── ecs/ # ECS cluster and services module 70 | ├── gitlab_cicd/ # Gitlab pipeline file 71 | ├── images/ # Architecture and CI/CD images 72 | ├── app/ # Python application code 73 | ├── .gitlab-ci.yml # GitLab CI/CD pipeline 74 | └── README.md # Project documentation 75 | ``` 76 | 77 | ### Terraform Backend 78 | 79 | This repository contains a Terraform configuration to manage state files and locking using AWS S3 and DynamoDB. It sets up an S3 bucket for state file storage and a DynamoDB table for state locking. 80 | 81 | --- 82 | 83 | #### Features 84 | 85 | - **S3 Bucket**: Stores the Terraform state file. 86 | - Versioning is enabled for safety. 87 | - Configured to allow state storage with high reliability. 88 | - **DynamoDB Table**: Provides state locking to prevent race conditions in multi-user environments. 89 | 90 | --- 91 | 92 | ### How It Works 93 | 94 | 1. **Manual S3 Bucket Creation**: 95 | - Before running Terraform, create the S3 bucket manually to ensure the bucket is not accidentally destroyed by Terraform. 96 | 97 | 2. **Local to Remote State Migration**: 98 | - Initially, the state is stored locally. After running Terraform to create required resources (S3 and DynamoDB), the backend is updated to use the S3 bucket for remote state storage. 99 | 100 | ## Installation and Usage 101 | 102 | ### Step 1: Initialize and Apply Configuration with Local Backend 103 | 104 | 1. Clone the repository: 105 | ```git 106 | git clone https://github.com/sahibgasimov/ecs-gitlab-terraform.git 107 | cd backend 108 | ``` 109 | Initialize Terraform: 110 | 111 | ```hcl 112 | terraform init 113 | terraform apply 114 | ``` 115 | 116 | ### Step 2: Configure Remote Backend with S3 117 | Uncomment the following block in main.tf and run terraform apply to create backend module. Once created update your existing s3 bucket to migrate state file to s3 bucket. 118 | 119 | ``` 120 | terraform { 121 | backend "s3" { 122 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 123 | key = "backend/terraform.tfstate" 124 | region = "us-east-1" 125 | dynamodb_table = "my-project-terraform-lock" 126 | encrypt = true 127 | } 128 | } 129 | ``` 130 | 131 | 132 | Migrate the state file to the S3 backend: 133 | 134 | ```hcl 135 | terraform init -migrate-state 136 | ``` 137 | 138 | ![alt text](images/terraform_state_backend.jpg) 139 | 140 | #### File Structure 141 | ``` 142 | 143 | ├── main.tf # Main configuration file 144 | ├── variables.tf # Input variables 145 | ├── terraform.tfvars # Variable values 146 | ├── outputs.tf # Outputs for debug and reference 147 | ├── README.md # Project documentation 148 | ``` 149 | 150 | 151 | #### Notes 152 | - Create S3 remote state bucket manually to prevent accidental deletion. 153 | - The backend configuration uses versioning for safety and state locking for consistency. 154 | - Dynamodb table locks Terraform state files to prevent concurrent modifications. 155 | 156 | ### Networking 157 | 158 | #### VPC Module for ECS Cluster 159 | 160 | This module configures an AWS Virtual Private Cloud (VPC) for the ECS cluster, including private and public subnets, NAT gateway routes for private subnet, and internet gateway for public subnet. 161 | 162 | --- 163 | 164 | ![alt text](images/image.png) 165 | 166 | ## Features 167 | 168 | - **VPC Creation**: 169 | - CIDR block: `10.0.0.0/16` 170 | - Public and private subnets across availability zones. 171 | - **NAT Gateway**: 172 | - Enabled for outbound internet access from private subnets. 173 | - **Internet Gateway** 174 | - Enabled for outbound internet access from public subnets. 175 | 176 | --- 177 | 178 | ## Prerequisites 179 | 180 | - Ensure the backend S3 bucket and DynamoDB table for Terraform state are already configured. Check Backend module for the steps above. 181 | - AWS credentials must be set up for the specified region. 182 | 183 | --- 184 | 185 | ## File Structure 186 | 187 | ```plaintext 188 | . 189 | ├── main.tf # Defines the VPC module and its configurations 190 | ├── provider.tf # Configures the AWS provider and backend 191 | ├── variables.tf # Declares input variables 192 | ├── terraform.tfvars # Defines variable values 193 | ├── outputs.tf # Exports outputs such as subnet and VPC IDs 194 | 195 | ``` 196 | ### Usage 197 | 198 | ``` 199 | terraform init 200 | terraform apply 201 | ``` 202 | 203 | ![alt text](images/terraform_plan.png) 204 | 205 | ### Notes 206 | - The terraform-aws-modules/vpc module is used to simplify VPC creation. 207 | - Ensure availability zones in your region support the configuration. 208 | - Update main.tf with CIDR block and subnet details if necessary. 209 | 210 | ### ECS Infrastructure 211 | 212 | ### ECS Configuration 213 | 214 | ECS Cluster has two ECS services with two task definitions for dev and prod. Application is listening on port 8080. 215 | 216 | 217 | ```hcl 218 | resource "aws_ecs_cluster" "app_cluster" { 219 | name = "${var.app_name}-cluster" 220 | 221 | setting { 222 | name = "containerInsights" 223 | value = "enabled" 224 | } 225 | 226 | tags = { 227 | Name = "${var.app_name}-cluster" 228 | } 229 | 230 | lifecycle { 231 | ignore_changes = [setting] 232 | } 233 | } 234 | 235 | resource "aws_ecs_cluster_capacity_providers" "app_cluster_providers" { 236 | cluster_name = aws_ecs_cluster.app_cluster.name 237 | 238 | capacity_providers = ["FARGATE", "FARGATE_SPOT"] 239 | 240 | default_capacity_provider_strategy { 241 | base = 1 242 | weight = 100 243 | capacity_provider = "FARGATE" 244 | } 245 | } 246 | 247 | ######## ECS SERVICE ########## 248 | # ECS Service for Dev 249 | resource "aws_ecs_service" "dev" { 250 | name = "${var.app_name}-dev" 251 | cluster = aws_ecs_cluster.app_cluster.id 252 | task_definition = aws_ecs_task_definition.dev.arn 253 | desired_count = 1 254 | launch_type = "FARGATE" 255 | 256 | network_configuration { 257 | assign_public_ip = false 258 | security_groups = [module.sg_ecs_dev.security_group_id] 259 | subnets = local.private_subnets 260 | } 261 | 262 | load_balancer { 263 | target_group_arn = aws_lb_target_group.dev_target_group.arn 264 | container_name = "dev-container" 265 | container_port = 8080 266 | } 267 | } 268 | 269 | # ECS Service for Prod 270 | resource "aws_ecs_service" "prod" { 271 | name = "${var.app_name}-prod" 272 | cluster = aws_ecs_cluster.app_cluster.id 273 | task_definition = aws_ecs_task_definition.prod.arn 274 | desired_count = 1 275 | launch_type = "FARGATE" 276 | 277 | network_configuration { 278 | assign_public_ip = false 279 | security_groups = [module.sg_ecs_prod.security_group_id] 280 | subnets = local.private_subnets 281 | } 282 | 283 | load_balancer { 284 | target_group_arn = aws_lb_target_group.prod_target_group.arn 285 | container_name = "prod-container" 286 | container_port = 8080 287 | } 288 | } 289 | 290 | 291 | ######### ECS TASK DEFINITION ########### 292 | 293 | resource "aws_ecs_task_definition" "dev" { 294 | family = "${var.app_name}-dev" 295 | container_definitions = file("${path.module}/container_definitions/web_app_dev.json") 296 | cpu = "256" 297 | memory = "512" 298 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 299 | task_role_arn = aws_iam_role.ecs_task_role.arn 300 | network_mode = "awsvpc" 301 | requires_compatibilities = ["FARGATE"] 302 | 303 | tags = { 304 | Name = "${var.app_name}-dev" 305 | } 306 | } 307 | 308 | resource "aws_ecs_task_definition" "prod" { 309 | family = "${var.app_name}-prod" 310 | container_definitions = file("${path.module}/container_definitions/web_app_prod.json") 311 | cpu = "512" 312 | memory = "1024" 313 | execution_role_arn = aws_iam_role.ecs_task_execution_role.arn 314 | task_role_arn = aws_iam_role.ecs_task_role.arn 315 | network_mode = "awsvpc" 316 | requires_compatibilities = ["FARGATE"] 317 | 318 | tags = { 319 | Name = "${var.app_name}-prod" 320 | } 321 | } 322 | 323 | 324 | ``` 325 | 326 | ### ALB Configuration 327 | 328 | The Application Load Balancer (ALB) manages traffic routing to ECS services based on the environment (dev or prod). There are two target groups dev and prod, listening on ecs task definition containers port 8080. There are two listeners port 443/https and 80/http permanent redirection to https. 329 | 330 | ```hcl 331 | resource "aws_lb" "app_alb" { 332 | name = "${var.app_name}-alb" 333 | internal = false 334 | load_balancer_type = "application" 335 | security_groups = [module.sg_alb.security_group_id] 336 | subnets = local.public_subnets 337 | 338 | enable_deletion_protection = false 339 | 340 | tags = { 341 | Project = var.app_name 342 | } 343 | } 344 | 345 | resource "aws_lb_listener" "https" { 346 | load_balancer_arn = aws_lb.app_alb.arn 347 | port = 443 348 | protocol = "HTTPS" 349 | ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" 350 | certificate_arn = aws_acm_certificate_validation.app_certificate_validation.certificate_arn 351 | 352 | default_action { 353 | type = "fixed-response" 354 | fixed_response { 355 | content_type = "text/plain" 356 | message_body = "Not Found" 357 | status_code = "404" 358 | } 359 | } 360 | } 361 | 362 | resource "aws_lb_listener" "http" { 363 | load_balancer_arn = aws_lb.app_alb.arn 364 | port = 80 365 | protocol = "HTTP" 366 | 367 | default_action { 368 | type = "redirect" 369 | redirect { 370 | port = "443" 371 | protocol = "HTTPS" 372 | status_code = "HTTP_301" 373 | } 374 | } 375 | } 376 | 377 | resource "aws_lb_listener_rule" "dev_rule" { 378 | listener_arn = aws_lb_listener.https.arn 379 | priority = 100 380 | 381 | condition { 382 | host_header { 383 | values = [var.alb_hostname_dev] 384 | } 385 | } 386 | 387 | action { 388 | type = "forward" 389 | target_group_arn = aws_lb_target_group.dev_target_group.arn 390 | } 391 | } 392 | 393 | resource "aws_lb_listener_rule" "prod_rule" { 394 | listener_arn = aws_lb_listener.https.arn 395 | priority = 200 396 | 397 | condition { 398 | host_header { 399 | values = [var.alb_hostname_prod] 400 | } 401 | } 402 | 403 | action { 404 | type = "forward" 405 | target_group_arn = aws_lb_target_group.prod_target_group.arn 406 | } 407 | } 408 | 409 | 410 | resource "aws_lb_target_group" "dev_target_group" { 411 | name = "${var.app_name}-dev-tg" 412 | port = 8080 413 | protocol = "HTTP" 414 | target_type = "ip" 415 | vpc_id = local.vpc.vpc_id 416 | 417 | health_check { 418 | path = "/api/health" 419 | interval = 30 420 | timeout = 5 421 | healthy_threshold = 2 422 | unhealthy_threshold = 2 423 | matcher = "200" 424 | } 425 | 426 | tags = { 427 | Environment = var.env_name_dev 428 | Project = var.app_name 429 | } 430 | } 431 | 432 | resource "aws_lb_target_group" "prod_target_group" { 433 | name = "${var.app_name}-prod-tg" 434 | port = 8080 435 | protocol = "HTTP" 436 | target_type = "ip" 437 | vpc_id = local.vpc.vpc_id 438 | 439 | health_check { 440 | path = "/api/health" 441 | interval = 30 442 | timeout = 5 443 | healthy_threshold = 2 444 | unhealthy_threshold = 2 445 | matcher = "200" 446 | } 447 | 448 | tags = { 449 | Environment = var.env_name_prod 450 | Project = var.app_name 451 | } 452 | } 453 | ``` 454 | 455 | ### ECR Configuration 456 | 457 | For this demo tutorial I have to create ECR manually since we have to push the docker image to ECR before we create ECS task definition deployment. I do however included ECR configuration in the ecr.tf file, you can alwaus import the resource with terraform import command. 458 | 459 | ### ACM Configuration 460 | 461 | ACM certificate is needed for ALB validation with Route53 domain for https connection. Terraform will automatically create necessary CNAME records for ACM validation. 462 | 463 | ```hcl 464 | resource "aws_acm_certificate" "app_certificate" { 465 | domain_name = var.alb_hostname_prod # Use prod domain for ACM validation 466 | validation_method = "DNS" 467 | 468 | subject_alternative_names = [ 469 | var.alb_hostname_dev # Add dev domain as SAN 470 | ] 471 | 472 | tags = { 473 | Environment = var.env_name_prod 474 | Project = var.app_name 475 | } 476 | } 477 | 478 | resource "aws_route53_record" "acm_validation" { 479 | for_each = { 480 | for dvo in aws_acm_certificate.app_certificate.domain_validation_options : 481 | dvo.domain_name => { 482 | name = dvo.resource_record_name 483 | type = dvo.resource_record_type 484 | value = dvo.resource_record_value 485 | } 486 | } 487 | 488 | zone_id = var.route53_zone_id 489 | name = each.value.name 490 | type = each.value.type 491 | records = [each.value.value] 492 | ttl = 300 493 | } 494 | 495 | resource "aws_acm_certificate_validation" "app_certificate_validation" { 496 | certificate_arn = aws_acm_certificate.app_certificate.arn 497 | validation_record_fqdns = [for record in aws_route53_record.acm_validation : record.fqdn] 498 | } 499 | 500 | ``` 501 | 502 | ### Route53 Configuration 503 | 504 | Two records will be created one for dev and one for prod domain. You can specify domain names and hostedzone in terraform.tfvars file. 505 | 506 | ```hcl 507 | 508 | resource "aws_route53_record" "dev_record" { 509 | zone_id = var.route53_zone_id 510 | name = var.alb_hostname_dev 511 | type = "A" 512 | 513 | alias { 514 | name = aws_lb.app_alb.dns_name 515 | zone_id = aws_lb.app_alb.zone_id 516 | evaluate_target_health = true 517 | } 518 | } 519 | 520 | resource "aws_route53_record" "prod_record" { 521 | zone_id = var.route53_zone_id 522 | name = var.alb_hostname_prod 523 | type = "A" 524 | 525 | alias { 526 | name = aws_lb.app_alb.dns_name 527 | zone_id = aws_lb.app_alb.zone_id 528 | evaluate_target_health = true 529 | } 530 | } 531 | 532 | ``` 533 | 534 | 535 | ### IAM Roles and Policies for ECS Tasks 536 | 537 | ### 1. Execution Role 538 | #### Role: `aws_iam_role.ecs_task_execution_role` 539 | 540 | #### Permissions 541 | 1. **Pull Docker images from ECR** 542 | - `ecr:GetDownloadUrlForLayer` 543 | - `ecr:BatchGetImage` 544 | - `ecr:BatchCheckLayerAvailability` 545 | 2. **Write logs to CloudWatch Logs** 546 | - `logs:PutLogEvents` 547 | - `logs:CreateLogStream` 548 | 3. **Access SSM Parameters** 549 | - `ssm:GetParameters` 550 | - `ssm:GetParameter` 551 | - `ssm:GetParameterHistory` 552 | 553 | #### Purpose 554 | - Pull container images from ECR. 555 | - Enable centralized monitoring via CloudWatch Logs. 556 | - Fetch configuration parameters from SSM Parameter Store. 557 | 558 | ### Policy: `ecs_task_execution_policy` 559 | Combines minimal permissions required for: 560 | - ECR image pulls. 561 | - CloudWatch Logs writing. 562 | - SSM Parameter access. 563 | 564 | --- 565 | 566 | ### 2. Task Role: `aws_iam_role.ecs_task_role` 567 | This role allows ECS tasks to perform application-specific operations. 568 | 569 | #### Permissions 570 | 1. **Access S3 Buckets** 571 | - `s3:ListBucket` 572 | - `s3:GetObject` 573 | 2. **Fetch secrets from Secrets Manager** 574 | - `secretsmanager:GetSecretValue` 575 | 3. **Write logs to CloudWatch Logs** 576 | - `logs:PutLogEvents` 577 | - `logs:CreateLogStream` 578 | 579 | #### Purpose 580 | - Access S3 resources for fetching required files. 581 | - Retrieve sensitive application credentials securely from AWS Secrets Manager (if required). 582 | - Log application activity to CloudWatch Logs. 583 | 584 | ### Policy: `ecs_task_custom_policy` 585 | Custom policy with fine-grained permissions for: 586 | - S3 bucket operations. 587 | - Secrets Manager access. 588 | - Application-level logging to CloudWatch Logs. 589 | 590 | 591 | ```hcl 592 | resource "aws_iam_role" "ecs_task_execution_role" { 593 | name = "${var.app_name}-task-execution" 594 | description = "Allows ECS tasks to call AWS services on your behalf" 595 | assume_role_policy = jsonencode({ 596 | Version = "2012-10-17", 597 | Statement = [ 598 | { 599 | Effect = "Allow", 600 | Principal = { Service = "ecs-tasks.amazonaws.com" }, 601 | Action = "sts:AssumeRole" 602 | } 603 | ] 604 | }) 605 | 606 | tags = { 607 | Name = "${var.app_name}-task-execution" 608 | } 609 | } 610 | 611 | resource "aws_iam_role_policy" "ecs_task_execution_policy" { 612 | name = "${var.app_name}-execution-policy" 613 | role = aws_iam_role.ecs_task_execution_role.name 614 | 615 | policy = jsonencode({ 616 | Version = "2012-10-17", 617 | Statement = [ 618 | { 619 | Effect = "Allow", 620 | Action = [ 621 | "ecr:GetDownloadUrlForLayer", 622 | "ecr:BatchGetImage", 623 | "ecr:BatchCheckLayerAvailability", 624 | "logs:PutLogEvents", 625 | "logs:CreateLogStream", 626 | "ssm:GetParameters", 627 | "ssm:GetParameter", 628 | "ssm:GetParameterHistory", 629 | "ecr:GetAuthorizationToken" 630 | 631 | ], 632 | Resource = "*" 633 | } 634 | ] 635 | }) 636 | } 637 | 638 | resource "aws_iam_role" "ecs_task_role" { 639 | name = "${var.app_name}-task" 640 | description = "Allows ECS tasks to assume this role" 641 | 642 | assume_role_policy = jsonencode({ 643 | Version = "2012-10-17", 644 | Statement = [ 645 | { 646 | Effect = "Allow", 647 | Principal = { Service = "ecs-tasks.amazonaws.com" }, 648 | Action = "sts:AssumeRole" 649 | } 650 | ] 651 | }) 652 | 653 | tags = { 654 | Name = "${var.app_name}-task" 655 | } 656 | } 657 | 658 | resource "aws_iam_role_policy" "ecs_task_custom_policy" { 659 | name = "${var.app_name}-custom-task-policy" 660 | role = aws_iam_role.ecs_task_role.name 661 | 662 | policy = jsonencode({ 663 | Version = "2012-10-17", 664 | Statement = [ 665 | { 666 | Effect = "Allow", 667 | Action = [ 668 | "s3:ListBucket", 669 | "s3:GetObject", 670 | "secretsmanager:GetSecretValue", 671 | "logs:PutLogEvents", 672 | "logs:CreateLogStream" 673 | ], 674 | Resource = "*" 675 | } 676 | ] 677 | }) 678 | } 679 | ``` 680 | 681 | ### 3. IAM User gitlab-cicd 682 | 683 | This user will be used for setting up GitLab CICD Pipelines. Access and Secret keys will be stored in Parameters Store you will need once we start setting up pipelines. User will have these two policies assigned AmazonEC2ContainerRegistryPowerUser and AmazonECS_FullAccess 684 | 685 | ```hcl 686 | 687 | # Create the IAM user 688 | resource "aws_iam_user" "gitlab_cicd" { 689 | name = "gitlab-cicd" 690 | } 691 | 692 | # Attach AmazonEC2ContainerRegistryPowerUser policy 693 | resource "aws_iam_user_policy_attachment" "ecr_power_user" { 694 | user = aws_iam_user.gitlab_cicd.name 695 | policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser" 696 | } 697 | 698 | # Attach AmazonECS_FullAccess policy 699 | resource "aws_iam_user_policy_attachment" "ecs_full_access" { 700 | user = aws_iam_user.gitlab_cicd.name 701 | policy_arn = "arn:aws:iam::aws:policy/AmazonECS_FullAccess" 702 | } 703 | 704 | # Create access keys for the user 705 | resource "aws_iam_access_key" "gitlab_cicd_key" { 706 | user = aws_iam_user.gitlab_cicd.name 707 | } 708 | 709 | # Store Access Key ID in SSM Parameter Store 710 | resource "aws_ssm_parameter" "access_key_id" { 711 | name = "/gitlab-cicd/access_key_id" 712 | description = "Access Key ID for gitlab-cicd user" 713 | type = "String" 714 | value = aws_iam_access_key.gitlab_cicd_key.id 715 | 716 | } 717 | 718 | # Store Secret Access Key in SSM Parameter Store 719 | resource "aws_ssm_parameter" "secret_access_key" { 720 | name = "/gitlab-cicd/secret_access_key" 721 | description = "Secret Access Key for gitlab-cicd user" 722 | type = "SecureString" 723 | value = aws_iam_access_key.gitlab_cicd_key.secret 724 | 725 | } 726 | ``` 727 | --- 728 | 729 | 730 | ### Security Groups Configuration 731 | 732 | This Terraform configuration sets up security groups for an Application Load Balancer (ALB) and ECS tasks in both development and production environments using the `terraform-aws-modules/security-group` module. 733 | 734 | ### Overview 735 | 736 | - **ALB Security Group**: 737 | - Allows inbound HTTP (port 80) and HTTPS (port 443) traffic from anywhere. 738 | - Allows all outbound traffic. 739 | 740 | - **ECS Tasks (Dev and Prod) Security Groups**: 741 | - Allow inbound traffic on port 8080 **from the ALB security group**. 742 | - Allow all outbound traffic. 743 | - Separate security groups for development and production services. 744 | 745 | ### 746 | 747 | ```hcl 748 | # Security Group for ALB 749 | module "sg_alb" { 750 | source = "terraform-aws-modules/security-group/aws" 751 | version = "~> 5.0" 752 | 753 | name = "${var.app_name}-alb" 754 | description = "Security group for ALB" 755 | vpc_id = local.vpc.vpc_id 756 | 757 | ingress_with_cidr_blocks = [ 758 | { 759 | from_port = 80 760 | to_port = 80 761 | protocol = "tcp" 762 | cidr_blocks = "0.0.0.0/0" 763 | }, 764 | { 765 | from_port = 443 766 | to_port = 443 767 | protocol = "tcp" 768 | cidr_blocks = "0.0.0.0/0" 769 | } 770 | ] 771 | 772 | egress_with_cidr_blocks = [ 773 | { 774 | from_port = 0 775 | to_port = 0 776 | protocol = "-1" 777 | cidr_blocks = "0.0.0.0/0" 778 | } 779 | ] 780 | 781 | tags = { 782 | Name = "${var.app_name}-alb" 783 | } 784 | } 785 | 786 | # Security Group for ECS Tasks (Dev) 787 | module "sg_ecs_dev" { 788 | source = "terraform-aws-modules/security-group/aws" 789 | version = "~> 5.0" 790 | 791 | name = "${var.app_name}-dev-ecs" 792 | description = "Security group for ECS tasks (Dev)" 793 | vpc_id = local.vpc.vpc_id 794 | 795 | ingress_with_source_security_group_id = [ 796 | { 797 | from_port = 8080 798 | to_port = 8080 799 | protocol = "tcp" 800 | source_security_group_id = module.sg_alb.security_group_id 801 | } 802 | ] 803 | 804 | egress_with_cidr_blocks = [ 805 | { 806 | from_port = 0 807 | to_port = 0 808 | protocol = "-1" 809 | cidr_blocks = "0.0.0.0/0" 810 | } 811 | ] 812 | 813 | tags = { 814 | Name = "${var.app_name}-dev-ecs" 815 | } 816 | } 817 | 818 | # Security Group for ECS Tasks (Prod) 819 | module "sg_ecs_prod" { 820 | source = "terraform-aws-modules/security-group/aws" 821 | version = "~> 5.0" 822 | 823 | name = "${var.app_name}-prod-ecs" 824 | description = "Security group for ECS tasks (Prod)" 825 | vpc_id = local.vpc.vpc_id 826 | 827 | ingress_with_source_security_group_id = [ 828 | { 829 | from_port = 8080 830 | to_port = 8080 831 | protocol = "tcp" 832 | source_security_group_id = module.sg_alb.security_group_id 833 | } 834 | ] 835 | 836 | egress_with_cidr_blocks = [ 837 | { 838 | from_port = 0 839 | to_port = 0 840 | protocol = "-1" 841 | cidr_blocks = "0.0.0.0/0" 842 | } 843 | ] 844 | 845 | tags = { 846 | Name = "${var.app_name}-prod-ecs" 847 | } 848 | } 849 | ``` 850 | 851 | ### Provider and Backend Configuration 852 | 853 | This Terraform configuration sets up the foundational settings for managing AWS infrastructure, including backend state management and required providers. 854 | 855 | 856 | #### Required Version 857 | - **Terraform**: `>= 1.6.0` 858 | 859 | #### Providers 860 | - **AWS**: `~> 5.73.0` 861 | - **Random**: `~> 3.6.0` 862 | 863 | #### Backend Configuration 864 | The state file is stored in an S3 bucket for secure and centralized management. State locking is enabled using DynamoDB. We will be using the s3 bucket that we configured in earlier from Backend module. 865 | 866 | - **S3 Bucket**: `my-project-terraform-state-prod` (replace with your bucket name) 867 | - **State File Key**: `ecs/terraform.tfstate` 868 | - **Region**: `us-east-1` (replace with your AWS region) 869 | - **DynamoDB Table**: `my-project-terraform-lock` (replace with your table name) 870 | - **Encryption**: Enabled 871 | 872 | #### AWS Provider Configuration 873 | - **Region**: Configured dynamically using `var.region`. 874 | - **Default Tags**: Optional feature to apply tags to all resources (to be configured as needed). 875 | 876 | #### Remote State Data Source 877 | - Pulls the networking state from a remote S3 bucket. 878 | - **Bucket**: `my-project-terraform-state-prod` (replace with your bucket name) 879 | - **State File Key**: `networking/terraform.tfstate` 880 | - **Region**: `us-east-1` 881 | - **DynamoDB Table**: `my-project-terraform-lock` 882 | - **Encryption**: Enabled 883 | 884 | ### Key Highlights 885 | - **Remote State Management**: Ensures infrastructure consistency across teams by using a centralized state file. 886 | - **State Locking**: Prevents concurrent modifications using DynamoDB. 887 | - **Modular Design**: Enables seamless integration with other infrastructure components via remote state. 888 | 889 | Replace placeholders with your project-specific details for production use. 890 | 891 | 892 | ```hcl 893 | terraform { 894 | required_version = ">= 1.6.0" 895 | 896 | required_providers { 897 | aws = { 898 | source = "hashicorp/aws" 899 | version = "~> 5.73.0" 900 | } 901 | 902 | random = { 903 | source = "hashicorp/random" 904 | version = "~> 3.6.0" 905 | } 906 | } 907 | 908 | backend "s3" { 909 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 910 | key = "ecs/terraform.tfstate" # Unique key for ECS state file 911 | region = "us-east-1" # Replace with your AWS region 912 | dynamodb_table = "my-project-terraform-lock" # Replace with your DynamoDB table name 913 | encrypt = true 914 | } 915 | } 916 | 917 | provider "aws" { 918 | region = var.region 919 | 920 | } 921 | 922 | data "terraform_remote_state" "vpc" { 923 | backend = "s3" 924 | config = { 925 | bucket = "my-project-terraform-state-prod" # Replace with your S3 bucket name 926 | key = "networking/terraform.tfstate" # Path to the networking state file 927 | region = "us-east-1" # Replace with your AWS region 928 | dynamodb_table = "my-project-terraform-lock" # Replace with your DynamoDB table name 929 | encrypt = true 930 | } 931 | } 932 | 933 | ``` 934 | 935 | ### terraform.tfvars 936 | 937 | #### Environment and Region Configuration 938 | 939 | 940 | - **Environment Names**: 941 | - Development: `dev` 942 | - Production: `prod` 943 | - **AWS Region**: `us-east-1` (update if using a different region) 944 | 945 | ### Application Details 946 | - **Application Name**: `web-app` 947 | 948 | ### AWS Account Details 949 | - **Account ID**: `123456789` (update with your AWS account ID) 950 | 951 | ### ALB Configuration 952 | - **Development ALB Hostname**: `web-app-dev.123456789.example.net` 953 | - **Production ALB Hostname**: `web-app-prod.123456789.example.net` 954 | 955 | ### DNS 956 | - **Route 53 Hosted Zone ID**: `ZSDFSDF3FDSSDFSDFDF` (update as per your hosted zone) 957 | 958 | ### 959 | ```hcl 960 | # Environment and Region 961 | env_name_prod = "prod" 962 | env_name_dev = "dev" # Change to "prod" for production 963 | region = "us-east-1" # AWS region 964 | 965 | # Application Details 966 | app_name = "web-app" # Your application name 967 | 968 | # AWS Account Details 969 | account_id = "452303021915" # Your AWS account ID 970 | 971 | # ALB Configuration 972 | alb_hostname_dev = "web-app-dev.452303021915.realhandsonlabs.net" 973 | alb_hostname_prod = "web-app-prod.452303021915.realhandsonlabs.net" 974 | 975 | # Subnet and VPC IDs 976 | 977 | route53_zone_id = "Z10075723METSXT8WC4VB" 978 | ``` 979 | 980 | ## Gitlab CI/CD Setup 981 | 982 | We will build a GitLab CI/CD pipeline from scratch to automate deployments to AWS ECS Fargate utilizing Docker containers, AWS ECR for image storage, and GitLab YAML for configuration. 983 | 984 | Pipeline Structure: Two branches (dev and prod) define the target ECS service/task based on the ECS service name and deploy the application. 985 | Stages: Automate validation, build, push, and deployment through defined CI/CD stages. 986 | 987 | - GitLab AWS IAM User 988 | - GitLab Project 989 | - Setup branch dev and main protection 990 | - Setup GitLab CICD Pipelines 991 | 992 | 993 | ![alt text](images/gitlab_cicd_01.png) 994 | 995 | ### GitLab IAM user 996 | 997 | Terraform will create an IAM user and store IAM acccess and secret credentials for 'gitlab-cicd' user. We will need to retrieve IAM access and sercret keys from SSM Parameters Store. This user doesn't need programmatic but terminal access only. 998 | 999 | ![alt text](images/gitlab_cicd.png) 1000 | ![alt text](images/parameters_store.png) 1001 | 1002 | Log in to your Gitlab account and create a new project 1003 | 1004 | ![alt text](images/new_project.png) 1005 | 1006 | Log in to your linux server and git initialize new repository 1007 | 1008 | ![alt text](images/git_pull.png) 1009 | 1010 | You will need to have two branches 'dev' and 'main': 1011 | ![alt text](images/branches.png) 1012 | 1013 | Protect both of your branch from deletion as well as push without merge 1014 | 1015 | ![alt text](images/protect_branches.png) 1016 | 1017 | Set 3 variables for your pipeline 1018 | ![alt text](images/variables.png) 1019 | 1020 | ![alt text](images/three_variables.png) 1021 | 1022 | ### Pipelines setup 1023 | ## 1024 | The .gitlab-ci.yml file automates the entire pipeline for deploying a web app on AWS ECS. It validates the environment, builds and pushes Docker images to AWS ECR, and deploys to ECS services (dev branch auto-deploys, main branch requires manual approval for production). It uses environment variables for flexibility and integrates ECS updates with forced new deployments for both dev and prod environments. 1025 | 1026 | https://gitlab.com/sahib.gasimov2/gitlabcicd-ecs - You can copy all CICD files from this repo. 1027 | 1028 | ![alt text](images/gitlabcicdyaml.png) 1029 | 1030 | ## .gitlab-ci.yml 1031 | 1032 | 1033 | ```yaml 1034 | stages: 1035 | - validate_environment 1036 | - build_and_publish 1037 | - deploy_to_dev 1038 | - deploy_to_production 1039 | - finalize_pipeline 1040 | 1041 | workflow: # Trigger pipeline only for specific branches 1042 | rules: 1043 | - if: '$CI_COMMIT_BRANCH == "main"' 1044 | - if: '$CI_COMMIT_BRANCH == "dev"' 1045 | 1046 | image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest 1047 | 1048 | variables: 1049 | AWS_ACCOUNT_NUMBER : "452303021915" 1050 | REGION : "us-east-1" 1051 | IMAGE_REPOSITORY : "web-app-repository" 1052 | CLUSTER_NAME : "web-app-cluster" 1053 | 1054 | DEV_SERVICE_NAME : "web-app-dev" 1055 | DEV_TASK_NAME : "web-app-dev" 1056 | 1057 | PROD_SERVICE_NAME : "web-app-prod" 1058 | PROD_TASK_NAME : "web-app-prod" 1059 | 1060 | test_environment: 1061 | stage: validate_environment 1062 | script: 1063 | - echo "Validating environment setup..." 1064 | - aws --version 1065 | - docker --version 1066 | - jq --version 1067 | - aws sts get-caller-identity 1068 | 1069 | build_and_push: 1070 | stage: build_and_publish 1071 | services: 1072 | - docker:dind 1073 | variables: 1074 | DOCKER_HOST: tcp://docker:2375 1075 | before_script: 1076 | - aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com 1077 | 1078 | script: 1079 | - echo "Building the Docker image..." 1080 | - docker build -t $IMAGE_REPOSITORY . 1081 | - echo "Tagging the image..." 1082 | - docker tag $IMAGE_REPOSITORY:latest $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-latest 1083 | - docker tag $IMAGE_REPOSITORY:latest $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA 1084 | - echo "Pushing the image to ECR..." 1085 | - docker push $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-latest 1086 | - docker push $AWS_ACCOUNT_NUMBER.dkr.ecr.$REGION.amazonaws.com/$IMAGE_REPOSITORY:$CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA 1087 | 1088 | deploy_to_dev: 1089 | stage: deploy_to_dev 1090 | rules: 1091 | - if: '$CI_COMMIT_BRANCH == "dev"' 1092 | script: 1093 | - echo "Deploying to development environment..." 1094 | - | 1095 | aws ecs update-service \ 1096 | --cluster $CLUSTER_NAME \ 1097 | --service $DEV_SERVICE_NAME \ 1098 | --task-definition $DEV_TASK_NAME \ 1099 | --force-new-deployment 1100 | 1101 | deploy_to_production: 1102 | stage: deploy_to_production 1103 | when: manual # Require manual confirmation for production 1104 | manual_confirmation: 'Proceed with production deployment?' 1105 | allow_failure: false # Must succeed to continue 1106 | rules: 1107 | - if: '$CI_COMMIT_BRANCH == "main"' 1108 | script: 1109 | - echo "Deploying to production environment..." 1110 | - | 1111 | aws ecs update-service \ 1112 | --cluster $CLUSTER_NAME \ 1113 | --service $PROD_SERVICE_NAME \ 1114 | --task-definition $PROD_TASK_NAME \ 1115 | --force-new-deployment 1116 | 1117 | finalize_pipeline: 1118 | stage: finalize_pipeline 1119 | script: 1120 | - echo "CI/CD Pipeline completed successfully!" 1121 | ``` 1122 | 1123 | This is how pipelines will look like 1124 | 1125 | ![alt text](images/cicd_pipeline_stages.jpg) 1126 | 1127 | ## Python application 1128 | 1129 | Small Python application for converting image to pdf. 1130 | 1131 | app.py 1132 | 1133 | ```python 1134 | from flask import Flask, request, send_file, jsonify 1135 | from PIL import Image 1136 | import os 1137 | 1138 | app = Flask(__name__) 1139 | UPLOAD_FOLDER = 'uploads' 1140 | OUTPUT_FOLDER = 'output' 1141 | os.makedirs(UPLOAD_FOLDER, exist_ok=True) 1142 | os.makedirs(OUTPUT_FOLDER, exist_ok=True) 1143 | 1144 | @app.route('/') 1145 | def home(): 1146 | return ''' 1147 | 1148 | Image to PDF Converter 1149 |

Upload an image to convert to PDF

1150 |
1151 | 1152 | 1153 |
1154 | ''' 1155 | 1156 | @app.route('/convert', methods=['POST']) 1157 | def convert_to_pdf(): 1158 | if 'image' not in request.files: 1159 | return "No file uploaded", 400 1160 | 1161 | file = request.files['image'] 1162 | if file.filename == '': 1163 | return "No selected file", 400 1164 | 1165 | try: 1166 | # Save the uploaded file 1167 | image_path = os.path.join(UPLOAD_FOLDER, file.filename) 1168 | file.save(image_path) 1169 | 1170 | # Convert to PDF 1171 | image = Image.open(image_path) 1172 | if image.mode != 'RGB': 1173 | image = image.convert('RGB') 1174 | pdf_path = os.path.join(OUTPUT_FOLDER, f"{os.path.splitext(file.filename)[0]}.pdf") 1175 | image.save(pdf_path, "PDF") 1176 | 1177 | # Send the PDF file as a response 1178 | return send_file(pdf_path, as_attachment=True) 1179 | except Exception as e: 1180 | return f"An error occurred: {e}", 500 1181 | 1182 | # Health check endpoint for ALB 1183 | @app.route('/api/health', methods=['GET']) 1184 | def health_check(): 1185 | return jsonify({"status": "healthy"}), 200 1186 | 1187 | if __name__ == '__main__': 1188 | app.run(host='0.0.0.0', port=8080, debug=True) 1189 | 1190 | ``` 1191 | 1192 | ### Dockerfile 1193 | 1194 | 1195 | 1196 | ```Dockerfile 1197 | # Use an official Python runtime as the base image 1198 | FROM python:3.9-slim 1199 | 1200 | # Set the working directory in the container 1201 | WORKDIR /app 1202 | 1203 | # Copy the application code and requirements into the container 1204 | COPY . /app 1205 | 1206 | # Install dependencies 1207 | RUN pip install --no-cache-dir -r requirements.txt 1208 | 1209 | # Expose port 8080 for the Flask application 1210 | EXPOSE 8080 1211 | 1212 | # Run the Flask application 1213 | CMD ["python", "app.py"] 1214 | 1215 | ``` 1216 | 1217 | --------------------------------------------------------------------------------