control-m-server.yaml
---

Control-M Linux Server Template

This CloudFormation template will deploy an Amazon Linux2 server with ssh permitted from limited networks, and a set of Control-M specific ports open to limited identified networks

AWSTemplateFormatVersion: '2010-09-09' Description: Control-M EnterpriseManager/Server Linux Instance

Parameters

These are the input parameters for this template. All of these parameters must be supplied for this template to be deployed.

Parameters: CMFoundationStack: Type: String Description: Name of the Control-M foundation stack. Default: Control-M-Foundation OracleRDSStack: Type: String Description: Name of the Oracle RDS stack. Default: Control-M-OracleRDS ServerName: Type: String Description: >- Enter the name of the host or service. Max 15 alphanumeric characters plus dash and underscore. The dash and underscore cannot be first or last in the name. AllowedPattern: '(^((?![-_])(?=.*[a-z].*)[a-z0-9-_]{1,20}(?<![-_]))$)' ConstraintDescription: Max 15 alphanumeric characters plus dash and underscore. The dash and underscore cannot be first or last in the name. CMSecondaryInstallDesired: Description: Install CTM Server and Enterprise Manager as secondary? Type: String Default: false AllowedValues: [false, true]

Default Operating System for EC2 instance.

OSType: Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 Description: Amazon Linux Latest AMI ID AllowedValues: - /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2

Default EC2 Instance Type for Application instances.

InstanceType: Description: >- EC2 Instance Type t3.medium (2vCPU x 4GB) t3.large (2vCPU x 8GB) t3.xlarge (4vCPU x 16GB) m5.large (2vCPU x 8GB) m5.xlarge (4vCPU x 16GB) Type: String Default: t3.xlarge AllowedValues: [t3.medium, t3.large, t3.xlarge, m5.large, m5.xlarge] RootVolumeSize: Description: Root Volume Size (/dev/xvda or /dev/nvme0n1) Type: Number MinValue: 20 MaxValue: 2048 Default: 60 FirstOptionalDataVolumeDesired: Description: Add Data Disk 1 (/dev/xvdb or /dev/nvme1n1)? Type: String Default: false AllowedValues: [false, true] FirstOptionalDataVolumeSize: Description: Data Disk 1 Volume Size (/dev/xvdb or /dev/nvme1n1) Type: Number MinValue: 10 MaxValue: 2048 Default: 10 SecondOptionalDataVolumeDesired: Description: Add Data Disk 2 (/dev/xvdc or /dev/nvme2n1)? Type: String Default: false AllowedValues: [false, true] SecondOptionalDataVolumeSize: Description: Data Disk 2 Volume Size (/dev/xvdc or /dev/nvme2n1) Type: Number MinValue: 10 MaxValue: 2048 Default: 10 KeyName: Description: Amazon EC2 Key Pair Type: AWS::EC2::KeyPair::KeyName AsgMaxSize: Description: >- Auto Scaling Group Max Inst Count [0-2]. Will be used for Desired Count too. Type: Number MinValue: 0 MaxValue: 2 Default: 1 CreatePrivateWebLoadBalancer: Description: Add Application Load Balancer? Type: String Default: false AllowedValues: [false, true] AlbSslCertificateARN: Type: String Description: Full ARN of the SSL Certificate to use on the load balancer (no default) AlbWebCIDR: Type: String Description: The CIDR range allowed to access the site. Default: 0.0.0.0/0 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: A properly formatted CIDR block (eg 10.23.45.0/24) AlbHostedZoneName: Type: String Description: route53 hosted zone name (lowercase only) Default: uits-prod-aws.arizona.edu AllowedPattern: '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$)' ConstraintDescription: A dot-delimited DNS zone name, permitting numbers, lowercase letters, and dashes AlbFQDNPrefix: Type: String Description: Fully Qualified Domain Prefix (lowercase only, eg mysite in mysite.mysubdomain.arizona.edu) Default: notavalidsitejustareallylongplaceholder0123456789 AllowedPattern: '(^((?!-)[a-z0-9-]{1,63}(?<!-))$)' ConstraintDescription: A domain prefix, permitting numbers, lowercase letters, and dashes, up to 63 characters long GroupName: Type: String Description: >- Enter the name of the LDAP Group of the owners. AllowedPattern: '(^((?![._-])[a-zA-Z0-9._-]{1,63}(?<![._-]))$)' Default: invalidplaceholdername DRS3BucketName: Type: String Description: >- Enter the name of the DR S3 Bucket. AllowedPattern: '(^((?![-])[a-z0-9-]{0,63}(?<![-]))$)'

Tags

The following tags are applied to all resources created by this template.

TagService: Type: String Description: Exact name of the Service as defined in the service catalog. Default: Control-M EnterpriseManager/Server TagEnvironment: Type: String Description: >- Used to distinguish between development, test, production,etc. environment types. AllowedValues: [dev, tst, prd] Default: tst TagContactNetID: Type: String Description: >- Used to identify the netid of the person most familiar with the usage of the resource. TagAccountNumber: Type: String Description: Identifies the financial system account number. TagTicketNumber: Type: String Description: >- Used to identify the Jira, Cherwell, or other ticketing system ticket number to link to more information about the need for the resource. TagBackupSLA: Type: String Description: >- Select Backup SLA from the following UITS Standard - Backup policy includes daily backups retained for one month and monthly backups retained for one year None - No backups performed AllowedValues: ["UITS Standard", "None"] Default: "UITS Standard"

Metadata

Metadata is mostly for organizing and presenting Parameters in a better way when using CloudFormation in the AWS Web UI.

Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: Environment Settings Parameters: - CMFoundationStack - OracleRDSStack - Label: default: Server Settings Parameters: - ServerName - OSType - InstanceType - RootVolumeSize - FirstOptionalDataVolumeDesired - FirstOptionalDataVolumeSize - SecondOptionalDataVolumeDesired - SecondOptionalDataVolumeSize - KeyName - AsgMaxSize - CreatePrivateWebLoadBalancer - AlbSslCertificateARN - AlbWebCIDR - AlbHostedZoneName - AlbFQDNPrefix - GroupName - DRS3BucketName - Label: default: Tagging Parameters: - TagService - TagEnvironment - TagContactNetID - TagAccountNumber - TagTicketNumber - TagBackupSLA ParameterLabels: TagEnvironment: default: 'Environment Type:' TagTicketNumber: default: 'Ticket Number:' TagContactNetID: default: 'Contact NetID:'

Conditions

Conditions: CreateFirstOptionalDataVolume: !Equals [true, !Ref FirstOptionalDataVolumeDesired] CreateSecondOptionalDataVolume: !Equals [true, !Ref SecondOptionalDataVolumeDesired] CreateLoadBalancer: !Equals [true, !Ref CreatePrivateWebLoadBalancer]

Resources

This is the EC2 instance deployed by the template.

Resources:

AWS Account Information

Lambda function to introspect VPCs, subnets, and select most available

AccountInfo: Type: Custom::AccountInfo Properties: ServiceToken: !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:fdn-cf-account-info" VPCInfo: - vpcid - public-subnet-a - public-subnet-b - private-subnet-a - private-subnet-b - choose-private-subnet - choose-public-subnet

EC2 Instance Launch Config

Launch Config to deploy the EC2 instance with some tags.

Ec2InstanceLc: Type: AWS::AutoScaling::LaunchConfiguration Properties: IamInstanceProfile: !Ref EnvInstanceProfile ImageId: !Ref OSType KeyName: !Ref KeyName InstanceType: !Ref InstanceType SecurityGroups: - Fn::ImportValue: !Sub "${CMFoundationStack}-ssh-sg" - Ref: InstanceSecurityGroup - Fn::ImportValue: !Sub "${CMFoundationStack}-cmem-sg" - Fn::ImportValue: !Sub "${CMFoundationStack}-cmserver-sg" BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: !Ref RootVolumeSize VolumeType: gp2 Encrypted: true - !If - CreateFirstOptionalDataVolume - DeviceName: /dev/xvdb Ebs: VolumeSize: !Ref FirstOptionalDataVolumeSize VolumeType: gp2 Encrypted: true - !Ref "AWS::NoValue" - !If - CreateSecondOptionalDataVolume - DeviceName: /dev/xvdc Ebs: VolumeSize: !Ref SecondOptionalDataVolumeSize VolumeType: gp2 Encrypted: true - !Ref "AWS::NoValue" UserData: Fn::Base64: !Sub - | #!/bin/bash export WORKINGPATH=/tmp/cftdeployscripts

Create temporary working folder if it doesn't already exist

if [ ! -d $WORKINGPATH ]; then mkdir -p $WORKINGPATH fi

Import CFT parameters into a local parameters file if they don't already exist

if [ ! -f $WORKINGPATH/cftdeploy-params ]; then cat <<EOFPARAMS > $WORKINGPATH/cftdeploy-params AlbHostedZoneName=${AlbHostedZoneName} ServerName=${ServerName} AWSRegion=${AWS::Region} GroupName=${GroupName} S3BucketName=${S3BucketName} EFSId=${EFSId} DBEndpoint=${DBEndpoint} DBPort=${DBPort} DBName=${DBName} CMSecondaryInstallDesired=${CMSecondaryInstallDesired} DRS3BucketName=${DRS3BucketName} WORKINGPATH=${!WORKINGPATH} SNS_NOTIFICATION_TOPIC=${SNS_NOTIFICATION_TOPIC} EOFPARAMS fi

Import the instance customization script from S3 if it doesn't already exist

if [ ! -f $WORKINGPATH/control-m-server.sh ]; then aws s3 sync s3://${S3BucketName}/scripts $WORKINGPATH/ chmod +x $WORKINGPATH/control-m-server.sh fi

Run the instance customization script

$WORKINGPATH/control-m-server.sh $WORKINGPATH/cftdeploy-params

Clean up on completion

rm -r $WORKINGPATH - DBEndpoint: Fn::ImportValue: !Sub "${OracleRDSStack}-Endpoint" DBPort: Fn::ImportValue: !Sub "${OracleRDSStack}-Port" DBName: Fn::ImportValue: !Sub "${OracleRDSStack}-DBName" S3BucketName: Fn::ImportValue: !Sub "${CMFoundationStack}-bucket" EFSId: Fn::ImportValue: !Sub "${CMFoundationStack}-fs-id" SNS_NOTIFICATION_TOPIC: Fn::ImportValue: fdn-logging-alarm-topic

Auto Scaling Group

Define the ASG for the Cluster Instances

Ec2InstanceAsg: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - !GetAtt AccountInfo.private-subnet-a - !GetAtt AccountInfo.private-subnet-b LaunchConfigurationName: !Ref Ec2InstanceLc MinSize: "0" MaxSize: !Ref AsgMaxSize DesiredCapacity: !Ref AsgMaxSize TargetGroupARNs: - !If - CreateLoadBalancer - !Ref AlbTargetGroup - !Ref AWS::NoValue MetricsCollection: - Granularity: 1Minute Tags: - Key: Name Value: !Sub Control-M-${ServerName} PropagateAtLaunch: true - Key: service Value: !Ref TagService PropagateAtLaunch: true - Key: environment Value: !Ref TagEnvironment PropagateAtLaunch: true - Key: contactnetid Value: !Ref TagContactNetID PropagateAtLaunch: true - Key: accountnumber Value: !Ref TagAccountNumber PropagateAtLaunch: true - Key: ticketnumber Value: !Ref TagTicketNumber PropagateAtLaunch: true - Key: BackupSLA Value: !Ref TagBackupSLA PropagateAtLaunch: true

Instance Security Group

Security group for the EC2 instance, that allows you to ssh into the instance TODO: Needs to be updated to permit passing this as a parameter.

InstanceSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Control-M instance members VpcId: !GetAtt AccountInfo.vpcid Tags: - Key: Name Value: !Sub Control-M-Inst-SG-${ServerName} - Key: service Value: !Ref TagService - Key: environment Value: !Ref TagEnvironment - Key: contactnetid Value: !Ref TagContactNetID - Key: accountnumber Value: !Ref TagAccountNumber - Key: ticketnumber Value: !Ref TagTicketNumber

EFS Security Group

Permit the instance access to the EFS export

EFSIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: Fn::ImportValue: !Sub "${CMFoundationStack}-efs-sg" IpProtocol: tcp FromPort: 2049 ToPort: 2049 SourceSecurityGroupId: !Ref InstanceSecurityGroup

SSH Security Group

Permit the instance ssh access to the other instances

SSHIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: Fn::ImportValue: !Sub "${CMFoundationStack}-ssh-sg" IpProtocol: tcp FromPort: 22 ToPort: 22 SourceSecurityGroupId: !Ref InstanceSecurityGroup Description: "Control-M self"

RDS Oracle DB Security Group

Permit the instance access to the RDS DB

OracleIngress: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: Fn::ImportValue: !Sub "${OracleRDSStack}-RDSSecurityGroupId" IpProtocol: tcp FromPort: Fn::ImportValue: !Sub "${OracleRDSStack}-Port" ToPort: Fn::ImportValue: !Sub "${OracleRDSStack}-Port" SourceSecurityGroupId: !Ref InstanceSecurityGroup

Control-M Enterprise Manager Security Group

Permit the ALB access to the Control-M EM ports on the instances

CMEMIngress1: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: Fn::ImportValue: !Sub "${CMFoundationStack}-cmem-sg" IpProtocol: tcp FromPort: 18080 ToPort: 18080 SourceSecurityGroupId: !Ref InstanceSecurityGroup Description: "HTTP From other instances" CMEMIngress2: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: Fn::ImportValue: !Sub "${CMFoundationStack}-cmem-sg" IpProtocol: tcp FromPort: 8443 ToPort: 8443 CidrIp: "0.0.0.0/0" Description: "HTTPS From Campus" CMEMIngress3: Type: AWS::EC2::SecurityGroupIngress Properties: GroupId: Fn::ImportValue: !Sub "${CMFoundationStack}-cmem-sg" IpProtocol: tcp FromPort: 19071 ToPort: 19092 SourceSecurityGroupId: !Ref InstanceSecurityGroup Description: "EM and Apache Kafka from other instances"

Instance Policy

This is the IAM policythat will be attached to the instance's IAM role. Any AWS specific permissions that the node might need should be defined here. TODO: Determine what policy is needed

InstancePolicy: Type: AWS::IAM::ManagedPolicy Properties: ManagedPolicyName: !Ref ServerName Description: Access to SSM parameters for deployment PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'ssm:DescribeParameters' Resource: - '*' - Effect: Allow Action: - 'ssm:GetParameter' Resource: - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/ad_join/*" - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/agents/*" - Effect: Allow Action: - 'ec2:Describe*' Resource: - '*' - Effect: Allow Action: - s3:GetObject - s3:ListBucket Resource: !Sub - "arn:aws:s3:::${S3Bucket}*" - S3Bucket: Fn::ImportValue: !Sub "${CMFoundationStack}-bucket" - Effect: Allow Action: - route53:ListHostedZones - route53:ListHostedZonesByName - route53:ChangeResourceRecordSets Resource: - "*" - Effect: Allow Action: - sns:publish Resource: - !ImportValue fdn-logging-alarm-topic Roles: - !Ref EnvInstanceRole

Instance Role

This is the IAM role that will be applied to the EC2 Instance. Any AWS specific permissions that the node might need should be defined here.

EnvInstanceRole: Type: AWS::IAM::Role Properties: RoleName: !Ref ServerName AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore #- arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy

Instance Profile

This is just a little construct to connect a set of roles together into a profile. The profile is referenced by the EC2 Instance.

EnvInstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" Roles: - !Ref EnvInstanceRole

Instance Route53 records

This creates placeholder Route53 records that are populated at boot time by the instance itself

InstanceRoute53RecordSetGroup: Type: AWS::Route53::RecordSetGroup Properties: Comment: Creating records Control-M Server instances HostedZoneName: !Sub "${AlbHostedZoneName}." RecordSets: - Name: !Sub "${ServerName}-a.${AlbHostedZoneName}." Type: A TTL: "200" ResourceRecords: - 127.0.0.0 - Name: !Sub "${ServerName}-b.${AlbHostedZoneName}." Type: A TTL: "200" ResourceRecords: - 127.0.0.0

Load Balancer

The load balancer (ALB) constructor along with the Security Group that allows client traffic to the ALB on ports 80 & 443

AlbLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Condition: CreateLoadBalancer Properties: Name: !Sub ALB-${ServerName} Scheme: internal Subnets: - !GetAtt AccountInfo.private-subnet-a - !GetAtt AccountInfo.private-subnet-b LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '50' SecurityGroups: - Ref: AlbSecurityGroup Tags: - Key: service Value: !Sub ${TagService}-ALB - Key: environment Value: !Ref TagEnvironment - Key: contactnetid Value: !Ref TagContactNetID - Key: accountnumber Value: !Ref TagAccountNumber - Key: ticketnumber Value: !Ref TagTicketNumber AlbSecurityGroup: Type: AWS::EC2::SecurityGroup Condition: CreateLoadBalancer Properties: GroupDescription: 'Allow internal web traffic to load balancer' VpcId: !GetAtt AccountInfo.vpcid SecurityGroupIngress: - CidrIp: !Ref AlbWebCIDR IpProtocol: tcp FromPort: 18080 ToPort: 18080 - CidrIp: !Ref AlbWebCIDR IpProtocol: tcp FromPort: 8443 ToPort: 8443 Tags: - Key: Name Value: !Sub Control-M-ALB-Web-SG-${ServerName} - Key: service Value: !Ref TagService - Key: environment Value: !Ref TagEnvironment - Key: contactnetid Value: !Ref TagContactNetID - Key: accountnumber Value: !Ref TagAccountNumber - Key: ticketnumber Value: !Ref TagTicketNumber

Target Group

Define the Target Group for adding Instances to the ALB as well as the health checks for those Instances

AlbTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Condition: CreateLoadBalancer Properties: HealthCheckIntervalSeconds: 30 HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 UnhealthyThresholdCount: 3 HealthCheckPath: / Matcher: HttpCode: "200-399" Name: !Sub ALB-TG-${ServerName} Port: 8443 Protocol: HTTPS VpcId: !GetAtt AccountInfo.vpcid Tags: - Key: service Value: !Ref TagService - Key: environment Value: !Ref TagEnvironment - Key: contactnetid Value: !Ref TagContactNetID - Key: accountnumber Value: !Ref TagAccountNumber - Key: ticketnumber Value: !Ref TagTicketNumber

ALB Listeners definitions

AlbListener18080: Type: AWS::ElasticLoadBalancingV2::Listener Condition: CreateLoadBalancer Properties: DefaultActions: - Type: redirect RedirectConfig: Protocol: HTTPS Port: "8443" Host: '#{host}' Path: '/#{path}' Query: '#{query}' StatusCode: HTTP_301 LoadBalancerArn: Ref: AlbLoadBalancer Port: 18080 Protocol: HTTP AlbListener8443: Type: AWS::ElasticLoadBalancingV2::Listener Condition: CreateLoadBalancer Properties: DefaultActions: - Type: forward TargetGroupArn: Ref: AlbTargetGroup LoadBalancerArn: Ref: AlbLoadBalancer Port: 8443 Protocol: HTTPS Certificates: - CertificateArn: !Ref AlbSslCertificateARN

ALB Route53 DNS record

EnvDnsRecord: Type: AWS::Route53::RecordSet Condition: CreateLoadBalancer Properties: HostedZoneName: !Sub "${AlbHostedZoneName}." Name: !Sub "${AlbFQDNPrefix}.${AlbHostedZoneName}." Type: "CNAME" TTL: 200 ResourceRecords: - !GetAtt AlbLoadBalancer.DNSName

Outputs

TODO: Determine what other outputs back to the caller will be needed.

Outputs: SshSecurityGroup: Description: "Instance Security Group" Value: !Ref InstanceSecurityGroup Export: Name: !Sub "${AWS::StackName}-inst-sg" AsgID: Description: "AutoScalingGroup ID" Value: !Ref Ec2InstanceAsg Export: Name: !Sub "${AWS::StackName}-asg-id" AlbSiteFQDN: Condition: CreateLoadBalancer Description: "FQDN associated with the ALB" Value: !Ref EnvDnsRecord Export: Name: !Sub "${AWS::StackName}-site-fqdn" CTMServerName: Description: "CTMServer Base Name" Value: !Ref ServerName Export: Name: !Sub "${AWS::StackName}-ctmserver-base" CTMDomainName: Description: "CTMServer Domain Name" Value: !Ref AlbHostedZoneName Export: Name: !Sub "${AWS::StackName}-ctmserver-domain"