Skip to content

Instantly share code, notes, and snippets.

@dgt0011
Last active March 24, 2024 05:11
Show Gist options
  • Save dgt0011/d25ee89ab3fce5c94c99aa7d8c72ac3f to your computer and use it in GitHub Desktop.
Save dgt0011/d25ee89ab3fce5c94c99aa7d8c72ac3f to your computer and use it in GitHub Desktop.
Cloudformation template to deploy SonarQube TO ECS Fargate. Note that this template depends on stacks created from the my-vpc.yaml and rds.yaml gists
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::SecretsManager-2020-07-23
Description: ECS Cluster for SonarQube CE.
Dependent on an RDS instance/stack and a VPC stack.
Parameters:
ServiceName:
Type: String
Default: sonarqube-app
Description: A name for the service.
This name will be used to create the ECS service, task definition,
and used as a prefix for other resources.
VpcId:
Type: AWS::EC2::VPC::Id
Description: The VPC where the service will be deployed
Change this default (or remove it) as appropriate
Default: vpc-0123456789abcdef0
PublicSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: The public subnets where the load balancer service will be deployed.
Change these defaults (or remove them) as appropriate
Default: "subnet-a123456789abcdef0,subnet-b123456789abcdef0b,subnet-c123456789abcdef0"
PrivateSubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: The private subnets where the containers will be deployed
Change these defaults (or remove them) as appropriate
Default: "subnet-d123456789abcdef0,subnet-e123456789abcdef0,subnet-f123456789abcdef0"
ECRRepository:
Type: String
Default: sonarqube
Description: The name of the ECR repository where the docker image is stored.
This is not set up as part of this stack and needs to exist prior to this template being used
ECRTag:
Type: String
Default: latest
Description: The tag of the docker image to use
ContainerPort:
Type: Number
Default: 9000
Description: What port number the application inside the docker container is binding to
ContainerCpu:
Type: Number
Default: 1024
Description: How much CPU to give the container. 1024 is 1 CPU
ContainerMemory:
Type: Number
Default: 3072
Description: How much memory in megabytes to give the container
Path:
Type: String
Default: "*"
Description: A path on the public load balancer that this service
should be connected to. Use * to send all load balancer
traffic to this service.
Priority:
Type: Number
Default: 1
Description: The priority for the routing rule added to the load balancer.
This only applies if your have multiple services which have been
assigned to different paths on the load balancer.
DesiredCount:
Type: Number
Default: 1
Description: How many copies of the service task to run.
Note cluster set up of SonarQube is exclusive to the Data Center Edition
CertificateId:
Type: String
Description: The ID of the certificate to use for HTTPS, e.g. 12345678-1234-1234-1234-123456789012
DBInstancePasswordSecretName:
Type: String
Description: The name of the secret in Secrets Manager that contains the database password
This will have been created as part of the stack creation for the 'rds.yaml' template
Resources:
# Security Groups
SonarQubeECSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${ServiceName}-ecs-sg
GroupDescription: Security group for SonarQube ECS cluster
VpcId: !Ref VpcId
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
DestinationSecurityGroupId: !ImportValue SonarQubeRDSSG
Description: Postgresql default port (Internal) for access from ECS cluster
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
Description: HTTPS access to the internet
- IpProtocol: tcp
FromPort: 53
ToPort: 53
CidrIp: !ImportValue vpc-cidr
Description: VPC DNS access
Tags:
- Key: Name
Value: !Sub ${ServiceName}-ecs-sg
SonarQubeEFSSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${ServiceName}-efs-sg
GroupDescription: Security group for SonarQube EFS
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 2049
ToPort: 2049
SourceSecurityGroupId: !Ref SonarQubeECSSecurityGroup
Description: NFS access from ECS cluster
Tags:
- Key: Name
Value: !Sub ${ServiceName}-efs-sg
SonarQubeELBSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${ServiceName}-elb-sg
GroupDescription: Security group for SonarQube ELB
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 9000
ToPort: 9000
DestinationSecurityGroupId: !Ref SonarQubeECSSecurityGroup
Tags:
- Key: Name
Value: !Sub ${ServiceName}-elb-sg
SonarQubeECSToEFSEgress:
Type: AWS::EC2::SecurityGroupEgress
Properties:
GroupId: !Ref SonarQubeECSSecurityGroup
IpProtocol: tcp
FromPort: 2049
ToPort: 2049
DestinationSecurityGroupId: !Ref SonarQubeEFSSecurityGroup
Description: EFS access from ECS cluster
SonarQubeECSFromELBIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref SonarQubeECSSecurityGroup
IpProtocol: tcp
FromPort: 9000
ToPort: 9000
SourceSecurityGroupId: !Ref SonarQubeELBSecurityGroup
Description: ELB to ECS access
SonarQubeECSToRDSIngress:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !ImportValue SonarQubeRDSSG
IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId: !Ref SonarQubeECSSecurityGroup
Description: Postgresql default port (Internal) for access from ECS cluster
#Load Balancer, TargetGroups and Listeners
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 300
HealthCheckProtocol: HTTP
HealthCheckPort: !Ref 'ContainerPort'
HealthCheckTimeoutSeconds: 120
HealthyThresholdCount: 2
TargetType: ip
Name: !Sub '${ServiceName}-tg'
Port: !Ref 'ContainerPort'
Protocol: HTTP
UnhealthyThresholdCount: 2
VpcId: !Ref VpcId
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub '${ServiceName}-elb'
Scheme: internet-facing
SecurityGroups:
- !Ref SonarQubeELBSecurityGroup
Subnets: !Ref PublicSubnetIds
Type: application
HttpLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
DependsOn:
- LoadBalancer
Properties:
DefaultActions:
- Type: redirect
RedirectConfig:
Protocol: HTTPS
Port: '443'
Host: '#{host}'
Path: '/#{path}'
Query: '#{query}'
StatusCode: HTTP_301
LoadBalancerArn:
Ref: LoadBalancer
Port: 80
Protocol: HTTP
HttpsLoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
DependsOn:
- LoadBalancer
Properties:
Certificates:
- CertificateArn: !Sub arn:aws:acm:${AWS::Region}:${AWS::AccountId}:certificate/${CertificateId}
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: 'forward'
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
HttpsLoadBalancerListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroup
Type: 'forward'
Conditions:
- Field: path-pattern
Values: [!Ref 'Path']
ListenerArn: !Ref HttpsLoadBalancerListener
Priority: !Ref 'Priority'
# EFS shared file system
FileSystem:
Type: AWS::EFS::FileSystem
Properties:
FileSystemTags:
- Key: Name
Value: !Sub '${ServiceName}-efs'
PerformanceMode: generalPurpose
Encrypted: true
LifecyclePolicies:
- TransitionToIA: AFTER_30_DAYS
ThroughputMode: bursting
BackupPolicy:
Status: DISABLED
AccessPoint1:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref FileSystem
PosixUser:
Uid: '1000'
Gid: '1000'
RootDirectory:
CreationInfo:
OwnerGid: '1000'
OwnerUid: '1000'
Permissions: '755'
Path: '/sonarqube_data'
AccessPointTags:
- Key: Name
Value: !Sub '${ServiceName}-data'
AccessPoint2:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref FileSystem
PosixUser:
Uid: '1000'
Gid: '1000'
RootDirectory:
CreationInfo:
OwnerGid: '1000'
OwnerUid: '1000'
Permissions: '755'
Path: '/sonarqube_extensions'
AccessPointTags:
- Key: Name
Value: !Sub '${ServiceName}-extensions'
AccessPoint3:
Type: AWS::EFS::AccessPoint
Properties:
FileSystemId: !Ref FileSystem
PosixUser:
Uid: '1000'
Gid: '1000'
RootDirectory:
CreationInfo:
OwnerGid: '1000'
OwnerUid: '1000'
Permissions: '755'
Path: '/sonarqube_logs'
AccessPointTags:
- Key: Name
Value: !Sub '${ServiceName}-logs'
MountTarget1:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: !Select [ 0, !Ref PrivateSubnetIds]
SecurityGroups:
- !Ref SonarQubeEFSSecurityGroup
MountTarget2:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: !Select [ 1, !Ref PrivateSubnetIds]
SecurityGroups:
- !Ref SonarQubeEFSSecurityGroup
MountTarget3:
Type: AWS::EFS::MountTarget
Properties:
FileSystemId: !Ref FileSystem
SubnetId: !Select [ 2, !Ref PrivateSubnetIds]
SecurityGroups:
- !Ref SonarQubeEFSSecurityGroup
# IAM Roles
SonarQubeTaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-task-role'
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies: []
SonarQubeTaskExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-execution-role'
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: [ecs-tasks.amazonaws.com]
Action: ['sts:AssumeRole']
Path: /
Policies:
- PolicyName: ECRTokenAccessPolicy
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'ecr:GetAuthorizationToken'
Resource: "*"
- PolicyName: ECRReadAccess
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'ecr:BatchCheckLayerAvailability'
- 'ecr:GetDownloadUrlForLayer'
- 'ecr:BatchGetImage'
Resource: !Sub 'arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${ECRRepository}'
- PolicyName: LogWriteAccess
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/ecs/${ServiceName}:log-stream:*'
- PolicyName: DatabaseSecretReadAccess
PolicyDocument:
Statement:
- Effect: Allow
Action:
- 'secretsmanager:GetSecretValue'
Resource:
!Join [
'',
[
'arn:aws:secretsmanager:',
!Ref AWS::Region,
':',
!Ref AWS::AccountId,
':',
'secret:',
!Ref DBInstancePasswordSecretName,
'-??????'
]
]
# log group
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${ServiceName}'
RetentionInDays: 14
# ECS Resources
SonarQubeCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub '${ServiceName}-cluster'
ClusterSettings:
- Name: containerInsights
Value: enabled
SonarQubeTaskDefinition:
Type: AWS::ECS::TaskDefinition
DependsOn:
- AccessPoint1
- AccessPoint2
- AccessPoint3
- FileSystem
Properties:
Family: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
ExecutionRoleArn: !Ref SonarQubeTaskExecutionRole
TaskRoleArn: !Ref SonarQubeTaskRole
Volumes:
- Name: sonarqube_data
EFSVolumeConfiguration:
FilesystemId: !Ref FileSystem
TransitEncryption: ENABLED
AuthorizationConfig:
AccessPointId: !Ref AccessPoint1
- Name: sonarqube_extensions
EFSVolumeConfiguration:
FilesystemId: !Ref FileSystem
TransitEncryption: ENABLED
AuthorizationConfig:
AccessPointId: !Ref AccessPoint2
- Name: sonarqube_logs
EFSVolumeConfiguration:
FilesystemId: !Ref FileSystem
TransitEncryption: ENABLED
AuthorizationConfig:
AccessPointId: !Ref AccessPoint3
ContainerDefinitions:
- Name: !Ref 'ServiceName'
Cpu: !Ref 'ContainerCpu'
Memory: !Ref 'ContainerMemory'
ReadonlyRootFilesystem: false
Essential: true
Image:
!Join [
'.',
[
!Ref AWS::AccountId,
'dkr.ecr',
!Ref AWS::Region,
!Sub 'amazonaws.com/${ECRRepository}:${ECRTag}'
]
]
PortMappings:
- ContainerPort: !Ref 'ContainerPort'
Name: !Sub '${ServiceName}-9000-tcp'
HostPort: 9000
AppProtocol: http
Protocol: tcp
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: ecs
Environment:
- Name: SONARQUBE_JDBC_URL
Value:
!Join [
'',
[
'jdbc:postgresql://',
!ImportValue SonarQubeDBInstanceEndpointAddress,
':5432/',
!ImportValue SonarQubeDBInstanceName
]
]
- Name: SONARQUBE_JDBC_USERNAME
Value: !Sub '{{resolve:secretsmanager:${DBInstancePasswordSecretName}:SecretString:username}}'
- Name: SONARQUBE_JDBC_PASSWORD
Value: !Sub '{{resolve:secretsmanager:${DBInstancePasswordSecretName}:SecretString:password}}'
- Name: SONAR_SEARCH_JAVAADDITIONALOPTS
Value: '-Dnode.store.allow_mmap=false,-Ddiscovery.type=single-node'
MountPoints:
- ContainerPath: /opt/sonarqube/data
SourceVolume: sonarqube_data
ReadOnly: false
- ContainerPath: /opt/sonarqube/extensions
SourceVolume: sonarqube_extensions
ReadOnly: false
- ContainerPath: /opt/sonarqube/logs
SourceVolume: sonarqube_logs
ReadOnly: false
Ulimits:
- HardLimit: 65535
Name: nofile
SoftLimit: 65535
SonarQubeService:
Type: AWS::ECS::Service
DependsOn: LoadBalancer
Properties:
ServiceName: !Ref 'ServiceName'
Cluster: !Ref SonarQubeCluster
LaunchType: FARGATE
DeploymentConfiguration:
MaximumPercent: 100
MinimumHealthyPercent: 0
DesiredCount: !Ref DesiredCount
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
SecurityGroups:
- !Ref SonarQubeECSSecurityGroup
Subnets:
- !Select [ 0, !Ref PrivateSubnetIds]
- !Select [ 1, !Ref PrivateSubnetIds]
- !Select [ 2, !Ref PrivateSubnetIds]
TaskDefinition: !Ref SonarQubeTaskDefinition
LoadBalancers:
- ContainerName: !Ref 'ServiceName'
ContainerPort: !Ref 'ContainerPort'
TargetGroupArn: !Ref 'TargetGroup'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment