środa, 4 lipca 2018

Własne Registry na klastrze kubernetesowym

A więc mamy klaster Kubernetesa i chcemy sobie na nim wdrożyć swoje własne Registry dockerowe.  W moim przypadku mam w klastrze 3 węzły z czego jeden główny, a wszystkie stoją na Ubuntu 16.04.4 LTS.

Abyśmy mogli w polu "image" ustawić nazwę FQDN naszej usługi Registry musimy zainstalować dnsmasq, które będzie korzystać z serwera DNS-owego Kubernetesa. Najpierw musimy sprawdzić jaki jest adres IP serwisu odwołującego się do serwera nazw domenowych Kubernetes (na jednym z węzłów zarządzających):
bkorpala@kubernetes-master1:~$ kubectl -n kube-system get services | grep kube-dns |  awk '{ print $3 }'
10.233.0.3
Następnie instalujemy na wszystkich węzłach usługę dnsmasq komendą "sudo apt install dnsmasq".

Kolejnym krokiem jest edycja pliku "/etc/dnsmasq.d/kube.conf" i wprowadzenie do niego poniższej zawartości (wraz z odpowiednim adresem IP usługi kube-dns):
server=/cluster.local/10.233.0.3
Teraz na wszystkich serwerach restartujemy sieć "sudo systemctl restart networking" oraz restartujemy dnsmasq poprzez "sudo systemctl restart dnsmasq".

Sprawdzamy czy np. adres "kube-dns.kube-system.svc.cluster.local" jest rozwiązywane na IP przy użyciu pinga. Musimy się także upewnić, że w "/etc/resolv.conf" zawsze na pierwszym miejscu będzie "nameserver 127.0.0.1" bo w przeciwnym wypadku nazwy mogą nie być rozwiązywane. Dobrze zrestartować serwer i sprawdzić co się pojawi w "/etc/resolv.conf". Dla pewności można też dodać wpis "nameserver 127.0.0.1" do "/etc/resolvconf/resolv.conf.d/head" i wykonać komendę "sudo resolvconf -u".

Następny punkt dotyczy wygenerowania ceryfikatu dla HTTPS w Registry:
bkorpala@MacBook-Pro-Bartlomiej:~|⇒  openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout registry.key -out registry.crt
Generating a 2048 bit RSA private key
........................+++
............................+++
writing new private key to 'registry.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) []:PL
State or Province Name (full name) []:Malopolskie
Locality Name (eg, city) []:Krakow
Organization Name (eg, company) []:Nazwa_firmy
Organizational Unit Name (eg, section) []:
Common Name (eg, fully qualified host name) []:kube-registry.registry.svc.cluster.local
Email Address []:adres_poczty
Tworzymy przestrzeń nazw komendą "kubectl create namespace registry".

Certyfikat i klucz prywatny będziemy trzymać w sekretach Kubernetesa, ale najpierw musimy je przekonwertować do Base64 jak np.:
bkorpala@kubernetes-master1:~$ cat registry.crt | base64 -w0
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR0RENDQXB3Q0NRQzlPSXoyN21EaDVEQU5CZ2txaGtpRzl3MEJBUXNGQURDQm16RUxNQWtHQTFVRUJoTUMKVUV3eEZEQVNCZ05WQkFnTUMwMWhiRzl3YjJ4emEybGxNUTh3RFFZRFZRUUhEQVpMY21GcmIzY3hFREFPQmdOVgpCQW9NQjAxcGNYVnBaRzh4TVRBdkJnTlZCQU1NS0d0MVltVXRjbVZuYVhOMGNua3VjbVZuYVhOMGNua3VjM1pqCkxtTnNkWE4wWlhJdWJHOWpZV3d4SURBZUJna3Foa2lHOXcwQkNRRVdFV2hsYkd4dlFHMXBjWFZwWkc4dVkyOXQKTUI0WERURTRNRGN3TWpFeU1ETXdOVm9YRFRFNU1EY3dNakV5TURNd05Wb3dnWnN4Q3pBSkJnTlZCQVlUQWxCTQpNUlF3RWdZRFZRUUlEQXROWVd4dmNHOXNjMnRwWlRFUE1BMEdBMVVFQnd3R1MzSmhhMjkzTVJBd0RnWURWUVFLCkRBZE5hWEYxYVdSdk1URXdMd1lEVlFRRERDaHJkV0psTFhKbFoybHpkSEo1TG5KbFoybHpkSEo1TG5OMll5NWoKYkhWemRHVnlMbXh2WTJGc01TQXdIZ1lKS29aSWh2Y05BUWtCRmhGb1pXeHNiMEJ0YVhGMWFXUnZMbU52YlRDQwpBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUs2RW1ZT2lKejZydmNPUjByV3RTVHkyClVhSTdPU2tPOU5kT1BIUUdQRjhsbjNpVUJ6MCtDeEJaRzEra2lxMmtSRVlJc1JDbkJ3RGMyNkdNYWowM0NicUcKbWlTOUpTSklHMCtBMEoxdWsrUDdqNnUzdURaMEhlRWdHeHpqK2ZmazNtUWpOcysrMms4TDVTRTFVM2lhRUhiRAphUVR4cVcyMFM5VXFXWjFxT21lZExaN0hhQ0VBa1BQU0xOeitwUnlIbElianVPM0lPam5Za1c1V2ZCRTBpU0ZTCnhxRFdpL2lYTGtHOFRwZDExYWZ0amFPRHJjRklISzF0NTJyV0w3cUtvOVFLdVpXSFM1RjNReFdYbHVKM09ndU0KSEVQZzVtaDk2MlRGeVVzN2gvOWVicXV0akxmMk9FY2Q1YlNUZXNqbk1sZWZNN2lrN2tUYXBwTC84TGVSNGo4QwpBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWRtbkc5eTlSYXlFaFhqdEtYcjB5cW1TUitmTFFHMkZXCnB6TDRwTDJ1ZzNIQWVDS0Y0ZEgyak9qRUhVYjN6TU9sVW5zRGRvMDlOa1I2eGRrWXFXYXJWc3I4RGlHZ3h1LzcKb2YrZnIybUdYNFJtNmlqekgwcXRIMWlsdzA0OGwrNER2L1VVcjJwZWthTUMrMHhmU1U2c3B0NXNYWk1LS0JPRQpMUjFKbE1BeWI0OEd1Y2E0Tko2bUw4L2dxcVUvb25WQTVPbDBoOGtyZHNONzUrUStQTThibitTYVQ2bjdaTlVKCmZ6dllyTmU1Ynp2aXdyc0ZYcnBOR2VBWVpnV01BZS9CU1NReUpjZHpDSjg4YW8yazlocHNYbi9XUGtOcUVJUFAKOGZOMkZNbHlaTDFQMmFDQ2ZjSUREZGZGYzlWWW0vbGQ0REZlN2IyT1IvWVlpWUtiNXhsMmdnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
Gdy już mamy zakodowaną wartość to tworzymy pierwszą część manifestu:
apiVersion: v1
kind: Secret
metadata:
  name: registry-certs
type: Opaque
data:
  registry.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR0RENDQXB3Q0NRQzlPSXoyN21EaDVEQU5CZ2txaGtpRzl3MEJBUXNGQURDQm16RUxNQWtHQTFVRUJoTUMKVUV3eEZEQVNCZ05WQkFnTUMwMWhiRzl3YjJ4emEybGxNUTh3RFFZRFZRUUhEQVpMY21GcmIzY3hFREFPQmdOVgpCQW9NQjAxcGNYVnBaRzh4TVRBdkJnTlZCQU1NS0d0MVltVXRjbVZuYVhOMGNua3VjbVZuYVhOMGNua3VjM1pqCkxtTnNkWE4wWlhJdWJHOWpZV3d4SURBZUJna3Foa2lHOXcwQkNRRVdFV2hsYkd4dlFHMXBjWFZwWkc4dVkyOXQKTUI0WERURTRNRGN3TWpFeU1ETXdOVm9YRFRFNU1EY3dNakV5TURNd05Wb3dnWnN4Q3pBSkJnTlZCQVlUQWxCTQpNUlF3RWdZRFZRUUlEQXROWVd4dmNHOXNjMnRwWlRFUE1BMEdBMVVFQnd3R1MzSmhhMjkzTVJBd0RnWURWUVFLCkRBZE5hWEYxYVdSdk1URXdMd1lEVlFRRERDaHJkV0psTFhKbFoybHpkSEo1TG5KbFoybHpkSEo1TG5OMll5NWoKYkhWemRHVnlMbXh2WTJGc01TQXdIZ1lKS29aSWh2Y05BUWtCRmhGb1pXeHNiMEJ0YVhGMWFXUnZMbU52YlRDQwpBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUs2RW1ZT2lKejZydmNPUjByV3RTVHkyClVhSTdPU2tPOU5kT1BIUUdQRjhsbjNpVUJ6MCtDeEJaRzEra2lxMmtSRVlJc1JDbkJ3RGMyNkdNYWowM0NicUcKbWlTOUpTSklHMCtBMEoxdWsrUDdqNnUzdURaMEhlRWdHeHpqK2ZmazNtUWpOcysrMms4TDVTRTFVM2lhRUhiRAphUVR4cVcyMFM5VXFXWjFxT21lZExaN0hhQ0VBa1BQU0xOeitwUnlIbElianVPM0lPam5Za1c1V2ZCRTBpU0ZTCnhxRFdpL2lYTGtHOFRwZDExYWZ0amFPRHJjRklISzF0NTJyV0w3cUtvOVFLdVpXSFM1RjNReFdYbHVKM09ndU0KSEVQZzVtaDk2MlRGeVVzN2gvOWVicXV0akxmMk9FY2Q1YlNUZXNqbk1sZWZNN2lrN2tUYXBwTC84TGVSNGo4QwpBd0VBQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQWRtbkc5eTlSYXlFaFhqdEtYcjB5cW1TUitmTFFHMkZXCnB6TDRwTDJ1ZzNIQWVDS0Y0ZEgyak9qRUhVYjN6TU9sVW5zRGRvMDlOa1I2eGRrWXFXYXJWc3I4RGlHZ3h1LzcKb2YrZnIybUdYNFJtNmlqekgwcXRIMWlsdzA0OGwrNER2L1VVcjJwZWthTUMrMHhmU1U2c3B0NXNYWk1LS0JPRQpMUjFKbE1BeWI0OEd1Y2E0Tko2bUw4L2dxcVUvb25WQTVPbDBoOGtyZHNONzUrUStQTThibitTYVQ2bjdaTlVKCmZ6dllyTmU1Ynp2aXdyc0ZYcnBOR2VBWVpnV01BZS9CU1NReUpjZHpDSjg4YW8yazlocHNYbi9XUGtOcUVJUFAKOGZOMkZNbHlaTDFQMmFDQ2ZjSUREZGZGYzlWWW0vbGQ0REZlN2IyT1IvWVlpWUtiNXhsMmdnPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
  registry.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQ3VoSm1Eb2ljK3E3M0QKa2RLMXJVazh0bEdpT3prcER2VFhUangwQmp4ZkpaOTRsQWM5UGdzUVdSdGZwSXF0cEVSR0NMRVFwd2NBM051aApqR285TndtNmhwb2t2U1VpU0J0UGdOQ2RicFBqKzQrcnQ3ZzJkQjNoSUJzYzQvbjM1TjVrSXpiUHZ0cFBDK1VoCk5WTjRtaEIydzJrRThhbHR0RXZWS2xtZGFqcG5uUzJleDJnaEFKRHowaXpjL3FVY2g1U0c0N2p0eURvNTJKRnUKVm53Uk5Ja2hVc2FnMW92NGx5NUJ2RTZYZGRXbjdZMmpnNjNCU0J5dGJlZHExaSs2aXFQVUNybVZoMHVSZDBNVgpsNWJpZHpvTGpCeEQ0T1pvZmV0a3hjbExPNGYvWG02cnJZeTM5amhISGVXMGszckk1ekpYbnpPNHBPNUUycWFTCi8vQzNrZUkvQWdNQkFBRUNnZ0VBSENPSE9TRkJJS3JDV3pFOC8wd2tmZVNMdnhPN2dMSkhxaHVVUmNUbm9SUEkKNWNGQWRaQjJhamxqMzRVQlUwUWtPZ0tXd2krY1FuaFo5VzlWaGU5RTQwMW10enZFTEFYaVdXeFV0cjJvbk43bgo2SEVrQTZ1dlVhaENsdUx2WUJnSC82OXAzQTlTMWVIK0hOK2pTTlBXaWIreVJEak83OEJkWmM2QlNvOWhRV2xqClVweHV2MXB1ZUZIK2trckljazh3aEhJQ2pnQWlwNTl4MWRUbmRNVWI5M0N3UkxZaW1HUG1yTE9ydTU1K0tEUEoKbkVLT0FPT0JQMXE1RTFaN25sZ3RQUEVDZHhxWnVMc2NaT24xVXBFR05WN2o0YkJYeUI3VTV1KzhVcWUwWUhvcwowdmdTa2hEbHBGZkx5M0xoVTBVelBCcFBlbXYrR2pZUDRiUGhDZll6b1FLQmdRRFZMR0duazNOeDI3MTZNR2krCngwTmtCaVJtVjVrWEJROVhpWjgySlNPTVF2ZmJ0d3ArU1o3MS9adm9yNmY2Tnhac1BIL2RTaE4wd2dnaG9OcHoKYkhYK09IQjRmZWxMc1NJeDhqQUNkdERnd1BkTCtPRStPdUNtT01KNWE4OE4yRmRHQXAyME0yRzBDK2oxOXE1Rwo5c2RDVnRUbkVJVm5nOHJkWit6NFFRUVcwUUtCZ1FEUmxDYVYyTmVybHJtY0EveFZ5OXo5UmVPRVRJVHVjbkNTCkhvQ0o3dlczRzRqNm9HR2swYXlJK1VodkNDUVBYWUpSaWY1bnpBaTY4TXo5b1JtVllKNW9lMW5zWk9YUGlXTlAKRzlaaXE4cjExU0VtbnlmV1MwQ20rcmlYYzhMWDUzSFIrWmZFVDB1SGtaWmZJUERKd1E2QnF6dlZsek9sVXFoNwpldUd0R1ViTUR3S0JnRUU3dHBxSVJjQ201TUc2aEtNMDZRdDMwYlc3d3E1SHJ4MHprUlFKbzJvdHFCbUZWdUcwCmQ3K0JIeS9DYVIzcHM2UGxrNTlOSnMwSS8ySUREalphRDNWL0lmUE5YQkg4bjZFM0lyZnVVaGNsRmNnYTNocWoKKzlFQjk0a2VrNXNDQkJyNWUvY0Q3amNobWVXQzJtdis0cFVMYTFWV09leW1hUEw5OVljMnBpQ3hBb0dBU3dxTApEbGlJVUs0MVk0R2gxbTZhZ0MySUc1VHlmQmFKN0NmenltKy9sNExLZDBMNTgzUUlIVHQxRGh5U0hOWlVsVEdkCjREaVVYNnRkR3V1V0o0Qmk5L0pDN252cU5YZHlFWjljRWhTRkphVGNxK0dNK0JRVjREUWg2RWpRMlZITDFXMVQKUkQxSVJCQ0RFU2pRVHMxTGptNXJqRDBKaHdhandTTHNyM2cvZTAwQ2dZRUFsZjkrU1NManVxU0JCbmFDY0NLbApTbW5rM2RJMmxYOG5kY0RiUTVyOXhWTHJXcXBIK1BwSkIrUk45VWpVTWlrdFZrYmYrbjNwOVp5T2NxeG9YVk9wClRYMXY0Y0NjbDR5cC9xOXhtOU5FNGhRSkVsRm10YTlwMVJ3a2JmeWpqTnVVOUlMZjljM21MMlN0STFTV3gyUjAKWmlpT3hvUXdUSlFvK2phdlR4dUJHeFU9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
Wgrywamy za pomocą komendy "kubectl apply -n registry -f registry_deployment.yml" i sprawdzamy jak nam poszło:
bkorpala@kubernetes-master1:~$ kubectl -n registry get secrets
NAME                  TYPE                                  DATA      AGE
default-token-k8x4m   kubernetes.io/service-account-token   3         51m
registry-certs        Opaque
Dalej musimy stworzyć usługę, która umożliwi nam komunikację z naszym kontenerem Registry (dodajemy do naszego manifestu oddzielając "---"):
apiVersion: v1
kind: Service
metadata:
  name: kube-registry
  labels:
    k8s-app: kube-registry-upstream
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeRegistry"
spec:
  selector:
    k8s-app: kube-registry-upstream
  ports:
  - name: registry
    port: 443
    protocol: TCP
Standardowo sprawdźmy co zrobiliśmy:
bkorpala@kubernetes-master1:~$ kubectl -n registry get services
NAME                TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
glusterfs-cluster   ClusterIP   10.233.28.208   <none>        1/TCP     18h
kube-registry       ClusterIP   10.233.29.16    <none>        443/TCP   10m
Teraz musimy zadeklarować obiekty PersistentVolume i PersistentVolumeClaim określające ilość miejsca, które możemy wykorzystać (dla mojego przykładu do przechowywania danych użyłem GlusterFS, na temat którego możecie przeczytać tutaj):
kind: PersistentVolume
apiVersion: v1
metadata:
  name: kube-registry-pv
  labels:
    kubernetes.io/cluster-service: "true"
spec:
  capacity:
    storage: 2Gi
  accessModes:
    - ReadWriteOnce
  glusterfs:
    endpoints: glusterfs-cluster
    path: registry
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: kube-registry-pvc
  labels:
    kubernetes.io/cluster-service: "true"
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1500M
Wynik powinien być zbliżony do tego:
bkorpala@kubernetes-master1:~$ kubectl -n registry get pv
NAME               CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS    CLAIM                        STORAGECLASS   REASON    AGE
kube-registry-pv   2Gi        RWO            Retain           Bound     registry/kube-registry-pvc                            9s
bkorpala@kubernetes-master1:~$ kubectl -n registry get pvc
NAME                STATUS    VOLUME             CAPACITY   ACCESS MODES   STORAGECLASS   AGE
kube-registry-pvc   Bound     kube-registry-pv   2Gi        RWO                           11s
Teraz najważniejsza rzecz czyli wdrożenie naszego strąka z Docker Registry:
apiVersion: v1
kind: ReplicationController
metadata:
  name: kube-registry-v0
  labels:
    k8s-app: kube-registry-upstream
    version: v0
    kubernetes.io/cluster-service: "true"
spec:
  replicas: 1
  selector:
    k8s-app: kube-registry-upstream
    version: v0
  template:
    metadata:
      labels:
        k8s-app: kube-registry-upstream
        version: v0
        kubernetes.io/cluster-service: "true"
    spec:
      containers:
      - name: registry
        image: registry:2
        resources:
          limits:
            cpu: 100m
            memory: 100Mi
        env:
        - name: REGISTRY_HTTP_ADDR
          value: :443
        - name: REGISTRY_HTTP_TLS_CERTIFICATE
          value: /certs/registry.crt
        - name: REGISTRY_HTTP_TLS_KEY
          value: /certs/registry.key
        - name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
          value: /var/lib/registry
        volumeMounts:
        - name: registry-vol
          mountPath: /var/lib/registry
          subPath: registry
        - name: registry-certs-vol
          mountPath: /certs
        ports:
        - containerPort: 443
          name: registry
          protocol: TCP
      volumes:
      - name: registry-vol
        persistentVolumeClaim:
          claimName: kube-registry-pvc
      - name: registry-certs-vol
        secret:
          secretName: registry-certs
Pobierzmy rezultat:
bkorpala@kubernetes-master1:~$ kubectl -n registry get rc
NAME               DESIRED   CURRENT   READY     AGE
kube-registry-v0   1         1         1         26s
bkorpala@kubernetes-master1:~$ kubectl -n registry get pods
NAME                     READY     STATUS    RESTARTS   AGE
kube-registry-v0-xshs8   1/1       Running   0          35s
Teraz z dowolnego węzła Kubernetes możemy pingować nasz strąk i nazwa domeny powinna zostać rozwiązana na adres IP:
bkorpala@kubernetes-master1:~$ ping kube-registry.registry.svc.cluster.local
PING kube-registry.registry.svc.cluster.local (10.233.29.16) 56(84) bytes of data.
Sprawdźmy zatem czy Registry działa:
bkorpala@kubernetes-master1:~$ curl -k https://kube-registry.registry.svc.cluster.local/v2/_catalog
{"repositories":[]}
Pasuje coś wrzucić do naszego rejestru. Niechaj to będzie Nginx. Za pomocą komendy "docker pull nginx" pobieramy ostatnią wersję obrazu. Teraz pobieramy identyfikator obrazu:
bkorpala@kubernetes-master1:~$ sudo docker images | grep latest | grep nginx
nginx                                                 latest              5699ececb21c        6 days ago          109 MB
Komendą "docker tag 5699ececb21c kube-registry.registry.svc.cluster.local/nginx" oznaczamy nasz obraz celem wypchnięcia go na prywatne Registry: "docker push kube-registry.registry.svc.cluster.local/nginx". Standardowo weryfikujemy:
bkorpala@kubernetes-master1:~$ curl -k https://kube-registry.registry.svc.cluster.local/v2/_catalog
{"repositories":["nginx"]}
bkorpala@kubernetes-master1:~$ curl -k https://kube-registry.registry.svc.cluster.local/v2/nginx/tags/list
{"name":"nginx","tags":["latest"]}
Teraz definiując manifest w polu "image" dla konkretnego kontenera możemy użyć wyżej wymienionego adresu.

Jeżeli krok z dnsmasq pominęliśmy lub zwyczajnie nie chcemy w ten sposób rozwiązywać nazw to możemy stworzyć usługę, która na każdym z węzłów wystawi nam Registry na konkretnym porcie:
apiVersion: v1
kind: Service
metadata:
  name: kube-registry-every-node
  labels:
    k8s-app: kube-registry-upstream
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: "KubeRegistry"
spec:
  selector:
    k8s-app: kube-registry-upstream
  type: NodePort
  ports:
  - name: registry-every-node
    nodePort: 31337
    port: 443
    protocol: TCP
Sprawdźmy jak poszło:
bkorpala@kubernetes-master1:~$ sudo kubectl -n registry get services
NAME                       TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)         AGE
glusterfs-cluster          ClusterIP   10.233.28.208   <none>        1/TCP           21h
kube-registry              ClusterIP   10.233.29.16    <none>        443/TCP         3h
kube-registry-every-node   NodePort    10.233.8.16     <none>        443:31337/TCP   30s
bkorpala@kubernetes-master1:~$ curl -k https://localhost:31337/v2/_catalog
{"repositories":["nginx"]}
Jeżeli chcemy tylko tymczasowo sprawdzić poprawność działania to możemy zwyczajnie przekazać konkretny port do naszej stacji roboczej:
bkorpala@kubernetes-master1:~$ sudo kubectl port-forward --namespace registry kube-registry-v0-xshs8 31337:443 &
[1] 20103
bkorpala@kubernetes-master1:~$ Forwarding from 127.0.0.1:31337 -> 443
bkorpala@kubernetes-master1:~$ curl -k https://localhost:31337/v2/_catalog
Handling connection for 31337
{"repositories":["nginx"]}
E0703 12:01:31.963822   20104 portforward.go:316] error copying from local connection to remote stream: read tcp4 127.0.0.1:31337->127.0.0.1:46754: read: connection reset by peer