Skip to content
This repository has been archived by the owner on Feb 12, 2024. It is now read-only.

Commit

Permalink
Merge pull request #26 from comtravo/chore/ecs-cluster
Browse files Browse the repository at this point in the history
Move ecs-cluster Terraform module to monorepo
  • Loading branch information
Puneeth-n committed Jan 26, 2022
2 parents 6818ef2 + aadace6 commit fc53e69
Show file tree
Hide file tree
Showing 6 changed files with 582 additions and 0 deletions.
44 changes: 44 additions & 0 deletions ecs-cluster/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Requirements

| Name | Version |
|------|---------|
| terraform | >= 0.12 |

## Providers

| Name | Version |
|------|---------|
| aws | n/a |
| null | n/a |
| template | n/a |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| aws\_ami | The AWS ami id to use | `string` | n/a | yes |
| cluster | The name of the cluster | `string` | n/a | yes |
| cluster\_attributes | The ECS atributes for the cluster | `map(string)` | `{}` | no |
| cluster\_uid | The name of the cluster | `string` | n/a | yes |
| custom\_userdata | Inject extra command in the instance template to be run on boot | `string` | `""` | no |
| depends\_id | Inter module dependency hack | `string` | n/a | yes |
| desired\_capacity | The desired capacity of the cluster | `number` | `1` | no |
| ebs\_root\_volume\_size | Size of the root volume in GB | `number` | n/a | yes |
| ecs\_logging | Adding logging option to ECS. | `list(string)` | <pre>[<br> "json-file",<br> "awslogs",<br> "gelf",<br> "syslog"<br>]</pre> | no |
| environment | The name of the environment | `string` | n/a | yes |
| fleet\_config | n/a | `map(string)` | n/a | yes |
| iam\_instance\_profile\_id | The id of the instance profile that should be used for the instances | `string` | n/a | yes |
| instance\_override | n/a | <pre>list(object({<br> instance_type = string<br> weighted_capacity = number<br> }))</pre> | n/a | yes |
| key\_name | SSH key name to be used | `string` | n/a | yes |
| launch\_template\_event\_emitter\_role | IAM role to assume while emitting launch template changes | `string` | n/a | yes |
| max\_size | Maximum size of the nodes in the cluster | `number` | `1` | no |
| min\_size | Minimum size of the nodes in the cluster | `number` | `10` | no |
| region | AWS\_REGION to emit cloudwatch event for updating launch configuration | `string` | n/a | yes |
| scale\_out | Scale out type. Defaults to MemoryReservation and threshold of 65% | `map(string)` | <pre>{<br> "adjustment": 1,<br> "cooldown": 300,<br> "evaluation_periods": 2,<br> "threshold": 65,<br> "type": "MemoryReservation"<br>}</pre> | no |
| security\_group\_ids | The list of security groups to place the instances in | `list(string)` | n/a | yes |
| subnet\_ids | The list of subnets to place the instances in | `list(string)` | n/a | yes |
| vpc\_id | The VPC id | `string` | n/a | yes |

## Outputs

No output.
154 changes: 154 additions & 0 deletions ecs-cluster/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
locals {
name = "${var.cluster}-${var.cluster_uid}"
}

resource "aws_launch_template" "ecs-lc" {
name_prefix = local.name
description = "ECS launch template for ${local.name}"
image_id = var.aws_ami
vpc_security_group_ids = var.security_group_ids
user_data = base64encode(data.template_file.user_data.rendered)
key_name = var.key_name
instance_type = var.instance_override[0].instance_type

iam_instance_profile {
name = var.iam_instance_profile_id
}

block_device_mappings {
device_name = "/dev/xvda"

ebs {
volume_type = "gp2"
volume_size = var.ebs_root_volume_size
delete_on_termination = true
}
}

disable_api_termination = false
instance_initiated_shutdown_behavior = "terminate"

lifecycle {
create_before_destroy = true
}

tag_specifications {
resource_type = "volume"

tags = {
Name = var.cluster
Environment = var.environment
}
}
}

resource "aws_autoscaling_group" "ecs-asg" {
name_prefix = local.name
max_size = var.max_size
min_size = var.min_size
desired_capacity = var.desired_capacity
force_delete = true
vpc_zone_identifier = var.subnet_ids
capacity_rebalance = true

mixed_instances_policy {
instances_distribution {
on_demand_percentage_above_base_capacity = var.fleet_config.on_demand_percentage_above_base_capacity
spot_max_price = lookup(var.fleet_config, "spot_max_price", "")
spot_allocation_strategy = "capacity-optimized"
spot_instance_pools = 0
}

launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.ecs-lc.id
version = aws_launch_template.ecs-lc.latest_version
}

dynamic "override" {
for_each = var.instance_override
content {
instance_type = override.value.instance_type
weighted_capacity = override.value.weighted_capacity
}
}
}
}

tag {
key = "Name"
value = var.cluster
propagate_at_launch = "true"
}

tag {
key = "Environment"
value = var.environment
propagate_at_launch = "true"
}

tag {
key = "DependsId"
value = var.depends_id
propagate_at_launch = "false"
}

lifecycle {
# prevent_destroy = true
# ignore_changes = [
# desired_capacity,
# max_size,
# ]
}
}

resource "aws_autoscaling_policy" "scale-out" {
name = "${local.name}-scale-out"
autoscaling_group_name = aws_autoscaling_group.ecs-asg.name
adjustment_type = "ChangeInCapacity"
policy_type = "SimpleScaling"
cooldown = var.scale_out.cooldown
scaling_adjustment = var.scale_out.adjustment
}

# Used magic formula
resource "aws_cloudwatch_metric_alarm" "scale-out" {
alarm_name = "${local.name}-scale-out"
namespace = "AWS/ECS"
metric_name = var.scale_out.type
threshold = var.scale_out.threshold
period = "60"
evaluation_periods = var.scale_out.evaluation_periods
statistic = "Average"
comparison_operator = "GreaterThanOrEqualToThreshold"

dimensions = {
ClusterName = var.cluster
}

alarm_description = "Scale out ECS cluster"
alarm_actions = [aws_autoscaling_policy.scale-out.arn]
}

data "template_file" "user_data" {
template = file("${path.module}/templates/user_data.sh")

vars = {
ecs_logging = jsonencode(var.ecs_logging)
cluster_name = var.cluster
attributes = jsonencode(var.cluster_attributes)
custom_userdata = var.custom_userdata
}
}

resource "null_resource" "launch-config-update" {
provisioner "local-exec" {
command = "AWS_METADATA_SERVICE_TIMEOUT=10 AWS_METADATA_SERVICE_NUM_ATTEMPTS=18 python3 ${path.module}/scripts/emit_launchconfig_event.py --role_to_assume ${var.launch_template_event_emitter_role} --environment ${var.environment} --region ${var.region} --launch_template_id ${aws_launch_template.ecs-lc.id} --launch_template_latest_version ${aws_launch_template.ecs-lc.latest_version} --autoscaling_group_name ${aws_autoscaling_group.ecs-asg.name} --cluster ${var.cluster}"
}

triggers = {
launchTemplateId = aws_launch_template.ecs-lc.id
launchTemplateVersion = aws_launch_template.ecs-lc.latest_version
}
}

136 changes: 136 additions & 0 deletions ecs-cluster/scripts/emit_launchconfig_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env python
"""
Emit terraform cloudwatch event on launch configuration changes
"""

import argparse
import boto3
import json
import logging as log
import os
import sys

ROOT_DIR = os.path.join(os.path.dirname(__file__), "../../../lib/")
sys.path.append(ROOT_DIR)

from assume_role import get_temporary_credentials
from functools import partial


def color(text, code='37', bold=False):
"""
colorful logging
"""
if bold:
code = "1;%s" % code
return "\033[%sm%s\033[0m" % (code, text)


red = partial(color, code='31')
green = partial(color, code='32')
yellow = partial(color, code='33')


def err(message):
"""
error logging
"""
log.error(red(message))
exit(1)


def info(message):
"""
info logging
"""
log.info(green(message))


def warn(message):
"""
warn logging
"""
log.warn(yellow(message))


def parse_cli():
"""
Parse CLI options
"""
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--role_to_assume', type=str, help='AWS IAM role to assume', required=True)
parser.add_argument('--region', type=str, help='AWS region', required=True)
parser.add_argument('--launch_template_id', type=str, help='AWS launch template id', required=True)
parser.add_argument('--launch_template_latest_version', type=str, help='AWS launch template version', required=True)
parser.add_argument('--autoscaling_group_name', type=str, help='AWS autoscaling group name', required=True)
parser.add_argument('--cluster', type=str, help='AWS ECS cluster name', required=True)
parser.add_argument('--environment', type=str, help='Environment we are dealing with', required=True)
parser.add_argument('--log_level', default='INFO', help='logging level', choices=['INFO', 'ERROR', 'WARN'])

return parser.parse_args()


def apply_cli_options(args):
"""
Apply CLI options
"""
log.basicConfig(level=args.log_level, format='[%(asctime)s] %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')

return args


def emit_cloudwatch_event(args):
"""
Emit cloudwatch event
"""

credentials = get_temporary_credentials(role_to_assume=args.role_to_assume,
region=args.region,
session_name=args.cluster + args.launch_template_id +
args.launch_template_latest_version)

client = boto3.client('events',
region_name=args.region,
aws_access_key_id=credentials["AccessKeyId"],
aws_secret_access_key=credentials["SecretAccessKey"],
aws_session_token=credentials["SessionToken"])

# AWS API resonses don't have consistent naming schema
# This event mimicks their response style
event = {
'Source':
'comtravo.terraform.{}'.format(args.environment),
'Resources': [args.launch_template_id, args.launch_template_latest_version],
'DetailType':
'ECS Launch Configuration Change',
'Detail':
json.dumps({
'autoscalingGroupName': args.autoscaling_group_name,
'clusterArn': args.cluster,
'agentConnected': False,
'status': 'ACTIVE'
})
}

try:
res = client.put_events(Entries=[event])
if res['FailedEntryCount'] != 0:
err('Error emitting cloudwatch event: {}. Got response: {}'.format(event, res))
else:
info('Event: {} emitted successfully!'.format(event))
info('Event emitted successfully! with response: {}'.format(res))
except Exception as _e:
err('Error emitting cloudwatch event {}. Got error {}'.format(event, _e))


def main():
"""
main
"""
args = parse_cli()
args = apply_cli_options(args)
emit_cloudwatch_event(args)


if __name__ == "__main__":
main()

0 comments on commit fc53e69

Please sign in to comment.