toolshed-hosts.yaml
---

Toolshed Application Hosts CloudFormation Deployment

This CloudFormation template will deploy two EC2 instances to act as the application hosts for the Toolshed service.

Important

There are many configuration files that must be loaded from the mounted NFS volume. If you are bootstrapping a new service and the NFS volume is empty, this deployment will fail the first time. Once the NFS volume has the required files in it this deployment will work.

AWSTemplateFormatVersion: "2010-09-09" Description: "Toolshed Application Hosts"

Parameters

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

Parameters:

Name of the Toolshed Base CloudFormation Stack.

BaseStackName: Type: String Description: "Toolshed Base Stack Name" Default: "toolshed-base"

Launch Template Version

LaunchTemplateVersion: Description: Launch Template Version Type: String Default: 1

Launch Template Version

BlueGreen: Description: Blue or Green Deployment Type: String AllowedValues: - "Blue" - "Green"

SSH Key Pair to be used on the application EC2 instances for emergency administrative access.

KeyName: Description: Amazon EC2 Key Pair Type: AWS::EC2::KeyPair::KeyName

Get the latest Amazon Linux AMI ID from the SSM Parameter Store

AmazonLinuxAmi: Type : AWS::SSM::Parameter::Value<String> Default: /aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2 Description: Amazon Linux Latest AMI ID AllowedValues: - /aws/service/ami-amazon-linux-latest/amzn-ami-hvm-x86_64-gp2

Load Balancer Settings

LBSubnets: Type: List<AWS::EC2::Subnet::Id> Description: public subnets for load balancer (2 or more) HostedZoneName: Type: String Description: Route53 Hosted Zone Name Default: "uits-prod-aws.arizona.edu" SSLCertificateARN: Type: String Description: Full ARN of the SSL Certificate to use on the load balancer Default: "arn:aws:acm:us-west-2:760232551367:certificate/8313253d-3f85-4cef-a6ef-ae66fd4f7322" DRS3Bucket: Description: 'Disaster Recovery Bucket Name' Default: "edu-arizona-east-1-toolshed-backup" Type: String

Tags

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

ServiceTag: Type: String Description: Exact name of the Service as defined in the service catalog. EnvironmentTag: Type: String Description: Used to distinguish between development, test, production,etc. environment types. AllowedValues: [dev, tst, prd, trn, stg, cfg, sup, rpt] Default: dev ContactNetidTag: Type: String Description: Used to identify the netid of the person most familiar with the usage of the resource. AccountNumberTag: Type: String Description: Identifies the financial system account number. TicketNumberTag: 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.

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: Instance Settings Parameters: - KeyName - LaunchTemplateVersion - BaseStackName - DRS3Bucket - Label: default: Load Balancer Settings Parameters: - LBSubnets - HostedZoneName - SSLCertificateARN - Label: default: Tags Parameters: - ServiceTag - EnvironmentTag - ContactNetidTag - AccountNumberTag - TicketNumberTag

Resources

This is the EC2 instance deployed by the template.

Resources:

Toolshed Autoscaling Group

Create an autoscaling group to manage the toolshed EC2 instances

InstanceAsg: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: - Fn::ImportValue: !Sub "${BaseStackName}-privsubnet-a" - Fn::ImportValue: !Sub "${BaseStackName}-privsubnet-b" LaunchTemplate: LaunchTemplateId: !Ref Ec2InstanceLaunchTemplate Version: !Ref LaunchTemplateVersion MinSize: 0 MaxSize: 1 DesiredCapacity: 1 TargetGroupARNs: - !Ref AlbTargetGroup Tags: - Key: Name Value: !Ref AWS::StackName PropagateAtLaunch: True - Key: service Value: !Ref ServiceTag PropagateAtLaunch: True - Key: environment Value: !Ref EnvironmentTag PropagateAtLaunch: True - Key: contactnetid Value: !Ref ContactNetidTag PropagateAtLaunch: True - Key: accountnumber Value: !Ref AccountNumberTag PropagateAtLaunch: True - Key: ticketnumber Value: !Ref TicketNumberTag PropagateAtLaunch: True

Launch Template

Create an EC2 Launch Template for toolshed hosts

Ec2InstanceLaunchTemplate: Type: "AWS::EC2::LaunchTemplate" Properties: LaunchTemplateName: !Sub "${AWS::StackName}-launchtemplate" LaunchTemplateData: ImageId: !Ref AmazonLinuxAmi KeyName: !Ref KeyName InstanceType: t2.micro IamInstanceProfile: Name: !Ref EnvInstanceProfile NetworkInterfaces: - AssociatePublicIpAddress: "false" DeviceIndex: "0" Groups: - Fn::ImportValue: !Sub "${BaseStackName}-ec2-sg" - Fn::ImportValue: !Sub "${BaseStackName}-efs-target-sg" TagSpecifications: - ResourceType: volume Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag UserData: Fn::Base64: Fn::Sub: - | #!/bin/bash -e

Basic Updates

sudo yum update -y sudo yum install -y git vim telnet

Install NFS bits

sudo yum install -y nfs-utils

Make our shared WWW directory for the EFS to mount at

sudo mkdir -p /var/www/shared sudo mkdir -p /var/www/etchttpd

Add EFS volume to fstab and mount the volumes

echo "${EFSID}.efs.${AWS::Region}.amazonaws.com:/etchttpd /var/www/etchttpd nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,nosharecache,context="system_u:object_r:httpd_config_t:s0" 0 0" >> /etc/fstab echo "${EFSID}.efs.${AWS::Region}.amazonaws.com:/sites /var/www/shared nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,nosharecache,context="system_u:object_r:httpd_sys_content_t:s0" 0 0" >> /etc/fstab mount -a -t nfs4

Install and configure CloudWatch Logs

sudo yum install -y awslogs

Overwrite default awscli.conf

cat << EOF > /etc/awslogs/awscli.conf [plugins] cwlogs = cwlogs [default] region = us-west-2 EOF

Overwrite default awslogs.conf

cat << EOF > /etc/awslogs/awslogs.conf [general] state_file = /var/lib/awslogs/agent-state buffer_duration = 5000 initial_position = start_of_file [/var/log/messages] datetime_format = %b %d %H:%M:%S file = /var/log/messages log_group_name = toolshed log_stream_name = {instance_id}-{hostname}-messages [/var/log/php_errors.log] datetime_format = [%d-%b-%Y %H:%M:%S file = /var/log/php_errors.log log_group_name = toolshed log_stream_name = {instance_id}-{hostname}-php_errors.log multi_line_start_pattern = {datetime_format} EOF

Turn on awslogs

sudo chkconfig awslogs on sudo service awslogs start

Install memcache and configure it

sudo yum install -y memcached sudo chkconfig memcached on sudo service memcached start

Install Python 3 and required modules

sudo yum install -y python36

Python memcache module

sudo /usr/bin/pip-3.6 install pymemcache

Python Box SDK module

sudo /usr/bin/pip-3.6 install "boxsdk>=1.5,<2.0" sudo /usr/bin/pip-3.6 install boxsdk[jwt]

Create a log folder for boxsync

sudo mkdir /var/log/boxsync sudo chown ec2-user:ec2-user /var/log/boxsync

Logrotate for boxsync

cat << EOF > /etc/logrotate.d/boxsync /var/log/boxsync/boxsync.log { daily rotate 10 delaycompress compress notifempty missingok } EOF

Raise the limit on number of open files so boxsync doesn't run into problems with too many sockets open. UITSAPPDEV-2

cat << EOF >> /etc/security/limits.conf ec2-user soft nofile 32768 ec2-user hard nofile 32768 EOF

Copy incommon certificates over for boxsync

sudo cp /var/www/etchttpd/ca-trust/* /etc/pki/ca-trust/source/anchors/

Install PHP and required packages (this also installs httpd)

sudo yum install -y php56 \ php56-devel \ php56-gd \ php56-ldap \ php56-mbstring \ php56-mysqlnd \ php56-pdo \ php56-soap \ php56-xml \ php56-xmlrpc \ php56-pecl-apcu \ php56-pecl-ssh2 \ php56-pecl-memcached.x86_64 \ libssh2-devel \ fcgi \ mod24_fcgid \ mariadb-devel \ mariadb

Un-comment NameVirtualHost in httpd.conf

sudo sed -i "/#NameVirtualHost \*:80/c\NameVirtualHost \*:80" /etc/httpd/conf/httpd.conf

Append some stuff to the end of httpd.conf

cat << EOF >> /etc/httpd/conf/httpd.conf

Deny access to all .svn and .git directories.

<Directory ~ "\.svn"> Order allow,deny Deny from all </Directory> <Directory ~ "\.git"> Order allow,deny Deny from all </Directory>
Header always append X-Frame-Options SAMEORIGIN Include vhosts.d/*.conf EOF

Setup symbolic links for httpd configs

sudo rm -rf /etc/httpd/conf.d sudo ln -s /var/www/etchttpd/conf.d/ /etc/httpd/conf.d sudo ln -s /var/www/etchttpd/vhosts.d/ /etc/httpd/vhosts.d sudo ln -s /var/www/shared/frameworks/ /var/www/frameworks

Symbolic Link for cakephp CLI tool

sudo ln -s /var/www/shared/frameworks/cakephp/lib/Cake/Console/cake /usr/local/bin/cake

Copy over php.ini file

sudo rm -rf /etc/php.ini sudo cp /var/www/etchttpd/php.ini /etc/php.ini sudo chcon system_u:object_r:etc_t:s0 /etc/php.ini

Create PHP session cache directory and make sure the web server owns it.

sudo mkdir /var/lib/php/session sudo chown apache /var/lib/php/session

Create the default php error log file just in case

sudo touch /var/log/php_errors.log sudo chown apache /var/log/php_errors.log

Add InCommon certificate chain to openldap

sudo cp /var/www/etchttpd/incommon_rsa_ca_bundle.pem /etc/openldap/certs/ sudo echo TLS_CACERT /etc/openldap/certs/incommon_rsa_ca_bundle.pem >> /etc/openldap/ldap.conf

Create a .gitconfig file

cat << EOF > /home/ec2-user/.gitconfig [user] name = Mark Fischer email = fischerm@email.arizona.edu EOF sudo chown ec2-user /home/ec2-user/.gitconfig

Copy git deployment private key out of SSM Parameter Store This should be a deploy-only key in prod, and read/write for nonprod. See src/ssh-parameter.py for loading SSH keys into the parameter store.

sudo mkdir -p /home/ec2-user/.ssh sudo aws ssm get-parameters --region ${AWS::Region} --names "/toolshed/bitbucket-ssh-key" --with-decryption --query 'Parameters[0].Value' --output text > /home/ec2-user/.ssh/id_rsa sudo chown ec2-user /home/ec2-user/.ssh/id_rsa sudo chmod 600 /home/ec2-user/.ssh/id_rsa

Replace sendmail with postfix

sudo yum install -y postfix sudo service sendmail stop sudo yum erase -y sendmail sudo sed -i "/#relayhost = uucphost/c\relayhost = [smtpgate.email.arizona.edu]" /etc/postfix/main.cf sudo service postfix start sudo chkconfig postfix on

Start apache

sudo chkconfig httpd on sudo service httpd start

Setup cron jobs

sudo cp /var/www/etchttpd/cron.d/toolshed /etc/cron.d/

Sym-link the web root directory to the ec2-user home directory

sudo ln -s /var/www/shared/ /home/ec2-user/sites

Update the hostname and restart

sudo sed -i "/HOSTNAME=localhost.localdomain/c\HOSTNAME=toolshed.uits.arizona.edu" /etc/sysconfig/network sudo reboot - EFSID: Fn::ImportValue: !Sub "${BaseStackName}-efs-id"

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: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: "/" ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy Policies: - PolicyName: "dr-backup-access" PolicyDocument: Version: '2012-10-17' Statement: - Sid: Stmt1452033379000 Effect: Allow Action: - s3:ListBucket - s3:PutObject Resource: - !Sub "arn:aws:s3:::${DRS3Bucket}" - !Sub "arn:aws:s3:::${DRS3Bucket}/*"

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

Load Balancer

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

LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing Subnets: !Ref LBSubnets LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '50' SecurityGroups: - Fn::ImportValue: !Sub "${BaseStackName}-alb-sg" Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag

Route53 DNS Record

DNS name to point at the load balancer

DnsRecord: Type: AWS::Route53::RecordSet Properties: HostedZoneName: !Sub "${HostedZoneName}." Name: !Sub "toolshed-${BlueGreen}.${HostedZoneName}." Type: "CNAME" TTL: "200" ResourceRecords: - !GetAtt LoadBalancer.DNSName

Add to Load Balancer

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 Properties: HealthCheckIntervalSeconds: 60 UnhealthyThresholdCount: 10 HealthCheckPath: / Matcher: HttpCode: "200-399" Port: 80 Protocol: HTTP VpcId: Fn::ImportValue: !Sub "${BaseStackName}-vpcid" Tags: - Key: service Value: !Ref ServiceTag - Key: environment Value: !Ref EnvironmentTag - Key: contactnetid Value: !Ref ContactNetidTag - Key: accountnumber Value: !Ref AccountNumberTag - Key: ticketnumber Value: !Ref TicketNumberTag

ALB Listeners definitions

HTTP listener is defined via the lambda function for now since CloudFormation doesn't support redirection rules yet.

AlbListener80: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: redirect RedirectConfig: Host: '#{host}' Path: '/#{path}' Port: 443 Protocol: HTTPS Query: '#{query}' StatusCode: HTTP_301 LoadBalancerArn: !Ref LoadBalancer Port: 80 Protocol: HTTP AlbListener443: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: Ref: AlbTargetGroup LoadBalancerArn: !Ref LoadBalancer Port: 443 Protocol: HTTPS SslPolicy: 'ELBSecurityPolicy-TLS-1-2-2017-01' Certificates: - CertificateArn: !Ref SSLCertificateARN

Outputs

Output values that can be viewed from the AWS CloudFormation console.

Outputs: LoadBalancerName: Description: The ALB Internal DNS Name Value: !GetAtt LoadBalancer.DNSName Export: Name: !Sub "${AWS::StackName}-alb-name" LoadBalancerARN: Description: The ALB itself Value: !Ref LoadBalancer Export: Name: !Sub "${AWS::StackName}-alb-arn" WebAddress: Description: The ALB URL Value: !Sub "https://${DnsRecord}/" Export: Name: !Sub "${AWS::StackName}-alb-url"