Full SonarQube installation via Ansible
I had the basic idea to install SonarQube on a server fully automatically. This is of course nothing new. I've set that challenge for me to level up my Ansible skills a bit and also get more details into the setup for a later step while going into a Kubernetes cluster setup.
In relation to that I've setting up a server with other components like Gitea, Reposiory Manager(artipie), CI solution Woodpecker. All these components are behind a Nginx-proxy to handle TLS etc.
So the first decision which has to be made was, using a plain machine setup or using containers (in particular Docker in this case). I've decided to go the way with docker in the first place (later I will try the same with podman instead).
So to run SonarQube within a container is more or less "easy" because there already exists a container image. This means I have to configure that container "only" correctly to get that working. Ok sounds promising.
The next thing which is given by the documentation of SonarQube is that it is recommended to run SonarQube with an external database (for example PostgreSQL, Microsoft SQL Server or Oracle) instead of the builtin H2 (yes "only" a thing of correct configuring that). So we have the next participant within the game: PostgreSQL.
The target machine was installed with Debian 11 (Bullseye) as the base OS. So based on the setup of Docker Community Edition (Docker CE) the following components have to be removed (if existing) which I will express here as an Ansible script part:
1- name: Removed Old Installation (docker)
2 become: yes
3 ansible.builtin.package:
4 name: "{{item}}"
5 state: absent
6 loop:
7 - docker
8 - docker-enginge
9 - docker.io
10 - containerd
11 - runc
So now it's necessary to install the Docker CE edition onto that system, but before that, we have to install some software which is required first:
1- name: "Docker CE Requirements (Tools)"
2 become: yes
3 ansible.builtin.package:
4 name: "{{item}}"
5 state: present
6 update_cache: yes
7 loop:
8 - apt-transport-https
9 - ca-certificates
10 - curl
11 - gnupg-agent
12 - software-properties-common
13 - lsb-release
So now it's the right time to start with the installation of Docker CE. I have to admit that this part contains
distro-dependent parts (I assume that it can be replaced with more general solutions; That's the reason for
those FIXME
's; Tipps are welcome.):
1# FIXME: Unfortunately the following is distro dependent!
2- name: Docker CE - GPG key
3 become: yes
4 apt_key:
5 url: https://download.docker.com/linux/debian/gpg
6 state: present
7
8# FIXME: Unfortunately the following is distro dependent!
9- name: Docker CE - Added Docker Repository
10 become: yes
11 apt_repository:
12 repo: deb https://download.docker.com/linux/debian bullseye stable
13 state: present
14- name: Docker CE - Installation
15 become: yes
16 ansible.builtin.package:
17 name: "{{item}}"
18 state: latest
19 update_cache: yes
20 loop:
21 - docker-ce
22 - docker-ce-cli
23 - containerd.io
24 - docker-compose-plugin
25- name: make sure docker is active
26 become: yes
27 ansible.builtin.service:
28 name: docker
29 state: started
30 enabled: yes
The basic software and machine setup contain the installation of nginx as well as the setup of Letsencrypt to support TLS for accessing this machine. Furthermore, a setup of a firewall configuration (only open port 80/443) etc. which are not part of this post.
So now we can focus on installing SonarQube. The first thing we have to do is to get the correct container image and afterwards we will configure them in the "hopefully" correct way.
If you assume that you have a running system which contains nginx as well as Letsencrypt and also assume that this machine is installed on the dangerous internet we have to take some precautions.
The first important step during the installation of SonarQube / PostgreSQL is to stop the nginx proxy on that machine to prevent access via http/https port during the installation because SonarQube has some defaults for username/passwords which can easily be figured out. So that results in the first step for the Ansible playbook:
1 tasks:
2 # Before we start to install SonarQube we deactivate
3 # nginx to prevent any access to SonarQube from outside
4 # during the installation.
5 - name: Stop nginx for security reasons.
6 ansible.builtin.service:
7 name: nginx
8 state: stopped
The next thing is to configure the sysctl configuration to run SonarQube. Nicely Ansible has direct support for that:
1# This is needed to run SonarQube (more accurate ElasticSearch):
2- name: "sysctl -w vm.max_map_count=262144"
3 ansible.posix.sysctl:
4 name: vm.max_map_count
5 value: '262144'
6 sysctl_set: yes
So another decision has to be made. Should SonarQube keep results during a machine/docker engine restart or upgrade
SonarQube? I would say it makes sense to keep the data. That means we have to configure persistent volumes for the
containers (Yes currently the user is root
incl. the group; one of the things I don't like at the moment.).
1- name: Create volume directories.
2 become: yes
3 ansible.builtin.file:
4 owner: root
5 group: root
6 name: "{{item}}"
7 state: directory
8 loop:
9 - "{{ sonarqube_volumes_base }}"
10 - "{{ sonarqube_volumes_base }}/conf"
11 - "{{ sonarqube_volumes_base }}/data"
12 - "{{ sonarqube_volumes_base }}/extensions"
13 - "{{ sonarqube_volumes_base }}/logs"
14 - "{{ sonarqube_volumes_base }}/postgresql"
The given sonarqube_volumes_base
is simply a variable which contains the base location of those volumes on the system.
So we continue pulling the container images (postgres_image
is set as postgres:15-alpine
):
1- name: Pull PostgreSQL Image
2 community.docker.docker_image:
3 name: "{{ postgres_image }}"
4 source: pull
5
6- name: Pull SonarQube Image
7 community.docker.docker_image:
8 name: sonarqube:latest
9 source: pull
So first we should start the PostgreSQL container because it's later being used by SonarQube:
1- name: Create a network (SonarQube)
2 community.docker.docker_network:
3 name: net_sonarqube
4
5- name: Start PostgreSQL container
6 community.docker.docker_container:
7 name: postgresql-sonarqube
8 image: "{{ postgres_image }}"
9 recreate: true
10 state: "started"
11 restart_policy: "unless-stopped"
12 env:
13 POSTGRES_PASSWORD: "{{ postgres_password }}"
14 POSTGRES_USER: "{{ postgres_user }}"
15 POSTGRES_DB: "{{ postgres_db }}"
16 networks:
17 - name: "net_sonarqube"
18 mounts:
19 - source: volume_sonar_postgres
20 target: /var/lib/postgresql/data
So I've created a separate network and started the postgresql container using some variables to set username, password and db name. I've extracted them into variables, to make them easier to access and definable via external sources or read them via encrypted file (Ansible vault).
So now the vital part of that setup. Starting the SonarQube container. It's necessary to mention that I've been using
the variable for the JDBC connection (username, password and DB as well.). The name postgresql-sonarqube
is the
hostname based on the previous name definition for the PostgreSQL database which can simply be used because both
containers are on the same network.
1- name: Start SonarQube container
2 community.docker.docker_container:
3 name: SonarQube
4 image: sonarqube:latest
5 recreate: true
6 state: "started"
7 restart_policy: "unless-stopped"
8 restart: true
9 ports:
10 - "{{ sonarqube_port }}:9000"
11 env:
12 SONAR_JDBC_USERNAME: "{{ postgres_user }}"
13 SONAR_JDBC_PASSWORD: "{{ postgres_password }}"
14 SONAR_JDBC_URL: "jdbc:postgresql://postgresql-sonarqube:5432/{{ postgres_db }}"
15 networks:
16 - name: "net_sonarqube"
17 mounts:
18 - source: volume_sonar_conf
19 target: /opt/sonarqube/conf
20 - source: volume_sonar_data
21 target: /opt/sonarqube/data
22 - source: volume_sonar_extensions
23 target: /opt/sonarqube/extensions
24 - source: volume_sonar_logs
25 target: /opt/sonarqube/logs
So we are done. No, unfortunately, we are not! If we would open the access to SonarQube (via starting the nginx proxy) this would mean having only configured the default username/password which is from a security point of view a bad idea. Also, does the setup really work? Is SonarQube really up and running? And how to access SonarQube to start an analysis?
So first we need to find a way to check if SonarQube is really ready to be used. Why? Simply we need to find a way to
change the default password of the admin
user. So let's start with checking if SonarQube is ready to be used. This can
be achieved by using the following:
1- name: "Wait until SonarQube Container is reachable via TCP/IP."
2 ansible.builtin.wait_for:
3 host: "localhost"
4 port: "{{ sonarqube_port }}"
5 state: drained
6 delay: 5
7 connect_timeout: 15
8 timeout: 30
By using that we simply wait until the container is accessible via TCP/IP. Don't be astonished during an automatic setup is a thing to check because it takes a period. So next check if the SonarQube container is ready (healthy). Naively you might go the way to check the URL of SonarQube within the container and if answers with a 200 HTTP status code everything is fine.
Yes, I'm admitting, that was my first thought as well. I have learned that this is not the correct approach here. The issue is that the URL of SonarQube is available very early and will answer with a 200 code but the SonarQube itself is not ready at that time. How did I find out? Failed the following steps to change the admin password.
So to check the health of SonarQube it's necessary to check the health status of SonarQube itself which
is reported via JSON information. The so-called
status GREEN
with HTTP status 200. This looks in Ansible like this:
1- name: Check SonarQube to be healthy.
2 ansible.builtin.uri:
3 url: "http://localhost:{{ sonarqube_port }}/api/system/health"
4 user: "{{ sonarqube_admin_login }}"
5 password: "{{ sonarqube_admin_password }}"
6 method: GET
7 force_basic_auth: yes
8 register: response
9 until:
10 - "response.status == 200"
11 - "'GREEN' == response.json.health"
12 retries: 20 # 20 * 5 seconds = 100 seconds.
13 delay: 5 # every 5 seconds
During an installation, you can observe that this can take two or three rounds of checking depending on the system's performance. Keep in mind we are using the default username/password. That makes it possible to access the rest API during the setup. After that step, we are not sure about two things. First SonarQube has started up correctly and a nice side effect is we have an option to change access information (change password etc.) for further setup steps.
For a real CI setup, we have to make it possible for a given CI system to have access to SonarQube to push analyse
information to SonarQube. That should be done by creating a separate user (not using admin account) and only accessible
via user token. To achieve that we have to create a new user first. Luckily that can be done via REST API as well. So
create a ci user via REST. One thing is important to encode the username correctly (urlencode
in Ansible) otherwise,
you might get issues while using other characters than ASCII in your sonarqube_ci_name
.
1- name: Create CI User
2 ansible.builtin.uri:
3 url: "http://localhost:{{ sonarqube_port }}/api/users/create?local=true&login={{ sonarqube_ci_login }}&name={{ sonarqube_ci_name | urlencode }}&password={{ sonarqube_ci_password }}"
4 user: "{{ sonarqube_admin_login }}"
5 password: "{{ sonarqube_admin_password }}"
6 method: POST
7 force_basic_auth: yes
8 register: ciresponse
9 failed_when: ciresponse.status != 200
That will create the given user (sonarqube_ci_name
) with the appropriate password (sonarqube_ci_password
). The next
thing is to create a user token which is used fom sending analysing results to SonarQube.
1- name: Create CI User Token.
2 ansible.builtin.uri:
3 url: "http://localhost:{{ sonarqube_port }}/api/user_tokens/generate?login={{ sonarqube_ci_login }}&name={{ sonarqube_ci_token_purpose | urlencode }}"
4 user: "{{ sonarqube_ci_login }}"
5 password: "{{ sonarqube_ci_password }}"
6 method: POST
7 force_basic_auth: yes
8 register: ciresponsetoken
9 failed_when: ciresponsetoken.status != 200
10- debug:
11 msg: "TOKEN: {{ ciresponsetoken.json }}"
This created token is necessary to transfer to a CI system like Woodpecker (or other tools
like Jenkins etc.). That means the steps here must be combined with the setup for Woodpecker (that's one of the lacking
things at the moment which I have not implemented yet). And yes I know that I'm writing the token to the console
via debug
!.
So finally we have to change the default password (admin user) of SonarQube via REST as well. Unfortunately, I have no way to make that during the initial setup of the container (env. variable or alike). I've found several ways to change the entry in the database (PostgreSQL) or reset it default but that is too complicated. So the simplest solution I've found is via REST:
1- name: Change Password of admin user.
2 ansible.builtin.uri:
3 url: "http://localhost:{{ sonarqube_port }}/api/users/change_password?login={{ sonarqube_admin_login }}&password={{ sonarqube_admin_newpassword }}&previousPassword={{ sonarqube_admin_password }}"
4 user: "{{ sonarqube_admin_login }}"
5 password: "{{ sonarqube_admin_password }}"
6 method: POST
7 force_basic_auth: yes
8 register: responsecpw
9 failed_when: responsecpw.status != 204
That will set the password of the admin
account to the given sonarqube_admin_newpassword
. So now the setup of
SonarQube including the database is done. We can make it accessible to the real world. So start the nginx proxy as the
final step.