Am Wochenende habe ich zum ersten Mal einen Escape Room spielen dürfen und ich muss sagen, heute habe ich mich ähnlich gefühlt, denn das Bugfixing ähnelt auch einer kreativen rätselratenden Schnitzeljagd.
Der Bug
Die von mir entwickelte Anwendung ist mittlerweile im Pilot-Betrieb und ein Benutzer meldet, dass er einen Vorgang nicht beenden kann: Vermeindlich hochgeladene Bilder, die zwingend erforderlich sind, 'verschwinden' und so kann er nicht abschließend speichern.
Die Bilder werden dankenswerterweise mitgeliefert, so dass ich auch versuchen kann, das Problem nachzustellen.
Es gibt auch einen Hinweis, das das Ausführen einer bestimmten Funktion während des Vorgangs das Problem verursacht haben könnte. ERROR-Einträge in den Logfiles unterstützen diese These, aber letztendlich hatte es damit nichts zu tun. Es war nur eine falsche Fährte, eine Ablenkung, wie bei jedem guten Spiel 😉 Und die Erkenntnis, dass da noch irgendwo ein weiterer Bug schlummert, der von mir gejagt und entdeckt werden will.
Bug reproduzieren
Als erstes versuche ich, den Bug lokal nachstellen zu können, also auf meinem Entwickler-Laptop. Das funktioniert aber nicht, da alles fehlerfrei funktioniert, incl. Upload der beiden Bilder.
Hier wäre jetzt eine gute Gelegenheit, das Ticket mit "Works on my machine" zu schließen.
Aber ich forsche weiter und versuche, das Problem auf dem Server nachzustellen. Und hier gelingt es: Die Bilder verschwinden auf magische Art und Weise. Und ohne Einträge in den Logs.
Analyse
Der Vorgang durchläuft einen Wizard, und nachdem man die Bilder hochgeladen hat, kann man einen Schritt weiter. Wenn man dann einen Schritt zurück geht, sind die Bilder weg.
Mit meiner Testdatei (WOW!!.png) funktioniert es hingegen.
Auffällig ist, dass die andere Datei nicht korrekt in die Tabelle der Dateien übertragen wird, sondern ewig in der Upload-Ansicht verharrt. Es gibt aber keine Fehlermeldung. Die Datei ist 6,4 MB groß, erlaubt sind Dateigrößen bis 10 MB; bei größeren Dateien kommt auch ein entsprechender Hinweis. Hier scheint der Upload nie enden zu wollen, was auch ein Hinweis ist, warum sie später verschwindet: sie war nie auf dem Server.
Mein erster Verdacht war, dass vielleicht ein Virenscanner dazwischenfunkt und die Datei auf dem Server direkt wieder löscht. Alles schon mal vorgekommen. Also lade ich die beiden Bilder per FTP auf den Server, was auch funktioniert und somit gegen die Virenscannerhypothese spricht.
Also zurück in den Browser und mal schauen, was sich so in der Entwicklerkonsole tut. Und dort ist auch ein Eintrag:
Der Upload wurde abgebrochen (413 - Request Entity Too Large). Das hat das JQuery- bzw. PrimeFaces-Framework nicht richtig erkannt und keine Fehlermeldung auf der Oberfläche angezeigt. Ein Bug im Framework, dass dieser spezielle Fall nicht richtig erkannt wird. Das interessiert mich aber nicht so sehr, viel spannender ist der Fehler 413. Erlaubt sind 10 MB, die Datei sind keine 7 MB. Es müsste also eigentlich passen.
In der Anwendung sind 10 MB erlaubt, das sollte nicht das Problem sein, bliebe noch der Anwendungsserver, ein Apache Tomcat, oder der davorgeschaltete ReverseProxy, ein Apache HTTP-Server.
Wenn man dem Netzwerkverkehr folgt sieht man, dass folgende Antwort beim Upload-Versuch zurück kommt:
Das Indiz für die Ursache des Problems ist das unscheinbare Wort: "nginx"
Der Verdacht erhärtet sich beim weiteren Testen: Wenn ich die WOW-Testdatei in der Anwendung hochlade, erscheint das POST in den Logs des vorgelagerten Apache-ReverseProxies:
Der Versuch des Hochladens des Problembildes erzeugt keinen Log-Eintrag.
Es scheint also so zu sein, dass der Upload bereits vor meiner Anwendung abgebrochen wird.
Der Server
Schauen wir mal, wie es auf dem Test Server aussieht:
Meine Anwendung starte ich über ein Docker-Compose, bestehend aus:
Apache HTTP als Reverse Proxy
Apache Tomcat
PG Admin
PostgreSQL Datenbank
Auf dem Server selbst läuft aber nicht nur meine Anwendung, sondern auch noch weitere, die uA über unterschiedliche Domainnamen erreichbar sind. Und das steuert ein Nginx-Server:
Und was finde ich, wenn ich das default File Limit für Nginx suche:
By default, Nginx has a limit of 1MB on file uploads.
Die Lösung
Wir müssen also den file upload für die Anwendung erhöhten auf 10MB, besser 15MB:
Nach der Änderung die Konfiguration prüfen, um sicher zu gehen, dass keine Fehler eingebaut wurden und beim Neustart (bzw. Reload) der ganze Nginx-Server lahm gelegt wird:
nginx -t
Erst dann den Neustart, bzw. Reload durchführen:
service nginx reload
Test
Abschließend muss natürlich noch der Test erfolgen, ob wir damit erfolgreich waren: Der Upload funktioniert jetzt! 🥳
Everyone talks about Kubernetes, therefore I thought, it might be a good idea to get some experiences with this tool and install it on my developer machine.
To run a single node cluster of Kubernetes on my machine I will install Minikube.
My developer machine is a Windows computer with Ubuntu 20.04 on WSL 2.
$ minikube start
😄 minikube v1.30.1 on Ubuntu 20.04
✨ Automatically selected the docker driver. Other choices: none, ssh
📌 Using Docker driver with root privileges
👍 Starting control plane node minikube in cluster minikube
🚜 Pulling base image ...
💾 Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 3.21 Mi
> gcr.io/k8s-minikube/kicbase...: 373.53 MiB / 373.53 MiB 100.00% 2.76 Mi
🔥 Creating docker container (CPUs=2, Memory=6300MB) ...
🐳 Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
▪ Generating certificates and keys ...
▪ Booting up control plane ...
▪ Configuring RBAC rules ...
🔗 Configuring bridge CNI (Container Networking Interface) ...
▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🔎 Verifying Kubernetes components...
🌟 Enabled addons: default-storageclass, storage-provisioner
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
Step 6: Minikube Basic operations
To check cluster status, run:
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:32769
CoreDNS is running at https://127.0.0.1:32769/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Note that Minikube configuration file is located under ~/.minikube/machines/minikube/config.json
Let's doublecheck that minikube is a running Docker container:
$ sudo docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e36590b3ea7e gcr.io/k8s-minikube/kicbase:v0.0.39 "/usr/local/bin/entr…" 28 minutes ago Up 28 minutes 127.0.0.1:32772->22/tcp, 127.0.0.1:32771->2376/tcp, 127.0.0.1:32770->5000/tcp, 127.0.0.1:32769->8443/tcp, 127.0.0.1:32768->32443/tcp minikube
ingo:~$ minikube dashboard --port=42827 &
[6] 55787
ingo:~$ 🤔 Verifying dashboard health ...
🚀 Launching proxy ...
🤔 Verifying proxy health ...
🎉 Opening http://127.0.0.1:42827/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
👉 http://127.0.0.1:42827/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/
Open URL in Browser:
kubernetes dashboard
To enable a module use command:
minikube addons enable <module>
Example:
$ minikube addons enable portainer
❗ portainer is a 3rd party addon and is not maintained or verified by minikube maintainers, enable at your own risk.
❗ portainer does not currently have an associated maintainer.
▪ Using image docker.io/portainer/portainer-ce:2.15.1
🌟 The 'portainer' addon is enabled
But I have no clue, what to do with the enabled 'portainer' addon. 🤷♂️
Deployments are the recommended way to manage the creation and scaling of Pods.
Use the kubectl create command to create a Deployment that manages a Pod. The Pod runs a Container based on the provided Docker image.
# Run a test container image that includes a webserver
kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
2. View the Deployment:
$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
hello-node 0/1 1 0 9s
3. View the Pod:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-node-7b87cd5f68-rj79x 1/1 Running 0 67s
4. View cluster events:
kubectl get events
5. View the kubectl configuration:
kubectl config view
Create a Service
By default, the Pod is only accessible by its internal IP address within the Kubernetes cluster. To make the hello-node Container accessible from outside the Kubernetes virtual network, you have to expose the Pod as a Kubernetes Service.
Expose the Pod to the public internet using the kubectl expose command:
The --type=LoadBalancer flag indicates that you want to expose your Service outside of the cluster.
The application code inside the test image only listens on TCP port 8080. If you used kubectl expose to expose a different port, clients could not connect to that other port.
2. View the Service you created:
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
hello-node LoadBalancer 10.101.148.235 <pending> 8080:31331/TCP 2m52s
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 71m
On cloud providers that support load balancers, an external IP address would be provisioned to access the Service. On minikube, the LoadBalancer type makes the Service accessible through the minikube service command.
3. Run the following command:
$ minikube service hello-node
|-----------|------------|-------------|---------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------------|-------------|---------------------------|
| default | hello-node | 8080 | http://192.168.49.2:31331 |
|-----------|------------|-------------|---------------------------|
🏃 Starting tunnel for service hello-node.
|-----------|------------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------------|-------------|------------------------|
| default | hello-node | | http://127.0.0.1:34597 |
|-----------|------------|-------------|------------------------|
🎉 Opening service default/hello-node in default browser...
👉 http://127.0.0.1:34597
❗ Because you are using a Docker driver on linux, the terminal needs to be open to run it.
Open http://127.0.0.1:34597/ in a browser:
hello-node
4. View Pods and Services created in 'default' namespace:
$ kubectl get pod,svc -n default
NAME READY STATUS RESTARTS AGE
pod/hello-node-7b87cd5f68-rj79x 1/1 Running 0 12m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/hello-node LoadBalancer 10.101.148.235 <pending> 8080:31331/TCP 8m54s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 77m
5. Cleanup
$ kubectl delete service hello-node
service "hello-node" deleted
$ kubectl delete deployment hello-node
deployment.apps "hello-node" deleted
kubectl apply -f deployment.yaml
kubectl get deployment
kubectl get service
Start Service:
minikube service nginx-service &
Check in Terminal:
$ http http://127.0.0.1:45137
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 615
Content-Type: text/html
Date: Thu, 29 Jun 2023 14:57:23 GMT
ETag: "6488865a-267"
Last-Modified: Tue, 13 Jun 2023 15:08:10 GMT
Server: nginx/1.25.1
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>This is a Heading</h1>
<p style="color: green;">This is a paragraph.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</body>
</html>
Dockerfile for an nginx webserver to deliver this website:
FROM nginx
COPY index.html /usr/share/nginx/html
EXPOSE 80
Build, run and test image:
docker build -t myweb-image .
docker run -it -p 80:80 --name myweb-container myweb-image
$ http http://localhost
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 763
Content-Type: text/html
Date: Thu, 29 Jun 2023 15:57:54 GMT
ETag: "649da8a5-2fb"
Last-Modified: Thu, 29 Jun 2023 15:52:05 GMT
Server: nginx/1.25.1
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>This is a Heading</h1>
<p style="color: green;">This is a paragraph.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</body>
</html>
kubectl apply -f myweb.yaml
kubectl get deployment
kubectl get service
Start Service:
minikube service myweb-service &
Check in Terminal:
$ http http://127.0.0.1:38915
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 763
Content-Type: text/html
Date: Thu, 29 Jun 2023 16:19:18 GMT
ETag: "649da8a5-2fb"
Last-Modified: Thu, 29 Jun 2023 15:52:05 GMT
Server: nginx/1.25.1
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>This is a Heading</h1>
<p style="color: green;">This is a paragraph.</p>
<p>Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.</p>
</body>
</html>
Cleanup:
$ kubectl delete -f myweb.yaml
Work in a pod
Start some pods & service and display them:
kubectl apply -f myweb.yaml
kubectl get all -o wide
Output:
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/myweb-deployment-565b64686c-2nnrl 1/1 Running 0 3m42s 10.244.0.39 minikube <none> <none>
pod/myweb-deployment-565b64686c-m4p4c 1/1 Running 0 3m42s 10.244.0.41 minikube <none> <none>
pod/myweb-deployment-565b64686c-sx6sx 1/1 Running 0 3m42s 10.244.0.40 minikube <none> <none>
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 23h <none>
service/myweb-service NodePort 10.97.251.106 <none> 80:32715/TCP 3m42s app=myweb-app
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/myweb-deployment 3/3 3 3 3m42s myweb-container myweb-image:1.0 app=myweb-app
NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR
replicaset.apps/myweb-deployment-565b64686c 3 3 3 3m42s myweb-container myweb-image:1.0 app=myweb-app,pod-template-hash=565b64686c
i
apt update
apt install httpie inetutils-ping -y
# Connect to another Pod via IP
http 10.244.0.39
# Connect to Service via IP
http 10.97.251.106
# Connect to Service via Service Name
http myweb-service
# Check IP of Service
ping myweb-service
## OUTPUT:
# PING myweb-service.default.svc.cluster.local (10.97.251.106): 56 data bytes
exit
Cleanup:
$ kubectl delete -f myweb.yaml
Environment Variable
Sample how to set an environment variable via deployment file:
Add env section to deployment file:
[...]
spec:
containers:
- name: myweb-container
image: myweb-image:1.0
ports:
- containerPort: 80
env:
- name: MY_ENV_1
value: My Value No 1
- name: MY_ENV_2
value: My Value No 2
---
[...]
Start Pods, jump into Pod and check values:
kubectl apply -f myweb.yaml
kubectl get all -o wide
kubectl exec -it myweb-deployment-864984686b-5p7dn -- /bin/bash
## Inside Pod:
echo $MY_ENV_1
# Output: My Value No 1
echo $MY_ENV_2
# Output: My Value No 2
exit
# Cleanup:
kubectl delete -f myweb.yaml
We have a very, very old application that needs to be migrated into AWS. So we copied all files into AWS EC2 instance and tried to start the application. After fixing a lot of minor problems we faced a tough challenge with a SAPJCO RFC Call.
The Exception message was something like this:
Exception in thread "main" java.lang.ExceptionInInitializerError: JCO.classInitialize(): Could not load middleware layer 'com.sap.mw.jco.rfc.MiddlewareRFC'
JCO.nativeInit(): Could not initialize dynamic link library sapjcorfc [sapjcorfc (Not found in java.library.path)]. java.library.path [/usr/lib/jvm/java-1.6.0-ibm.x86_64/jre/lib/amd64/default:/usr/lib/jvm/java-1.6.0-ibm.x86_64/jre/lib/amd64:/usr/lib]
at com.sap.mw.jco.JCO.<clinit>(JCO.java:871)
at java.lang.J9VMInternals.initializeImpl(Native Method)
at java.lang.J9VMInternals.initialize(J9VMInternals.java:199)
I guess, with a JCO Version 3 we would not have much trouble, but in this ancient application JCO Version 2 is used and we cannot update to Version 3 without a huge efford. In other projects I had the luck that I could migrate to Version.
The application is running on a Linux system. But belive me: it would have been much harder on a Windows machine.
Analysis
To find the cause of the problem I wrote the simpliest JCO Test Programm I can image:
import com.sap.mw.jco.JCO;
public class TestMain {
public static void main(String[] args) {
System.out.println(JCO.getVersion());
}
}
Exception in thread "main" java.lang.ExceptionInInitializerError: JCO.classInitialize(): Could not load middleware layer 'com.sap.mw.jco.rfc.MiddlewareRFC'
JCO.nativeInit(): Could not initialize dynamic link library sapjcorfc [/app/JCo/libsapjcorfc.so: librfccm.so: cannot open shared object file: No such file or directory]. java.library.path [/app/JCo]
at com.sap.mw.jco.JCO.<clinit>(JCO.java:871)
at TestMain.main(TestMain.java:11)
Need to set an environment property first:
export LD_LIBRARY_PATH=/app/JCo
Run command line to start programm again and got another error:
Exception in thread "main" java.lang.ExceptionInInitializerError: JCO.classInitialize(): Could not load middleware layer 'com.sap.mw.jco.rfc.MiddlewareRFC'
JCO.nativeInit(): Could not initialize dynamic link library sapjcorfc [/app/JCo/libsapjcorfc.so: libstdc++.so.5: cannot open shared object file: No such file or directory]. java.library.path [/app/JCo]
at com.sap.mw.jco.JCO.<clinit>(JCO.java:871)
at TestMain.main(TestMain.java:11)
The interesting part of the error message:
Could not initialize dynamic link library sapjcorfc [/app/JCo/libsapjcorfc.so: libstdc++.so.5
Solution
We need the libstdc++.so.5 library, but installed is libstdc++.so.6
To get libstdc++.so.5 we installed package compat-libstdc++-33-3.2.3-66.x86_64:
yum install compat-libstdc++-33-3.2.3-66.x86_64
## to be honest, I am not exactly 100% sure, what I did in my investigations, so the command may be a little differend, ex:
# yum install compat-libstdc++-33-3
# yum install compat-libstdc++-33
# yum install compat-libstdc++-33 libstdc++.so.5
Test
Run from command line:
java -cp ".:/app/JCo/sapjco.jar" TestMain
That gave me no error, but SAPJCo Version number.:
libstdc++.so.5 is a very old version of the standard c++ library.
Some Analysis Details
Writing this article is giving me the feeling, that this was all super easy. But in reality it was a real pain in the allerwertesten.
To isolate the source of the problem, I did not only write the small Java (JCO.getVersion) application, I also set up a Docker environment.
One challenge was to find a useful Docker image to start from. I started with an OpenJDK Image that was already deprecated. Deprecated was not the problem, but I could not install libstdc++.so.5.
Next I tried to use the newer, undeprecated Eclipse-Temurin Image. But still could not install libstdc++.so.5
So I finally ended in a Debian Image and self installed Java where I was able to install libstdc++5.
But there is one problem: You can only interact with a running container. But the TestMain-Programm is executed and immediately closed.
So I wrote another Test Programm, that keeps running, so I can enter the running container and test stuff (install packages, compile Java programm, etc.):
import java.io.BufferedReader;
import java.io.InputStreamReader;
import com.sap.mw.jco.JCO;
public class TestMain {
public static void main(String[] args) {
System.out.println("Hello World");
//System.out.println("JCO Version: " + JCO.getVersion());
while (true) {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Enter Input : ");
try {
String s = br.readLine();
System.out.println(s);
}catch(Exception e) {
System.out.println(e);
}
}
}
}
Version 2 aus dem Beispiel Reverse Proxy mit OneLogin soll zu Testzwecken in der Cloud zum Laufen gebracht werden.
Bei dem PoC handelt es sich um einen Apache HTTP-Server mit simpler Public Page und OneLogin Authentifizierung für den Zugriff auf die Private Page: der ShowHeaders Anwendung in einem Apache Tomcat-Server. Beide Server laufen in Docker Containern, die über Docker Compose gestartet werden.
Cloud Server einrichten
Auf dem Server muss Docker etc. eingerichtet werden:
sudo su
yum install docker -y
systemctl umask docker
systemctl start docker
docker version
curl -L https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-`uname -s`-`uname -m` -o /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose
docker-compose --version
# # Output:
# Docker Compose version v2.2.3
yum install git -y
mkdir /app
cd /app
git clone https://github.com/DerIngo/DockerOneLoginApacheSample.git
vim /app/DockerOneLoginApacheSample/version2/reverseproxy/conf/reverseproxy.conf
Fehler
Auf dem lokalen Entwicklerlaptop läuft der PoC und entsprechend soll er auch in der Cloud gestartet werden:
sudo su
cd /app/DockerOneLoginApacheSample/version2
docker-compose up
Zuerst sieht alles wie gewohnt aus, bis Maven ShowHeaders bauen soll:
root@showheaders:/usr/local/tomcat/ShowHeaders# mvn package
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------< deringo:ShowHeaders >-------------------------
[INFO] Building ShowHeaders 0.0.1-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-catalina/8.5.53/tomcat-catalina-8.5.53.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.956 s
[INFO] Finished at: 2022-06-07T12:28:46Z
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal on project ShowHeaders: Could not resolve dependencies for project deringo:ShowHeaders:war:0.0.1-SNAPSHOT: Failed to collect dependencies at org.apache.tomcat:tomcat-catalina:jar:8.5.53: Failed to read artifact descriptor for org.apache.tomcat:tomcat-catalina:jar:8.5.53: Could not transfer artifact org.apache.tomcat:tomcat-catalina:pom:8.5.53 from/to central (https://repo.maven.apache.org/maven2): Transfer failed for https://repo.maven.apache.org/maven2/org/apache/tomcat/tomcat-catalina/8.5.53/tomcat-catalina-8.5.53.pom: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/DependencyResolutionException
Es kann eine Dependency nicht aufgelöst werden und im Text findet sich dieser Satz: "PKIX path building failed". Also irgendwas mit Zertifikaten, wie mir die Erfahrung sagt.
Nach einer Recherche ist klar: In dem Cloud Setup wird der ausgehende Traffic durch einen Proxy geleitet und dessen Zertifikate sind nicht in den Standard-Zertifikaten enthalten.
Simple Lösung: Validierung aushebeln
Die schnellste Lösung: Einfach auf jedwede Validierung verzichten.
In dem Dockerfile von ShowHeaders wird der Maven Aufruf um obige Parameter ergänzt:
WORKDIR ShowHeaders
# aus
RUN mvn package
# wird
RUN mvn package -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true
und schon kann das Image wieder erfolgreich gebaut werden.
Der gleiche Fehler
Die Images werden gebaut, die Container starten und die Public Page wird korrekt angezeigt auf http://localhost:80/public
Aber: die Private Page auf http://localhost:80/private wird nicht angezeigt, bzw. wird nicht mal das OneLogin Login angezeigt.
Das Problem findet sich in der OpenID Konfiguration für OneLogin:
Das funktioniert prinzipiell, dauert aber sehr lange. Außerdem wird in einem ausführlichen Kommentar darauf hingewiesen, dass die Intermediate Certificates so nicht gezogen würden.
Eine ausführliche Anleitung findet sich auf Baeldung, deren Ansatz ist wesentlich schneller (von mir nicht auf Intermediate C. getestet):
Leider kann momentan nur auf der Konsole getestet werden:
curl localhost/private/
Aber der Docker Output sieht nicht gut aus: "SSL certificate problem: unable to get local issuer certificate":
version2-reverseproxy-1 | [Tue Jun 07 13:48:25.115383 2022] [auth_openidc:error] [pid 9:tid 140428228966144] [client 172.21.0.1:57644] oidc_util_http_call: curl_easy_perform() failed on: https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration (SSL certificate problem: unable to get local issuer certificate)
version2-reverseproxy-1 | [Tue Jun 07 13:48:25.115513 2022] [auth_openidc:error] [pid 9:tid 140428228966144] [client 172.21.0.1:57644] oidc_provider_static_config: could not retrieve metadata from url: https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configurationversion2-reverseproxy-1 | 172.21.0.1 - - [07/Jun/2022:13:48:25 +0000] "GET /private/ HTTP/1.1" 500 531
version2-reverseproxy-1 | 172.21.0.1 - - [07/Jun/2022:13:48:27 +0000] "GET /private/ HTTP/1.1" 302 478
Allerdings, nach einem Neustart sieht es doch gut aus in der Konsole:
curl localhost/private/
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1><p>The document has moved <a href="https://deringo-dev.onelogin.com/oidc/2/auth?response_type=code&scope=openid&client_id=781a5c80-a77f-013a-c94a-020f214c554637671&state=4RRlolG4XrN45Q3yCOc15Hf_6Ns&redirect_uri=http%3A%2F%2Flocalhost%2Fprivate%2Fredirect_uri&nonce=B-PSe0ZSm6ZIoHebvpVtNQnv_JHqQLeSMpkI5tuoEi0">here</a>.</p>
</body></html>
Anschließend wieder gestoppt, Reverse Proxy Image neu bauen lassen, Container wieder gestartet: Beim ersten curl kommt der "SSL certificate problem: unable to get local issuer certificate"-Fehler, bei weiteren curls der "The document has moved"-Hinweis.
Schauen wir mal, wie das im Container aussieht:
Vorbereitung:
# frisch starten:
docker-compose down -v
docker-compose build reverseproxy
docker-compose up
# vor dem ersten Aufruf: Ab in den Container:
docker exec -it version2-reverseproxy-1 bash
Im Container:
# Im Container:
apt install curl
# 1. Aufruf
curl https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
# 2. Aufruf
curl https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
# 3. Aufruf
curl https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
{"acr_values_supported":["onelogin:nist:level:1:re-auth"],"authorization_endpoint":"https://deringo-dev.onelogin.com/oidc/2/auth","claims_parameter_suppor[...]
Ich habe es noch ein paar mal ausprobiert, ungefähr jedes zweite bis dritte Mal hat es funktioniert.
Offensichtlich ist da noch ein LoadBalancer zwischen. 🤦♂️🤦♂️🤦♂️
Mehrmaliges pingen gab zwei verschiedene IPs zurück.
apt install nmap
nmap deringo-dev.onelogin.com
Starting Nmap 7.80 ( https://nmap.org ) at 2022-06-07 14:18 UTC
Nmap scan report for deringo-dev.onelogin.com (52.29.255.229)
Host is up (0.0052s latency).
Other addresses for deringo-dev.onelogin.com (not scanned): 52.29.255.230
rDNS record for 52.29.255.229: ec2-52-29-255-229.eu-central-1.compute.amazonaws.com
Not shown: 994 filtered ports
PORT STATE SERVICE
21/tcp open ftp
53/tcp open domain
80/tcp open http
443/tcp open https
554/tcp open rtsp
1723/tcp open pptp
Aha! Es gibt zwei IPs zu dem Domainnamen und anscheinend ist OneLogin in den AWS.
Nochmal ein anderer Test; Wie ist das denn, wenn ich nicht im Container, sondern direkt vom Host curle:
Jeder Aufruf funktioniert, diesmal sieht es wirklich gut aus.
Umsetzung
Das Dockerfile des Reverse Proxies:
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
COPY /etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt /usr/local/share/ca-certificates/ZscalerCloudChain.crt
RUN update-ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Anschließend das Image neu bauen:
docker-compose build reverseproxy
Ich fasse es nicht:
Step 3/6 : COPY /etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt /usr/local/share/ca-certificates/ZscalerCloudChain.crt
1 error occurred:
* Status: COPY failed: file not found in build context or excluded by .dockerignore: stat etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt: filedoes not exist, Code: 1
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
COPY ./ZscalerCloudChain.crt /usr/local/share/ca-certificates/ZscalerCloudChain.crt
RUN update-ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
cp /etc/pki/ca-trust/source/anchors/ZscalerCloudChain.crt /app/DockerOneLoginApacheSample/version2/reverseproxy/ZscalerCloudChain.crt
docker-compose build reverseproxy
docker-compose up
Test
curl localhost/private/
Keine Fehler im Log! 🎉
Test im Browser mit Login
Steht noch aus, aktuell ist der Server nur über Console zu erreichen.
UPDATE: Inzwischen wurde der Zugriff per Browser auf den Server freigeschaltet (bzw. eine fehlerhafte Netzwerkkonfiguration gefixt) und die Public Seiten sind zugänglich.
Bei Aufruf der Private Seiten kommt allerdings eine Fehlermeldung:
Ich hatte zuerst versucht, den Reverse Proxy über scale zu vervielfältigen, aber das funktionierte nicht, da jeder RP den selben Hostnamen zugewiesen bekommt. Laut Forenkommentaren soll man das Problem wohl mittels Scripte oder Docker Swarm lösen können, für dieses kleine Projekt war es hingegen völlig ausreichend, den Block für den RP zu duplizieren.
Load Balancer
Neu hinzugekommen ist der den beiden RPs vorgeschaltete Load Balancer.
Die Regel, nach der das Loadbalancing erfolgt, ist hier nicht relevant und wird nicht explizit gesetzt.
Der erste Versuch, bei dem die Anfragen abwechselnd auf den RPs verteilt werden funktioniert für die public Pages.
Der Login bei OneLogin funktioniert, aber nicht das öffnen der Seite. Anscheinend harmoniert der OneLogin Flow nicht mit diesem Setup, es scheint so, als ob die Antwort der Anfrage von RP1 an OneLogin von RP2 erhalten wird, dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP1 erhalten wird , dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP2 erhalten wird , dieser aber nichts damit anfangen kann und eine neue Authentifizierungsanfrage an OneLogin schickt, deren Antwort wiederum von RP1 erhalten wird, [...]
Es ist also notwendig, das wir immer auf dem selben RP landen. Das Load Balancing darf nur einmal am Anfang statt finden.
Um das zu erreichen, setzten wir einen Header, der die Route zum RP enthält und setzten die Session sticky.
Im nächsten Schritt möchte ich verschiedene Netzwerke und Server verwenden.
Der User kommt aus dem Internet und geht über den Load Balancer in die DMZ, in der er über die RPs Zugang zu den Public Servern hat und nach Authentifizierung über OneLogin (Internet) gelangt er in das Interne Netz wo er Zugang auf den ShowHeaders und die Privaten Server hat.
Vorbereitet wird auch schon die Authorisierung über die RPs und OneLogin: Falls der Benutzer die Rolle user hat, bekommt er Zugang auf den User Server, falls er die Rolle admin hat, bekommt er Zugang auf den Admin Server.
In der Docker Compose Datei werden all die Server in der Services Sektion angelegt und den jeweiligen Netzwerken zugewiesen, in der darauf folgenden Networks Sektion definiert werden:
Das Public Netzwerk muss angelegt werden, anschließend kann Docker Compose gestartet werden:
docker network create public_network
docker-compose up
Test
Wie zuvor: Über die Logausgaben in dem Terminalfenster, in dem Docker Compose gestartet wurde, kann man gut nachvollziehen, welche Server aufgerufen werden.
Vor dem Login werden über Load Balancer und RPs die Seiten der beiden Public Server angezeigt.
Nach dem Login werden auch die Private und ShowHeaders Seiten angezeigt.
Außerdem werden auch die Seiten der User und Admin Server angezeigt. Das sollte nur erfolgen, wenn der eingeloggte Benutzer auch die entsprechenden Rollen hat, wird aber momentan noch nicht abgefragt. Die Umsetzung wird weiter unten beschrieben, sobald ich herausgefunden habe, wie sie zu implementieren ist.
Der Zugang zu den Seiten der User und Admin Server soll nur mit entsprechenden Rollen erfolgen.
Die Implementierung ist noch offen.
UPDATE: Inzwischen konnte ich mit einem Experten für OneLogin sprechen und wurde aufgeklärt, dass es seitens OneLogin gar nicht vorgesehen ist, dass die Anwendungs-Rollen in OneLogin gepflegt werden.
Folglich kann keine Authorisierung durch den RP mit OneLogin erfolgen.
Im letzten Post habe ich mir OneLogin angeschaut und zwei Javascript Beispiele zum laufen gebracht.
In diesem Post möchte ich einen Reverse Proxy aufbauen, der eine öffentlich zugängliche Seite bereit stellt und eine private Seite nur für eingeloggte Mitglieder.
OneLogin
Auf der OneLogin Applications Seite sammle ich folgende Informationen ein, die später in der ReverseProxy Konfiguration benötigt werden:
Client ID
Client Secret
Issuer URL
Außerdem wird der Token Endpoint auf Basic gesetzt.
In der Configuration muss eine Redirect URI eingetragen werden, in diesem Fall: http://localhost/private/redirect_uri
Reverse Proxy
Den Reverse Proxy wird mit Docker aufgebaut.
Der Reverse Proxy wird eine Startseite bereit stellen und von dort auf eine öffentlich zugängliche Unterseite und einen geschützten Bereich verlinken.
In der ersten Version wird der geschützte Bereich lediglich eine weitere Unterseite sein.
In der zweiten Version wird ein weiterer geschützter Bereich mit der ShowHeaders App hinzugefügt.
In Eclipse habe ich zuerst das neue GitHub-Repository hinzugefügt und ausgechecked, dann händisch .project angelegt und konnte dann in Eclipse über Import das Project hinzufügen. Fühlt sich viel zu umständlich an, aber ich muss das zum Glück nicht so oft machen, als dass ich dem jetzt weiter auf dem Grund gehen müsste, wie das besser geht.
Es wird ein Docker Image angelegt, das wiederum über Docker-Compose gestartet wird, um Dateien des Filesystems einzubinden. Das ist für die Entwicklung leichter, am Ende könnte man natürlich alles in ein Image packen und starten.
Das Docker Image basiert auf dem offiziellen Apache HTTPD Image. Es wird mod_auth_openidc hinzugefügt, sowie ca-certificates um eine verschlüsselte Verbindung per HTTPS zum OneLogin-Server aufbauen zu können. Die Datei für den mod_auth_openidc muss noch an die richtige Stelle verschoben werden und ein Backup der originalen httpd.conf angelegt werden.
FROM httpd:2.4
RUN apt update && apt install -y \
libapache2-mod-auth-openidc \
ca-certificates
RUN cp /usr/lib/apache2/modules/mod_auth_openidc.so /usr/local/apache2/modules/
RUN mv conf/httpd.conf conf/container_httpd.conf
CMD ["httpd-foreground"]
Im Docker-Compose wird das Image gebaut und die HTML Seiten sowie Konfigurationsdateien eingebunden.
Dabei wird die Datei reverseproxy_httpd.conf als httpd.conf eingebunden und über diese Datei wird die zuvor gesicherte, originale httpd.conf und anschließend die reverseproxy.conf geladen. Das ist eine einfache Möglichkeit, die ursprüngliche Konfiguration zu erhalten. Für ein produktives Setup ist das vermutlich nicht die beste Wahl.
Die Variable ${PWD} ist unter Linux verfügbar, daher starte ich den Container unter Windows WSL.
Die Datei reverseproxy_httpd.conf (bzw. httpd.conf im Container) ist simpel aufgebaut und enthält nur die Includes zur ursprünglichen httpd.conf und zu unserer reverseproxy.conf:
# load original configuration first
Include conf/container_httpd.conf
# customized configuration
ServerName reverseproxy
Include conf/reverseproxy.conf
Apache HTTPD Konfiguration
Die in der OneLogin Seite eingesammelten Werte müssen entsprechend in die Konfiguration eingetragen werden.
Geschützt wird der Bereich, der unter /private liegt.
LoadModule proxy_module modules/mod_proxy.so
LoadModule xml2enc_module modules/mod_xml2enc.so
LoadModule proxy_html_module modules/mod_proxy_html.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule auth_openidc_module modules/mod_auth_openidc.so
<VirtualHost *:80>
ServerAdmin deringo@github.com
DocumentRoot "/usr/local/apache2/htdocs"
ServerName localhost
## mod_auth_openidc
## https://github.com/zmartzone/mod_auth_openidc
#this is required by mod_auth_openidc
OIDCCryptoPassphrase a-random-secret-used-by-apache-oidc-and-balancer
OIDCProviderMetadataURL https://deringo-dev.onelogin.com/oidc/2/.well-known/openid-configuration
OIDCClientID geheim-a91c-013a-175a-02471d082b0b208817
OIDCClientSecret wirklich-ganz-geheim
# OIDCRedirectURI is a vanity URL that must point to a path protected by this module but must NOT point to any content
OIDCRedirectURI http://localhost/private/redirect_uri
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
<Location /private>
AuthType openid-connect
Require valid-user
</Location>
</VirtualHost>
Im laufenden Docker Container den Apache neu durchstarten:
apachectl -t && apachectl restart
Testen
In einem neuen Browserfenster, im Inkognito Modus die Seite öffnen: http://localhost.
Der Link Index Page führt auf diese Index-Seite, der Public Page Link auf die öffentlich zugängliche Seite und Private Page auf die Seite, die nur für OneLogin User zugänglich ist.
Die Public Page:
Die Private Page führt im ersten Schritt zum OneLogin Login:
Erst nach erfolgreichem Login sehen wir die private Seite:
Reverse Proxy - mit ShowHeaders
Der Reverse Proxy schreibt einige Informationen in den Header, diese werden aber nur dem Server gesendet, der Client (zB unser Webbrowser) sieht davon nichts. Um sehen zu können, welche Informationen übermittelt werden, verwende ich eine kleine App, die nichts anderes macht, als die Header anzuzeigen, daher auch der Name ShowHeaders.
Bisher existierte noch kein Dockerfile für ShowHeaders, daher habe ich das für diesen Test entwickelt und hinzugefügt:
FROM tomcat:8.5-jdk8-openjdk-slim
RUN apt update && apt install -y \
maven
RUN git clone https://github.com/DerIngo/ShowHeaders.git
WORKDIR ShowHeaders
RUN mvn package
WORKDIR $CATALINA_HOME
RUN mv ShowHeaders/target/ROOT.war webapps
EXPOSE 8080
CMD ["catalina.sh", "run"]
ShowHeaders wird in die Docker Konfiguration mit aufgenommen:
Die OIDCRemoteUserClaim-Konfiguration scheint keinen Einfluss zu haben:
# maps the email/prefered_username claim to the REMOTE_USER environment variable
OIDCRemoteUserClaim email
#OIDCRemoteUserClaim preferred_username
Auf der ShowHeaders-Seite werden oidc_claim_email und oidc_claim_preferred_username angezeigt. Hingegen wird keine Header REMOTE_USER angezeigt.
Das Entfernen der OIDCRemoteUserClaim-Konfiguration hat auch keinen Einfluss auf die angezeigten Header.
Anscheinend macht diese Konfiguration nicht das, was ich erwartet hatte, daher entferne ich sie wieder. Weitere Recherchen dazu sind für diesen Test nicht notwendig, daher belasse ich es dabei.
Weitere Informationen zur Konfiguration des Mod Auth OpenIDC finden sich in der kommentierten Beispielkonfiguration auf GitHub.
GitHub
Die Dateien zu diesem Post sind im OneLogin-GitHub-Projekt unter version1 und version2 zu finden.
In meinem letzten Blogeintrag habe ich eine geDockerte Anwendung auf einem Server mit Ubuntu 18 zum laufen gebracht. Aus verschiedenen Gründen war das aber nur ein Zwischenschritt, um zu testen, ob die Anwendung grundsätzlich in solch einer Umgebung lauffähig ist. Neben den beschriebenen Problemen gab es noch viele weitere, die gelöst werden mussten.
Als nächsten Schritt möchte ich die Anwendung in die AWS umziehen, immerhin bin ich ja inzwischen ein zertifizierter Cloud Practitioner.
AWS User
Mit dem Stammbenutzer einen neuen IAM Nutzer für die Anwendung anlegen. Dieser bekommt erstmal umfangreiche Rechte, was nicht best Practice ist und später sollte ich diese Rechte auf das unbedingt benötigte zurücksetzen.
EC2 Server
Die Anwendung soll erstmal mit dem Docker Setup auf einem EC2 Server laufen.
Mit dem neuen IAM Nutzer wechsele ich zuerst auf die Europa Zone ec-central-1.
Ich lege eine neue EC2 Server Instanz an, wobei ich als Sparfuchs nach "nur kostenloses Kontingent" filtere und ein AMI für Ubuntu Server 20.04 LTS (x64) und Typ t2.micro auswähle. Es wird ein neues Schlüsselpaar erzeugt und ich speichere den privaten Schlüssel.
Über EC2 > Instances > Server-Instanz auswählen.
Über Verbinden lässt sich im Browser ein Terminal öffnen. Hier lässt sich aber auch am einfachsten die öffentliche IP und vor allem der Benutzername finden:
Ich habe allerdings nicht die Web Shell verwendet, sondern die Daten, sowie den privaten Schlüssel genommen, um eine Verbindung in WinSCP einzurichten. So kann ich später leicht die Daten auf den Server kopieren und per Klick eine PuTTY-Shell öffnen.
Port Freigabe
Standardmäßig ist für den Server nur Port 22 für SSH frei gegeben.
Weitere Ports, wie zB der benötigte HTTP Port 80 oder HTTPS 443, lassen sich über die AWS Management Console frei geben.
Die EC2-Server-Instanz auswählen und unter Sicherheit findet sich die Sicherheitsgruppe:
In der Sicherheitsgruppe können die Regeln für den eingehenden Datenverkehr erweitert werden. Dabei ist zu beachten, dass man weitere Regeln hinzufügen muss und nicht den bestehenden Typ SSH auf zB HTTP ändert und speichert, weil das diesen nur ändert und nicht als neue, weitere Regel hinzufügt. Dann kann man zwar die Seiten des Webservers bewundern, aber sich nicht mehr per SSH einloggen.
Server einrichten
Auf der Linux Konsole des EC2-Servers wird dieser eingerichtet, dazu wird Docker Compose installiert, was als Abhängigkeit Docker mitbringt.
apt list --upgradable
sudo apt update
sudo apt upgrade -y
sudo apt install docker-compose -y
sudo docker version # -> 20.10.7
sudo docker-compose version # -> 1.25.0
sudo service docker status # -> running
sudo docker run hello-world
Docker läuft und es werden die Daten der Anwendung auf den Server kopiert und anschließend über Docker Compose gestartet.
sudo docker-compose up
Leider führte das zu einem Fehler, wie er schon bei der Ubuntu 18 Installation aufgetreten ist. Das zuvor gewonnene Wissen kann ich jetzt zur schnellen Fehlerbehebung anwenden:
sudo apt-get remove docker-compose -y
sudo curl -L https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --version
# Output:
-bash: /usr/bin/docker-compose: No such file or directory
# Lösung: neue Shell, zb per tmux, starten
# und dann nochmals testen
docker-compose --version
# Output:
Docker Compose version v2.2.3
Anschließend ließ sich die Anwendung per Docker Compose starten und per cURL, bzw. HTTPie, über localhost:80 und <öffentlicheIP>:80 aufrufen. Der Aufruf <öffentlicheIP>:80 vom Entwickler Laptop funktioniert auch.
Der Start dauerte etwas länger, die Webanwendung selbst ließ sich anschließend aber angenehm schnell bedienen. Zumindest als Test-Server scheint der "Gratis"-EC2-Server völlig auszureichen.
Ausblick
Auf dem kostenfreien Server laufen ein Tomcat Webserver, eine PostgreSQL Datenbank und PGAdmin und das, zumindest den ersten Tests nach, mit völlig ausreichender Performance.
Als nächstes möchte ich dem Docker Compose Konstrukt noch um einen Reverse Proxy erweitern, der eine (vermutlich nur selbstsignierte) verschlüsselte Verbindung per HTTPS anbietet und über Port 80 und 443 die Anwendung und den PGAdmin erreichbar macht. Außerdem soll es einen einfachen Authentifizierungs- und ggf. Authorisierungsmechanismus geben. Das wird mit einem Apache HTTP Server realisiert werden und sollte keinen besonderen Ressourcenbedarf haben.
Falls sich die Zeit findet, möchte ich das um Keycloak erweitern und den Zugriff auf Anwendung und PGAdmin erst nach erfolgreicher Authentifizierung und Authorisierung erlauben. Vielleicht ist das noch mit dem Apache HTTP Server realisierbar, ggf. werde ich aber auf zB Traefik umstellen. Bei dem Setup kann ich mir schon vorstellen, dass die Ressourcen des kleinen Server nicht mehr ausreichen und es zu spürbaren Performanceeinbrüchen kommen wird.
Eine ansprechendere URL, anstelle der generierten AWS URL, wäre wünschenswert.
Docker Compose Datei vom Entwicklungsrechner auf den Server kopieren, kleinere Anpassungen vornehmen und ausführen. So einfach habe ich es mir vorgestellt, aber es gab dann leider doch noch Herausforderungen zu bewältigen:
Docker Compose Updaten
Ich habe einen Server mit dem nicht mehr ganz taufrischen Ubuntu 18 am laufen und wollte dort ein Docker Compose Skript ausführen.
Das Skript läuft auf meinem Entwicklungsrechner, aber auf dem Server wurde lediglich eine Fehlermeldung ausgegeben:
dockeruser@myServer:~/myproject$ docker-compose up
ERROR: Version in "./docker-compose.yml" is unsupported. You might be seeing this error because you're using the wrong Compose file version. Either specify a supported version (e.g "2.2" or "3.3") and place your service definitions under the `services` key, or omit the `version` key and place your service definitions at the root of the file to use version 1.
For more on the Compose file format versions, see https://docs.docker.com/compose/compose-file/
Wie sich herausstellte, war für Ubuntu 18 bei Docker Compose 1.17.1 Schluss und ich muss händisch upgraden:
Docker Compose entfernen:
sudo apt-get remove docker-compose
Die aktuelle Docker Compose Version ermitteln (heute: 2.2.3): https://github.com/docker/compose/releases
Auf dieser Seite kann man auch den direkten Link zum Download finden, falls es beim ausführen des nächsten Befehls zu Problemen kommt.
Beispielsweise ist die Versionsnummer v2.2.3, also mit einem kleinen "v" am Anfang und wenn das fehlt, schlägt der Download fehl.
So lautet der Link für mein Ubuntu: https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-x86_64
Looks like this is because it defaults to use the secretservice executable which seems to have some sort of X11 dependency for some reason. If you install and configure pass docker will use that instead which seems to solve the problem.
# substitute with the latest version
url=https://github.com/docker/docker-credential-helpers/releases/download/v0.6.4/docker-credential-pass-v0.6.4-amd64.tar.gz
# download and untar the binary
wget $url
tar -xzvf $(basename $url)
# move the binary to a dir in your $PATH
sudo mv docker-credential-pass /usr/local/bin
# verify it works
docker-credential-pass list
# cleanup
rm docker-credential-pass-v0.6.4-amd64.tar.gz
gpg: agent_genkey failed: Keine Berechtigung Schlüsselerzeugung fehlgeschlagen: Keine Berechtigung
Und eine Erklärung mit Lösungsvorschlag findet sich hier:
Expected behavior. Here's why. At the point of failure, gen-key is about to ask the user for a passphrase. For security purposes, rather than using stdin/stdout, it wants to directly open the controlling terminal for the session and use that handle to write the prompt and receive the passphrase. When you use su to switch to some other user, the owner of the controlling terminal device file does not change; it remains associated with the user who actually logged in (i.e. received a real terminal from getty or got a pty from telnet or ssh or whatever). That device file is protected mode 600, so it can't be opened by anyone else.
The solution is to sudo-chown the device file to the user-who-needs-to-gen-the-key before su'ing to that user. Create the key within the su'd environment, then exit back to the original environment. Then, finally, sudo-chown the terminal back to yourself.
Glücklicherweise geht es auch einfacher, indem man einfach das Programm tmux verwendet. 🙂
tmux
# create a gpg2 key
gpg2 --gen-key
# list key information
gpg2 -k
# Copy the key id (from the line labelled [uid]) and do
pass init "whatever key id you have"
Jetzt sollte der Docker Login funktionieren, aber:
Auch wieder kein neues Problem, dass zB bereits hier und hier diskutiert wurde.
mkdir ~/.docker
touch ~/.docker/config.json
# brachte jeweils keine Änderung
/usr/local/bin/docker-credential-pass
# Output:
-bash: /usr/local/bin/docker-credential-pass: Keine Berechtigung
# Erfolg kam mit diesem Befehl:
sudo chmod +x /usr/local/bin/docker-credential-pass
#Zumindest funktioniert dieser Aufruf:
docker-credential-pass list
# Ein weiterer Fehler ließ sich beheben durch:
export GPG_TTY=$(tty)
Ich musste die einzelnen Images per docker pull imagename ziehen, erst danach konnte ich docker-compose ausführen.