A brief history Link to heading

The Gogs project has been proudly sponsored by DigitalOcean since 2015 via its Open Source Credits program. Benefited from that, the official website (https://gogs.io) and demo site (https://try.gogs.io) infrastructure has been hosted on DigitalOcean ever since.

For a very long time, we have been using a beefy machine (a DigitalOcean Droplet) to host everything we use, including the service itself, the data storage, the database, and almost everything that’s related.

Over the years, DigitalOcean has added a lot more features than we ever anticipated. Therefore, in the beginning of this year, we decided to get things to be state-of-the-art by revamping the project infrastructure setup, fully utilizing the amazing platform provided by DigitalOcean, as well as adopting the cool kid Infrastructure as Code (IaC) to replace hand-rolled scripts and playbooks.

In this post, I will walk you through how the new infrastructure of the Gogs project is set up with some details.

The grand scheme Link to heading

First of all, let’s present to you the often the most exciting and attracting part of an infrastructure setup, the architecture diagram!

Architecture diagram

As you can see, the actual infrastructure is fully managed by the IaC provider (in this case, the Pulumi, will get to that in a bit), all the way from the DNS, to the IP address of the droplet, the firewall, the container registry, the monitoring, the data storage, finally to the database backends. It’s truly amazing to see how many features we are able to get out of the DigitalOcean!

Continuous deployment Link to heading

On every commit to the main branch of the gogs/gogs repository, it builds and pushes a new Docker image to the DigitalOcean Container Registry:

...
      - name: Login to DigitalOcean Container registry
        uses: docker/login-action@v3
        with:
          registry: registry.digitalocean.com
          username: ${{ secrets.DIGITALOCEAN_USERNAME }}
          password: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
      - name: Build and push images
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          push: true
          tags: |
            ...
            registry.digitalocean.com/gogs/gogs:latest            
...

Then, we have a cron job setup on the droplet gogs-do-nyc3-01 to pull down the latest Docker image and restart the demo deployment. This makes sure we dogfood our commits in the fastest way possible to address any obvious show-stoppers if they ever occur.

Database connections Link to heading

The demo site takes full advantages of the DigitalOcean Managed Databases by using its managed PostgreSQL and Caching (which is actually Redis). The Gogs service is configured to connect to them via private connections through the VPC network, and TLS is always required for both of them:

[database]
TYPE     = postgres
HOST     = private-gogs-postgresql-nyc3-01-....ondigitalocean.com:...
NAME     = gogs
SCHEMA   = public
USER     = ...
PASSWORD = ...
SSL_MODE = require

[session]
PROVIDER = redis
PROVIDER_CONFIG = network=tcp,addr=private-gogs-redis-nyc3-01-....ondigitalocean.com:...,password=...,tls=true

Infrastructure as Code Link to heading

Based on our research, Terraform and Pulumi are two-most popular providers and competitors in the IaC space. Because we have used Terraform extensively at work (and of course, encountered some dissatisfactions), and one of our former colleague strongly recommended Pulumi (because he was the founding engineer of Pulumi 😂), we decided to give Pulumi a try see how it compares to Terraform (this will be a separate blog post for another time).

Below is a quick peak of how the Pulumi stack look like for the Gogs project infrastructure:

$ pulumi stack -s default
...
Current stack resources:
    TYPE                                                       NAME
    pulumi:pulumi:Stack                                        gogs-default
    ├─ digitalocean:index/uptimeCheck:UptimeCheck              try.gogs.io-uptime-check
    ├─ cloudflare:index/record:Record                          gogs.io
    ├─ cloudflare:index/record:Record                          try.gogs.io-TXT-SPF
    ├─ cloudflare:index/record:Record                          gogs.io-AAAA
    ├─ cloudflare:index/record:Record                          dl.gogs.io
    ├─ cloudflare:index/record:Record                          dl.gogs.io-AAAA
    ├─ cloudflare:index/record:Record                          www.gogs.io
    ├─ cloudflare:index/record:Record                          crowdin.gogs.io
    ├─ cloudflare:index/record:Record                          email.mg.gogs.io
    ├─ cloudflare:index/record:Record                          email.try.gogs.io
    ├─ cloudflare:index/record:Record                          gogs.io-MX-1
    ├─ cloudflare:index/record:Record                          try.gogs.io-MX-1
    ├─ cloudflare:index/record:Record                          gogs.io-MX-2
    ├─ cloudflare:index/record:Record                          try.gogs.io-MX-2
    ├─ digitalocean:index/containerRegistry:ContainerRegistry  gogs
    ├─ cloudflare:index/record:Record                          gogs.io-MX-3
    ├─ digitalocean:index/volume:Volume                        gogs-volume-nyc3-01
    ├─ cloudflare:index/record:Record                          admin._domainkey.gogs.io
    ├─ digitalocean:index/databaseCluster:DatabaseCluster      gogs-postgresql-nyc3-01
    ├─ cloudflare:index/record:Record                          gogs.io-TXT-SPF
    ├─ digitalocean:index/databaseCluster:DatabaseCluster      gogs-redis-nyc3-01
    ├─ cloudflare:index/record:Record                          krs._domainkey.try.gogs.io
    ├─ cloudflare:index/record:Record                          mg.gogs.io-TXT-SPF
    ├─ digitalocean:index/uptimeCheck:UptimeCheck              gogs.io-uptime-check
    ├─ cloudflare:index/record:Record                          pic._domainkey.mg.gogs.io
    ├─ digitalocean:index/uptimeAlert:UptimeAlert              try.gogs.io-uptime-alert
    ├─ digitalocean:index/droplet:Droplet                      gogs-do-nyc3-01
    ├─ digitalocean:index/databaseDb:DatabaseDb                gogs-postgresql-nyc3-01-db-gogs
    ├─ digitalocean:index/uptimeAlert:UptimeAlert              gogs.io-uptime-alert
    ├─ cloudflare:index/record:Record                          try.gogs.io-AAAA
    ├─ digitalocean:index/project:Project                      gogs
    ├─ digitalocean:index/reservedIp:ReservedIp                gogs-demo-ip
    ├─ digitalocean:index/firewall:Firewall                    gogs-demo-firewall
    ├─ digitalocean:index/databaseFirewall:DatabaseFirewall    gogs-redis-nyc3-01-firewall
    ├─ digitalocean:index/monitorAlert:MonitorAlert            gogs-demo-monitor-cpu
    ├─ digitalocean:index/databaseFirewall:DatabaseFirewall    gogs-postgresql-nyc3-01-firewall
    ├─ cloudflare:index/record:Record                          try.gogs.io
    ├─ digitalocean:index/monitorAlert:MonitorAlert            gogs-demo-monitor-memory
    ├─ digitalocean:index/monitorAlert:MonitorAlert            gogs-demo-monitor-disk
    ├─ pulumi:providers:digitalocean                           default_4_34_0
    └─ pulumi:providers:cloudflare                             default_5_42_0
    ...

Because we do not use the Pulumi Cloud, we need to find a self-managed state backend for the Pulumi to work. Luckily, we can use DigitalOcean Spaces because it is S3-compatible. All we need to do is to initialize Pulumi using a “S3” backend:

aws --profile digitalocean configure set region "us-east-1" # Can be any region
aws --profile digitalocean configure set aws_access_key_id "{ACCESS KEY}"
aws --profile digitalocean configure set aws_secret_access_key "{SECRET KEY}"

pulumi login "s3://{BUCKET NAME}?endpoint=nyc3.digitaloceanspaces.com&profile=digitalocean"

We commit all the code (written in Go) into a GitHub repository, and implemented CI/CD for the Pulumi setup via GitHub Actions.

On every pull request, a pulumi preview will be ran to preview the diff:

...
jobs:
  preview:
    if: ${{ github.event_name == 'pull_request'}}
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
      cancel-in-progress: true
    name: Preview
    permissions:
      contents: read
      pull-requests: write
    strategy:
      matrix:
        project: [ "gogs" ]
    runs-on: ubuntu-latest
    steps:
      ...
      - name: Set up AWS CLI
        run: |
          aws --profile digitalocean configure set region "us-east-1"
          aws --profile digitalocean configure set aws_access_key_id "${{ secrets.DIGITALOCEAN_ACCESS_KEY }}"
          aws --profile digitalocean configure set aws_secret_access_key "${{ secrets.DIGITALOCEAN_SECRET_KEY }}"          
      - name: Install Pulumi
        uses: pulumi/actions@v6
      - name: Pulumi preview
        id: pulumi-preview
        run: |
          cd ${{ matrix.project }}
          pulumi login --cloud-url "s3://{BUCKET NAME}?endpoint=nyc3.digitaloceanspaces.com&profile=digitalocean"
          pulumi preview --stack default --diff --non-interactive |& tee preview.txt
          echo 'PREVIEW<<EOF' >> "$GITHUB_OUTPUT"
          cat preview.txt >> "$GITHUB_OUTPUT"
          echo 'EOF' >> "$GITHUB_OUTPUT"          
      - name: Comment on PR
        uses: thollander/actions-comment-pull-request@v3
        with:
          message: |
            #### :tropical_drink: `preview` on ${{ matrix.project }}/default

            <details>
            <summary>Pulumi report</summary>
            <pre>
            ${{ steps.pulumi-preview.outputs.PREVIEW }}
            </pre>
            </details>            
          comment-tag: execution

The diff output will also be posted to the pull request as a comment:

Pulumi preview comment

Upon merging the commit into the main branch, the pulumi up is ran to actually apply the changes:

jobs:
  up:
    if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
    concurrency:
      group: ${{ github.workflow }}-${{ github.ref }}
    name: Up
    strategy:
      matrix:
        project: [ "gogs" ]
    runs-on: ubuntu-latest
    steps:
      ...
      - name: Pulumi up
        run: |
          cd ${{ matrix.project }}
          pulumi login --cloud-url "s3://{BUCKET NAME}?endpoint=nyc3.digitaloceanspaces.com&profile=digitalocean"
          pulumi up --stack default --diff --non-interactive --skip-preview          

Monitoring Link to heading

The last but not the least puzzle of an infrastructure setup is of course, the monitoring!

We have two types of monitoring checks and their alerts set up using DigitalOcean Monitoring:

  1. Resource monitoring and alerts Resource monitoring
  2. Uptime monitoring and alerts Resource monitoring

By having these alerts set up, we will be able to get timely notifications regarding any possible outage of the services we manage.

What’s next? Link to heading

While we have got the most our way to the grand scheme, there are still things left due to reasons, including:

  • Set up Prometheus and Grafana Cloud for application monitoring
  • Deploy Bytebase via DigitalOcean App Platform for managing and inspecting database backends with proper access control
  • Implement the Secrets Storage to stop persisting API and access tokens all over the places

Stay tuned!