Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform infrastructure #202

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions terraform/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions terraform/cloudwatch.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "aws_cloudwatch_log_group" "cloudwatch_log_group_ecs" {
name = "TwiggyBot"
retention_in_days = 7
}
24 changes: 24 additions & 0 deletions terraform/data.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
data "aws_iam_policy_document" "assume_role_policy_ecs_tasks" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]

principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}

data "aws_iam_policy_document" "assume_role_policy_lambda" {
statement {
effect = "Allow"

principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}

actions = ["sts:AssumeRole"]
}
}
21 changes: 21 additions & 0 deletions terraform/ec2.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
resource "aws_vpc" "vpc" {
cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "subnet" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.0.0/16"
}

resource "aws_security_group" "security_group" {
name = "twiggy"
vpc_id = aws_vpc.vpc.id

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = ["::/0"]
}
}
26 changes: 26 additions & 0 deletions terraform/ecr.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "aws_ecr_repository" "ecr_repository" {
name = "twiggy"
}

resource "aws_ecr_lifecycle_policy" "ecr_lifecycle_policy" {
repository = aws_ecr_repository.ecr_repository.name

policy = jsonencode({
rules : [
{
rulePriority : 1
description : "Delete untagged images"
selection : {
tagStatus : "untagged"
countType : "sinceImagePushed"
countUnit : "days"
countNumber : 1
}
action : {
type : "expire"
}
}
]
})
depends_on = [aws_ecr_repository.ecr_repository]
}
119 changes: 119 additions & 0 deletions terraform/ecs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
resource "aws_ecs_cluster" "ecs_cluster" {
name = "twiggy"
}

resource "aws_ecs_service" "ecs_primary_service" {
name = "twiggy-primary"
cluster = aws_ecs_cluster.ecs_cluster.arn

desired_count = 1
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = 1
}
platform_version = "LATEST"
task_definition = aws_ecs_task_definition.ecs_task_definition.arn
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
enable_execute_command = true
propagate_tags = "SERVICE"

network_configuration {
assign_public_ip = true
subnets = [
aws_subnet.subnet.id
]
security_groups = [
aws_security_group.security_group.id
]
}
health_check_grace_period_seconds = 0
scheduling_strategy = "REPLICA"

lifecycle {
ignore_changes = [capacity_provider_strategy]
}
}

resource "aws_ecs_service" "ecs_fallback_service" {
name = "twiggy-fallback"
cluster = aws_ecs_cluster.ecs_cluster.arn

desired_count = 0
capacity_provider_strategy {
capacity_provider = "FARGATE"
weight = 1
}
platform_version = "LATEST"
task_definition = aws_ecs_task_definition.ecs_task_definition.arn
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
enable_execute_command = true
propagate_tags = "SERVICE"

network_configuration {
assign_public_ip = true
subnets = [
aws_subnet.subnet.id
]
security_groups = [
aws_security_group.security_group.id
]
}
health_check_grace_period_seconds = 0
scheduling_strategy = "REPLICA"
}

resource "aws_ecs_task_definition" "ecs_task_definition" {
family = "twiggy"
task_role_arn = aws_iam_role.ecs_task_role.arn
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
network_mode = "awsvpc"
requires_compatibilities = [
"FARGATE"
]
cpu = "256"
memory = "512"
runtime_platform {
cpu_architecture = "X86_64"
operating_system_family = "LINUX"
}
container_definitions = jsonencode([
{
name : "twiggy"
essential : true
image : "${aws_ecr_repository.ecr_repository.repository_url}:twiggy"
logConfiguration : {
logDriver : "awslogs"
options : {
awslogs-create-group : "true"
awslogs-group : aws_cloudwatch_log_group.cloudwatch_log_group_ecs.name
awslogs-region : "eu-west-1"
awslogs-stream-prefix : "twiggy"
}
},
secrets : [
{
"name" : "DISCORD_TOKEN",
"valueFrom" : aws_ssm_parameter.ssm_parameter_discord_token.arn
},
{
"name" : "ITAD_TOKEN",
"valueFrom" : aws_ssm_parameter.ssm_parameter_itad_token.arn
},
{
"name" : "TWITCH_SECRET",
"valueFrom" : aws_ssm_parameter.ssm_parameter_twitch_secret.arn
},
{
"name" : "TWITCH_CLIENT_ID",
"valueFrom" : aws_ssm_parameter.ssm_parameter_twitch_client_id.arn
},
{
"name" : "OPEN_WEATHER_TOKEN",
"valueFrom" : aws_ssm_parameter.ssm_parameter_open_weather_token.arn
}
]
}
])
}
36 changes: 36 additions & 0 deletions terraform/events.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
resource "aws_cloudwatch_event_rule" "cloudwatch_event_rule" {
name_prefix = "ecs-fargate-fallback-rule"
event_pattern = jsonencode({
source : [
"aws.ecs"
],
detail-type : [
"ECS Service Action"
],
resources : [
aws_ecs_service.ecs_primary_service.id
]
detail : {
eventName : ["SERVICE_STEADY_STATE", "SERVICE_TASK_PLACEMENT_FAILURE"],
}
})
}

resource "aws_cloudwatch_event_target" "cloudwatch_event_target" {
arn = aws_lambda_function.lambda_function.arn
rule = aws_cloudwatch_event_rule.cloudwatch_event_rule.id

input_transformer {
input_paths = {
event_name = "$.detail.eventName"
}
input_template = <<EOF
{
"cluster_arn": "${aws_ecs_cluster.ecs_cluster.arn}",
"primary_service_arn": "${aws_ecs_service.ecs_primary_service.id}",
"fallback_service_arn": "${aws_ecs_service.ecs_fallback_service.id}",
"event_name": "<event_name>"
}
EOF
}
}
69 changes: 69 additions & 0 deletions terraform/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
resource "aws_iam_role" "ecs_task_role" {
name = "twiggy-ecs_task_role"
path = "/service-role/"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy_ecs_tasks.json
inline_policy {
name = "ecs-exec"
policy = jsonencode({
Statement : [
{
Effect : "Allow",
Action : [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
Resource : "*"
}
]
})
}
}

resource "aws_iam_role" "ecs_task_execution_role" {
name = "twiggy-ecs_task_execution_role"
path = "/service-role/"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy_ecs_tasks.json
managed_policy_arns = [
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
]
inline_policy {
name = "systems-manager-parameter-store-access"
policy = jsonencode({
Statement : [
{
Effect : "Allow",
Action : "ssm:GetParameter",
Resource : "arn:aws:ssm:eu-west-1:866826529066:/twiggy/*"
}
]
})
}
}

resource "aws_iam_role" "lambda" {
name = "twiggy-lambda"
assume_role_policy = data.aws_iam_policy_document.assume_role_policy_lambda.json
managed_policy_arns = [
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
]
inline_policy {
name = "ecs-update-service"
policy = jsonencode({
Statement : [
{
Effect : "Allow",
Action : [
"ecs:DescribeServices",
"ecs:UpdateService",
],
Resource = [
aws_ecs_service.ecs_primary_service.id,
aws_ecs_service.ecs_fallback_service.id,
]
}
]
})
}
}
32 changes: 32 additions & 0 deletions terraform/lambda.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
import boto3


def handler(event, context):
logging.getLogger().setLevel(logging.INFO)
logging.info('Creating ECS client')
ecs_client = boto3.client('ecs')

print(event)

logging.info(f'Primary Service ARN: {event["primary_service_arn"]}')
logging.info(f'Fallback Service ARN: {event["fallback_service_arn"]}')

if event['event_name'] == 'SERVICE_TASK_PLACEMENT_FAILURE':
logging.info('Service task failed to be placed. Initiating fallback.')

logging.info(f'Describing {event["primary_service_arn"]} in cluster {event["primary_service_arn"]}')
result = ecs_client.describe_services(cluster=event['cluster_arn'], services=[event['primary_service_arn']])
primary_service_desired_count = result['services'][0]['desiredCount']

logging.info(f'Setting desired count of {event["fallback_service_arn"]} to {primary_service_desired_count}')
ecs_client.update_service(cluster=event['cluster_arn'], service=event['fallback_service_arn'], desiredCount=primary_service_desired_count)

elif event['event_name'] == 'SERVICE_STEADY_STATE':
logging.info('The primary service reached steady state.')

logging.info(f'Setting desired count of {event["fallback_service_arn"]} to 0')
ecs_client.update_service(cluster=event['cluster_arn'], service=event['fallback_service_arn'], desiredCount=0)

else:
logging.warning(f'Received an unsupported event {event["event_name"]}')
Loading
Loading