Privates Docker Repository mit Nexus

Ziel soll es sein ein secure repository für Docker Images bereitzustellen. In der Sprache von Docker bedeutet dieses, dass das Repository über HTTPS angesprochen wird. Dieses soll privat sein, da es nicht öffentliche Images beinhalten soll. Dazu werden 2 + 1 Domainnames benötigt, da wir ein Mapping in den Nexus Container auf unterschiedliche Ports vornehmen müssen. In diesem Beispiel gibt es die Hostnamen nexus, docker und docker-proxy.

FQDN Port
nexus.XXX.org 9000
docker.XXX.org 9001
docker-proxy.XXX.org 9002

Den NginX Reverse Proxy konfigurieren

Dazu benötigen wir folgende Einstellungen für den NGinX Reverse Proxy:

###############################
#                             #
# Public Nexus Repository     #
#                             #
# nexus.XXX.org               #
#                             #
###############################

#    
# Nexus Server
# map to port 9000
#

# Redirect http -> https
server {
    listen 80;
    server_name nexus.XXX.org;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;

    server_name nexus.XXX.org;

    # logging
    error_log /var/log/nginx/nexus_error.log debug;
    access_log /var/log/nginx/nexus_access.log;

    # Push large objects to nexus
    client_max_body_size 2G;

    # SSL
    include ssl/*.XXX.org;

    location / {
      # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
      proxy_pass http://127.0.0.1:9000/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }

}

#    
# Docker Repository
# map to port 9001
#

server {
    listen 443 ssl http2;

    server_name docker.XXX.org;

    # logging
    error_log /var/log/nginx/nexus_docker_error.log debug;
    access_log /var/log/nginx/nexus_docker_access.log;

    # Push large objects to nexus
    client_max_body_size 2G;

    # SSL
    include ssl/*.XXX.org;

    # optimize downloading files larger than 1G
    #proxy_max_temp_file_size 2G;

    location / {
      # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
      proxy_pass http://127.0.0.1:9001/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme; 
    }

}

#    
# Docker Proxy
# map to port 9002
#

server {
    listen 443 ssl http2;

    server_name docker-proxy.XXX.org;

    # logging
    error_log /var/log/nginx/nexus_docker_error.log debug;
    access_log /var/log/nginx/nexus_docker_access.log;

    # Push large objects to nexus
    client_max_body_size 2G;

    # SSL
    include ssl/*.XXX.org;

    location / {
      # Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
      proxy_pass http://127.0.0.1:9002/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme; 
    }

}

Nexus Repository erstellen

Damit Docker Images gespeichert werden können, muss zunächst ein neues Repository vom Typ Docker Host angelegt werden. Wichtig hierbei ist, dass das Repostitory auf einen HTTP Connector mit dem Port aus dem Mapping, also 9002, übereinstimmt. Es muss auch nur der HTTP Connector sein, da wir uns per HTTPS auf dem Reverse Proxy später anmelden.

Docker Login

Um die Konfiguration zu Testen, kann man Docker Login verwenden.

docker login docker.XXX.org

Docker Proxy

Aufgrund eines Bugs in Nexus, ist die Konfiguration von Nexus für einen Docker Proxy nicht ganz so einfach, wie man es sich wünschen würde und es hat auch eher den Character eines Hacks.

Wie bereits oben beschrieben, ist ein weiterer Port 9002 in dem Mapping vorhanden. Der FQDN docker-proxy.XXX.org ist auf den Port 9002 gemappt.

In Nexus erstellen Sie nun ein weiteres Repository (Docker-Proxy) und vergeben dem Repository den Port 9002.

Lokales Docker Repository verwenden

Möchte man Docker Images zum Bauen einer Anwendung in Drone verwenden, dann muss i.d.R. Credentials für Basic Auth am Nexus Docker Repository übergeben werden. Dazu muss den Inhalt der config.json in ein neues Secret in Drone schreiben.

Dazu muss man sich an der Konsole einmal an dem Repository docker login anmelden.

docker login docker.XXXX.org

Das JSON der config Datei sieht in etwa dann so aus:

        "auths": {
                "docker.XXXX.org": {
                        "auth": "VXXXXXXX="
                }
        },

Den Inhalt kopieren sie bitte in ein neues Secret mit dem Key dockerconfigjson. Aus diesem wird Drone die Informationen für die Basic Authentication an dem Docker Repository extrahieren. In der .drone.yml muss nur noch image_pull_secrets eingefügt werden.

kind: pipeline
name: default

steps:
- name: setup
  image: docker.XXXX.org/edvpfau/archlinux-base-image:latest
  environment:
    NEXUS_URL:
      from_secret: NEXUS_URL
    NEXUS_USERNAME:
      from_secret: NEXUS_USERNAME
    NEXUS_PASSWORD:
      from_secret: NEXUS_PASSWORD
    NEXUS_REPOSITORY:
      from_secret: NEXUS_REPOSITORY
    SONAR_HOST:
      from_secret: SONAR_HOST
    SONAR_TOKEN:
      from_secret: SONAR_TOKEN
  commands:
    - JAVA_HOME=/usr/lib/jvm/default ./gradlew -Dsonar.host.url=$SONAR_HOST -Dsonar.login=$SONAR_TOKEN clean build test sonarqube

image_pull_secrets:
- dockerconfigjson

Nun steht der Nutzung eigener privater Images zum Bauen der Software nichts mehr im Wege. Das Image kann dann speziell an die Verwendete Software angepasst werden. Hier in dem Beispiel verwende ich ein Image, welches den Gradle Wrapper vorinstalliert hat und somit in jedem Durchgang nicht nur Zeit sondern auch Netzwerktraffic eingesparrt werden.

Maven Caching

Warum sollte man einen zentralen Cache verwenden? Nun, wenn man in einem Betrieb mit mehreren Mitarbeitern arbeitet, dann kann es unter Umständen schon zu Engpässen in der Bandbreite führen. Dieses gilt auch heute bei Modernen DSL Anschlüssen, da diese oftmals auch Telefonie bereitstellen und daher wegen QoS diese zusätzlich von der Verfügung stehenden Bandbreite abgezogen werden müssen.

Es gibt aber auch noch weitere Gründe warum man den Zugang zu dem Maven Repository zentralisieren sollte. Es kann sein, dass bestimmte Projekte sehr strenge Vorgaben an die zu verwendenden Dependencies machen. Dieses können zum Beispiel verwendete Lizenzen oder CVEs sein.

Nexus

Zunächst muss man in Nexus ein Maven 2 Proxy anlegen. Ich gehe hier davon aus, dass eine laufende Instanz von dem Nexus Server im internen Netz vorhanden ist. Der Nexus Server ist auch als Docker Image vorhanden und lässt sich in ein paar Minuten aufsetzen.

Proxy Einrichten

Wie bereits beschrieben muss zunächst ein Maven Proxy angelegt werden. Dazu meldet man sich als Administrator an und geht in den Einstellungen in die Repositories. Dort kann man mit create ein neues Repository anlegen. Wählen sie den Type Maven2(Proxy). Vergeben sie als Namen maven-central-proxy. Der Name ist dann Teil der URL unter dem dann im Nexus der Maven Proxy erreichbar ist.

Für die Upstream URL für das Maven Repository https://repo1.maven.org/maven2 ein.

Maven

In Maven kann man die Repositories in der settings.xml verwalten.

<settings>
  <mirrors>
    <mirror>
      <!--This sends everything else to /public -->
      <id>nexus</id>
      <mirrorOf>*</mirrorOf>
      <url>http://nexus.XXXX.de/repository/maven-central-proxy/</url>
    </mirror>
  </mirrors>
  <profiles>
    <profile>
      <id>nexus</id>
      <!--Enable snapshots for the built in central repo to direct -->
      <!--all requests to nexus via the mirror -->
      <repositories>
        <repository>
          <id>central</id>
          <url>http://central</url>
          <releases><enabled>true</enabled></releases>
          <snapshots><enabled>true</enabled></snapshots>
        </repository>
      </repositories>
     <pluginRepositories>
        <pluginRepository>
          <id>central</id>
          <url>http://central</url>
          <releases><enabled>true</enabled></releases>
          <snapshots><enabled>true</enabled></snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  </profiles>
  <activeProfiles>
    <!--make the profile active all the time -->
    <activeProfile>nexus</activeProfile>
  </activeProfiles>

  <!-- set if non anonymous connection allowed -->
  <servers>
    <server>
      <id>nexus</id>
      <username>XXX</username>
      <password>YYY</password>
    </server>
  </servers>    
</settings>

Gradle

In Gradle hat man mehrere Möglichkeiten ein anderes Repository zu verwenden. Das Naheliegendste ist in dem build.gradle das Repository direkt anzugeben. Doch Gradle bietet noch mehr Wege, um hier einzugreifen.

In https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.dsl.RepositoryHandler.html wird beschrieben, dass das Maven Central Repository mit der URL https://repo.maven.apache.org/maven2/ verwendet wird. Diese lässt sich zentral für alle Projekte auf dem Host durch ein Skript ersetzen, welches ich in dem nächsten Abschnitt vorstelle.

Gradle Init Skript

Das Gralde Init-Skript wird ausgeführt, wenn man Gradle startet bzw. ein Task ausgeführt wird. Das Init-Skript kann eine Datei sein die init.gradle heißt und im Ordner ~/.gradle liegt oder es ist eine Datei die mit .gradle endet und in dem Verzeichnis ~/.gradle/init.d liegt.

Ich habe mit für letzteres entschieden und eine Datei maven.gradle in dem Verzeichnis ~/.gradle/init.d erstellt. Es muss zunächst die nexusUrl angepasst werden. Diese setzt sich aus der Server URL und dem Namen des Maven2 Proxy Repositories zusammen. Hier also maven-central-proxy. Das Skript filter Repositories die mit https://repo.maven.apache.org/maven2 heraus und fügt dafür die URL von dem Nexus Server ein. Das Ganze wird auf der Konsole geloggt, sodass man auch gleich erkennt welche Repositories in dem Projekt verwendet werden.

Das Skript maven.gradle:

//
// init.d/maven.gradle
// -------------------
//
// This replaces all occurance of mavenCentral repositories with the local Nexus repository
//

def nexusUrl = "https://nexus.XXXX.de/repository/maven-central-proxy"
def prefix = "> Init.d/maven.gradle :"

allprojects {
    buildscript {
        repositories {
            all { ArtifactRepository repo -> 
                if ((repo instanceof MavenArtifactRepository) && repo.url.toString().startsWith("https://repo.maven.apache.org/maven2")) {
                    project.logger.warn "${prefix}Buildscript Repository: ${repo.url} removed. Only $nexusUrl is allowed"
                    remove repo
                }
            }
            maven {
                url "$nexusUrl"
            }
        }
    }

    repositories {
        all { ArtifactRepository repo ->
            if ((repo instanceof MavenArtifactRepository) && repo.url.toString().startsWith("https://repo.maven.apache.org/maven2")) {
                project.logger.warn "${prefix}Repository: change ${repo.url} ---> $nexusUrl"
                remove repo
            } else {
               println "${prefix}Repository: " + repo.url.toString()
            }
        }
        maven { 
            url "$nexusUrl" 
        }
    }
}

Hinweis: Gradle missachtet hier die settings.xml. Wenn eine Authentifizierung am Nexus nötig ist (kein anonymous), dann müssen in den beiden maven-Blöcken jeweils ein Credentials Block eingefügt werden. Sonst erhält man ein 401 HTTP Error.

credentials {
  username "XXX"
  password "YYY"
} 

Gradle Build

Wenn man jetzt einen Build startet, dann kann man auf der Konsole erkennen, wie das Skript das Maven Central Repository, welches über mavenCentral() eingebunden wurde, ersetzt wird.

[sascha@Workstation TestServer]$ ./gradlew build
> Init.d/maven.gradle :Repository: https://nexus.XXXX.de/repository/maven-central-proxy

> Configure project :
> Init.d/maven.gradle :Repository: change https://repo.maven.apache.org/maven2/ ---> https://nexus.XXXX.de/repository/maven-central-proxy
> Init.d/maven.gradle :Repository: https://repo.spring.io/snapshot
> Init.d/maven.gradle :Repository: https://repo.spring.io/milestone