Source code for ecs_composex.compute.hosts_template

#  -*- 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