Onlangs introduceerden we ondersteuning voor Node.js en Python. Er zijn inmiddels al veel klanten enthousiast mee aan de slag gegaan. Dat vinden we mooi om te zien! Een veelgehoorde vraag is hoe je op een eenvoudige manier de deployment van Node.js- en Python-applicaties kunt automatiseren. In dit artikel leg ik je uit hoe je dit aanpakt.
Bij PHP-websites kom je een heel eind door je bestanden naar je pakket te uploaden via SFTP of SCP. Python- en Node.js-webapplicaties hebben echter een paar aanvullende stappen via SSH nodig om de deployment te automatiseren. Denk aan het updaten van de packages op de server en het herstarten van de webapplicatie om de nieuwe wijzigingen door te voeren. Ik licht graag toe hoe je voorkomt dat je deze acties telkens handmatig moet uitvoeren.
Direct naar Python of Node.js
Deze methodes werken enkel met SSH-toegang
De kans is klein dat je met Python of Node.js bent begonnen, zonder dat je SSH-toegang hebt ingesteld. Heb je dit nog niet gedaan, volg dan eerst de SSH-instructies. Verder gaan we er vanuit dat je voor het versiebeheer van je project gebruikmaakt van Git en dat je basiskennis hebt over het gebruik van de command line.
Stappenplan voor deployment
Of je nu met Python of Node.js werkt, de stappen die we adviseren bij het automatiseren van de deployment zijn vrijwel gelijk. Je hoogt de versie van je project op (bv. versie 2.0.1 naar 2.0.2) en laat een deployment script de volgende stappen afhandelen.
- Het uploaden van actuele code vanuit je lokale machine naar de server.
- De eventueel benodigde pakketten updaten/installeren op productie.
- Het vervangen van de oude webapplicatie voor de nieuwe.
- De webapplicatie herstarten om de wijzigingen door te voeren.
Voor de eenvoud van deze uitleg laat ik eventuele databasewijzigingen die nodig zijn voor een deployment buiten beschouwing. Deze dien je dus zelf handmatig uit te voeren.
Python deployen met Bump2version en Fabric
Om een Python-project te deployen, heb je twee softwarepakketten nodig:
- Bump2version voor een consistente versienummering van het project.
- Fabric voor het daadwerkelijk deployen naar de server.
Installatie van beide pakketten kan eenvoudig met pip. Gebruik het volgende commando om dit te regelen:
pip install fabric bump2version
Versienummering met Bump2version
De tool Bump2version helpt om je project van consistente versienummering te voorzien. Dit voorkomt verwarring, want met een duidelijk versienummer is het achteraf makkelijker na te gaan welke wijzigingen live staan. Als eerste heb je een standaard setup.py nodig voor je project met daarin het versienummer. Die ziet er bijvoorbeeld als volgt uit:
from setuptools import setup, find_packages
__version__ = '1.0.0'
list_dependencies = [
"django",
]
setup(name='projectnaam',
version=__version__,
packages=find_packages(),
install_requires=list_dependencies)
Bump2version is te configureren met het bestand .bumpversion.cfg. Hierin staat de initiële versie en activeren we het automatisch aanmaken van een Git commit en Git tag bij het ophogen van de versie:
[bumpversion]
current_version = 1.0.0
commit = True
tag = True
[bumpversion:file:setup.py]
Na deze initialisatie kun je het versienummer ophogen met:
bumpversion patch
Bump2version maakt een commit en tag aan bij het ophogen van de versie. Je kunt daardoor achteraf gemakkelijk opzoeken welke wijzigingen in welke release zijn gedeployed.
De mogelijkheden van Python
Op onze pagina over Python kun je meer lezen over onderwerpen als beginnen met Python, leren programmeren, de Python-selector en Django.
Commando’s uitvoeren met Fabric
Het softwarepakket Fabric maakt het mogelijk om vanuit een Python-script commando’s uit te voeren. Dit kan zowel lokaal als remote via SSH. Zo kun je een eenvoudige alle stappen uitvoeren die nodig zijn voor een deployment.
Fabric voert standaard het script fabfile.py uit. Hieronder staat een voorbeeld met daarin een aantal veelgebruikte deployment-acties:
import tempfile
from fabric import task
from invoke import run as local
# Je debnummer en projectnaam (uit de Python selector) vul je hier in
ACCOUNT = '<deb_nummer>'
PROJECT = '<projectnaam>'
REMOTE_BASE_DIR = f'/home/{ACCOUNT}'
# Dit script maakt een nieuwe map 'releases/' aan, waarin de geuploadde releases terechtkomen
REMOTE_RELEASES_DIR = f'{REMOTE_BASE_DIR}/releases'
# 'current/' wordt een symlink naar de actuele release
REMOTE_CURRENT_RELEASE_DIR = f'{REMOTE_BASE_DIR}/current'
# De pip uit de virtualenv van de Python selector
REMOTE_PIP = f'{REMOTE_BASE_DIR}/virtualenv/{PROJECT}/3.7/bin/pip'
# Bepaal de releaseversie op basis van de Git tag (zie het stukje over bumpversion)
release = response = local('git describe --tags HEAD', hide=True).stdout.strip().replace('/', '-')
# De naam van het archief met daarin de release
archive = f'{PROJECT}_{release}.tar.gz'
local_archive = f'{tempfile.gettempdir()}/{archive}'
remote_archive = f'/tmp/{archive}'
# De releasemap op de server, hier wordt het archief in uitgepakt
release_dir = f'{REMOTE_RELEASES_DIR}/{release}'
@task(name='deploy')
def deploy(c):
"""
deploy() is het aanspreekpunt van het script, dit roept de rest van de functionaliteit aan
De variabele 'c' bevat de SSH verbinding naar de server
Met local() draai je een commando lokaal
Met c.run() draai je een commando via SSH op de server
"""
archive_release(c)
upload_release(c)
extract_release(c)
install_dependencies(c)
symlink_release(c)
restart_application(c)
clean_release_archives(c)
def archive_release(c):
# Exporteer met git het project naar een .tar.gz archief in de lokale temp directory
local(f'git archive HEAD --format=tar.gz -o {local_archive}', echo=True)
def upload_release(c):
# Upload het archief (met SCP) naar de remote temp directory
c.put(local_archive, remote_archive)
def extract_release(c):
# Maak de 'releases' map aan indien deze nog niet bestaat
c.run(f'mkdir -p {REMOTE_RELEASES_DIR}', echo=True)
# Maak een nieuwe map aan voor de release
c.run(f'mkdir -p {release_dir}', echo=True)
# Pak het geuploadde archief uit in de nieuwe releasemap
c.run(f'tar -xaf {remote_archive} -C {release_dir}', echo=True)
def install_dependencies(c):
# Installeer de softwarepakketten op basis van de setup.py uit de nieuwe release
c.run(f'{REMOTE_PIP} install -e {release_dir}', echo=True)
def symlink_release(c):
# Laat de symlink 'current/' doorverwijzen naar de zojuist geplaatste release
c.run(f'ln -fns {release_dir} {REMOTE_CURRENT_RELEASE_DIR}', echo=True)
@task(name='restart')
def restart_application(c):
# Herstart de applicatie zoals aanbevolen met een touch op restart.txt
c.run(f'mkdir -p {REMOTE_BASE_DIR}/{PROJECT}/tmp', echo=True)
c.run(f'touch {REMOTE_BASE_DIR}/{PROJECT}/tmp/restart.txt', echo=True)
def clean_release_archives(c):
# Ruim de archiefbestanden op uit de temp directories na afloop van een release
local(f'rm {local_archive}', echo=True)
c.run(f'rm {remote_archive}', echo=True)
De commando’s uit dit script zijn voorbeelden
Het biedt geen garantie dat je webapplicatie na release direct werkt. De compatibiliteit tussen de actieve release en de geïnstalleerde packages in virtualenv worden tijdens de release niet gegarandeerd. Maak vooraf een backup en gebruik dit script niet voor een productieomgeving.
Python deployment uitvoeren
Met het commando fab activeer je fabric voor jouw fabfile. De parameter -H staat voor host. Hier vul je je gebruikersnaam en domeinnaam in:
fab -H <debnummer>@<domeinnaam> deploy
Dit deployment script is getest op Linux
Wil je het op Windows proberen, zie dan de suggesties voor het gebruik van Fabric op dit besturingssyteem. Houd er rekening mee dat je mogelijk wat paden in het script moet aanpassen naar de Windows-variant.
Nieuwe releasemap in gebruik nemen
Je remote project is bij de eerste deployment niet daadwerkelijk vervangen. Klopt de inhoud van current/ op de server? Verwijs dan je huidige projectmap door naar de de huidige release met een symlink. Herstart hierna de webapplicatie om te testen of het werkt.
mv <projectnaam> <projectnaam>.old
ln -fns current <projectnaam>
touch <projectnaam>/tmp/restart
Node.js deployen met ShipIt.js
ShipIt.js is de Node.js tegenhanger van Fabric. Ook deze tool maakt het eenvoudig om commando’s lokaal en via SSH draaien, maar dan met een JavaScript-syntax. Installatie van de benodigde softwarepakketten kan heel eenvoudig met NPM. Gebruik het volgende commando:
npm install shipit-cli --save-dev
Versienummering met NPM
Met NPM kun je zonder verdere configuratie de versienummering van je project bijwerken met:
npm version patch
NPM maakt een commit en tag aan bij het ophogen van de versie. Je kunt daardoor achteraf gemakkelijk opzoeken welke wijzigingen in welke release zijn gedeployed.
Deployen met ShipIt.js
Het configureren van de deployment gaat bij ShipIt.js met een shipitfile.js. Hieronder staat een voorbeeld van een shipit file met een kleine toelichting bij elke stap.
const os = require('os');
// Je debnummer en projectnaam (uit de Node.js selector) vul je hier in
var ACCOUNT = `<deb_nummer>`;
var PROJECT = `<projectnaam>`;
var DOMAIN = `<domeinnaam>`;
// Bepaal de lokale temp directory, afhankelijk van het OS
var LOCAL_TEMP = os.tmpdir();
// De remote home
var REMOTE_BASE_DIR = `/home/${ACCOUNT}`;
// Dit script maakt een nieuwe map `releases/` aan, waarin de geuploadde releases terechtkomen
var REMOTE_RELEASES_DIR = `${REMOTE_BASE_DIR}/releases`;
// `current/` wordt een symlink naar de actuele release
var REMOTE_CURRENT_RELEASE_DIR = `${REMOTE_BASE_DIR}/current`;
// De NPM uit de nodevenv van de Node.js selector
var REMOTE_NPM = `${REMOTE_BASE_DIR}/nodevenv/${PROJECT}/12/bin/npm`;
var release = release_dir = archive = local_archive = remote_archive = '';
module.exports = shipit => {
//De basisconfiguratie, naar welk domein willen we deployen en met welke inloggegevens
shipit.initConfig({
hostingserver: {
servers: `${ACCOUNT}@${DOMAIN}`,
},
})
shipit.task(`default`, async () => {
// De default handler wordt als eerste aangeroepen en activeert de rest van de tasks synchroon
shipit.start(['determine_version_data', 'archive_release', 'upload_release', 'extract_release',
'install_dependencies', 'symlink_release', 'restart_application', 'clean_release_archives']);
})
shipit.blTask(`archive_release`, async () => {
// Exporteer met git het project naar een .tar.gz archief in de lokale temp directory
await shipit.local(`git archive --format=tar.gz HEAD -o ${local_archive}`);
})
shipit.blTask(`upload_release`, async () => {
// Upload het archief naar de remote temp directory
await shipit.copyToRemote(local_archive, remote_archive);
})
shipit.blTask(`extract_release`, async () => {
// Maak de `releases` map aan indien deze nog niet bestaat
await shipit.remote(`mkdir -p ${REMOTE_RELEASES_DIR}`);
// Maak een nieuwe map aan voor de release
await shipit.remote(`mkdir -p ${release_dir}`);
// Pak het geuploadde archief uit in de nieuwe releasemap
await shipit.remote(`tar -xaf ${remote_archive} -C ${release_dir}`);
})
shipit.blTask(`install_dependencies`, async () => {
// Installeer de softwarepakketten op basis van de package.json uit de nieuwe release
await shipit.remote(`${REMOTE_NPM} --prefix ${release_dir} install`);
})
shipit.blTask(`symlink_release`, async () => {
// Laat de symlink `current/` doorverwijzen naar de zojuist geplaatste release
await shipit.remote(`ln -fns ${release_dir} ${REMOTE_CURRENT_RELEASE_DIR}`);
})
shipit.blTask(`restart_application`, async () => {
// Herstart de applicatie zoals aanbevolen met een touch op restart.txt
await shipit.remote(`mkdir -p ${REMOTE_BASE_DIR}/${PROJECT}/tmp`);
await shipit.remote(`touch ${REMOTE_BASE_DIR}/${PROJECT}/tmp/restart.txt`);
})
shipit.blTask(`clean_release_archives`, async () => {
// Ruim de archiefbestanden op uit de temp directories na afloop van een release
await shipit.local(`rm ${local_archive}`);
await shipit.remote(`rm ${remote_archive}`);
})
shipit.blTask(`determine_version_data`, async () => {
await shipit.local(`git describe --tags HEAD`)
.then(({ stdout }) => {
// Bepaal de releaseversie op basis van de Git tag (deze wordt gezet met npm version patch)
release = stdout.trim().replace(`/`, `-`);
release_dir = `${REMOTE_RELEASES_DIR}/${release}`;
// De naam van het archief waarin de release wordt ingepakt
archive = `${PROJECT}_${release}.tar.gz`;
local_archive = `${LOCAL_TEMP}/${archive}`;
remote_archive = `/tmp/${archive}`;
})
})
}
De commando’s uit dit script zijn voorbeelden
Het biedt geen garantie dat je webapplicatie na release direct werkt. De compatibiliteit tussen de actieve release en de geïnstalleerde packages in nodevenv worden tijdens de release niet gegarandeerd. Maak vooraf een backup en gebruik dit script niet voor een productieomgeving.
Node.js deployment uitvoeren
Gebruik voor het uitvoeren van het deployment script dit commando:
npx shipit hostingserver
Dit deployment script is getest op Linux
Wil je het op Windows proberen, zie dan de suggesties voor het gebruik van ShipIt.js op dit besturingssyteem. Merk op dat je mogelijk wat paden in het script moet aanpassen naar de Windows-variant.
Nieuwe releasemap in gebruik nemen
Je remote project is bij de eerste deployment niet daadwerkelijk vervangen. Klopt de inhoud van current/ op de server? Verwijs dan je huidige projectmap door naar de de huidige release met een symlink. Herstart hierna de webapplicatie om te testen of het werkt.
mv <projectnaam> <projectnaam>.old
ln -fns current <projectnaam>
touch <projectnaam>/tmp/restart
De mogelijkheden van Node.js
Op onze pagina over Node.js kun je meer artikelen lezen over onderwerpen als beginnen met Node.js, de Node.js-selector en Express.
Waarom lokaal deployen en niet direct vanuit GitHub?
Het onderdeel Actions van GitHub lijkt op het eerste gezicht een aantrekkelijke manier om een webapplicatie te deployen. Het biedt namelijk ondersteuning voor deployment via SSH op het moment dat een specifieke Git branch wordt gepushed of als er naar de branch wordt gemerged. De configuratie is simpel en de tools draaien in de cloud op je gratis GitHub-account.
Helaas is dit laatste het knelpunt waardoor een directe integratie van GitHub met onze webhostingservers niet werkt. Om brute force SSH-inlogpogingen op je hostingpakket te voorkomen, staan we bij Antagonist enkel toe dat je een SSH-sleutel vertrouwt op basis van een specifiek IP-adres. De cloud runners van GitHub die de deployment zouden kunnen draaien, hebben variabele IP-adressen. GitHub kan niet garanderen dat je deployments telkens van een specifiek IP-adres komen. Hierdoor is het onmogelijk om deze integratie betrouwbaar te laten werken.
Veel succes met deployen
Met behulp van bovenstaande stappen kun je de deployment van Python- en Node.js-webapplicaties automatiseren. Heb je nog vragen over dit onderwerp? Stel ze gerust. Ik hoop dat je iets aan dit artikel hebt gehad en wens je veel succes met de uitvoering!
P.S. Blijf op de hoogte en volg ons via Facebook, Twitter, Instagram, e-mail en RSS. Heb je vragen, tips of opmerkingen? Laat het achter als reactie. Vond je het artikel nuttig? Deel het dan met anderen!
Waar kan ik de reacties bekijken. Misschien staan hier antwoorden op eventuele vragen tussen
Als er reacties zijn, dan staan ze onder het artikel. In dit geval ben je hier de eerste! Kom je ergens niet uit en kun je het in de handleidingen over Python of Node.js niet vinden, stuur dan gerust even een e-mail naar support@antagonist.nl.