AWSTemplateFormatVersion: 2010-09-09 Description: >- This client template deploys a RHEL client instance in an existing VPC. This client connects to the RHEL local mirror instance. **WARNING** This template creates EC2 instances and related resources. You will be billed for the AWS resources used if you create a stack from this template. (qs-1t47u37lm) Metadata: cfn-lint: config: ignore_checks: - W9006 AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Network configuration Parameters: - VPCID - VPCCIDR - PrivateSubnet1ID - Label: default: Amazon EC2 configuration Parameters: - KeyPairName - WorkloadInstanceType - Label: default: Local mirror configuration Parameters: - LocalMirrorPrivateIp ParameterLabels: KeyPairName: default: SSH key name PrivateSubnet1ID: default: Private subnet 1 ID VPCID: default: VPC ID VPCCIDR: default: VPC CIDR WorkloadInstanceType: default: Workload server instance type LocalMirrorPrivateIp: default: Local mirror private IP Parameters: KeyPairName: Description: Name of an existing EC2 key pair. All instances will launch with this key pair. Type: AWS::EC2::KeyPair::KeyName PrivateSubnet1ID: Description: ID of private subnet 1 in Availability Zone 1 for the workload (e.g., subnet-a0246dcd). Type: AWS::EC2::Subnet::Id VPCID: Description: ID of your existing VPC for deployment. Type: AWS::EC2::VPC::Id VPCCIDR: 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])(\/(1[6-9]|2[0-8]))$ ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/16-28 Default: 10.0.0.0/16 Description: CIDR block for the VPC. Type: String WorkloadInstanceType: AllowedValues: # x86_64 allowed instance types - t2.micro - t2.small - t2.medium - t3.micro - t3.small - t3.medium ConstraintDescription: Must contain valid instance type Default: t2.micro Description: Type of EC2 instance for the client instance. Type: String LocalMirrorPrivateIp: AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}) ConstraintDescription: Must be a valid IP address Description: The Private IP address of the local mirror Type: String Rules: KeyPairsNotEmpty: Assertions: - Assert: Fn::Not: - Fn::EachMemberEquals: - Fn::RefAll: AWS::EC2::KeyPair::KeyName - '' AssertDescription: All key pair parameters must not be empty SubnetsInVPC: Assertions: - Assert: Fn::EachMemberIn: - Fn::ValueOfAll: - AWS::EC2::Subnet::Id - VpcId - Fn::RefAll: AWS::EC2::VPC::Id AssertDescription: All subnets must in the VPC Resources: Instance: Type: AWS::EC2::Instance Properties: IamInstanceProfile: !Ref InstanceProfile ImageId: !GetAtt Rhel8AMIInfo.Id InstanceType: !Ref WorkloadInstanceType KeyName: !Ref KeyPairName SecurityGroupIds: - !Ref InstanceSecurityGroup SubnetId: !Ref PrivateSubnet1ID BlockDeviceMappings: - DeviceName: /dev/sda1 Ebs: Encrypted: true VolumeSize: 10 VolumeType: gp3 DeleteOnTermination: true UserData: Fn::Base64: !Sub | #!/bin/bash -xe dnf update -y dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm systemctl enable amazon-ssm-agent systemctl start amazon-ssm-agent # Disable default repositories dnf config-manager --set-disabled ansible-2-for-rhel-8-rhui-rpms dnf config-manager --set-disabled rhel-8-appstream-rhui-rpms dnf config-manager --set-disabled rhel-8-baseos-rhui-rpms dnf config-manager --set-disabled rhui-client-config-server-8 # Configure refs to local mirror cat <<"EOF" > /etc/yum.repos.d/localmirror.repo [local-mirror-ansible-2-for-rhel-8-rhui-rpms] name=local-mirror-ansible-2-for-rhel-8-rhui-rpms baseurl=http://${LocalMirrorPrivateIp}/localmirror/ansible-2-for-rhel-8-rhui-rpms/ gpgcheck=0 enabled=1 [local-mirror-rhel-8-appstream-rhui-rpms] name=local-mirror-rhel-8-appstream-rhui-rpms baseurl=http://${LocalMirrorPrivateIp}/localmirror/rhel-8-appstream-rhui-rpms/ gpgcheck=0 enabled=1 [local-mirror-rhel-8-baseos-rhui-rpms] name=local-mirror-rhel-8-baseos-rhui-rpms baseurl=http://${LocalMirrorPrivateIp}/localmirror/rhel-8-baseos-rhui-rpms/ gpgcheck=0 enabled=1 [local-mirror-rhui-client-config-server-8] name=local-mirror-rhui-client-config-server-8 baseurl=http://${LocalMirrorPrivateIp}/localmirror/rhui-client-config-server-8/ gpgcheck=0 enabled=1 EOF Tags: - Key: Name Value: rhel8-local-mirror-client InstanceIAMRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Sid: AssumeRole Action: - sts:AssumeRole Effect: Allow Principal: Service: ec2.amazonaws.com ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - !Ref InstanceIAMRole InstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Metadata: cfn_nag: rules_to_suppress: - id: W5 reason: VPC Endpoints not used to reduce cost. Egress on 443 to 0.0.0.0/0 used for Session Manager Connection Properties: GroupDescription: Allow access to the Workload instances VpcId: !Ref VPCID SecurityGroupEgress: - Description: HTTP traffic from the VPC IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: !Ref VPCCIDR - Description: HTTPS traffic from the EC2 instance. For Session Manager connection IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: "0.0.0.0/0" Rhel8AMIInfo: Type: Custom::Rhel8AMIInfo Properties: ServiceToken: !GetAtt AMIInfoFunction.Arn Region: !Ref AWS::Region AMIName: RHEL-8.4.0_HVM* Architecture: x86_64 Owner: "309956199498" AMIInfoFunction: Type: AWS::Lambda::Function Metadata: cfn_nag: rules_to_suppress: - id: W89 reason: This is not required for this use-case, adding VPC will have additional complexity. This Lambda only retrieves AMI IDs and does not process sensitive information. - id: W92 reason: There is no need for Reserved Concurrent Executions. Properties: Role: !GetAtt AMIInfoFunctionLambdaExecutionRole.Arn Timeout: 30 Handler: index.lambda_handler Runtime: python3.10 MemorySize: 128 Description: Get AMI ID for the latest RHEL8 image Environment: Variables: LOG_LEVEL: INFO Code: ZipFile: | import logging import os import boto3 import cfnresponse ec2_client = boto3.client('ec2') LOG_LEVEL = os.getenv('LOG_LEVEL') def lambda_handler(event, context): global log_level log_level = str(LOG_LEVEL).upper() if log_level not in { 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' }: log_level = 'ERROR' logging.getLogger().setLevel(log_level) # For Delete requests, immediately send a SUCCESS response. if event['RequestType'] == 'Delete': cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) return try: # Get AMI IDs with the specified name pattern and owner describe_images_result = ec2_client.describe_images( Filters = [ { 'Name': 'name', 'Values': [event['ResourceProperties']['AMIName']] }, { 'Name': 'architecture', 'Values': [event['ResourceProperties']['Architecture']] } ], Owners = [event['ResourceProperties']['Owner']] ) images = describe_images_result['Images'] # Sort images by name in descending order. The names contain the AMI version, formatted as YYYY.MM.Ver. images.sort(key=lambda x: x['CreationDate'], reverse=True) for image in images: if is_beta(image['Name']): continue response_data = { 'Id': image['ImageId'] } cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) return except Exception as e: logging.error(e) response_data = {'Error': 'DescribeImages call failed'} cfnresponse.send(event, context, cfnresponse.FAILED, response_data) return # Check if the image is a beta or rc image. The Lambda function won't return any of those images. def is_beta(image_name): return 'beta' in image_name.lower() or '.rc' in image_name.lower() AMIInfoFunctionLambdaExecutionRole: Type: AWS::IAM::Role Metadata: cfn_nag: rules_to_suppress: - id: W11 reason: "* required per EC2 documentation" Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub arn:${AWS::Partition}:logs:*:*:* - Effect: Allow Action: - ec2:DescribeImages Resource: "*" Outputs: InstancePrivateIp: Description: Private IP of RHEL Client Value: !GetAtt Instance.PrivateIp