This repository has been archived by the owner on Feb 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from comtravo/chore/ecs-cluster
Move ecs-cluster Terraform module to monorepo
- Loading branch information
Showing
6 changed files
with
582 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.