Ein Setup für alle Backend Development Fälle

Inhalt
Dieser Blogartikel ist etwas länger geworden - insofern hier für die es heute nicht mehr gewohnten Augen eine Kleine Übersicht der Kapitel.

Worum geht's?
Ein bisschen TechStack
Herausforderungen
Beispiele
Code
Fazit

Worum geht's?

Als ein Team mit verschiedenen Erfahrungen und Vorlieben war uns von Anfang an klar, dass ein jeder am liebsten auf "seinem" OS Arbeiten und leben möchte.

Wir hatten also die Wahl: Entweder ein einheitliches Setup schaffen, durch das für alle Beteiligten die Entwicklung identisch wird. Oder wir finden ein OS, mit dem alle gleichermaßen zufrieden sind. (bzw. auf das ein Teil des Teams einfach gezwungen wird.)

Wir entschieden uns dafür ein gemeinsames Setup zu nutzen, völlig unabhängig der persönlichen Vorlieben. Unser Anspruch sah vor den Zugang für alle Entwickler gleichermaßen zu vereinfachen, sodass keine Voraussetzungen an Erfahrungen und Skills gesetzt wurden.

Wir alle wussten von unseren vorherigen Stationen, wie divers Teams sein können und wie unterschiedlich jeder Entwickler arbeitet. Deshalb räumten wir der Thematik eine hohe Priorität ein und versuchten Einiges, um ein möglichst gutes und für uns Entwickler homogenes Ergebnis zu erhalten.

User TechStack

OS

Aus der Unix-Welt nehmen wir einiges mit, theoretisch wäre auch Windows möglich. Aktuell nutzt keiner von uns dies zum Arbeiten. Vielleicht findet sich jemand mit Spaß am Ausprobieren? Man hört ja so einiges (Gutes) über WSL.

  • Arch mit GNOME
  • macOS 11
  • Arch Manjaro
  • Ubuntu 20.10 LTS
     

Backend

Als Pimcore-Agentur ist in jedem Projekt auf jeden Fall schon einmal das Backend weitestgehend definiert. Allerdings haben wir durch die Bandbreite an unterschiedlichen Projekten (alte, wie neue) auch mittlerweile eine ganze Reihe an verschiedenen Stacks beisammen.

  • PHP 7.4|8.1
  • Pimcore 6.6|6.9|10.3
  • Symfony 4.4|5.6
     

Frontend

Das Thema Frontend wird noch einen eigenen Artikel erhalten, aber soviel sei schon einmal gesagt: Unsere Frontends sind recht divers aufgestellt - immer ausgewählt nach den jeweiligen Kundenanforderungen.

  • Vue2|3
  • AlpineJS
  • Webpack
  • Reines Twig

Sprüche, die man bei uns NICHT hören wird

Aber bei mir ging das eben noch!

Ich will nicht in das andere Projekt gucken, da mir das Aufsetzen zu lange dauert.

Ich kann an dem Ticket nicht arbeiten, da ich keine/n Testdaten/content habe.

[FE] Wie geht das nochmal mit dem Projekt (Backend) aufsetzen?

[BE] Mein Browser findet keine Assets.

Wie kann ich nochmal die Seite lokal aufrufen?

Herausforderungen

Es gab im Prozess einige Probleme zu überwinden. Dabei definierten sie bei jeder internen Diskussion einen wichtigen Eckstein, der uns zeigte wo wir hin wollten bzw. was wir in der jeweiligen Situation vermeiden sollten.

Schnelles Aufsetzen von Projekten

Der Klassiker - ein Kunde oder Kollege bittet kurz einmal darum zur Unterstützung in ein Projekt zu schauen. Nicht ganz unbekannt ist, dass so etwas je nach Projekt schon einmal einen halben bis ganzen Tag dauern kann.

Unser eigener Anspruch war hier, dass wir nach dem Klonen des git repositorys innerhalb von 5-10 Minuten auch bereits arbeitsfähig sind.

Kompatibilität der eingesetzten Systeme

Wie schon oben erwähnt, nutzen wir nicht alle die gleichen Systeme zum Arbeiten. Wichtig war uns hier nicht nur, dass alle Kollegen gleichermaßen arbeiten können, sondern auch, dass für alle die gleichen Regeln und Prozesse gelten. Das heißt in der Umsetzung, dass wir uns alle gegenseitig sehr stark unterstützen können. Auch dann, wenn der Mac-User keine Ahnung von Arch und vice versa hat.

Voraussetzungsloser Einstieg

Die Hürde mit unserem Setup zu arbeiten sollte für alle, unabhängig des persönlichen Erfahrungsschatzes, gleichermaßen niedrig sein. So kam es, dass alles Enwicklungsbezogene durch ein paar universelle Befehle vollziehbar ist.

Möglichkeit zum parallelen Arbeiten

Ein weiterer Klassiker - Entwickler können nur eine kleine Anzahl an Projekten gleichzeitig aufgesetzt haben und aktiv laufen lassen, da die Ressourcen der Maschinen nicht ausreichend sind.

Bei uns kann ein Backend-Entwickler lokal alle Projekte gleichzeitig laufen lassen und daran arbeiten, solange der RAM die vielen Instanzen der eigenen IDE überlebt. D.h. auf Deutsch: Ressourcenarmes Betreiben der eingesetzten Services.

Eingesetzte Layer

Ohne Docker Compose 2 wäre einiges viel umständlicher. Glücklicherweise gibt es mittlerweile auch viele OpenSource Projekte, welche direkt mit Docker Compose ausliefern, sodass für uns nur kleine Änderungen notwendig sind.

Mutagen Compose ist unser Kompatiblitäts-Layer zwischen der Mac und der reinen Linux-Welt. Mutagen Compose setzt auf die Synchronisation von mutagen.io den bekannen Docker Compose Layer.

Make ist unser Arbeitspferd für so einige repetitive Aufgaben.

Dockerised Services. Alle extra-Services pro Projekt können mit aufgenommen werden. Sei es Redis, AthenaPDF, Elastic oder weitere.

Einfach git. Wie es sich gehört. Wir nutzen übrigens weitestgehend den GitFlow-Standard.

Wir nutzen træfik für alle internen (development) Routings. Dadurch haben dann alle Entwickler die gleichen lokalen Routen mit einer TLD, welche nicht kollidieren kann.

Beispiele

Hier sind ein paar Beispiele, bevor wir uns in den Code begeben, die verdeutlichen sollen, wie einfach es für uns alle nun ist. Folgende Beispiele kommen quasi jeden Tag vor und gehen flott von der Hand.
Fallen Euch hier noch weitere ein?

Neues Projekt aufsetzen

Kurz den link aus dem Repo suchen, klonen und fast fertig. Je nach Maschine kann der init etwas dauern, da hier auch direkt die Assets für das FE mit gebaut werden.

git clone xy
cd xy
make init

Projekt starten

Wenn der (Mutagen) Sync noch "frisch" ist dauert es 5 Sekunden. Falls nicht, so können es 2 min sein.

make start

Zerstörtes Projekt neu aufsetzen

Jeder Entwickler kennt es: Irgendetwas hat man gerade versucht und dann bekommt man das System / das Projekt nicht mehr gefixt. Nun könnte man stundenlang mit der Suche nach dem Fehler verbringen - oder einfach das zerlegte Projekt dezent beseitigen, neu aufsetzen und dort weitermachen, wo bis eben nichts funktionierte.

make clear -v

(wenn ganz schlimm: docker service neustarten: systemctl restart docker.service)

cd xy
rm -Rf xy
git clone xy
cd xy
make init

Projekt stoppen

Auch wir gehen mal ins Bett und räumen hinter uns auf.

make clear

Sprüche, die man sehr wohl bei uns hört

Shit, schon wieder lokal und Stage verwechselt! (Die Systeme sind bei uns zum Verwechseln ähnlich)

Hast du mal make init gemacht?

Pull' mal, ich habe dir einen Service gebaut.

Code

docker-compose.yaml

In der YAML wird ein ganzes Projekt für die lokale Entwicklung (backendseitig) komplett durch dekliniert. Neben den Standards, wie der Datenbank (maria), dem front-facing webserver (nginx) und dem Pimcore Container (php8.1-fpm) haben wir noch einen zweiten debug-container für die harten Fälle und optionale Komponenten. Aktuell setzen wir hier ein: redis, athena-pdf, elastic. Je nach Projektbedarf kann hier einfach hinzugefügt werden und tiefgreifender konfiguriert.

                                version: '3.7'

services:

    redis:
        image: redis:alpine
        labels:
            - "traefik.enable=false"
        networks:
            - internal

    db:
        image: mariadb:10.5
        working_dir: /application
        command: [ mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb-large-prefix=1, --innodb-file-per-table=1 ]
        volumes:
            - db:/var/lib/mysql
        environment:
            MYSQL_ALLOW_EMPTY_PASSWORD: 1
        env_file:
            -   .env.local
        networks:
            - internal
        labels:
            - "traefik.enable=false"

    nginx:
        image: nginx:stable-alpine
        working_dir: /var/www/html
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=traefik_network"
            - "traefik.http.routers.${PROJECT_NAME}.rule=Host(`${PROJECT_NAME}.lntc`)"
        depends_on:
            - pimcore
            - pimcore-debug
        volumes:
            - pimcore:/var/www/html:ro
            - ./.docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
        networks:
            - internal
            - traefik_network

    pimcore: &pimcore
        user: "${USER_ID}:${GROUP_ID}"
        build:
            context: .
            dockerfile: .docker/Dockerfile.pimcore
        environment: &pimcore-environment
            APACHE_DOCUMENT_ROOT: /var/www/html/public
            COMPOSER_HOME: /var/www/html
        env_file:
            - .env.local
        labels:
            - "traefik.enable=false"
        depends_on:
            - db
            - redis
            - athena
        volumes:
            - pimcore:/var/www/html:cached
        networks:
            - internal

    pimcore-debug:
        <<: *pimcore
        build:
            context: .
            dockerfile: .docker/Dockerfile.pimcore-debug
        environment:
            <<: *pimcore-environment
            PHP_IDE_CONFIG: "serverName=${PROJECT_NAME}.lntc"

    athena:
        labels:
            - "traefik.enable=true"
            - "traefik.docker.network=traefik_network"
            - "traefik.http.routers.${PROJECT_NAME}-athena.rule=Host(`athena.${PROJECT_NAME}.lntc`)"
            - "traefik.port=8080"
        image: arachnysdocker/athenapdf-service:3
        environment:
            WEAVER_AUTH_KEY: "leanatic_secure_password_not123"
        networks:
            - traefik_network

elastic:
        labels:
            - "traefik.enable=false"
        image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0
        environment:
            - node.name=elastic
            - cluster.name=${PROJECT_NAME}-docker-cluster
            - discovery.type=single-node
            - bootstrap.memory_lock=true
            - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
        ulimits:
            memlock:
                soft: -1
                hard: -1
        volumes:
            - elastic:/usr/share/elasticsearch/data
        ports:
            - "9200:9200"
        networks:
            - internal
 
volumes:
    db:
    pimcore:

networks:
    traefik_network:
        external: true
    internal:

x-mutagen:
    sync:

        defaults:
            symlink:
                mode: "posix-raw"
            ignore:
                vcs: true

        pimcore:
            alpha: "./"
            beta: "volume://pimcore"
            mode: "two-way-resolved"
            permissions:
                defaultOwner: "id:${USER_ID}"
                defaultGroup: "id:${GROUP_ID}"
                defaultFileMode: 0644
                defaultDirectoryMode: 0755
            ignore:
                paths:
                    - "/symfony-cache/"
                            

Wie schon erwähnt nutzen wir træfik für das Routing. Dies könnte in Zukunft noch ein eigener Artikel werden. Hier gibt es noch ein wenig zu beachten bei der Installation und Einrichtung.

Im letzten Teil definieren wir neben den Volumes und dem einen geteilten Netzwerk für træfik den Mutagen-sync. Das ist der eine Teil, welches es uns erlaubt komplett gleichartig auf allen Unix-oiden zu arbeiten. Ansonsten hatten (und hätten) wir Probleme mit der Docker-synchronisation auf dem Mac (auch nicht bei allen, wie es scheint).

Makefile

Wie schon erwähnt ist das Makefile unser Arbeitspferd in der täglichen Entwicklung. Es nimmt gerade viele kleine (auf die Dauer sehr anstrengende) Themen ab und reduziert die Probleme miteinander.

So haben wir nur eine überschaubare Anzahl an Befehlen, welche alles backendseitige Aufzetzen und Warten entweder komplett abnehmen (für die FE'ler), oder zumindest stark vereinfachen für alle BE'ler.

                                # Making sure we are having our variables from .env
include .env
include .env.local
export

USER_ID = $(shell id -u)
GROUP_ID = $(shell id -g)

# Other variables
PIMCORE_DIR = .
PIMCORE_SNAPSHOT_DIR = $(PIMCORE_DIR)/etc/snapshot

# The base command to start the docker setup.
DOCKER_COMPOSE = GROUP_ID=$(GROUP_ID) USER_ID=$(USER_ID) mutagen-compose

#############################################################################################
######################################### Utilities #########################################
#############################################################################################

.PHONY: start
start:
	# Starting mutagen daemon if not already up
	mutagen daemon start
	# Staring Docker Compose with mutagen
	$(DOCKER_COMPOSE) up -d

.PHONY: stop
stop:
	# Killing mutagen compose
	$(DOCKER_COMPOSE) stop

.PHONY: clear
clear:
	# Killing mutagen compose
	$(DOCKER_COMPOSE) down

.PHONY: remove_volumes
remove_volumes:
	# Killing mutagen compose
	$(DOCKER_COMPOSE) down -v

.PHONY: init
init: initialize

.PHONY: initialize
initialize:
	$(MAKE) start
	$(MAKE) fix_permissions
	$(MAKE) pimcore_assets
	$(MAKE) pimcore_packages
	$(MAKE) pimcore_restore
	$(MAKE) pimcore_classes_rebuild
	$(MAKE) pimcore_clear_cache

# Mac users tend to have an issue without
.PHONY: fix_permissions
fix_permissions:
	$(DOCKER_COMPOSE) exec --user root pimcore chown -R $(USER_ID):$(GROUP_ID) /var/www/

.PHONY: pimcore_assets
pimcore_assets:
	# Build Symfony encore assets
	cd $(PIMCORE_DIR) && yarn install && yarn build

.PHONY: pimcore_packages
pimcore_packages:
	# Run composer install inside the pimcore container
	$(DOCKER_COMPOSE) exec pimcore composer --no-ansi --no-interaction install

# Just a convenience alias
.PHONY: snapshot
snapshot: pimcore_snapshot

.PHONY: pimcore_snapshot
pimcore_snapshot: _create_pimcore_snapshot_folders
	# Save assets via zip
	zip -r -v $(PIMCORE_SNAPSHOT_DIR)/public/var/assets/assets.zip $(PIMCORE_DIR)/public/var/assets/

	# Save database
	# Removing all the definers - which would break on importing
	$(DOCKER_COMPOSE) exec db mysqldump --routines --add-drop-table -u root $(MYSQL_DATABASE) | grep -v 'SQL SECURITY DEFINER' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*\*/\*/' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*PROCEDURE/PROCEDURE/' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*FUNCTION/FUNCTION/' > $(PIMCORE_SNAPSHOT_DIR)/dump.sql

# Just a convenience alias
.PHONY: restore
restore: pimcore_restore

.PHONY: pimcore_restore
pimcore_restore: _create_pimcore_snapshot_folders
	# Clear assets to make sure we do not add unused assets when we take a snapshot again
	rm -rf $(PIMCORE_DIR)/public/var/assets
	# Restore assets via zip
	unzip -o $(PIMCORE_SNAPSHOT_DIR)/public/var/assets/assets.zip

	# Restore the database
	$(DOCKER_COMPOSE) exec -T db mysql -u root $(MYSQL_DATABASE) < $(PIMCORE_SNAPSHOT_DIR)/dump.sql

.PHONY: _create_pimcore_snapshot_folders
_create_pimcore_snapshot_folders:
	mkdir -p $(PIMCORE_SNAPSHOT_DIR)/public/var/assets
	mkdir -p $(PIMCORE_DIR)/public/var/assets

.PHONY: pimcore_classes_rebuild
pimcore_classes_rebuild:
	# Rebuilding classes
	$(DOCKER_COMPOSE) exec pimcore bin/console pimcore:deployment:classes-rebuild --no-interaction --create-classes -v

.PHONY: pimcore_clear_cache
pimcore_clear_cache:
	# Killing cache
	$(DOCKER_COMPOSE) exec pimcore bin/console cache:clear --no-interaction -v
	$(DOCKER_COMPOSE) exec pimcore bin/console pimcore:cache:clear --no-interaction -v

.PHONY: pimcore_check_var_dump
pimcore_check_var_dump:
	# Killing cache
	$(DOCKER_COMPOSE) exec pimcore vendor/bin/var-dump-check src/ --symfony

.PHONY: pimcore_console
pimcore_console: console

.PHONY: console
console:
	docker-compose exec pimcore bin/console $(filter-out $@,$(MAKECMDGOALS))
                            

Vielleicht fiel auf, dass wir eine gemeinsame Entwicklungsdatenbank haben, welche auch immer mit ins Repo eingechecked wird. Somit haben wir für alle nicht Admin, oder Backend-bezogenen auch direkt einen fertigen (funktionierenden) Stand mit Zugangsdaten und notwendigen Datenobjekten (Usern etc.).

Dies führt auch dazu, dass wir sehr schnell einen Stand löschen und komplett neu aufsetzen können, sollte man mal wieder (was ja mal vorkommt) seine Instanz komplett zerschossen haben.

Fazit

Wir haben mit einigem Trial-and-Error eine Lösung gefunden, sodass alle unsere beteiligten Gewerke einfach und schnell miteinander, jeder an seinen Themen, arbeiten können. Weiter haben wir es geschafft uns zu öffnen für viele Möglichkeiten ohne potentielle neue Mitglieder unseres Teams einzuschränken.
Unser Onboarding wird damit massiv vereinfacht und verkürzt.



Wie seht Ihr das? Habt Ihr dazu Fragen? Anregungen? Was sind Eure Probleme und wie habt Ihr sie überwunden?

Sag Hallo