# -*- coding: utf-8 -*-
# SPDX-License-Identifier: MPL-2.0
# Copyright 2020-2021 John Mille <john@compose-x.io>
"""
Module to create the Launch Template for the hosts and the associated security group and
IAM Role (with Instance Profile).
The Launch Template uses CFN-INIT in order to bootstrap the machine at runtime, avoiding
any hardcoding, especially not for the ECS Service
These settings are all documented on AWS official documentation:
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-agent-config.html
"""
from troposphere import Base64, GetAtt, Join, Ref, Sub, Tags, cloudformation
from troposphere.ec2 import (
EBSBlockDevice,
IamInstanceProfile,
LaunchTemplate,
LaunchTemplateBlockDeviceMapping,
LaunchTemplateData,
Monitoring,
SecurityGroup,
TagSpecifications,
)
from troposphere.iam import InstanceProfile, Policy, Role
from ecs_composex.compute import compute_params
from ecs_composex.compute.compute_params import HOST_PROFILE_T, HOST_ROLE_T, NODES_SG_T
from ecs_composex.ecs.ecs_params import CLUSTER_NAME_T
from ecs_composex.iam import service_role_trust_policy
from ecs_composex.vpc import vpc_params
[docs]def add_hosts_profile(template):
"""
Adds role to the template
:parm template: EC2 Cluster template to add the role and profile to
:type template: troposphere.Template
:returns: troposphere IAM Role for EC2 hosts
:rtype: troposphere.iam.Role
"""
ecs_policy = Policy(
PolicyName="AllowEcsSpecific",
PolicyDocument={
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:RegisterContainerInstance",
"ecs:UpdateContainerInstancesState",
"ecs:DeregisterContainerInstance",
],
"Resource": [
Sub(
"arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:"
f"cluster/${{{CLUSTER_NAME_T}}}"
)
],
},
{
"Effect": "Allow",
"Action": [
"ecs:StartTelemetrySession",
"ecs:DiscoverPollEndpoint",
"ecs:Submit*",
"ecs:Poll",
],
"Resource": ["*"],
},
],
},
)
role = Role(
HOST_ROLE_T,
template=template,
AssumeRolePolicyDocument=service_role_trust_policy("ec2"),
ManagedPolicyArns=["arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"],
Policies=[ecs_policy],
)
InstanceProfile(
HOST_PROFILE_T,
template=template,
Roles=[Ref(role)],
)
return role
[docs]def add_hosts_security_group(template):
"""
Function to add a security group for the host
:parm template: EC2 Cluster template to add the SG to
:type template: troposphere.Template
:returns: troposphere IAM Role for EC2 hosts
:rtype: troposphere.ec2.SecurityGroup
"""
return SecurityGroup(
NODES_SG_T,
template=template,
GroupDescription=Sub(f"Group for hosts in ${{{CLUSTER_NAME_T}}}"),
VpcId=Ref(vpc_params.VPC_ID),
)
[docs]def add_launch_template(template, hosts_sg):
"""Function to create a launch template.
:param template: ECS Cluster template
:type template: troposphere.Template
:param hosts_sg: security group for the EC2 hosts
:type hosts_sg: troposphere.ec2.SecurityGroup
:return: launch_template
:rtype: troposphere.ec2.LaunchTemplate
"""
launch_template = LaunchTemplate(
"LaunchTemplate",
template=template,
Metadata=cloudformation.Metadata(
cloudformation.Init(
cloudformation.InitConfigSets(
default=[
"awspackages",
"dockerconfig",
"ecsconfig",
"awsservices",
]
),
awspackages=cloudformation.InitConfig(
packages={"yum": {"awslogs": [], "amazon-ssm-agent": []}},
commands={
"001-check-packages": {"command": "rpm -qa | grep amazon"},
"002-check-packages": {"command": "rpm -qa | grep aws"},
},
),
awsservices=cloudformation.InitConfig(
services={
"sysvinit": {
"amazon-ssm-agent": {
"enabled": True,
"ensureRunning": True,
}
}
}
),
dockerconfig=cloudformation.InitConfig(
commands={
"001-stop-docker": {"command": "systemctl stop docker"},
"098-reload-systemd": {"command": "systemctl daemon-reload"},
},
files={
"/etc/sysconfig/docker": {
"owner": "root",
"group": "root",
"mode": "644",
"content": Join(
"\n",
[
"DAEMON_MAXFILES=1048576",
Join(
" ",
["OPTIONS=--default-ulimit nofile=1024:4096"],
),
"DAEMON_PIDFILE_TIMEOUT=10",
"#EOF",
"",
],
),
}
},
services={
"sysvinit": {
"docker": {
"enabled": True,
"ensureRunning": True,
"files": ["/etc/sysconfig/docker"],
"commands": ["098-reload-systemd"],
}
}
},
),
ecsconfig=cloudformation.InitConfig(
files={
"/etc/ecs/ecs.config": {
"owner": "root",
"group": "root",
"mode": "644",
"content": Join(
"\n",
[
Sub(f"ECS_CLUSTER=${{{CLUSTER_NAME_T}}}"),
"ECS_ENABLE_TASK_IAM_ROLE=true",
"ECS_ENABLE_SPOT_INSTANCE_DRAINING=true",
"ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true",
"ECS_ENABLE_CONTAINER_METADATA=true",
"ECS_ENABLE_UNTRACKED_IMAGE_CLEANUP=true",
"ECS_UPDATES_ENABLED=true",
"ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION=15m",
"ECS_IMAGE_CLEANUP_INTERVAL=10m",
"ECS_NUM_IMAGES_DELETE_PER_CYCLE=100",
"ECS_ENABLE_TASK_ENI=true",
"ECS_AWSVPC_BLOCK_IMDS=true",
"ECS_TASK_METADATA_RPS_LIMIT=300,400",
"ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE=true",
'ECS_AVAILABLE_LOGGING_DRIVERS=["awslogs", "json-file"]',
"#EOF",
],
),
}
},
commands={
"0001-restartecs": {
"command": "systemctl --no-block restart ecs"
}
},
),
)
),
LaunchTemplateData=LaunchTemplateData(
BlockDeviceMappings=[
LaunchTemplateBlockDeviceMapping(
DeviceName="/dev/xvda",
Ebs=EBSBlockDevice(DeleteOnTermination=True, Encrypted=True),
)
],
ImageId=Ref(compute_params.ECS_AMI_ID),
InstanceInitiatedShutdownBehavior="terminate",
IamInstanceProfile=IamInstanceProfile(
Arn=Sub(f"${{{HOST_PROFILE_T}.Arn}}")
),
TagSpecifications=[
TagSpecifications(
ResourceType="instance",
Tags=Tags(
Name=Sub(f"EcsNodes-${{{CLUSTER_NAME_T}}}"),
StackName=Ref("AWS::StackName"),
StackId=Ref("AWS::StackId"),
),
)
],
InstanceType="m5a.large",
Monitoring=Monitoring(Enabled=True),
SecurityGroupIds=[GetAtt(hosts_sg, "GroupId")],
UserData=Base64(
Join(
"\n",
[
"#!/usr/bin/env bash",
"export PATH=$PATH:/opt/aws/bin",
"cfn-init -v || yum install aws-cfn-bootstrap -y",
Sub(
"cfn-init --region ${AWS::Region} -r LaunchTemplate -s ${AWS::StackName}"
),
"# EOF",
],
)
),
),
LaunchTemplateName=Ref(CLUSTER_NAME_T),
)
return launch_template
[docs]def add_hosts_resources(template):
"""Function to add the LaunchTemplate, SG and IAM Profile to go along with the ECS Cluster
:param template: the ecs_cluster template to add the hosts config to
:type template: troposphere.Template
:return: launch_template
:rtype: troposphere.ec2.LaunchTemplate
"""
hosts_sg = add_hosts_security_group(template)
add_hosts_profile(template)
launch_template = add_launch_template(template, hosts_sg)
return launch_template