As part of a project that uses Terraform to build its infrastructure, I have dealt with the issue of providing Terraform modules via a private registry. Since I had no experience with this until now, I decided to implement a small PoC. Incidentally, Terraform Cloud wasn’t an option in this case, so I had to look elsewhere. I quickly realized that this topic is still in its infancy: There are very few private registries that support the Terraform API specification. After comparing Citizen and Anthology, I finally decided to give Citizen a try. With Citizen, I just had the feeling that it was a bit more mature than Anthology due to better documentation - although Citizen is far from mature either.

In the absence of a suitable test environment, I decided to set up the PoC locally using Docker.

First of all I needed a working Docker installation. Since I used Windows, I had a local Docker Desktop installation running under the hood on Ubuntu 20.04 (WSL). To simplify the whole thing, I also used Docker compose. However, this is not absolutely necessary.

First steps

A first run with docker run -d -p "3000:3000" ghcr.io/outsideris/citizen:latest and subsequent browser-call via localhot:3000 quickly showed me that the Docker image worked. So I started playing around with the environment variables and mounts. Transferred to a docker-compose.yaml file it looked like this:

services:
    citizen:
        image: ghcr.io/outsideris/citizen:latest
        ports:
            - 3000:3000
        volumes:
            - ./citizen/cit_modules:/var/modules
            - ./citizen/cit_db:/var/cit_db
        environment:
            - CITIZEN_STORAGE_PATH=/var/modules
            - CITIZEN_STORAGE=file
            - CITIZEN_DB_DIR=/var/cit_db

What I did here was basically to persist the database as well as the published modules by mounting two folders into the docker container along with configuring the paths to store this data. I also provided some environment variables to control, where citizen stores its data and how it is persisted. Have a look at the citizen documentation to see the full list of possibilities.

Publishing a module

The next step now was to publish a module. Fortunately, citizen comes with a handy citizen cli utility to do so. But before I was able to publish a module, I needed to create one. Fortunately again, this was a very easy task. I just needed to create a folder with an empty main.tf file:

mkdir simonmod
cd simonmod
touch main.tf

Setting up SSL

Before I was able to publish a module, I needed to setup an SSL certificate so that I was able to reach citizen using HTTPS. Doing so locally required some more steps (inspired by this blog entry):

# switch to root user
sudo -s
# install necessary packages
apt-get update
apt install libnss3-tools -y
# download mkcert
wget https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64
# move mkcert to correct folder, mame it executable
mv mkcert-v1.4.3-linux-amd64 /usr/local/bin
ln -s mkcert-v1.4.3-linux-amd64 mkcert
chmod +x mkcert-v1.4.3-linux-amd64
# install mkcert and create certificate
mkcert -install
mkcert -cert-file localhost.crt -key-file localhost.key citizen.local

The above steps install mkcert which is required to create SSL certificates for local testing. The result is two files, the certificate along with a private key. I created this certificate for the domain citizen.local. In order to resolve this, I needed to edit my etc/hosts file accordingly.

Furthermore, I needed a proxy which was able to deliver content via SSL. To so so, I had to enhance the docker-compose.yaml mentioned earlier:

services:
    citizen:
        image: ghcr.io/outsideris/citizen:latest
        ports:
            - 3000:3000
        volumes:
            - ./citizen/cit_modules:/var/modules
            - ./citizen/cit_db:/var/cit_db
        environment:
            - CITIZEN_STORAGE_PATH=/var/modules
            - CITIZEN_STORAGE=file
            - CITIZEN_DB_DIR=/var/cit_db
    proxy:
        image: nginx:1.19.10-alpine
        ports:
            - 80:80
            - 443:443
        volumes:
            - ./proxy/conf/nginx.conf:/etc/nginx/nginx.conf
            - ./proxy/certs:/etc/nginx/certs
        depends_on: - citizen

As you can see in above mentioned file, the proxy also includes two mounts. One mount is used to include the nginx.conf file to configure the nginx proxy. The other is required to include the previously generated certificate files. The contents of the nginx.conf is as follows:

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;
        server_name citizen.local;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl;
        server_name citizen.local;

        ssl_certificate /etc/nginx/certs/localhost.crt;
        ssl_certificate_key /etc/nginx/certs/localhost.key;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;

        location / {
            proxy_buffering off;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-Host $host;
            proxy_set_header X-Forwarded-Port $server_port;

            proxy_pass http://citizen:3000;
        }
    }
}

The directory structure now looked as follows:

Now everything was in place and I was able to start the two containers with docker-compose up -d.

Publish

After setting up the prerequisites, I was able to move on to publish the module. When at the modules directory, thanks to docker using the cli utility of citizen was a one-liner:

docker run -e CITIZEN_ADDR=https://citizen.local:3000 ghcr.io/outsideris/citizen:latest citizen module simon simonmod aws 0.0.1

Visiting the citizen UI again showed me that publishing the module was successful:

I was also able to see this by the files created:

Include the module

Including the module was an easy part, again. All I needed to do was to create a terraform file (e.g. main.tf) and include the module there:

terraform {
    required_providers {
        aws = {
            source = "hashicorp/aws"
            version = "~> 3.0"
        }
    }
}

module "simonmod" {
    source = "citizen.local/simon/simonmod/aws"
    version = "0.0.1"
}

After saving the file, running terraform init showed me that downloading and including the module worked as expected.