Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
2 contributors

Users who have contributed to this file

@michaelwittig @guizmaii
622 lines (621 sloc) 23.2 KB
---
# Copyright 2018 widdix GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
AWSTemplateFormatVersion: '2010-09-09'
Description: 'EC2: instance with auto-recovery, a cloudonaut.io template'
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: 'Parent Stacks'
Parameters:
- ParentVPCStack
- ParentSSHBastionStack
- ParentAlertStack
- ParentZoneStack
- ParentClientStack1
- ParentClientStack2
- ParentClientStack3
- Label:
default: 'EC2 Parameters'
Parameters:
- InstanceType
- Name
- SubnetName
- KeyName
- IAMUserSSHAccess
- SystemsManagerAccess
- LogsRetentionInDays
- SubDomainNameWithDot
- UserData
- IngressTcpPort1
- IngressTcpPort2
- IngressTcpPort3
Parameters:
ParentVPCStack:
Description: 'Stack name of parent VPC stack based on vpc/vpc-*azs.yaml template.'
Type: String
ParentSSHBastionStack:
Description: 'Optional but recommended stack name of parent SSH bastion host/instance stack based on vpc/vpc-*-bastion.yaml template.'
Type: String
Default: ''
ParentAlertStack:
Description: 'Optional but recommended stack name of parent alert stack based on operations/alert.yaml template.'
Type: String
Default: ''
ParentZoneStack:
Description: 'Optional stack name of parent zone stack based on vpc/zone-*.yaml template.'
Type: String
Default: ''
ParentClientStack1:
Description: 'Optional stack name of parent Client Security Group stack based on state/client-sg.yaml template to allow network access from the EC2 instance to whatever uses the client security group.'
Type: String
Default: ''
ParentClientStack2:
Description: 'Optional stack name of parent Client Security Group stack based on state/client-sg.yaml template to allow network access from the EC2 instance to whatever uses the client security group.'
Type: String
Default: ''
ParentClientStack3:
Description: 'Optional stack name of parent Client Security Group stack based on state/client-sg.yaml template to allow network access from the EC2 instance to whatever uses the client security group.'
Type: String
Default: ''
KeyName:
Description: 'Optional key pair of the ec2-user to establish a SSH connection to the EC2 instance.'
Type: String
Default: ''
IAMUserSSHAccess:
Description: 'Synchronize public keys of IAM users to enable personalized SSH access (Doc: https://cloudonaut.io/manage-aws-ec2-ssh-access-with-iam/).'
Type: String
Default: false
AllowedValues:
- true
- false
SystemsManagerAccess:
Description: 'Enable AWS Systems Manager agent and authorization.'
Type: String
Default: true
AllowedValues:
- true
- false
InstanceType:
Description: 'The instance type for the EC2 instance.'
Type: String
Default: 't2.micro'
Name:
Description: 'The name for the EC2 instance.'
Type: String
Default: 'test'
SubnetName:
Description: 'Subnet name of parent VPC stack based on vpc/vpc-*azs.yaml template.'
Type: String
Default: SubnetAPublic
AllowedValues:
- SubnetAPublic
- SubnetAPrivate
- SubnetBPublic
- SubnetBPrivate
- SubnetCPublic
- SubnetCPrivate
- SubnetDPublic
- SubnetDPrivate
LogsRetentionInDays:
Description: 'Specifies the number of days you want to retain log events.'
Type: Number
Default: 14
AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
SubDomainNameWithDot:
Description: 'Name that is used to create the DNS entry with trailing dot, e.g. §{SubDomainNameWithDot}§{HostedZoneName}. Leave blank for naked (or apex and bare) domain. Requires ParentZoneStack parameter!'
Type: String
Default: ''
UserData:
Description: 'Optional Bash script executed on first instance launch.'
Type: String
Default: ''
IngressTcpPort1:
Description: 'Optional port allowing ingress TCP traffic.'
Type: String
Default: ''
IngressTcpPort2:
Description: 'Optional port allowing ingress TCP traffic.'
Type: String
Default: ''
IngressTcpPort3:
Description: 'Optional port allowing ingress TCP traffic.'
Type: String
Default: ''
Mappings:
RegionMap:
'ap-south-1':
AMI: 'ami-0937dcc711d38ef3f'
'eu-west-3':
AMI: 'ami-0854d53ce963f69d8'
'eu-north-1':
AMI: 'ami-6d27a913'
'eu-west-2':
AMI: 'ami-0664a710233d7c148'
'eu-west-1':
AMI: 'ami-0fad7378adf284ce0'
'ap-northeast-2':
AMI: 'ami-018a9a930060d38aa'
'ap-northeast-1':
AMI: 'ami-0d7ed3ddb85b521a6'
'sa-east-1':
AMI: 'ami-0b04450959586da29'
'ca-central-1':
AMI: 'ami-0de8b8e4bc1f125fe'
'ap-southeast-1':
AMI: 'ami-04677bdaa3c2b6e24'
'ap-southeast-2':
AMI: 'ami-0c9d48b5db609ad6e'
'eu-central-1':
AMI: 'ami-0eaec5838478eb0ba'
'us-east-1':
AMI: 'ami-035be7bafff33b6b6'
'us-east-2':
AMI: 'ami-04328208f4f0cf1fe'
'us-west-1':
AMI: 'ami-0799ad445b5727125'
'us-west-2':
AMI: 'ami-032509850cf9ee54e'
Conditions:
HasKeyName: !Not [!Equals [!Ref KeyName, '']]
HasIAMUserSSHAccess: !Equals [!Ref IAMUserSSHAccess, 'true']
HasSystemsManagerAccess: !Equals [!Ref SystemsManagerAccess, 'true']
HasSSHBastionSecurityGroup: !Not [!Equals [!Ref ParentSSHBastionStack, '']]
HasNotSSHBastionSecurityGroup: !Equals [!Ref ParentSSHBastionStack, '']
HasAlertTopic: !Not [!Equals [!Ref ParentAlertStack, '']]
HasZone: !Not [!Equals [!Ref ParentZoneStack, '']]
HasIngressTcpPort1: !Not [!Equals [!Ref IngressTcpPort1, '']]
HasIngressTcpPort2: !Not [!Equals [!Ref IngressTcpPort2, '']]
HasIngressTcpPort3: !Not [!Equals [!Ref IngressTcpPort3, '']]
HasClientSecurityGroup1: !Not [!Equals [!Ref ParentClientStack1, '']]
HasClientSecurityGroup2: !Not [!Equals [!Ref ParentClientStack2, '']]
HasClientSecurityGroup3: !Not [!Equals [!Ref ParentClientStack3, '']]
Resources:
RecordSet:
Condition: HasZone
Type: 'AWS::Route53::RecordSet'
Properties:
HostedZoneId: {'Fn::ImportValue': !Sub '${ParentZoneStack}-HostedZoneId'}
Name: !Sub
- '${SubDomainNameWithDot}${HostedZoneName}'
- SubDomainNameWithDot: !Ref SubDomainNameWithDot
HostedZoneName: {'Fn::ImportValue': !Sub '${ParentZoneStack}-HostedZoneName'}
ResourceRecords:
- !Ref ElasticIP
TTL: '60'
Type: A
ElasticIP:
Type: 'AWS::EC2::EIP'
Properties:
Domain: vpc
NetworkInterface:
Type: 'AWS::EC2::NetworkInterface'
Properties:
GroupSet:
- !Ref SecurityGroup
- !If [HasClientSecurityGroup1, {'Fn::ImportValue': !Sub '${ParentClientStack1}-ClientSecurityGroup'}, !Ref 'AWS::NoValue']
- !If [HasClientSecurityGroup2, {'Fn::ImportValue': !Sub '${ParentClientStack2}-ClientSecurityGroup'}, !Ref 'AWS::NoValue']
- !If [HasClientSecurityGroup3, {'Fn::ImportValue': !Sub '${ParentClientStack3}-ClientSecurityGroup'}, !Ref 'AWS::NoValue']
SubnetId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-${SubnetName}'}
EIPAssociation:
Type: 'AWS::EC2::EIPAssociation'
Properties:
AllocationId: !GetAtt 'ElasticIP.AllocationId'
NetworkInterfaceId: !Ref NetworkInterface
Logs:
Type: 'AWS::Logs::LogGroup'
Properties:
RetentionInDays: !Ref LogsRetentionInDays
SecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: !Ref Name
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
SecurityGroupInSSHBastion:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasSSHBastionSecurityGroup
Properties:
GroupId: !Ref SecurityGroup
IpProtocol: tcp
FromPort: 22
ToPort: 22
SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentSSHBastionStack}-SecurityGroup'}
SecurityGroupInSSHWorld:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasNotSSHBastionSecurityGroup
Properties:
GroupId: !Ref SecurityGroup
IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: '0.0.0.0/0'
SecurityGroupIngressTcpPort1:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasIngressTcpPort1
Properties:
GroupId: !Ref SecurityGroup
IpProtocol: tcp
FromPort: !Ref IngressTcpPort1
ToPort: !Ref IngressTcpPort1
CidrIp: '0.0.0.0/0'
SecurityGroupIngressTcpPort2:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasIngressTcpPort2
Properties:
GroupId: !Ref SecurityGroup
IpProtocol: tcp
FromPort: !Ref IngressTcpPort2
ToPort: !Ref IngressTcpPort2
CidrIp: '0.0.0.0/0'
SecurityGroupIngressTcpPort3:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasIngressTcpPort3
Properties:
GroupId: !Ref SecurityGroup
IpProtocol: tcp
FromPort: !Ref IngressTcpPort3
ToPort: !Ref IngressTcpPort3
CidrIp: '0.0.0.0/0'
InstanceProfile:
Type: 'AWS::IAM::InstanceProfile'
Properties:
Path: '/'
Roles:
- !Ref IAMRole
IAMRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- 'ec2.amazonaws.com'
Action:
- 'sts:AssumeRole'
Path: '/'
ManagedPolicyArns: !If [HasSystemsManagerAccess, ['arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM'], []] # TODO get rid of managed policy
Policies:
- PolicyName: logs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
- 'logs:DescribeLogStreams'
Resource: !GetAtt 'Logs.Arn'
IAMPolicySSHAccess:
Type: 'AWS::IAM::Policy'
Condition: HasIAMUserSSHAccess
Properties:
Roles:
- !Ref IAMRole
PolicyName: iam
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'iam:ListUsers'
Resource:
- '*'
- Effect: Allow
Action:
- 'iam:ListSSHPublicKeys'
- 'iam:GetSSHPublicKey'
Resource:
- !Sub 'arn:aws:iam::${AWS::AccountId}:user/*'
VirtualMachine:
DependsOn: EIPAssociation
Type: 'AWS::EC2::Instance'
Metadata:
'AWS::CloudFormation::Init':
configSets:
default: !If [HasIAMUserSSHAccess, [awslogs, ssh-access, config], [awslogs, config]]
awslogs:
packages:
yum:
awslogs: []
files:
'/etc/awslogs/awscli.conf':
content: !Sub |
[default]
region = ${AWS::Region}
[plugins]
cwlogs = cwlogs
mode: '000644'
owner: root
group: root
'/etc/awslogs/awslogs.conf':
content: !Sub |
[general]
state_file = /var/lib/awslogs/agent-state
[/var/log/amazon/ssm/amazon-ssm-agent.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/amazon/ssm/amazon-ssm-agent.log
log_stream_name = {instance_id}/var/log/amazon/ssm/amazon-ssm-agent.log
log_group_name = ${Logs}
[/var/log/amazon/ssm/errors.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/amazon/ssm/errors.log
log_stream_name = {instance_id}/var/log/amazon/ssm/errors.log
log_group_name = ${Logs}
[/var/log/audit/audit.log]
file = /var/log/audit/audit.log
log_stream_name = {instance_id}/var/log/audit/audit.log
log_group_name = ${Logs}
[/var/log/awslogs.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/awslogs.log
log_stream_name = {instance_id}/var/log/awslogs.log
log_group_name = ${Logs}
[/var/log/boot.log]
file = /var/log/boot.log
log_stream_name = {instance_id}/var/log/boot.log
log_group_name = ${Logs}
[/var/log/cfn-hup.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/cfn-hup.log
log_stream_name = {instance_id}/var/log/cfn-hup.log
log_group_name = ${Logs}
[/var/log/cfn-init-cmd.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/cfn-init-cmd.log
log_stream_name = {instance_id}/var/log/cfn-init-cmd.log
log_group_name = ${Logs}
[/var/log/cfn-init.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/cfn-init.log
log_stream_name = {instance_id}/var/log/cfn-init.log
log_group_name = ${Logs}
[/var/log/cfn-wire.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/cfn-wire.log
log_stream_name = {instance_id}/var/log/cfn-wire.log
log_group_name = ${Logs}
[/var/log/cloud-init-output.log]
file = /var/log/cloud-init-output.log
log_stream_name = {instance_id}/var/log/cloud-init-output.log
log_group_name = ${Logs}
[/var/log/cloud-init.log]
datetime_format = %b %d %H:%M:%S
file = /var/log/cloud-init.log
log_stream_name = {instance_id}/var/log/cloud-init.log
log_group_name = ${Logs}
[/var/log/cron]
datetime_format = %b %d %H:%M:%S
file = /var/log/cron
log_stream_name = {instance_id}/var/log/cron
log_group_name = ${Logs}
[/var/log/dmesg]
file = /var/log/dmesg
log_stream_name = {instance_id}/var/log/dmesg
log_group_name = ${Logs}
[/var/log/grubby_prune_debug]
file = /var/log/grubby_prune_debug
log_stream_name = {instance_id}/var/log/grubby_prune_debug
log_group_name = ${Logs}
[/var/log/maillog]
datetime_format = %b %d %H:%M:%S
file = /var/log/maillog
log_stream_name = {instance_id}/var/log/maillog
log_group_name = ${Logs}
[/var/log/messages]
datetime_format = %b %d %H:%M:%S
file = /var/log/messages
log_stream_name = {instance_id}/var/log/messages
log_group_name = ${Logs}
[/var/log/secure]
datetime_format = %b %d %H:%M:%S
file = /var/log/secure
log_stream_name = {instance_id}/var/log/secure
log_group_name = ${Logs}
[/var/log/yum.log]
datetime_format = %b %d %H:%M:%S
file = /var/log/yum.log
log_stream_name = {instance_id}/var/log/yum.log
log_group_name = ${Logs}
mode: '000644'
owner: root
group: root
services:
sysvinit:
awslogsd:
enabled: true
ensureRunning: true
packages:
yum:
- awslogs
files:
- '/etc/awslogs/awslogs.conf'
- '/etc/awslogs/awscli.conf'
ssh-access:
files:
'/opt/authorized_keys_command.sh':
content: |
#!/bin/bash -e
if [ -z "$1" ]; then
exit 1
fi
UnsaveUserName="$1"
UnsaveUserName=${UnsaveUserName//".plus."/"+"}
UnsaveUserName=${UnsaveUserName//".equal."/"="}
UnsaveUserName=${UnsaveUserName//".comma."/","}
UnsaveUserName=${UnsaveUserName//".at."/"@"}
aws iam list-ssh-public-keys --user-name "$UnsaveUserName" --query "SSHPublicKeys[?Status == 'Active'].[SSHPublicKeyId]" --output text | while read -r KeyId; do
aws iam get-ssh-public-key --user-name "$UnsaveUserName" --ssh-public-key-id "$KeyId" --encoding SSH --query "SSHPublicKey.SSHPublicKeyBody" --output text
done
mode: '000755'
owner: root
group: root
'/opt/import_users.sh':
content: |
#!/bin/bash -e
aws iam list-users --query "Users[].[UserName]" --output text | while read User; do
SaveUserName="$User"
SaveUserName=${SaveUserName//"+"/".plus."}
SaveUserName=${SaveUserName//"="/".equal."}
SaveUserName=${SaveUserName//","/".comma."}
SaveUserName=${SaveUserName//"@"/".at."}
if [ "${#SaveUserName}" -le "32" ]; then
if ! id -u "$SaveUserName" >/dev/null 2>&1; then
#sudo will read each file in /etc/sudoers.d, skipping file names that end in ‘~’ or contain a ‘.’ character to avoid causing problems with package manager or editor temporary/backup files.
SaveUserFileName=$(echo "$SaveUserName" | tr "." " ")
/usr/sbin/useradd "$SaveUserName"
echo "$SaveUserName ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/$SaveUserFileName"
fi
else
echo "Can not import IAM user ${SaveUserName}. User name is longer than 32 characters."
fi
done
mode: '000755'
owner: root
group: root
'/etc/cron.d/import_users':
content: |
*/10 * * * * root /opt/import_users.sh
mode: '000644'
owner: root
group: root
commands:
'a_configure_sshd_command':
command: 'sed -e ''/AuthorizedKeysCommand / s/^#*/#/'' -i /etc/ssh/sshd_config; echo -e ''\nAuthorizedKeysCommand /opt/authorized_keys_command.sh'' >> /etc/ssh/sshd_config'
test: '! grep -q ''^AuthorizedKeysCommand /opt/authorized_keys_command.sh'' /etc/ssh/sshd_config'
'b_configure_sshd_commanduser':
command: 'sed -e ''/AuthorizedKeysCommandUser / s/^#*/#/'' -i /etc/ssh/sshd_config; echo -e ''\nAuthorizedKeysCommandUser nobody'' >> /etc/ssh/sshd_config'
test: '! grep -q ''^AuthorizedKeysCommandUser nobody'' /etc/ssh/sshd_config'
'c_import_users':
command: './import_users.sh'
cwd: '/opt'
services:
sysvinit:
sshd:
enabled: true
ensureRunning: true
commands:
- 'a_configure_sshd_command'
- 'b_configure_sshd_commanduser'
config:
files:
'/etc/cfn/cfn-hup.conf':
content: !Sub |
[main]
stack=${AWS::StackId}
region=${AWS::Region}
interval=1
mode: '000400'
owner: root
group: root
'/etc/cfn/hooks.d/cfn-auto-reloader.conf':
content: !Sub |
[cfn-auto-reloader-hook]
triggers=post.update
path=Resources.VirtualMachine.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init --verbose --stack=${AWS::StackName} --region=${AWS::Region} --resource=VirtualMachine
runas=root
services:
sysvinit:
cfn-hup:
enabled: true
ensureRunning: true
files:
- '/etc/cfn/cfn-hup.conf'
- '/etc/cfn/hooks.d/cfn-auto-reloader.conf'
amazon-ssm-agent:
enabled: !If [HasSystemsManagerAccess, true, false]
ensureRunning: !If [HasSystemsManagerAccess, true, false]
Properties:
IamInstanceProfile: !Ref InstanceProfile
ImageId: !FindInMap [RegionMap, !Ref 'AWS::Region', AMI]
InstanceType: !Ref InstanceType
KeyName: !If [HasKeyName, !Ref KeyName, !Ref 'AWS::NoValue']
NetworkInterfaces:
- DeviceIndex: '0'
NetworkInterfaceId: !Ref NetworkInterface
UserData:
'Fn::Base64': !Sub |
#!/bin/bash -ex
trap '/opt/aws/bin/cfn-signal -e 1 --region ${AWS::Region} --stack ${AWS::StackName} --resource VirtualMachine' ERR
${UserData}
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource VirtualMachine --region ${AWS::Region}
/opt/aws/bin/cfn-signal -e 0 --region ${AWS::Region} --stack ${AWS::StackName} --resource VirtualMachine
Tags:
- Key: Name
Value: !Ref Name
CreationPolicy:
ResourceSignal:
Count: 1
Timeout: PT10M
RecoveryAlarm:
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Recovering instance when underlying hardware fails.'
Namespace: 'AWS/EC2'
MetricName: StatusCheckFailed_System
Statistic: Minimum
Period: 60
EvaluationPeriods: 5
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- !Sub 'arn:aws:automate:${AWS::Region}:ec2:recover'
Dimensions:
- Name: InstanceId
Value: !Ref VirtualMachine
CPUTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Average CPU utilization over last 10 minutes higher than 80%'
Namespace: 'AWS/EC2'
MetricName: CPUUtilization
Statistic: Average
Period: 600
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 80
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: InstanceId
Value: !Ref VirtualMachine
Outputs:
TemplateID:
Description: 'cloudonaut.io template id.'
Value: 'ec2/ec2-auto-recovery'
TemplateVersion:
Description: 'cloudonaut.io template version.'
Value: '__VERSION__'
StackName:
Description: 'Stack name.'
Value: !Sub '${AWS::StackName}'
InstanceId:
Description: 'The EC2 instance id.'
Value: !Ref VirtualMachine
Export:
Name: !Sub '${AWS::StackName}-InstanceId'
IPAddress:
Description: 'The public IP address of the EC2 instance.'
Value: !Ref ElasticIP
Export:
Name: !Sub '${AWS::StackName}-IPAddress'
PrivateIPAddress:
Description: 'The private IP address of the EC2 instance.'
Value: !GetAtt 'NetworkInterface.PrimaryPrivateIpAddress'
Export:
Name: !Sub '${AWS::StackName}-PrivateIPAddress'
You can’t perform that action at this time.