Skip to content

Commit

Permalink
ci(infra): Terraform stack for EC2 + SSM port forwarding deployment (T…
Browse files Browse the repository at this point in the history
…racecatHQ#291)

* Add terraform paths to .gitignore

* Add single instance EC2 stack

* fix: Missing vpc endpoints for ssm in private subset

* Missing curl Caddyfile

* Add user input for prod / dev mode and public IP

* Parameterize tracecat version

* fix: tracecat_version

* Parameterize disable_ssl for postgres

* refactor: Consolidate public and internal api / app urls

* Add region to local-exec command

* Bump image versions
  • Loading branch information
topher-lo authored Aug 6, 2024
1 parent f3e2fcb commit a905dc4
Show file tree
Hide file tree
Showing 11 changed files with 450 additions and 26 deletions.
21 changes: 14 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
LOG_LEVEL=INFO
COMPOSE_PROJECT_NAME=tracecat

# --- Shared URL env vars ---
PUBLIC_APP_URL=http://localhost
PUBLIC_API_URL=http://localhost/api/
INTERNAL_API_URL=http://api:8000

# -- Caddy env vars ---
BASE_DOMAIN=:80
# Note: replace with your server's IP address
Expand All @@ -20,20 +25,22 @@ TRACECAT__SERVICE_KEY=your-tracecat-service-key
# Can be generated using `openssl rand -hex 32`
TRACECAT__SIGNING_SECRET=your-tracecat-signing-secret
# API Service URL
TRACECAT__API_URL=http://api:8000
TRACECAT__API_URL=${INTERNAL_API_URL}
# Root path to deal with extra path prefix behind the reverse proxy
TRACECAT__API_ROOT_PATH=/api
# Public Runner URL
# This is the public URL for incoming webhooks
# If you wish to expose your webhooks to the internet, you can use a tunneling service like ngrok.
# If using ngrok, run `ngrok http --domain=INSERT_STATIC_NGROK_DOMAIN_HERE 8001`
# to start ngrok and update this with the forwarding URL
TRACECAT__PUBLIC_RUNNER_URL=http://localhost:8000
TRACECAT__PUBLIC_RUNNER_URL=${PUBLIC_API_URL}
# CORS (comman separated string of allowed origins)
TRACECAT__ALLOW_ORIGINS=http://localhost:3000,http://localhost
TRACECAT__ALLOW_ORIGINS=http://localhost:3000,${PUBLIC_APP_URL}
# Postgres SSL model
TRACECAT__DB_SSLMODE=disable

# --- CLI env vars ---
TRACECAT__PUBLIC_API_URL=http://localhost/api/
TRACECAT__PUBLIC_API_URL=${PUBLIC_API_URL}

# --- Postgres ---
TRACECAT__POSTGRES_USER=postgres
Expand All @@ -48,11 +55,11 @@ TRACECAT__DB_URI=postgresql+psycopg://${TRACECAT__POSTGRES_USER}:${TRACECAT__POS
NODE_ENV=development
NEXT_PUBLIC_APP_ENV=development
# The frontend app URL
NEXT_PUBLIC_APP_URL=http://localhost
NEXT_PUBLIC_APP_URL=${PUBLIC_APP_URL}
# Allows the browser to communicate with the backend
NEXT_PUBLIC_API_URL=http://localhost/api/
NEXT_PUBLIC_API_URL=${PUBLIC_API_URL}
# Allows the frontend server (inside docker) to communicate with the backend server (inside docker)
NEXT_SERVER_API_URL=http://api:8000
NEXT_SERVER_API_URL=${INTERNAL_API_URL}

# --- Authentication + Clerk ---
# Controls auth for both the API and the frontend server + client
Expand Down
61 changes: 45 additions & 16 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,17 +1,47 @@
# RabbitMQ volume
.rabbitmq/

# CDK stack
cdk.out/

# Database
db.sqlite3

# Tracecat
.tracecat/

# MacOS
.DS_Store
# Terraform-related files
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl
.terraform.tfstate.*

# EC2 user data logs
ssm_command_id.txt
user_data_log.txt

# Terraform crash logs
crash.log

# Terraform variables and overrides
*.tfvars
*.tfvars.json
*.tfvars.local
override.tf
override.tf.json
_private_key.pem
*_auto.tfvars

# Terraform plan file
*.tfplan

# Sensitive files
*.pem
*.key
*.crt
*.env
*.zip
*.tar.gz

# Editor and IDE-specific files
.vscode/
*.code-workspace
.idea/
*.sublime-workspace
*.sublime-project

# Logs and backup files
*.log
*.bak

# Byte-compiled / optimized / DLL files
__pycache__/
Expand All @@ -29,7 +59,7 @@ dist/
downloads/
eggs/
.eggs/
/lib/
lib/
lib64/
parts/
sdist/
Expand Down Expand Up @@ -71,7 +101,6 @@ cover/
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
Expand Down
208 changes: 208 additions & 0 deletions deployments/aws/ec2/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
provider "aws" {
region = var.aws_region
}

data "aws_ami" "this" {
most_recent = true
owners = ["amazon"]

filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}

filter {
name = "virtualization-type"
values = ["hvm"]
}
}

locals {
project_tags = {
Project = var.project_name
Environment = var.environment
}
}

module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"

name = "${var.project_name}-vpc"
cidr = var.vpc_cidr

azs = [var.aws_availability_zone]
private_subnets = [var.private_subnet_cidr]
public_subnets = [var.public_subnet_cidr]

enable_dns_hostnames = true
enable_dns_support = true
enable_nat_gateway = true
single_nat_gateway = true

tags = merge(local.project_tags, {
Name = "${var.project_name}-vpc"
})
}

resource "aws_vpc_endpoint" "ssm" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.ssm"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.vpc_endpoints.id]
subnet_ids = module.vpc.private_subnets
private_dns_enabled = true

tags = merge(local.project_tags, {
Name = "${var.project_name}-ssm-endpoint"
})
}

resource "aws_vpc_endpoint" "ec2messages" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.ec2messages"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.vpc_endpoints.id]
subnet_ids = module.vpc.private_subnets
private_dns_enabled = true

tags = merge(local.project_tags, {
Name = "${var.project_name}-ec2messages-endpoint"
})
}

resource "aws_vpc_endpoint" "ssmmessages" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.aws_region}.ssmmessages"
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.vpc_endpoints.id]
subnet_ids = module.vpc.private_subnets
private_dns_enabled = true

tags = merge(local.project_tags, {
Name = "${var.project_name}-ssmmessages-endpoint"
})
}

resource "aws_security_group" "vpc_endpoints" {
name = "${var.project_name}-vpc-endpoints-sg"
description = "Security group for VPC endpoints"
vpc_id = module.vpc.vpc_id

ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
description = "Allow HTTPS from VPC CIDR"
}

tags = merge(local.project_tags, {
Name = "${var.project_name}-vpc-endpoints-sg"
})
}

resource "aws_security_group" "this" {
name = "${var.project_name}-sg"
description = "Security group for ${var.project_name} EC2 instance"
vpc_id = module.vpc.vpc_id

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}

ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
description = "Caddy HTTP server"
}

tags = merge(local.project_tags, {
Name = "${var.project_name}-sg"
})
}

resource "aws_iam_role" "this" {
name = "${var.project_name}-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})

tags = local.project_tags
}

resource "aws_iam_role_policy_attachment" "ssm" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.this.name
}

resource "aws_iam_instance_profile" "this" {
name = "${var.project_name}-profile"
role = aws_iam_role.this.name

tags = local.project_tags
}

resource "aws_instance" "this" {
ami = data.aws_ami.this.id
instance_type = var.instance_type
subnet_id = module.vpc.private_subnets[0]
vpc_security_group_ids = [aws_security_group.this.id]
iam_instance_profile = aws_iam_instance_profile.this.name

metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 2
}

user_data = base64encode(templatefile("${path.module}/user_data.tpl", {
tracecat_version = var.tracecat_version
}))

provisioner "local-exec" {
command = <<-EOT
aws ec2 wait instance-status-ok --instance-ids ${self.id} --region ${var.aws_region} && \
sleep 60 && \
aws ssm send-command \
--instance-ids ${self.id} \
--document-name "AWS-RunShellScript" \
--parameters '{"commands":["cat /var/log/user-data.log"]}' \
--output text \
--region ${var.aws_region} \
--query "Command.CommandId" > ssm_command_id.txt && \
sleep 10 && \
aws ssm get-command-invocation \
--command-id $(cat ssm_command_id.txt) \
--instance-id ${self.id} \
--query "StandardOutputContent" \
--region ${var.aws_region} \
--output text > user_data_log.txt && \
if grep -q "ERROR:" user_data_log.txt; then
echo "Error detected in user data log. Log content:"
cat user_data_log.txt
exit 1
else
echo "User data script completed successfully"
fi
EOT
}

tags = merge(local.project_tags, {
Name = "${var.project_name}-instance"
})
}
23 changes: 23 additions & 0 deletions deployments/aws/ec2/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
output "instance_id" {
description = "The ID of the EC2 instance"
value = aws_instance.this.id
}

output "instance_private_ip" {
description = "The private IP address of the EC2 instance"
value = aws_instance.this.private_ip
}

output "vpc_id" {
description = "The ID of the VPC"
value = module.vpc.vpc_id
}

output "security_group_id" {
description = "The ID of the instance security group"
value = aws_security_group.this.id
}

output "nat_gateway_id" {
value = module.vpc.natgw_ids[0]
}
12 changes: 12 additions & 0 deletions deployments/aws/ec2/ssm-start-session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/bash
# Requires SSM Session Manager plugin to be installed on the local machine:
# https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html

INSTANCE_ID=$(terraform output -raw instance_id)
LOCAL_PORT=8080
REMOTE_PORT=80

aws ssm start-session \
--target $INSTANCE_ID \
--document-name AWS-StartPortForwardingSession \
--parameters "{\"portNumber\":[\"$REMOTE_PORT\"],\"localPortNumber\":[\"$LOCAL_PORT\"]}"
Loading

0 comments on commit a905dc4

Please sign in to comment.