Skip to content

Commit

Permalink
Terraform init
Browse files Browse the repository at this point in the history
  • Loading branch information
Agwebberley committed Jan 3, 2025
1 parent 4032c57 commit 8f6cc96
Show file tree
Hide file tree
Showing 22 changed files with 1,016 additions and 2 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,7 @@ tags
.idea/

.pytest_cache/
terraform/terraform.tfstate
terraform/terraform.tfstate.backup
terraform.tfstate
terraform/.terraform.lock.hcl
27 changes: 25 additions & 2 deletions {{cookiecutter.project_slug}}/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@
.pre-commit-config.yaml
.readthedocs.yml
.travis.yml
venv
.git
.envs/
*.pyc
*.pyo
*.mo
*.db
*.css.map
*.egg-info
*.sql.gz
.cache
.project
terraform/
.pydevproject
.DS_Store
.sass-cache
.vagrant/
__pycache__
dist
docs
env
logs
src/node_modules
web/media
web/static/CACHE
stats
Dockerfile
venv/
5 changes: 5 additions & 0 deletions {{cookiecutter.project_slug}}/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,8 @@ vendors.js
{{ cookiecutter.project_slug }}/static/webpack_bundles/
webpack-stats.json
{%- endif %}

terraform/terraform.tfstate
terraform/terraform.tfstate.backup
terraform.tfstate
terraform/.terraform.lock.hcl
30 changes: 30 additions & 0 deletions {{cookiecutter.project_slug}}/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# pull official base image
FROM python:3.12.0-slim-bookworm

# set work directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install APT dependencies
RUN apt-get update \
&& apt-get install -y git gcc libpq-dev python3-dev postgresql libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 \
&& apt-get clean


# install dependencies
RUN pip install --upgrade pip
COPY ./requirements/base.txt .
COPY ./requirements/production.txt .
RUN pip install -r production.txt

# Create a user with UID 1000 and GID 1000
RUN groupadd -g 1000 appgroup && \
useradd -r -u 1000 -g appgroup appuser
# Switch to this user
USER 1000:1000

# copy project
COPY . .
98 changes: 98 additions & 0 deletions {{cookiecutter.project_slug}}/deploy/update-ecs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import subprocess

import boto3
import click


def get_terraform_output(output_name):
try:
return subprocess.check_output(
["terraform", "output", "-state=../terraform/terraform.tfstate", output_name],
text=True
).strip()
except Exception as e:
print(f"Error fetching Terraform output {output_name}: {e}")
return None

def get_current_task_definition(client, cluster, service):
response = client.describe_services(cluster=cluster, services=[service])
current_task_arn = response["services"][0]["taskDefinition"]
return client.describe_task_definition(taskDefinition=current_task_arn)

def run_collectstatic_task(client, cluster, task_definition_name):
print("Running collectstatic task...")

response = client.run_task(
cluster=cluster,
taskDefinition=task_definition_name,
launchType="FARGATE",
networkConfiguration=network_configuration, # Add the network configuration here
)

if response.get("tasks"):
print(f"collectstatic task started with task ARN: {response['tasks'][0]['taskArn']}")
else:
print("Failed to start collectstatic task.")
if response.get("failures"):
for failure in response["failures"]:
print(f"Reason: {failure['reason']}")

subnets = [s.strip(' "\n[]') for s in get_terraform_output("subnets").split(",") if "subnet" in s]
security_group = get_terraform_output("security_group").strip(' "\n')
network_configuration = {
"awsvpcConfiguration": {
"subnets": subnets,
"securityGroups": [security_group],
"assignPublicIp": "ENABLED"
}
}

@click.command()
@click.option("--cluster", help="Name of the ECS cluster", required=True)
@click.option("--service", help="Name of the ECS service", required=True)
@click.option("--image", help="Docker image URL for the updated application", required=True)
@click.option("--container-name", help="Name of the container to update", required=True)
@click.option("--collectstatic-task", help="Name of the collectstatic task definition", default="django-collectstatic-task")
def deploy(cluster, service, image, container_name, collectstatic_task):
client = boto3.client("ecs")

# Run the collectstatic task
run_collectstatic_task(client, cluster, collectstatic_task)

# Fetch the current task definition
print("Fetching current task definition...")
response = get_current_task_definition(client, cluster, service)

# Iterate over container definitions and update the image for the matching container
container_definitions = response["taskDefinition"]["containerDefinitions"]
for container in container_definitions:
if container["name"] == container_name:
container["image"] = image
print(f"Updated {container_name} image to: {image}")

# Register a new task definition
print("Registering new task definition...")
response = client.register_task_definition(
family=response["taskDefinition"]["family"],
volumes=response["taskDefinition"]["volumes"],
containerDefinitions=container_definitions,
cpu="256", # Modify based on your needs
memory="512", # Modify based on your needs
networkMode="awsvpc",
requiresCompatibilities=["FARGATE"],
executionRoleArn="ecs_task_execution_role_prod",
taskRoleArn="ecs_task_execution_role_prod"
)
new_task_arn = response["taskDefinition"]["taskDefinitionArn"]
print(f"New task definition ARN: {new_task_arn}")

# Update the service with the new task definition
print("Updating ECS service with new task definition...")
client.update_service(
cluster=cluster, service=service, taskDefinition=new_task_arn,
)
print("Service updated!")


if __name__ == "__main__":
deploy()
5 changes: 5 additions & 0 deletions {{cookiecutter.project_slug}}/nginx/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
FROM nginx:1.25.3-alpine

RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d
EXPOSE 80
20 changes: 20 additions & 0 deletions {{cookiecutter.project_slug}}/nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
upstream todo {
server localhost:8000;
}

server {

listen 80;

location /staticfiles/ {
alias /efs/staticfiles/; # Path to static files on EFS
}

location / {
proxy_pass http://todo;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}

}
7 changes: 7 additions & 0 deletions {{cookiecutter.project_slug}}/terraform/01_provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# core

provider "aws" {
region = var.region
}


88 changes: 88 additions & 0 deletions {{cookiecutter.project_slug}}/terraform/02_network.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Production VPC
resource "aws_vpc" "production-vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
}

# Public subnets
resource "aws_subnet" "public-subnet-1" {
cidr_block = var.public_subnet_1_cidr
vpc_id = aws_vpc.production-vpc.id
availability_zone = var.availability_zones[0]
}
resource "aws_subnet" "public-subnet-2" {
cidr_block = var.public_subnet_2_cidr
vpc_id = aws_vpc.production-vpc.id
availability_zone = var.availability_zones[1]
}

# Private subnets
resource "aws_subnet" "private-subnet-1" {
cidr_block = var.private_subnet_1_cidr
vpc_id = aws_vpc.production-vpc.id
availability_zone = var.availability_zones[0]
}
resource "aws_subnet" "private-subnet-2" {
cidr_block = var.private_subnet_2_cidr
vpc_id = aws_vpc.production-vpc.id
availability_zone = var.availability_zones[1]
}

# Route tables for the subnets
resource "aws_route_table" "public-route-table" {
vpc_id = aws_vpc.production-vpc.id
}
resource "aws_route_table" "private-route-table" {
vpc_id = aws_vpc.production-vpc.id
}

# Associate the newly created route tables to the subnets
resource "aws_route_table_association" "public-route-1-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-1.id
}
resource "aws_route_table_association" "public-route-2-association" {
route_table_id = aws_route_table.public-route-table.id
subnet_id = aws_subnet.public-subnet-2.id
}
resource "aws_route_table_association" "private-route-1-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-1.id
}
resource "aws_route_table_association" "private-route-2-association" {
route_table_id = aws_route_table.private-route-table.id
subnet_id = aws_subnet.private-subnet-2.id
}

# Elastic IP
resource "aws_eip" "elastic-ip-for-nat-gw" {
domain = "vpc"
depends_on = [aws_internet_gateway.production-igw]
}

# NAT gateway
resource "aws_nat_gateway" "nat-gw" {
allocation_id = aws_eip.elastic-ip-for-nat-gw.id
subnet_id = aws_subnet.public-subnet-1.id
depends_on = [aws_eip.elastic-ip-for-nat-gw]
}
resource "aws_route" "nat-gw-route" {
route_table_id = aws_route_table.private-route-table.id
nat_gateway_id = aws_nat_gateway.nat-gw.id
destination_cidr_block = "0.0.0.0/0"
}

# Internet Gateway for the public subnet
resource "aws_internet_gateway" "production-igw" {
vpc_id = aws_vpc.production-vpc.id
}

# Route the public subnet traffic through the Internet Gateway
resource "aws_route" "public-internet-igw-route" {
route_table_id = aws_route_table.public-route-table.id
gateway_id = aws_internet_gateway.production-igw.id
destination_cidr_block = "0.0.0.0/0"
}


84 changes: 84 additions & 0 deletions {{cookiecutter.project_slug}}/terraform/03_securitygroups.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ALB Security Group (Traffic Internet -> ALB)
resource "aws_security_group" "load-balancer" {
name = "load_balancer_security_group"
description = "Controls access to the ALB"
vpc_id = aws_vpc.production-vpc.id

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

# ECS Fargate Security group (traffic ALB -> ECS Fargate Tasks)
resource "aws_security_group" "ecs-fargate" {
name = "ecs_fargate_security_group"
description = "Allows inbound access from the ALB only"
vpc_id = aws_vpc.production-vpc.id

ingress {
from_port = 0
to_port = 0
protocol = "-1"
security_groups = [aws_security_group.load-balancer.id]
}

# No SSH ingress rule since Fargate tasks are abstracted and not directly accessible via SSH

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

# RDS Security Group (traffic Fargate -> RDS)
resource "aws_security_group" "rds" {
name = "rds-security-group"
description = "Allows inbound access from Fargate only"
vpc_id = aws_vpc.production-vpc.id

ingress {
protocol = "tcp"
from_port = "5432"
to_port = "5432"
security_groups = [aws_security_group.ecs-fargate.id]
}

egress {
protocol = "-1"
from_port = 0
to_port = 0
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_security_group" "efs_sg" {
name = "EFS Security Group"
description = "Allow ECS to EFS communication"
vpc_id = aws_vpc.production-vpc.id

ingress {
from_port = 2049 # NFS port
to_port = 2049
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Modify this based on your security requirements
}
}
Loading

0 comments on commit 8f6cc96

Please sign in to comment.