Bij Kinsta hebben we projecten van alle groottes voor Applicatie Hosting, Database Hosting en Managed WordPress Hosting.
Met de cloudhostingoplossingen van Kinsta kun je applicaties deployen in een groot aantal talen en frameworks, zoals NodeJS, PHP, Ruby, Go, Scala en Python. Elke applicatie kun je deployen met een Dockerfile. Je kunt je Git repository (gehost op GitHub, GitLab of Bitbucket) koppelen om je code direct op Kinsta te implementeren.
Je kunt MariaDB, Redis, MySQL en PostgreSQL databases out-of-the-box hosten, waardoor je tijd overhoudt om je te richten op het ontwikkelen van je applicaties in plaats van je bezig te houden met hosting configuraties.
En als je kiest voor onze Managed WordPress Hosting, ervaar je de kracht van Google Cloud C2 machines op hun Premium Tier netwerk en Cloudflare geïntegreerde beveiliging, waardoor je WordPress websites de snelste en veiligste op de markt zijn.
De uitdaging van het ontwikkelen van cloud-native applicaties binnen een gedistribueerd team overwinnen
Een van de grootste uitdagingen bij het ontwikkelen en onderhouden van cloud-native applicaties op bedrijfsniveau is een consistente ervaring gedurende de hele levenscyclus van de ontwikkeling. Dit is nog moeilijker voor bedrijven die op afstand werken met remote teams die werken op verschillende platforms, met verschillende setups en asynchrone communicatie. We moeten dus een consistente, betrouwbare en schaalbare oplossing bieden die werkt voor:
- Developers en kwaliteitsmonitoringsteams, ongeacht hun besturingssystemen, een eenvoudige en minimale setup creëren voor het ontwikkelen en testen van features.
- DevOps-, SysOps- en Infra-teams, voor het configureren en onderhouden van test- en productieomgevingen.
Bij Kinsta vertrouwen we sterk op Docker voor deze consistente ervaring bij elke stap, van ontwikkeling tot productie. In dit bericht leiden we je door:
- Hoe je Docker Desktop kunt gebruiken om de productiviteit van developers te verhogen.
- Hoe we Docker images bouwen en naar Google Container Registry pushen via CI pipelines met CircleCI en GitHub Actions.
- Hoe we CI pipelines gebruiken om incrementele wijzigingen naar productie te promoten met behulp van Docker images, Google Kubernetes Engine en Cloud Deploy.
- Hoe het QA team naadloos gebruik maakt van voorgebouwde Docker images in verschillende omgevingen.
Docker Desktop gebruiken om de ervaring voor developers te verbeteren
Om een applicatie lokaal te runnen, moeten developers de omgeving minutieus voorbereiden, alle dependencies installeren, servers en services instellen en ervoor zorgen dat ze goed geconfigureerd zijn. Als je meerdere applicaties runt, kan dit lastig zijn, vooral als het gaat om complexe projecten met meerdere dependencies. Als je aan deze variabele ook nog eens meerdere medewerkers met meerdere besturingssystemen toevoegt, ontstaat er chaos. Om dit te voorkomen, gebruiken we Docker.
Met Docker kun je de omgevingsconfiguraties aangeven, de dependencies installeren en images bouwen met alles op de plek waar ze horen. Iedereen, waar dan ook, met welk OS dan ook kan dezelfde images gebruiken en precies dezelfde ervaring hebben als ieder ander.
Je configuratie declaren met Docker Compose
Om te beginnen maak je een Docker Compose bestand, docker-compose.yml
. Het is een declaratief configuratiebestand geschreven in YAML format dat Docker vertelt wat de gewenste status van je applicatie is. Docker gebruikt deze informatie om de omgeving voor je applicatie in te stellen.
Docker Compose bestanden zijn erg handig als je meer dan één container hebt draaien en er dependencies zijn die je gebruikt voor meerdere containers.
Om je docker-compose.yml
bestand te maken:
- Begin met het kiezen van een
image
als basis voor onze applicatie. Zoek op Docker Hub en probeer een Docker image te vinden die al de dependencies van je app bevat. Zorg ervoor dat je een specifieke image tag gebruikt om fouten te voorkomen. Het gebruik van de taglatest
kan onvoorziene fouten in je applicatie veroorzaken. Je kunt meerdere base images gebruiken voor meerdere dependencies. Bijvoorbeeld één voor PostgreSQL en één voor Redis. - Gebruik
volumes
om gegevens op je host te bewaren als dat nodig is. Door gegevens op de hostmachine te bewaren voorkom je dat je gegevens kwijtraakt als docker-containers worden verwijderd of als je ze opnieuw moet maken. - Gebruik
networks
om je opstelling te isoleren om netwerkconflicten met de host en andere containers te voorkomen. Het helpt je containers ook om elkaar gemakkelijk te vinden en met elkaar te communiceren.
Als we alles samenbrengen, hebben we een docker-compose.yml
die er als volgt uitziet:
version: '3.8'services:
db:
image: postgres:14.7-alpine3.17
hostname: mk_db
restart: on-failure
ports:
- ${DB_PORT:-5432}:5432
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${DB_USER:-user}
POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
POSTGRES_DB: ${DB_NAME:-main}
networks:
- mk_network
redis:
image: redis:6.2.11-alpine3.17
hostname: mk_redis
restart: on-failure
ports:
- ${REDIS_PORT:-6379}:6379
networks:
- mk_network
volumes:
db_data:
networks:
mk_network:
name: mk_network
De applicatie containeriseren
Een Docker image bouwen voor je applicatie
Eerst moeten we een Docker image bouwen met behulp van een Dockerfile
, en die callen vanuit docker-compose.yml
.
Om je Dockerfile
bestand te maken:
- Begin met het kiezen van een image als basis. Gebruik het kleinste basisimage dat werkt voor de app. Meestal zijn alpine images zeer minimaal en zijn er bijna geen extra pakketten geïnstalleerd. Je kunt beginnen met een alpine image en daarop verder bouwen:
FROM node:18.15.0-alpine3.17
- Soms moet je een specifieke CPU architectuur gebruiken om conflicten te voorkomen. Stel bijvoorbeeld dat je een
arm64-based
processor gebruikt, maar je moet eenamd64
image bouwen. Je kunt dat doen door de-- platform
op te geven inDockerfile
:FROM --platform=amd64 node:18.15.0-alpine3.17
- Definieer de applicatiemap en installeer de dependencies en kopieer de uitvoer naar je hoofdmap:
WORKDIR /opt/app COPY package.json yarn.lock ./ RUN yarn install COPY . .
- Call de
Dockerfile
vanafdocker-compose.yml
:services: ...redis ...db app: build: context: . dockerfile: Dockerfile platforms: - "linux/amd64" command: yarn dev restart: on-failure ports: - ${PORT:-4000}:${PORT:-4000} networks: - mk_network depends_on: - redis - db
- Implementeer auto-reload zodat wanneer je iets verandert in de sourcecode, je direct een voorbeeld van je wijzigingen kunt bekijken zonder dat je de applicatie handmatig opnieuw hoeft te bouwen. Om dat te doen, bouw je eerst de image en voer je die uit in een aparte service:
services: ... redis ... db build-docker: image: myapp build: context: . dockerfile: Dockerfile app: image: myapp platforms: - "linux/amd64" command: yarn dev restart: on-failure ports: - ${PORT:-4000}:${PORT:-4000} volumes: - .:/opt/app - node_modules:/opt/app/node_modules networks: - mk_network depends_on: - redis - db - build-docker volumes: node_modules:
Pro Tip: Merk op dat node_modules
ook expliciet wordt gemount om platform-specifieke problemen met pakketten te voorkomen. Dit betekent dat in plaats van de node_modules
op de host te gebruiken, de docker container de zijne gebruikt, maar deze op de host in een apart volume mapt.
Incrementeel de productie-image bouwen met continue integratie
De meeste van onze apps en diensten gebruiken CI/CD voor de deployment. Docker speelt een belangrijke rol in het proces. Elke wijziging in de main branch activeert onmiddellijk een bouwpipeline via GitHub Actions of CircleCI. De algemene workflow is heel eenvoudig: het installeert de dependencies, voert de tests uit, bouwt het docker image en pusht het naar Google Container Registry (of Artifact Registry). Het deel dat we in dit artikel bespreken is de bouwstap.
De Docker images bouwen
We gebruiken builds in meerdere fasen om veiligheids- en prestatieredenen.
Fase 1: Builder
In dit stadium kopiëren we de hele codebase met alle broncode en configuratie, installeren we alle dependencies, inclusief dev dependencies, en bouwen we de app. Er wordt een map dist/
aangemaakt en de gebouwde versie van de code wordt daarheen gekopieerd. Maar dit image is veel te groot met een enorme set footprints om te gebruiken voor productie. Omdat we private NPM registries gebruiken, gebruiken we in deze fase ook onze private NPM_TOKEN
. We willen dus absoluut niet dat deze stap wordt blootgesteld aan de buitenwereld. Het enige dat we nodig hebben van deze stap is de map dist/
.
Stap 2: Productie
De meeste mensen gebruiken dit stadium voor runtime, omdat het heel dicht ligt bij wat we nodig hebben om de app te draaien. We moeten echter nog steeds productie-dependencies installeren en dat betekent dat we footprints achterlaten en de NPM_TOKEN
nodig hebben. Dit stadium is dus nog niet klaar voor gebruik. Let ook op yarn cache clean
op regel 19. Dat kleine commando vermindert de grootte van onze afbeelding met wel 60%.
Fase 3: Runtime
De laatste stap moet zo lean mogelijk zijn met minimale footprints. Dus we kopiëren simpelweg de app uit productie en gaan verder. We laten alle yarn en NPM_TOKEN
dingen achter ons en draaien alleen de app.
Dit is de uiteindelijke Dockerfile.production
:
# Stage 1: build the source code
FROM node:18.15.0-alpine3.17 as builder
WORKDIR /opt/app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
# Stage 2: copy the built version and build the production dependencies FROM node:18.15.0-alpine3.17 as production
WORKDIR /opt/app
COPY package.json yarn.lock ./
RUN yarn install --production && yarn cache clean
COPY --from=builder /opt/app/dist/ ./dist/
# Stage 3: copy the production ready app to runtime
FROM node:18.15.0-alpine3.17 as runtime
WORKDIR /opt/app
COPY --from=production /opt/app/ .
CMD ["yarn", "start"]
Let op dat we voor alle stappen eerst de bestanden package.json
en yarn.lock
kopiëren, de dependencies installeren en dan de rest van de codebasis kopiëren. De reden hiervoor is dat Docker elk commando bouwt als een laag bovenop de vorige. En elke build kan de vorige lagen gebruiken als die beschikbaar zijn en alleen de nieuwe lagen bouwen omwille van de prestaties.
Stel dat je iets hebt veranderd in src/services/service1.ts
zonder de pakketten aan te raken. Dat betekent dat de eerste vier lagen van builder fase onaangeroerd zijn en hergebruikt kunnen worden. Dat maakt het bouwproces ongelooflijk veel sneller.
De app naar Google Container Registry pushen via CircleCI pipelines
Er zijn verschillende manieren om een Docker image te bouwen in CircleCI pipelines. In ons geval hebben we ervoor gekozen om circleci/gcp-gcr orbs
te gebruiken:
executors:
docker-executor:
docker:
- image: cimg/base:2023.03
orbs:
gcp-gcr: circleci/[email protected]
jobs:
...
deploy:
description: Build & push image to Google Artifact Registry
executor: docker-executor
steps:
...
- gcp-gcr/build-image:
image: my-app
dockerfile: Dockerfile.production
tag: ${CIRCLE_SHA1:0:7},latest
- gcp-gcr/push-image:
image: my-app
tag: ${CIRCLE_SHA1:0:7},latest
Dankzij Docker is er een minimale configuratie nodig om onze app te bouwen en te pushen.
De app naar Google Container Registry pushen via GitHub acties
Als alternatief voor CircleCI kunnen we GitHub Actions gebruiken om de applicatie continu te deployen. We stellen gcloud
in en bouwen en pushen de Docker image naar gcr.io
:
jobs:
setup-build:
name: Setup, Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get Image Tag
run: |
echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- uses: google-github-actions/setup-gcloud@master
with:
service_account_key: ${{ secrets.GCP_SA_KEY }}
project_id: ${{ secrets.GCP_PROJECT_ID }}
- run: |-
gcloud --quiet auth configure-docker
- name: Build
run: |-
docker build
--tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
--tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"
.
- name: Push
run: |-
docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"
Bij elke kleine wijziging die naar de main branch wordt gepushed, bouwen en pushen we een nieuwe Docker image naar het register.
Wijzigingen deployen naar Google Kubernetes Engine met behulp van Google Delivery pipelines
Het hebben van kant-en-klare Docker images voor elke wijziging maakt het ook gemakkelijker om te deployen naar productie of terug te rollen als er iets mis gaat. We gebruiken Google Kubernetes Engine om onze apps te beheren en te serveren en gebruiken Google Cloud Deploy en Delivery Pipelines voor ons Continuous Deployment proces.
Wanneer de Docker image is gebouwd na elke kleine wijziging (met de CI pipeline hierboven getoond) gaan we nog een stap verder en deployen we de wijziging naar ons dev cluster met behulp van gcloud
. Laten we eens kijken naar die stap in de CircleCI pipeline:
- run:
name: Create new release
command: gcloud deploy releases create release-${CIRCLE_SHA1:0:7} --delivery-pipeline my-del-pipeline --region $REGION --annotations commitId=$CIRCLE_SHA1 --images my-app=gcr.io/${PROJECT_ID}/my-app:${CIRCLE_SHA1:0:7}
Dit activeert een releaseproces om de wijzigingen uit te rollen in ons dev Kubernetes cluster. Na het testen en het verkrijgen van de goedkeuringen, promoten we de wijziging naar test en vervolgens productie. Dit is allemaal mogelijk omdat we voor elke wijziging een lean geïsoleerd Docker image hebben dat bijna alles heeft wat het nodig heeft. We hoeven de deployment alleen maar te vertellen welke tag hij moet gebruiken.
Hoe het Quality Assurance team van dit proces profiteert
Het QA team heeft meestal een pre-productie cloudversie van de te testen apps nodig. Soms moeten ze echter een vooraf gebouwde app lokaal draaien (met alle dependencies) om een bepaalde feature te testen. In deze gevallen willen of hoeven ze niet al die moeite te doen om het hele project te klonen, npm-pakketten te installeren, de app te bouwen, developersfouten tegen te komen en het hele ontwikkelproces te doorlopen om de app werkend te krijgen. Nu alles al beschikbaar is als Docker image op Google Container Registry, hebben ze alleen nog een service nodig in het Docker compose bestand:
services:
...redis
...db
app:
image: gcr.io/${PROJECT_ID}/my-app:latest
restart: on-failure
ports:
- ${PORT:-4000}:${PORT:-4000}
environment:
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- DATABASE_URL=postgresql://${DB_USER:-user}:${DB_PASSWORD:-password}@db:5432/main
networks:
- mk_network
depends_on:
- redis
- db
Met deze service kunnen ze de applicatie op hun lokale machines opstarten met Docker containers door deze uit te voeren:
docker compose up
Dit is een enorme stap in het vereenvoudigen van testprocessen. Zelfs als QA besluit om een specifieke tag van de app te testen, kunnen ze eenvoudig de image tag op regel 6 wijzigen en het Docker compose commando opnieuw uitvoeren. Zelfs als ze besluiten om verschillende versies van de app tegelijkertijd te vergelijken, kunnen ze dat eenvoudig bereiken met een paar tweaks. Het grootste voordeel is dat ons QA team niet wordt geconfronteerd met de uitdagingen die developers op moeten lossen.
Voordelen van het gebruik van Docker
- Bijna geen footprints voor dependencies: Als je ooit besluit om de versie van Redis of Postgres te upgraden, kun je simpelweg 1 regel veranderen en de app opnieuw uitvoeren. Je hoeft niets op je systeem te veranderen. Bovendien kun je, als je twee apps hebt die allebei Redis nodig hebben (misschien zelfs met verschillende versies), ze allebei in hun eigen geïsoleerde omgeving laten draaien zonder conflicten met elkaar.
- Meerdere instances van de app: Er zijn veel gevallen waarin we dezelfde app met een ander commando moeten uitvoeren. Zoals het initialiseren van de DB, het uitvoeren van tests, het bekijken van DB veranderingen of het luisteren naar berichten. In elk van deze gevallen, omdat we de gebouwde image al klaar hebben, voegen we gewoon een andere service toe aan het Docker compose bestand met een ander commando, en we zijn klaar.
- Eenvoudigere testomgeving: Vaker wel dan niet hoef je alleen maar de app te draaien. Je hebt de code, de pakketten of lokale databaseverbindingen niet nodig. Je wilt alleen zeker weten dat de app goed werkt of je hebt een draaiende instantie nodig als backend service terwijl je aan je eigen project werkt. Dat kan ook het geval zijn voor QA, Pull Request reviewers, of zelfs UX mensen die willen controleren of hun ontwerp goed is geïmplementeerd. Onze docker setup maakt het voor al deze mensen heel gemakkelijk om aan de slag te gaan zonder al te veel technische problemen.
Dit artikel is oorspronkelijk gepubliceerd op Docker.
Laat een reactie achter