In today’s fast-paced development environment, maintaining high code quality is crucial. SonarQube is a powerful, open-source tool that helps achieve this by continuously inspecting code for quality and security issues. It can be hosted on-premise or on any cloud provider’s instances, making it versatile for various deployment needs. Setting up SonarQube on a host can be a tedious task, involving the installation and configuration of various packages. However, this process can be significantly simplified using Docker.
AWS Cloud provides a robust platform for deploying applications, while AWS CDK (Cloud Development Kit) simplifies cloud infrastructure management through code.
In this blog, I’ll provide a brief overview of SonarQube and AWS CDK and walk you through setting up a SonarQube container on an AWS EC2 instance using AWS CDK. This streamlined approach will have you up and running in just minutes.
In case you wish to set up SonarQube without writing the CDK code yourself, you can clone my aws-cdk-sonarqube repo from GitHub and follow the instructions in the README file to quickly set up SonarQube. For a detailed understanding, follow along with me in this blog to create your own CDK project.
Prerequisites
Before we dive in, ensure you have the following:
- An active AWS account.
- AWS CLI Installed and configured.
- Node.js must be installed.
- AWS CDK installed globally. If not, install it with:
npm install -g aws-cdk
Step 1: Create a CDK Project
Let’s create a new CDK project
mkdir aws-cdk-sonarqube
cd aws-cdk-sonarqube
cdk init app --language typescript
Install the necessary libraries:
npm install @aws-cdk/aws-ec2 @aws-cdk/aws-iam @aws-cdk/core cdk-ec2-key-pair dotenv path
Step 2: Define the CDK Stack
We will define two CDK stacks:
- VPC Stack: This stack sets up the VPC network and subnet.
- SonarQube Stack: This stack launches an EC2 instance and configures SonarQube.
2.1. VPC Stack
First, we will delete the lib/sonarqube-setup-stack.ts
file and create a new file lib/vpc-stack.ts
. This stack will handle the creation of the VPC and its subnets.
Delete the existing file:
rm lib/sonarqube-setup-stack.ts
Create and define the VPC stack in lib/vpc-stack.ts
:
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { Vpc, SubnetType } from "aws-cdk-lib/aws-ec2";
export class VpcStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Creating a VPC with 1 AZ and 1 Public Subnet
new Vpc(this, "vpc", {
vpcName: `sonar-vpc`,
maxAzs: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: "public-subnet",
subnetType: SubnetType.PUBLIC,
},
],
});
}
}
Explanation:
The VPC stack creates the network infrastructure required for our SonarQube instance. It includes:
- VPC: A virtual network that isolates our AWS resources from other networks. It provides a secure and isolated environment where our resources can operate.
- Public Subnet: A subnet within the VPC that is exposed to the internet. This allows the EC2 instance running SonarQube to be reachable from the outside.
2.2. SonarQube Stack
Next, we will create the SonarQube stack to launch an EC2 instance and configure SonarQube.
Create a new file lib/sonarqube-stack.ts
:
import * as cdk from "aws-cdk-lib";
import {
Instance,
InstanceType,
MachineImage,
SubnetType,
SecurityGroup,
Peer,
Port,
Vpc,
BlockDeviceVolume,
EbsDeviceVolumeType,
CfnEIP,
CfnEIPAssociation,
} from "aws-cdk-lib/aws-ec2";
import { Role, ServicePrincipal, ManagedPolicy } from "aws-cdk-lib/aws-iam";
import { Construct } from "constructs";
import path from "path";
import * as fs from "fs";
import { KeyPair } from "cdk-ec2-key-pair";
export class SonarqubeStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Lookup the existing VPC from VPC stack
const vpc = Vpc.fromLookup(this, "vpc", {
vpcName: process.env.VPC_NAME,
});
// Create a Security Group for SonarQube
const securityGroup = new SecurityGroup(this, "sonarqubesg", {
securityGroupName: `sonarqube-sg`,
vpc,
description: "Allow ssh and http access to EC2 instance",
allowAllOutbound: true,
});
// Allow inbound traffic on SSH Port 22 and SonarQube Port 9000
securityGroup.addIngressRule(
Peer.anyIpv4(),
Port.tcp(22),
"Allow SSH access from anywhere"
);
securityGroup.addIngressRule(
Peer.anyIpv4(),
Port.tcp(9000),
"Allow SonarQube access from anywhere"
);
// Create an IAM role for the EC2 instance
const role = new Role(this, "sonarqubeinstancerole", {
assumedBy: new ServicePrincipal("ec2.amazonaws.com"),
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName(
"AmazonEC2ContainerRegistryReadOnly"
),
ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ReadOnlyAccess"),
],
});
// Create a new key pair and store it into Secrets Manager
const key = new KeyPair(this, "mykeypair", {
keyPairName: `aws-sonarqube-keypair`,
storePublicKey: true,
});
key.grantReadOnPublicKey(role); // Grant read access to the instance role
// Define the EC2 instance
// Root volume is encrypted and Delete on termination flag is disabled
const instance = new Instance(this, "sonarqubeinstance", {
vpc,
instanceType: new InstanceType("t3.medium"),
machineImage: MachineImage.latestAmazonLinux2023(),
vpcSubnets: { subnetType: SubnetType.PUBLIC },
securityGroup,
role,
keyName: key.keyPairName,
blockDevices: [
{
deviceName: "/dev/xvda", // Root volume
volume: BlockDeviceVolume.ebs(30, {
encrypted: true,
deleteOnTermination: false,
volumeType: EbsDeviceVolumeType.GP3,
}),
},
],
});
// Create a Elastic IP // OPTIONAL
const eip = new CfnEIP(this, "eip");
// Output the Elastic IP address
new cdk.CfnOutput(this, "ElasticIPAddress", { value: eip.ref, description: "Elastic IP Address of SonarQube Server" });
// Associate the Elastic IP with the EC2 instance
new CfnEIPAssociation(this, "eipassociation", {
eip: eip.ref,
instanceId: instance.instanceId,
});
// Path to the docker-compose file
const dockerComposePath = path.resolve(__dirname, "docker-compose.yml");
// Read the docker-compose file content and encode it in Base64
const dockerComposeContent = fs.readFileSync(dockerComposePath, "utf8");
const dockerComposeBase64 =
Buffer.from(dockerComposeContent).toString("base64");
// Add User Data to install Docker, Docker Compose, and run Docker Compose
instance.addUserData(
`#!/bin/bash
dnf update -y
dnf install -y docker
systemctl start docker
systemctl enable docker
usermod -aG docker ec2-user
# Install Docker Compose
curl -L "https://github.com/docker/compose/releases/download/v2.17.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
echo "vm.max_map_count=262144" >> /etc/sysctl.conf
echo "fs.file-max=65536" >> /etc/sysctl.conf
sudo sysctl -p
sudo usermod -aG docker $USER
# Decode the Base64 encoded docker-compose.yml content and write it to a file
echo "${dockerComposeBase64}" | base64 --decode > /home/ec2-user/docker-compose.yml
chown ec2-user:ec2-user /home/ec2-user/docker-compose.yml
# Run Docker Compose
cd /home/ec2-user
docker-compose up -d
`
);
}
}
Explanation:
The SonarQube stack sets up the EC2 instance and configures it to run SonarQube using Docker. Here’s what each part does:
Security Group:
- Allows inbound SSH access on port 22, enabling remote management of the instance.
- Allows inbound HTTP access on port 9000, which is necessary for accessing the SonarQube web interface.
IAM Role:
- Grants the EC2 instance permissions to interact with AWS services, such as reading from Amazon ECR (if Docker images are stored there) and basic EC2 access.
Key Pair:
- Creates a key pair for secure SSH access to the EC2 instance.
EC2 Instance:
- Runs SonarQube in a Docker container. The instance is provisioned with:
- Instance Type:
t3.medium
for a balance of resources. - Machine Image: Amazon Linux 2023 for a secure and up-to-date OS.
- Block Device: An encrypted EBS volume for storing SonarQube data, ensuring data security and persistence.
- Elastic IP: Provides a static IP for consistent access and outputs it into a console.
User Data Script:
- Automates the setup of Docker and Docker Compose on the instance.
- Creates and runs a
docker-compose.yml
file to deploy SonarQube and its PostgreSQL database in Docker containers. - Configures system settings to ensure SonarQube runs smoothly.
Create a new file lib/docker-compose.yml
:
version: "3"
services:
sonarqube:
image: sonarqube:community
restart: unless-stopped
depends_on:
- db
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: sonar
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
ports:
- "9000:9000"
db:
image: postgres:12
restart: unless-stopped
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: sonar
volumes:
- postgresql:/var/lib/postgresql
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql:
postgresql_data:
The above docker-compose configuration sets up SonarQube and PostgreSQL in separate containers. SonarQube runs on port 9000 and uses persistent volumes to store data, extensions, and logs. It connects to PostgreSQL, which also uses persistent volumes to maintain database files and data. This setup ensures that both SonarQube and PostgreSQL are automatically restarted if stopped, and their data remains intact across container restarts, simplifying deployment and management.
NOTE: SonarQube requires a database to store code quality and security scan results. Although SonarQube includes an embedded H2 database for testing, a dedicated production database is recommended.
2.3. Update the Bin File
In the bin/aws-cdk-sonarqube.ts
file, import and instantiate our stacks.
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { VpcStack } from '../lib/vpc-stack';
import { SonarqubeStack } from '../lib/sonarqube-stack';
import path from 'path';
import * as dotenv from 'dotenv';
dotenv.config({ path: path.resolve(__dirname, '../.env') });
// Create an application instance
const app = new cdk.App();
const awsenvironment = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION };
const VPCstack = new VpcStack(app, 'VPC-stack', {
env: awsenvironment,
stackName: `vpcstack`,
description: `VPC Network stack for SonarQube`,
});
const SonarQubeStack = new SonarqubeStack(app, 'Sonarqube-stack', {
env: awsenvironment,
stackName: `sonarqubestack`,
description: `SonarQube stack`,
});
SonarQubeStack.addDependency(VPCstack);
2.4. Add Environment Variables
Create a .env
file in the project root:
CDK_DEFAULT_ACCOUNT=12345678910
CDK_DEFAULT_REGION=ap-south-1
VPC_NAME=sonarqube-vpc
Our CDK stack is now ready and we are now good to deploy the stack into our AWS account.
Step 3: Deploy the Stack
- Bootstrap the CDK Environment
cdk bootstrap
This command sets up the initial resources that AWS CDK needs in our AWS account, such as an S3 bucket for storing the CDK assets and an IAM role for deploying stacks.
Once cdk is bootstrapped, new stack CDKToolkit will be created.
2. Synthesize & deploy the VPC-stack
cdk synth VPC-stack
cdk deploy VPC-stack
Wait a few minutes until the VPC stack is deployed.
As our VPC-stack is created successfully, now let’s deploy the Sonarqube-stack.
3. Synthesize & deploy the Stack:
cdk synth Sonarqube-stack
cdk deploy Sonarqube-stack
Wait a few minutes until the SonarQube stack is deployed.
Once both stacks are successfully deployed, it’s time to access SonarQube.
Step 4: Accessing the SonarQube
After deploying the stacks, copy the Elastic IP address from the output of the SonarQube stack and navigate to:
http://<ELASTIC_IP>:9000
The SonarQube server should be up and running, listening on port 9000. Use the default credentials (username: admin
, password: admin
) to log in. You will be prompted to set new admin credentials.
Once the password is set successfully, you will be redirected to the main SonarQube projects screen.
Congratulations 🎉, Our SonarQube server is now ready to create projects and inspect the code for quality and security issues.
Step 5: Destroy the CDK Stack
If you have followed along with me for learning purpose, make sure to destroy the CDK stack; otherwise, AWS will be charging us for our resources.
Run the below command to destroy our CDK stack.
cdk destroy --all
Conclusion
I hope you found this blog helpful for setting up SonarQube on AWS Cloud using AWS CDK. By leveraging AWS CDK and Docker, we’ve streamlined the deployment process, ensuring a robust and scalable setup for continuous code quality analysis. SonarQube will now serve as a valuable tool in your CI/CD pipeline, helping to maintain high code quality and security standards.
If you found this post helpful, give it a 👏 and follow for more useful blogs. Thanks, and have a great day!