AWSTemplateFormatVersion: '2010-09-09' Description: 'Bottlerocket ECS updater automation & resources' Parameters: ClusterName: Description: 'Name of ECS cluster to manage Bottlerocket instances in' Type: String Subnets: Description: 'List of VPC Subnet IDs where the updater should run. The subnets must have a route to the Internet via an Internet Gateway.' Type: List UpdaterImage: Description: 'Bottlerocket updater container image' Type: String Default: 'public.ecr.aws/bottlerocket/bottlerocket-ecs-updater:v0.2.2' LogGroupName: Description: 'Log group name for Bottlerocket updater logs' Type: String ScheduleState: Description: 'Schedule events rule state; allows disabling of scheduling' Type: String Default: 'ENABLED' Resources: ExecutionRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - 'ecs-tasks.amazonaws.com' Action: - 'sts:AssumeRole' Policies: - PolicyName: CreateLogGroupPolicy PolicyDocument: Version: '2012-10-17' Statement: # Allows creating log group if it does not exist - Effect: Allow Action: - 'logs:CreateLogGroup' Resource: - 'arn:aws:logs:*:*:*' Path: !Sub /${AWS::StackName}/ ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' TaskRole: Type: AWS::IAM::Role Properties: Description: 'Role allowing the Bottlerocket ECS Updater to manage Bottlerocket instances' Path: !Sub '/${AWS::StackName}/' AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: 'ecs-tasks.amazonaws.com' Action: - 'sts:AssumeRole' Policies: - PolicyName: 'BottlerocketEcsUpdaterPolicy' PolicyDocument: Version: 2012-10-17 Statement: # Allows listing all container instances in a cluster - Effect: Allow Action: - 'ecs:ListContainerInstances' Resource: - !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ClusterName}' # Allows describe container instances to get ec2 instance ID and ecs attributes to filter Bottlerocket instances # Allows list tasks to filter instances running standalone tasks # Allows update container instance state for draining # Allows describe tasks to identify tasks not started by service - Effect: Allow Action: - 'ecs:DescribeContainerInstances' - 'ecs:ListTasks' - 'ecs:UpdateContainerInstancesState' - 'ecs:DescribeTasks' Resource: '*' Condition: ArnEquals: ecs:cluster: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ClusterName}' # Allows ssm send command to make Bottlerocket update API calls - Effect: Allow Action: - 'ssm:SendCommand' Resource: - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${UpdateCheckCommand}" - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${UpdateApplyCommand}" - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:document/${RebootCommand}" - !Sub "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*" # Allows get command invocation to get Bottlerocket API calls output - Effect: Allow Action: - 'ssm:GetCommandInvocation' Resource: - !Sub "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:*" # Allows checking the EC2 instance state after an update occurs - Effect: Allow Action: - 'ec2:DescribeInstanceStatus' Resource: '*' UpdaterTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: NetworkMode: awsvpc RequiresCompatibilities: - FARGATE Cpu: "256" Memory: "0.5GB" ExecutionRoleArn: !GetAtt ExecutionRole.Arn TaskRoleArn: !GetAtt TaskRole.Arn ContainerDefinitions: - Name: BottlerocketEcsUpdaterService Image: !Ref UpdaterImage Command: - -cluster - !Ref ClusterName - -region - !Ref AWS::Region - -check-document - !Ref UpdateCheckCommand - -apply-document - !Ref UpdateApplyCommand - -reboot-document - !Ref RebootCommand LogConfiguration: LogDriver: awslogs Options: awslogs-create-group: 'true' awslogs-region: !Ref AWS::Region awslogs-group: !Ref LogGroupName awslogs-stream-prefix: !Sub '/ecs/bottlerocket-updater/${ClusterName}' BottlerocketUpdaterSchedule: Type: AWS::Events::Rule Properties: Description: "Check for Bottlerocket updates on a schedule" # Run Task every 12 hours ScheduleExpression: "rate(12 hours)" State: !Ref ScheduleState Targets: - Id: ecs-updater-fargate-task RoleArn: !GetAtt CronRole.Arn Arn: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ClusterName}' Input: !Sub | { "containerOverrides": [ { "name": "BottlerocketEcsUpdaterService", "environment": [ { "name" : "TASK_DEFINITION_ARN", "value": "${UpdaterTaskDefinition}" } ] } ] } EcsParameters: LaunchType: FARGATE TaskCount: 1 TaskDefinitionArn: !Ref UpdaterTaskDefinition NetworkConfiguration: AwsVpcConfiguration: # The Bottlerocket ECS Updater does not need a public IP for its operations. The public IP # is only required to pull images from ECR as a Fargate task AssignPublicIp: ENABLED Subnets: !Ref Subnets CronRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "events.amazonaws.com" Action: - "sts:AssumeRole" Path: !Sub '/${AWS::StackName}/' Policies: - PolicyName: "BottlerocketEcsUpdaterSchedulerPolicy" PolicyDocument: Statement: - Effect: "Allow" Condition: ArnEquals: ecs:cluster: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ClusterName}' Action: "ecs:RunTask" Resource: - !Ref UpdaterTaskDefinition - Effect: "Allow" Condition: ArnEquals: ecs:cluster: !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ClusterName}' Action: - "iam:PassRole" Resource: - !GetAtt TaskRole.Arn - !GetAtt ExecutionRole.Arn UpdateCheckCommand: Type: AWS::SSM::Document Properties: DocumentType: Command Content: schemaVersion: "2.2" description: "Bottlerocket - Check available updates" mainSteps: - action: "aws:runShellScript" name: "CheckUpdate" precondition: StringEquals: - platformType - Linux inputs: timeoutSeconds: '1800' runCommand: - "apiclient update check" UpdateApplyCommand: Type: AWS::SSM::Document Properties: DocumentType: Command Content: schemaVersion: "2.2" description: "Bottlerocket - Apply update" mainSteps: - action: "aws:runShellScript" name: "ApplyUpdate" precondition: StringEquals: - platformType - Linux inputs: timeoutSeconds: '1800' runCommand: - "apiclient update apply" RebootCommand: Type: AWS::SSM::Document Properties: DocumentType: Command Content: schemaVersion: "2.2" description: "Bottlerocket - Reboot" mainSteps: - action: "aws:runShellScript" name: "Reboot" precondition: StringEquals: - platformType - Linux inputs: timeoutSeconds: '1800' runCommand: - "apiclient reboot" Outputs: UpdaterTaskDefinitionArn: Description: 'Updater task definition ARN' Value: !Ref UpdaterTaskDefinition Export: Name: !Sub "${AWS::StackName}:UpdaterTaskDefinition"