Skip to content

Instantly share code, notes, and snippets.

@nkhine
Created April 28, 2020 16:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nkhine/9b16c6929ab6d05ae816495d187ef697 to your computer and use it in GitHub Desktop.
Save nkhine/9b16c6929ab6d05ae816495d187ef697 to your computer and use it in GitHub Desktop.
HA Crucible
---
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Crucible: highly available Fisheye/Crucible template.'
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: 'Parent Stacks'
Parameters:
- ParentVPCStack
- ParentSSHBastionStack
- ParentAuthProxyStack
- ParentAlertStack
- ParentS3StackAccessLog
- ParentZoneStack
- Label:
default: 'EC2 Parameters'
Parameters:
- KeyName
- IAMUserSSHAccess
- SystemsManagerAccess
- SubDomainNameWithDot
- ManagedPolicyArns
- Label:
default: 'EFS Parameters'
Parameters:
- EFSProvisionedThroughputInMibps
- EFSBackupRetentionPeriod
- EFSBackupScheduleExpression
- Label:
default: 'Master Parameters'
Parameters:
- MasterSubnetsReach
- MasterELBScheme
- MasterInstanceType
- MasterLogsRetentionInDays
- MasterVolumeSize
- MasterLoadBalancerIdleTimeout
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: ''
ParentAuthProxyStack:
Description: 'Optional stack name of parent auth proxy stack based on security/auth-proxy-*.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: ''
ParentS3StackAccessLog:
Description: 'Optional stack name of parent s3 stack based on state/s3.yaml template (with Access set to ElbAccessLogWrite) to store access logs.'
Type: String
Default: ''
ParentZoneStack:
Description: 'Optional stack name of parent zone stack based on vpc/zone-*.yaml template.'
Type: String
Default: ''
KeyName:
Description: 'Optional key pair of the ec2-user to establish a SSH connection to the Jenkins master and agents.'
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
MasterSubnetsReach:
Description: 'Should the master have direct access to the Internet or do you prefer private subnets with NAT?'
Type: String
Default: Public
AllowedValues:
- Public
- Private
MasterELBScheme:
Description: 'Indicates whether the load balancer in front of the Jenkins master is Internet-facing or internal.'
Type: String
Default: 'internet-facing'
AllowedValues:
- 'internet-facing'
- internal
MasterInstanceType:
Description: 'The instance type of the Jenkins master.'
Type: String
Default: 't2.micro'
MasterLogsRetentionInDays:
Description: 'Specifies the number of days you want to retain log events in the specified log group.'
Type: Number
Default: 14
AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653]
MasterVolumeSize:
Description: 'The root volume size, in Gibibytes (GiB). Keep in mind that Jenkins home lives on EFS.'
Type: Number
Default: 8
ConstraintDescription: 'Must be in the range [8-1024]'
MinValue: 8
MaxValue: 1024
MasterLoadBalancerIdleTimeout:
Description: 'The idle timeout value, in seconds.'
Type: Number
Default: 60
MinValue: 1
MaxValue: 4000
MasterEnableMetrics:
Description: 'Should the master have Group Metrics collection enabled?'
Type: String
Default: "true"
AllowedValues: ["true", "false"]
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: 'crucible.'
ManagedPolicyArns:
Description: 'Optional comma-delimited list of IAM managed policy ARNs to attach to the instance''s IAM role'
Type: String
Default: ''
EFSProvisionedThroughputInMibps:
Description: 'The provisioned throughput for the Elastic File System (EFS) in Mibps. Default is 0 which enables the bursting mode and disables provisioned throughput.'
Type: Number
Default: 0
EFSBackupRetentionPeriod:
Description: 'The number of days to keep backups of the EFS file system (set to 0 to disable).'
Type: Number
MinValue: 0
MaxValue: 35
Default: 30
EFSBackupScheduleExpression:
Description: 'A CRON expression specifying when AWS Backup initiates a backup job.'
Type: String
Default: 'cron(0 5 ? * * *)'
CertificateArn:
Description: 'SSL certificate arn'
Type: String
Mappings:
RegionMap:
'eu-north-1':
AMI: 'ami-0b7a46b4bd694e8a6'
'ap-south-1':
AMI: 'ami-0470e33cd681b2476'
'eu-west-3':
AMI: 'ami-00077e3fed5089981'
'eu-west-2':
AMI: 'ami-01a6e31ac994bbc09'
'eu-west-1':
AMI: 'ami-06ce3edf0cff21f07'
'ap-northeast-2':
AMI: 'ami-01288945bd24ed49a'
'me-south-1':
AMI: 'ami-0fde637e0db57a2ab'
'ap-northeast-1':
AMI: 'ami-0f310fced6141e627'
'sa-east-1':
AMI: 'ami-003449ffb2605a74c'
'ca-central-1':
AMI: 'ami-054362537f5132ce2'
'ap-east-1':
AMI: 'ami-dd7731ac'
'ap-southeast-1':
AMI: 'ami-0ec225b5e01ccb706'
'ap-southeast-2':
AMI: 'ami-0970010f37c4f9c8d'
'eu-central-1':
AMI: 'ami-076431be05aaf8080'
'us-east-1':
AMI: 'ami-0323c3dd2da7fb37d'
'us-east-2':
AMI: 'ami-0f7919c33c90f5b58'
'us-west-1':
AMI: 'ami-06fcc1f0bc2c8943f'
'us-west-2':
AMI: 'ami-0d6621c01e8c2de2c'
AWSRegionsNameMapping:
eu-west-1:
RegionName: Ireland
eu-west-2:
RegionName: London
SSLmapping:
ssl1:
London: 'arn:aws:acm:eu-west-2:xxxxxxxxx:certificate/xxxxxxxxxxxxx'
Ireland: 'arn:aws:acm:eu-west-1:xxxxxxxxx:certificate/xxxxxxxxxxxxx'
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, '']
HasAuthProxySecurityGroup: !Not [!Equals [!Ref ParentAuthProxyStack, '']]
HasNotAuthProxySecurityGroup: !Equals [!Ref ParentAuthProxyStack, '']
HasMasterELBSchemeInternal: !Equals [!Ref MasterELBScheme, 'internal']
HasAlertTopic: !Not [!Equals [!Ref ParentAlertStack, '']]
HasS3Bucket: !Not [!Equals [!Ref ParentS3StackAccessLog, '']]
HasZone: !Not [!Equals [!Ref ParentZoneStack, '']]
HasManagedPolicyArns: !Not [!Equals [!Ref ManagedPolicyArns, '']]
HasEFSProvisionedThroughput: !Not [!Condition HasNotEFSProvisionedThroughput]
HasNotEFSProvisionedThroughput: !Equals [!Ref EFSProvisionedThroughputInMibps, '0']
HasAlertTopicAndEFSProvisionedThroughput: !And [!Condition HasAlertTopic, !Condition HasEFSProvisionedThroughput]
HasAlertTopicAndNotEFSProvisionedThroughput: !And [!Condition HasAlertTopic, !Condition HasNotEFSProvisionedThroughput]
HasEFSBackupRetentionPeriod: !Not [!Equals [!Ref EFSBackupRetentionPeriod, 0]]
HasMasterMetrics: !Equals [!Ref MasterEnableMetrics, "true"]
Resources:
MasterStorageSG:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'crucible-master'
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref MasterSG
FromPort: 2049
ToPort: 2049
IpProtocol: tcp
MasterStorage:
Type: 'AWS::EFS::FileSystem'
Properties:
ThroughputMode: !If [HasEFSProvisionedThroughput, 'provisioned', 'bursting']
ProvisionedThroughputInMibps: !If [HasEFSProvisionedThroughput, !Ref EFSProvisionedThroughputInMibps, !Ref 'AWS::NoValue']
FileSystemTags:
- Key: Name
Value: 'crucible-master-storage'
PerformanceMode: generalPurpose
MasterStorageBurstCreditBalanceTooLowAlarm:
Condition: HasAlertTopicAndNotEFSProvisionedThroughput
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Average burst credit balance over last 10 minutes too low, expect a significant performance drop soon.'
Namespace: 'AWS/EFS'
MetricName: BurstCreditBalance
Statistic: Average
Period: 600
EvaluationPeriods: 1
ComparisonOperator: LessThanThreshold
Threshold: 192000000000 # 192 GB in Bytes (last hour where you can burst at 100 MB/sec)
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: FileSystemId
Value: !Ref MasterStorage
MasterStoragePercentIOLimitTooHighAlarm:
Condition: HasAlertTopicAndNotEFSProvisionedThroughput
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'I/O limit has been reached, consider using Max I/O performance mode.'
Namespace: 'AWS/EFS'
MetricName: PercentIOLimit
Statistic: Maximum
Period: 600
EvaluationPeriods: 3
ComparisonOperator: GreaterThanThreshold
Threshold: 95
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: FileSystemId
Value: !Ref MasterStorage
MasterStorageThroughputAlarm:
Condition: HasAlertTopicAndEFSProvisionedThroughput
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Reached 90% of the provisioned throughput over last 10 minutes.'
Namespace: 'AWS/EFS'
MetricName: TotalIOBytes
Statistic: Sum
Period: 600
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: !GetAtt MaxThroughputCalculator.Threshold
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: FileSystemId
Value: !Ref MasterStorage
MasterStorageMountTargetA:
Type: 'AWS::EFS::MountTarget'
Properties:
FileSystemId: !Ref MasterStorage
SecurityGroups:
- !Ref MasterStorageSG
SubnetId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetA${MasterSubnetsReach}'}
MasterStorageMountTargetB:
Type: 'AWS::EFS::MountTarget'
Properties:
FileSystemId: !Ref MasterStorage
SecurityGroups:
- !Ref MasterStorageSG
SubnetId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetB${MasterSubnetsReach}'}
MasterELBSG:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'crucible-elb-master'
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
# Was missing
MasterELBSGInWorld:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasNotAuthProxySecurityGroup
Properties:
GroupId: !Ref MasterELBSG
IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: '0.0.0.0/0'
MasterELBSGInAuthProxy:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasAuthProxySecurityGroup
Properties:
GroupId: !Ref MasterELBSG
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentAuthProxyStack}-SecurityGroup'}
MasterELBHTTPSSGInAuthProxy:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasAuthProxySecurityGroup
Properties:
GroupId: !Ref MasterELBSG
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentAuthProxyStack}-SecurityGroup'}
MasterHTTPCodeELB5XXTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer returns 5XX HTTP status codes'
Namespace: 'AWS/ApplicationELB'
MetricName: HTTPCode_ELB_5XX_Count
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt MasterELB.LoadBalancerFullName
TreatMissingData: notBreaching
MasterHTTPCodeTarget5XXTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer receives 5XX HTTP status codes from targets'
Namespace: 'AWS/ApplicationELB'
MetricName: HTTPCode_Target_5XX_Count
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt MasterELB.LoadBalancerFullName
TreatMissingData: notBreaching
MasterRejectedConnectionCountTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer rejected connections because the load balancer had reached its maximum number of connections'
Namespace: 'AWS/ApplicationELB'
MetricName: RejectedConnectionCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt MasterELB.LoadBalancerFullName
TreatMissingData: notBreaching
MasterTargetConnectionErrorCountTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer could not connect to targets'
Namespace: 'AWS/ApplicationELB'
MetricName: TargetConnectionErrorCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt MasterELB.LoadBalancerFullName
TreatMissingData: notBreaching
RecordSet:
Condition: HasZone
Type: 'AWS::Route53::RecordSet'
Properties:
AliasTarget:
HostedZoneId: !GetAtt 'MasterELB.CanonicalHostedZoneID'
DNSName: !GetAtt 'MasterELB.DNSName'
HostedZoneId: {'Fn::ImportValue': !Sub '${ParentZoneStack}-HostedZoneId'}
Name: !Sub
- '${SubDomainNameWithDot}${HostedZoneName}'
- SubDomainNameWithDot: !Ref SubDomainNameWithDot
HostedZoneName: {'Fn::ImportValue': !Sub '${ParentZoneStack}-HostedZoneName'}
Type: A
MasterELB:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
LoadBalancerAttributes:
- Key: 'idle_timeout.timeout_seconds'
Value: !Ref MasterLoadBalancerIdleTimeout
- Key: 'routing.http2.enabled'
Value: 'true'
- Key: 'access_logs.s3.enabled'
Value: !If [HasS3Bucket, 'true', 'false']
- !If [HasS3Bucket, {Key: 'access_logs.s3.prefix', Value: !Ref 'AWS::StackName'}, !Ref 'AWS::NoValue']
- !If [HasS3Bucket, {Key: 'access_logs.s3.bucket', Value: {'Fn::ImportValue': !Sub '${ParentS3StackAccessLog}-BucketName'}}, !Ref 'AWS::NoValue']
Scheme: !Ref MasterELBScheme
SecurityGroups:
- !Ref MasterELBSG
Subnets: !If
- HasMasterELBSchemeInternal
- - {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetAPrivate'}
- {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetBPrivate'}
- - {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetAPublic'}
- {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetBPublic'}
Tags:
- Key: Name
Value: 'crucible-master'
MasterELBTargetGroup: # not monitored, but MasterELB is monitored!
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
HealthCheckIntervalSeconds: 30
HealthCheckPath: '/login'
HealthCheckPort: '8060'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 25
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
Matcher:
HttpCode: '200-302'
Port: 8060
Protocol: HTTP
Tags:
- Key: Name
Value: 'crucible-master'
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
TargetGroupAttributes:
- Key: 'deregistration_delay.timeout_seconds'
Value: '30'
# Redirect to SSL
MasterELBListener: # This is the ALB Listener used to access the Jenkins Master
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: "redirect"
RedirectConfig:
Protocol: "HTTPS"
Port: "443"
Host: "#{host}"
Path: "/#{path}"
Query: "#{query}"
StatusCode: "HTTP_301"
LoadBalancerArn: !Ref MasterELB
Port: 80
Protocol: HTTP
MasterHTTPSListener: # This is the HTTPS ALB Listener used to access the Jenkins Master
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: !Ref CertificateArn
DefaultActions:
- Type: forward
TargetGroupArn: !Ref MasterELBTargetGroup
LoadBalancerArn: !Ref MasterELB
Port: 443
Protocol: HTTPS
MasterIP:
Type: 'AWS::IAM::InstanceProfile'
Properties:
Roles:
- !Ref MasterIAMRole
MasterIAMRole:
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'ec2.amazonaws.com'
Action: 'sts:AssumeRole'
ManagedPolicyArns: !If [HasManagedPolicyArns, !Split [',', !Ref ManagedPolicyArns], !Ref 'AWS::NoValue']
Policies:
- !If
- HasSystemsManagerAccess
- PolicyName: ssm
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'ssmmessages:*' # SSM Agent by https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up-messageAPIs.html
- 'ssm:UpdateInstanceInformation' # SSM agent by https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up-messageAPIs.html
- 'ec2messages:*' # SSM Session Manager by https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-setting-up-messageAPIs.html
- 'ssm:ListAssociations'
- 'ssm:ListInstanceAssociations'
Resource: '*'
- !Ref 'AWS::NoValue'
- PolicyName: cloudwatch
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: write
Effect: Allow
Action: 'cloudwatch:PutMetricData'
Resource: '*'
- PolicyName: autoscaling
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: write
Effect: Allow
Action:
- 'autoscaling:CompleteLifecycleAction'
- 'cloudformation:SignalResource'
Resource: '*'
- PolicyName: logs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
- 'logs:DescribeLogStreams'
Resource: !GetAtt 'MasterLogs.Arn'
- PolicyName: sts
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: 'sts:AssumeRole'
Resource: '*'
MasterIAMPolicySSHAccess:
Type: 'AWS::IAM::Policy'
Condition: HasIAMUserSSHAccess
Properties:
Roles:
- !Ref MasterIAMRole
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/*'
MasterSG:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'crucible-master'
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref MasterELBSG
FromPort: 8060
ToPort: 8060
IpProtocol: tcp
MasterSGInSSHWorld:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasNotSSHBastionSecurityGroup
Properties:
GroupId: !Ref MasterSG
IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: '51.19.124.4/32'
MasterLogs:
Type: 'AWS::Logs::LogGroup'
Properties:
RetentionInDays: !Ref MasterLogsRetentionInDays
MasterLC:
Type: 'AWS::AutoScaling::LaunchConfiguration'
Properties:
IamInstanceProfile: !Ref MasterIP
InstanceType: !Ref MasterInstanceType
KeyName: !If [HasKeyName, !Ref KeyName, !Ref 'AWS::NoValue']
SecurityGroups:
- !Ref MasterSG
ImageId: !FindInMap [RegionMap, !Ref 'AWS::Region', AMI]
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: !Ref MasterVolumeSize
VolumeType: gp2
UserData:
'Fn::Base64': !Sub |
#!/bin/bash -x
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource MasterLC --region ${AWS::Region}
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource MasterASG --region ${AWS::Region}
Metadata:
'AWS::CloudFormation::Init':
configSets:
default: [awslogs, !If [HasIAMUserSSHAccess, ssh-access, !Ref 'AWS::NoValue'], mount, extras, install, installFisheye, setup, custom]
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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/var/log/audit/audit.log]
file = /var/log/audit/audit.log
log_stream_name = {instance_id}/var/log/audit/audit.log
log_group_name = ${MasterLogs}
[/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 = ${MasterLogs}
[/var/log/boot.log]
file = /var/log/boot.log
log_stream_name = {instance_id}/var/log/boot.log
log_group_name = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/var/log/dmesg]
file = /var/log/dmesg
log_stream_name = {instance_id}/var/log/dmesg
log_group_name = ${MasterLogs}
[/var/log/grubby_prune_debug]
file = /var/log/grubby_prune_debug
log_stream_name = {instance_id}/var/log/grubby_prune_debug
log_group_name = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/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 = ${MasterLogs}
[/var/log/fisheye.out]
datetime_format = %b %d %H:%M:%S
file = /var/lib/atlassian/application-data/fecru/var/log/fisheye.out
log_stream_name = {instance_id}/var/log/fisheye.out
log_group_name = ${MasterLogs}
mode: '000644'
owner: root
group: root
'/etc/awslogs/config/efs.conf':
content: !Sub |
[/var/log/amazon/efs/mount.log]
datetime_format = %Y-%m-%d %H:%M:%S
file = /var/log/amazon/efs/mount.log
log_stream_name = {instance_id}/var/log/amazon/efs/mount.log
log_group_name = ${MasterLogs}
mode: '000644'
owner: root
group: root
'/etc/awslogs/config/fisheye.conf':
content: !Sub |
[/var/atlassian/application-data/fecru/var/log]
datetime_format = %d %b %Y %H:%M:%S
multi_line_start_pattern = {datetime_format}
file = /var/atlassian/application-data/fecru/var/log/fisheye.out
log_stream_name = {instance_id}/var/atlassian/application-data/fecru/var/log/fisheye.out
log_group_name = ${MasterLogs}
mode: '000644'
owner: root
group: root
services:
sysvinit:
awslogsd:
enabled: true
ensureRunning: true
packages:
yum:
- awslogs
files:
- '/etc/awslogs/awscli.conf'
- '/etc/awslogs/awslogs.conf'
- '/etc/awslogs/config/efs.conf'
- '/etc/awslogs/config/fisheye.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'
mount:
packages:
yum:
'amazon-efs-utils': []
commands:
'a_useradd':
command: 'adduser -s /bin/false -d /opt/fecru-4.8.1 -M -c ''Fisheye Crucible Server'' fisheye'
test: 'if grep -q fisheye: /etc/passwd; then exit 1; else exit 0; fi'
'b_mount':
command: !Sub 'mkdir -p /var/lib/atlassian/application-data/fecru && chown -R fisheye:fisheye /var/lib/atlassian && echo "${MasterStorage}:/ /var/lib/atlassian efs tls,_netdev 0 0" >> /etc/fstab && mount -a -t efs'
test: '[ ! -d /var/atlassian ]'
extras:
commands:
'a_enable_corretto8':
command: 'amazon-linux-extras enable corretto8'
test: "! grep -Fxq '[amzn2extra-corretto8]' /etc/yum.repos.d/amzn2-extras.repo"
install:
packages:
yum:
'java-1.8.0-amazon-corretto': []
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.MasterLC.Metadata.AWS::CloudFormation::Init
action=/opt/aws/bin/cfn-init --verbose --stack=${AWS::StackName} --region=${AWS::Region} --resource=MasterLC
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]
installFisheye:
commands:
InstallFisheye01:
command: >-
cd /tmp
InstallFisheye02:
command: >-
wget https://www.atlassian.com/software/crucible/downloads/binary/crucible-4.8.1.zip
InstallFisheye03:
command: >-
unzip -d /opt/ crucible-4.8.1.zip
InstallFisheye04:
command: >-
chown -R fisheye:fisheye /opt/fecru-4.8.1
setup:
files:
'/lib/systemd/system/fisheye.service':
content: |
[Unit]
Description=Fisheye the on-premise source code repository browser for enterprise teams
After=syslog.target network.target
[Service]
Type=forking
User=fisheye
ExecStart=/opt/fecru-4.8.1/bin/fisheyectl.sh start
ExecStop=/opt/fecru-4.8.1/bin/fisheyectl.sh stop
[Install]
WantedBy=multi-user.target
mode: '000700'
owner: root
group: root
commands:
'a_systemctl_reload':
command: 'systemctl daemon-reload'
# TODO: DO NOT USE /tmp directory. Use some other permanent ec2-user readable directory.
test: '[ ! -f /var/lib/atlassian/setup_done.txt ]'
'b_systemctl_enable_fisheye':
command: 'systemctl enable fisheye'
test: '[ ! -f /var/lib/atlassian/setup_done.txt ]'
'c_systemctl_start_fisheye':
command: 'systemctl start fisheye'
test: '[ ! -f /var/lib/atlassian/setup_done.txt ]'
'd_await_fisheye':
command: 'until $(curl -s -m 60 -o /dev/null -I -f --url "http://localhost:8060"); do printf "."; sleep 1; done'
test: '[ ! -f /var/lib/atlassian/setup_done.txt ]'
'z_create_setup_done_file':
command: 'echo "Setup done. Do not delete this file." > /var/lib/atlassian/setup_done.txt'
test: '[ ! -f /var/lib/atlassian/setup_done.txt ]'
custom:
packages:
yum:
git: []
subversion: []
MasterASG:
Type: 'AWS::AutoScaling::AutoScalingGroup'
DependsOn:
- MasterStorageMountTargetA
- MasterStorageMountTargetB
Properties:
LaunchConfigurationName: !Ref MasterLC
MinSize: '1'
MaxSize: '1'
HealthCheckGracePeriod: 1800
HealthCheckType: ELB
NotificationConfigurations: !If
- HasAlertTopic
- - NotificationTypes:
- 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR'
- 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'
TopicARN: {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
- []
VPCZoneIdentifier:
- {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetA${MasterSubnetsReach}'}
- {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetB${MasterSubnetsReach}'}
TargetGroupARNs:
- !Ref MasterELBTargetGroup
Tags:
- Key: Name
Value: 'crucible-master'
PropagateAtLaunch: true
MetricsCollection:
- Fn::If:
- HasMasterMetrics
- Granularity: 1Minute
- !Ref 'AWS::NoValue'
CreationPolicy:
ResourceSignal:
Timeout: PT15M
UpdatePolicy:
AutoScalingRollingUpdate:
PauseTime: PT15M
SuspendProcesses:
- HealthCheck
- ReplaceUnhealthy
- AZRebalance
- AlarmNotification
- ScheduledActions
WaitOnResourceSignals: true
MasterCPUTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Master 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: AutoScalingGroupName
Value: !Ref MasterASG
LambdaRole:
Condition: HasEFSProvisionedThroughput
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'lambda.amazonaws.com'
Action: 'sts:AssumeRole'
LambdaPolicy:
Condition: HasEFSProvisionedThroughput
Type: 'AWS::IAM::Policy'
Properties:
Roles:
- !Ref LambdaRole
PolicyName: lambda
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: !GetAtt 'LambdaLogGroup.Arn'
LambdaFunction: # needs no monitoring because it is used as a custom resource
Condition: HasEFSProvisionedThroughput
Type: 'AWS::Lambda::Function'
Properties:
Code:
ZipFile: |
'use strict';
const response = require('cfn-response');
exports.handler = (event, context, cb) => {
const throughput = parseInt(event.ResourceProperties.ThroughputInMibps, 10);
const threshold = Math.round(throughput * 1048576 * 600 * 0.9);
response.send(event, context, response.SUCCESS, {Threshold: threshold});
};
Handler: 'index.handler'
MemorySize: 128
Role: !GetAtt 'LambdaRole.Arn'
Runtime: 'nodejs12.x'
Timeout: 60
LambdaLogGroup:
Condition: HasEFSProvisionedThroughput
Type: 'AWS::Logs::LogGroup'
Properties:
LogGroupName: !Sub '/aws/lambda/${LambdaFunction}'
RetentionInDays: !Ref MasterLogsRetentionInDays
MaxThroughputCalculator:
Condition: HasEFSProvisionedThroughput
Type: 'Custom::MaxThroughputCalculator'
DependsOn:
- LambdaLogGroup
- LambdaPolicy
Version: '1.0'
Properties:
ThroughputInMibps: !Ref EFSProvisionedThroughputInMibps
ServiceToken: !GetAtt 'LambdaFunction.Arn'
BackupVault: # cannot be deleted with data
Condition: HasEFSBackupRetentionPeriod
Type: 'AWS::Backup::BackupVault'
Properties:
BackupVaultName: !Ref 'AWS::StackName'
Notifications: !If [HasAlertTopic, {BackupVaultEvents: [BACKUP_JOB_STARTED, BACKUP_JOB_COMPLETED, RESTORE_JOB_STARTED, RESTORE_JOB_COMPLETED, RECOVERY_POINT_MODIFIED], SNSTopicArn: {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}}, !Ref 'AWS::NoValue']
BackupPlan:
Condition: HasEFSBackupRetentionPeriod
Type: 'AWS::Backup::BackupPlan'
Properties:
BackupPlan:
BackupPlanName: !Ref 'AWS::StackName'
BackupPlanRule:
- CompletionWindowMinutes: 1440
Lifecycle:
DeleteAfterDays: !Ref EFSBackupRetentionPeriod
RuleName: !Ref 'AWS::StackName'
ScheduleExpression: !Ref EFSBackupScheduleExpression
StartWindowMinutes: 60
TargetBackupVault: !Ref BackupVault
BackupRole:
Condition: HasEFSBackupRetentionPeriod
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'backup.amazonaws.com'
Action: 'sts:AssumeRole'
Policies:
- PolicyName: backup
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'elasticfilesystem:Backup'
- 'elasticfilesystem:DescribeTags'
Resource: !Sub 'arn:${AWS::Partition}:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${MasterStorage}'
BackupSelection:
Condition: HasEFSBackupRetentionPeriod
Type: 'AWS::Backup::BackupSelection'
Properties:
BackupPlanId: !Ref BackupPlan
BackupSelection:
IamRoleArn: !GetAtt 'BackupRole.Arn'
Resources:
- !Sub 'arn:${AWS::Partition}:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${MasterStorage}'
SelectionName: !Ref 'AWS::StackName'
Outputs:
TemplateID:
Description: 'template id.'
Value: 'crucible/fisheye-crucible'
TemplateVersion:
Description: 'template version.'
Value: '__VERSION__'
StackName:
Description: 'Stack name.'
Value: !Sub '${AWS::StackName}'
DNSName:
Description: 'The DNS name for the Crucible load balancer.'
Value: !GetAtt 'MasterELB.DNSName'
Export:
Name: !Sub '${AWS::StackName}-DNSName'
URL:
Description: 'URL to the Crucible Master.'
Value: !Sub 'https://${MasterELB.DNSName}'
Export:
Name: !Sub '${AWS::StackName}-URL'
MasterIAMRole:
Description: 'Use this IAm Role to reference API calls from the Crucible master'
Value: !GetAtt 'MasterIAMRole.Arn'
Export:
Name: !Sub '${AWS::StackName}-MasterIAMRole'
MasterELBSG:
Description: 'Master ELB SG'
Value: !GetAtt 'MasterELBSG.GroupId'
Export:
Name: !Sub '${AWS::StackName}-MasterELBSG'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment