├── 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 | 
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 | 
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 |
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 |
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 |
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
675 |
676 |
677 | Log in to your Gitlab account and create a new project
678 |
679 | 
680 |
681 | Log in to your linux server and git initialize new repository
682 |
683 | 
684 |
685 | You will need to have two branches 'dev' and 'main':
686 | 
687 |
688 | Protect both of your branch from deletion as well as push without merge
689 |
690 | 
691 |
692 | Set 3 variables for your pipeline
693 | 
694 |
695 | 
696 |
697 | ### Pipelines setup
698 | ##
699 |
700 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
1000 | 
1001 |
1002 | Log in to your Gitlab account and create a new project
1003 |
1004 | 
1005 |
1006 | Log in to your linux server and git initialize new repository
1007 |
1008 | 
1009 |
1010 | You will need to have two branches 'dev' and 'main':
1011 | 
1012 |
1013 | Protect both of your branch from deletion as well as push without merge
1014 |
1015 | 
1016 |
1017 | Set 3 variables for your pipeline
1018 | 
1019 |
1020 | 
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 | 
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 | 
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 |
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 |
--------------------------------------------------------------------------------