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.