Automating CI/CD with TeamCity and Ansible

In part one of this series we are going to explore a CI/CD option you may not be familiar with but should definitely be on your radar. I used Jetbrains TeamCity for several months at my last company and really enjoyed my time with it. A couple of the things I like most about it are:

  • Ability to declare global variables and have them be passed down to all projects
  • Ability to declare variables that are made up of other variables

I like to use private or self hosted Docker registries for a lot of my projects and one of the pain points I have had with some other solutions (well mostly Bitbucket) is that they don’t integrate well with these private registries and when I run into a situation where I am pushing an image to or pulling an image from a private registry it get’s a little messy. TeamCity is nice in that I can add a connection to my private registry in my root project and them simply add that as a build feature to any projects that may need it. Essentially, now I only have one place where I have to keep those credentials and manage that connection.

Another reason I love it is the fact that you can create really powerful build templates that you can reuse. This became very powerful when we were trying to standardize our build processes. For example, most of the apps we build are .NET backends and React frontends. We built docker images for every project and pushed them to our private registry. TeamCity gave us the ability to standardize the naming convention and really streamline the build process. Enough about that though, the rest of this series will assume that you are using TeamCity. This post will focus on getting up and running using Ansible.


Installation and Setup

For this I will assume that you already have Ansible on your machine and that you will be installing TeamCity locally. You can simply follow along with the installation guide here. We will be creating an Ansible playbook based on the following steps. If you just want the finished code, you can find it on my Gitea instance here:

Step 1 : Create project and initial playbook

To get started go ahead and create a new directory to hold our configuration:

mkdir ~/projects/teamcity-configuration-ansible 
touch install-teamcity-server.yml

Now open up install-teamcity-server.yml and add a task to install Java 17 as it is a prerequisite. You will need sudo for this task. ***As of this writing TeamCity does not support Java 18 or 19. If you try to install one of these you will get an error when trying to start TeamCity.

---
- name: Install Teamcity
  hosts: localhost
  become: true
  become_user: sudo

 # Add some variables to make our lives easier
  vars:
    java_version: "17"
    teamcity:
      installation_path: /opt/TeamCity
      version: "2023.11.4"
  
  tasks:
  - name: Install Java
    ansible.builtin.apt:
      name: openjdk-{{ java_version }}-jre-headless
      update_cache: yes
      state: latest
      install_recommends: no

The next step is to create a dedicated user account. Add the following task to install-teamcity-server.yml

  - name: Add Teamcity User
    ansible.builtin.user:
      name: teamcity

Next we will need to download the latest version of TeamCity. 2023.11.4 is the latest as of this writing. Add the following task to your install-teamcity-server.yml

  - name: Download TeamCity Server
    ansible.builtin.get_url:
      url: https://download.jetbrains.com/teamcity/TeamCity-{{teamcity.version}}.tar.gz
      dest: /opt/TeamCity-{{teamcity.version}}.tar.gz
      mode: '0770'

Now to install TeamCity Server add the following:

  - name: Install TeamCity Server
    ansible.builtin.shell: |
      tar xfz /opt/TeamCity-{{teamcity.version}}.tar.gz
      rm -rf /opt/TeamCity-{{teamcity.version}}.tar.gz
    args:
      chdir: /opt

Now that we have everything set up and installed we want to make sure that our new teamcity user has access to everything they need to get up and running. We will add the following lines:

  - name: Update permissions
    ansible.builtin.shell: chown -R teamcity:teamcity /opt/TeamCity

This gives us a pretty nice setup. We have TeamCity server installed with a dedicated user account. The last thing we will do is create a systemd service so that we can easily start/stop the server. For this we will need to add a few things.

  1. A service file that tells our system how to manage TeamCity
  2. A j2 template file that is used to create this service file
  3. A handler that tells the system to run systemctl daemon-reload once the service has been installed.

Go ahead and create a new templates folder with the following teamcity.service.j2 file

[Unit]
Description=JetBrains TeamCity
Requires=network.target
After=syslog.target network.target
[Service]
Type=forking
ExecStart={{teamcity.installation_path}}/bin/runAll.sh start
ExecStop={{teamcity.installation_path}}/bin/runAll.sh stop
User=teamcity
PIDFile={{teamcity.installation_path}}/teamcity.pid
Environment="TEAMCITY_PID_FILE_PATH={{teamcity.installation_path}}/teamcity.pid"
[Install]
WantedBy=multi-user.target

Your project should now look like the following:

$: ~/projects/teamcity-ansible-terraform
 .
├── install-teamcity-server.yml
└── templates
    └── teamcity.service.j2

1 directory, 2 files

That’s it! Now you should have a fully automated installed of TeamCity Server ready to be deployed wherever you need it. Here is the final playbook file, also you can find the most up to date version in my repo:

---
- name: Install Teamcity
  hosts: localhost
  become: true
  become_method: sudo

  vars:
    java_version: "17"
    teamcity:
      installation_path: /opt/TeamCity
      version: "2023.11.4"

  tasks:
  - name: Install Java
    ansible.builtin.apt:
      name: openjdk-{{ java_version }}-jdk # This is important because TeamCity will fail to start if we try to use 18 or 19
      update_cache: yes
      state: latest
      install_recommends: no

  - name: Add TeamCity User
    ansible.builtin.user:
      name: teamcity

  - name: Download TeamCity Server
    ansible.builtin.get_url:
      url: https://download.jetbrains.com/teamcity/TeamCity-{{teamcity.version}}.tar.gz
      dest: /opt/TeamCity-{{teamcity.version}}.tar.gz
      mode: '0770'

  - name: Install TeamCity Server
    ansible.builtin.shell: |
      tar xfz /opt/TeamCity-{{teamcity.version}}.tar.gz
      rm -rf /opt/TeamCity-{{teamcity.version}}.tar.gz
    args:
      chdir: /opt

  - name: Update permissions
    ansible.builtin.shell: chown -R teamcity:teamcity /opt/TeamCity

  - name: TeamCity | Create environment file
    template: src=teamcity.service.j2 dest=/etc/systemd/system/teamcityserver.service
    notify:
      - reload systemctl
  - name: TeamCity | Start teamcity
    service: name=teamcityserver.service state=started enabled=yes

  # Trigger a reload of systemctl after the service file has been created.
  handlers:
    - name: reload systemctl
      command: systemctl daemon-reload