AWSTemplateFormatVersion: '2010-09-09' Description: This template sets up a VPC and ec2 instance for the purposes of demonstrating connecting to bastion instance in public subnet using EC2 Instance Connect. (qs-1pri98mak) Parameters: LatestAmiId: Type: 'AWS::SSM::Parameter::Value' Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' AvailabilityZones: Description: List of Availability Zones to use for the subnets in the VPC. Only two Availability Zones are used for this deployment, and the logical order of your selections is preserved. Type: List RemoteAccessCIDR: AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$ ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/x Description: CIDR IP range that is permitted to access the bastions. We recommend that you set this value to a trusted IP range. Type: String Mappings: DefaultConfiguration: MachineConfiguration: EC2InstanceType: t3.micro NetworkConfiguration: VPCCIDR: 10.0.0.0/16 PublicSubnet1CIDR: 10.0.128.0/20 PublicSubnet2CIDR: 10.0.144.0/20 Resources: EC2InstanceConnectRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: AWS: - Fn::Sub: arn:aws:iam::${AWS::AccountId}:root Action: "sts:AssumeRole" Path: "/" ManagedPolicyArns: - !Ref EC2InstanceConnectCLIPolicy EC2InstanceConnectSSHPolicy: Type: AWS::IAM::ManagedPolicy Properties: Description: Policy to use SSH client to connect to an instance using EC2 Instance Connect PolicyDocument: Version: "2012-10-17" Statement: - Action: - ec2-instance-connect:SendSSHPublicKey Effect: Allow Resource: Fn::Sub: arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/${BastionInstance} Condition: StringEquals: ec2:osuser: "ec2-user" EC2InstanceConnectCLIPolicy: Type: AWS::IAM::ManagedPolicy Properties: Description: Policy to connect to an instance using EC2 Instance Connect CLI PolicyDocument: Version: "2012-10-17" Statement: - Action: - ec2-instance-connect:SendSSHPublicKey Effect: Allow Resource: Fn::Sub: arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/${BastionInstance} Condition: StringEquals: ec2:osuser: "ec2-user" - Action: - ec2:DescribeInstances Effect: Allow Resource: "*" VPCStack: Type: AWS::CloudFormation::Stack Properties: TemplateURL: Fn::Sub: https://aws-quickstart.s3.amazonaws.com/quickstart-aws-vpc/templates/aws-vpc.template Parameters: AvailabilityZones: Fn::Join: - ',' - Ref: AvailabilityZones NumberOfAZs: '2' CreatePrivateSubnets: 'false' PublicSubnet1CIDR: !FindInMap - DefaultConfiguration - NetworkConfiguration - PublicSubnet1CIDR PublicSubnet2CIDR: !FindInMap - DefaultConfiguration - NetworkConfiguration - PublicSubnet2CIDR VPCCIDR: !FindInMap - DefaultConfiguration - NetworkConfiguration - VPCCIDR BastionSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enables SSH Access to Bastion Hosts VpcId: Fn::GetAtt: - VPCStack - Outputs.VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: !Ref 'RemoteAccessCIDR' - IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: !GetAtt FetchIPRange.ip_range - IpProtocol: icmp FromPort: '-1' ToPort: '-1' CidrIp: !Ref 'RemoteAccessCIDR' BastionInstance: Type: AWS::EC2::Instance Properties: ImageId: !Ref LatestAmiId InstanceType: !FindInMap - DefaultConfiguration - MachineConfiguration - EC2InstanceType SecurityGroupIds: - !Ref BastionSecurityGroup SubnetId: Fn::GetAtt: - VPCStack - Outputs.PublicSubnet1ID Tags: - Key: Name Value: EC2-Instance-Connect-Demo UserData: Fn::Base64: !Sub | #!/bin/bash sudo yum install -y ec2-instance-connect BasicLambdaExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: ["lambda.amazonaws.com"] Action: "sts:AssumeRole" Path: "/" Policies: - PolicyName: "lambda_policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" FetchIPRangeLambda: Type: "AWS::Lambda::Function" Properties: Code: ZipFile: | import urllib import urllib.request import json import os import cfnresponse def get_url_content(url): try: template_raw_data = urllib.request.urlopen(url).read().decode('utf-8') return template_raw_data.strip() except: return "ERROR" def get_ip_range(service, region): ip_ranges_url = os.environ['ip_ranges_url'] ip_ranges_text = get_url_content(ip_ranges_url) ip_ranges_json = json.loads(ip_ranges_text) prefix_dict = ip_ranges_json["prefixes"] connect_prefix = [x for x in prefix_dict if x['service'] == service and x['region'] == region] return connect_prefix[0]['ip_prefix'] def handler(event, context): print('Received event: %s' % json.dumps(event)) status = cfnresponse.SUCCESS try: if event['RequestType'] == 'Delete': cfnresponse.send(event, context, status, {}) else: region = event['ResourceProperties']['Region'] responseData = {} responseData['ip_range'] = get_ip_range('EC2_INSTANCE_CONNECT', region) cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) except Exception as e: logging.error('Exception: %s' % e, exc_info=True) status = cfnresponse.FAILED cfnresponse.send(event, context, status, {}, None) Environment: Variables: ip_ranges_url: 'https://ip-ranges.amazonaws.com/ip-ranges.json' Handler: "index.handler" Runtime: "python3.6" Timeout: "30" Role: !GetAtt BasicLambdaExecutionRole.Arn FetchIPRange: Type: "Custom::GenerateCronExpression" Version: "1.0" Properties: ServiceToken: !GetAtt FetchIPRangeLambda.Arn Region: !Ref AWS::Region Outputs: BastionInstance: Description: The instance ID of the demo bastion instance. Value: !Ref BastionInstance BastionDNSName: Description: The DNS name of the bastion instance. Value: !GetAtt BastionInstance.PublicDnsName DeployAZ: Description: The Availability Zone where the bastion instance is launched. Value: !GetAtt BastionInstance.AvailabilityZone BastionSecurityGroup: Description: Bastion Security Group ID Value: !Ref BastionSecurityGroup EC2InstanceConnectRole: Description: The ARN of the EC2 Instance Connect Role Value: !Ref EC2InstanceConnectRole