Eines Abends, als ich von der Arbeit nach Hause kam, beschloss ich, ein paar Hausaufgaben zu machen. Ich habe einige Änderungen vorgenommen und wollte sofort damit experimentieren. Aber vor den Experimenten musste ich in die VPS gehen, Änderungen vornehmen, den Container neu erstellen und ausführen. Dann entschied ich, dass es Zeit war, sich um die kontinuierliche Lieferung zu kümmern.

Anfangs hatte ich die Wahl zwischen Circle CI, Travis oder Jenkins. Ich habe Jenkins fast sofort ausgeschlossen, weil ich kein so leistungsfähiges Werkzeug benötigte. Nach einer kurzen Lektüre über Travis kam ich zu dem Schluss, dass es praktisch ist, es zusammenzubauen und zu testen, aber Sie können sich keine Lieferung damit vorstellen. Ich habe von Circle CI durch übermäßig aufdringliche Anzeigen auf YouTube erfahren. Ich habe angefangen, mit Beispielen zu experimentieren, aber irgendwann habe ich mich geirrt und hatte ewige Tests, die mich viele kostbare Minuten der Montage gekostet haben (Im Allgemeinen gibt es eine ausreichende Grenze, um mir keine Sorgen zu machen, aber es hat mich getroffen). Ich nahm die Suche wieder auf und stolperte über Github Actions. Nachdem ich mit den Get Started-Beispielen gespielt hatte, hatte ich einen positiven Eindruck und nach einem kurzen Blick auf die Dokumentation kam ich zu dem Schluss, dass es sehr cool ist, Geheimnisse für die Montage, das Sammeln und die praktische Bereitstellung von Projekten an einem Ort aufzubewahren. Mit leuchtenden Augen zeichnete er schnell den gewünschten Kurs und die Zahnräder drehten sich.

Zuerst werden wir versuchen, die Tests durchzuführen. Als Experiment habe ich einen einfachen Webserver auf Flask mit 2 Endpunkten geschrieben:
Auflisten einer einfachen Webanwendungfrom flask import Flask from flask import request, jsonify app = Flask(__name__) def validate_post_data(data: dict) -> bool: if not isinstance(data, dict): return False if not data.get('name') or not isinstance(data['name'], str): return False if data.get('age') and not isinstance(data['age'], int): return False return True @app.route('/', methods=['GET']) def hello(): return 'Hello World!' @app.route('/api', methods=['GET', 'POST']) def api(): """ /api entpoint GET - returns json= {'status': 'test'} POST - { name - str not null age - int optional } :return: """ if request.method == 'GET': return jsonify({'status': 'test'}) elif request.method == 'POST': if validate_post_data(request.json): return jsonify({'status': 'OK'}) else: return jsonify({'status': 'bad input'}), 400 def main(): app.run(host='0.0.0.0', port=8080) if __name__ == '__main__': main()
Und ein paar Tests:
Liste der Anwendungstests import unittest import app as tested_app import json class FlaskAppTests(unittest.TestCase): def setUp(self): tested_app.app.config['TESTING'] = True self.app = tested_app.app.test_client() def test_get_hello_endpoint(self): r = self.app.get('/') self.assertEqual(r.data, b'Hello World!') def test_post_hello_endpoint(self): r = self.app.post('/') self.assertEqual(r.status_code, 405) def test_get_api_endpoint(self): r = self.app.get('/api') self.assertEqual(r.json, {'status': 'test'}) def test_correct_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps({'name': 'Den', 'age': 100})) self.assertEqual(r.json, {'status': 'OK'}) self.assertEqual(r.status_code, 200) r = self.app.post('/api', content_type='application/json', data=json.dumps({'name': 'Den'})) self.assertEqual(r.json, {'status': 'OK'}) self.assertEqual(r.status_code, 200) def test_not_dict_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps([{'name': 'Den'}])) self.assertEqual(r.json, {'status': 'bad input'}) self.assertEqual(r.status_code, 400) def test_no_name_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps({'age': 100})) self.assertEqual(r.json, {'status': 'bad input'}) self.assertEqual(r.status_code, 400) def test_bad_age_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps({'name': 'Den', 'age': '100'})) self.assertEqual(r.json, {'status': 'bad input'}) self.assertEqual(r.status_code, 400) if __name__ == '__main__': unittest.main()
: import unittest import app as tested_app import json class FlaskAppTests(unittest.TestCase): def setUp(self): tested_app.app.config['TESTING'] = True self.app = tested_app.app.test_client() def test_get_hello_endpoint(self): r = self.app.get('/') self.assertEqual(r.data, b'Hello World!') def test_post_hello_endpoint(self): r = self.app.post('/') self.assertEqual(r.status_code, 405) def test_get_api_endpoint(self): r = self.app.get('/api') self.assertEqual(r.json, {'status': 'test'}) def test_correct_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps({'name': 'Den', 'age': 100})) self.assertEqual(r.json, {'status': 'OK'}) self.assertEqual(r.status_code, 200) r = self.app.post('/api', content_type='application/json', data=json.dumps({'name': 'Den'})) self.assertEqual(r.json, {'status': 'OK'}) self.assertEqual(r.status_code, 200) def test_not_dict_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps([{'name': 'Den'}])) self.assertEqual(r.json, {'status': 'bad input'}) self.assertEqual(r.status_code, 400) def test_no_name_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps({'age': 100})) self.assertEqual(r.json, {'status': 'bad input'}) self.assertEqual(r.status_code, 400) def test_bad_age_post_api_endpoint(self): r = self.app.post('/api', content_type='application/json', data=json.dumps({'name': 'Den', 'age': '100'})) self.assertEqual(r.json, {'status': 'bad input'}) self.assertEqual(r.status_code, 400) if __name__ == '__main__': unittest.main()
Coverage-Ausgabe:
coverage report Name Stmts Miss Cover ------------------------------------------------------------------ src/app.py 28 2 93% src/tests.py 37 0 100% ------------------------------------------------------------------ TOTAL 65 2 96%
Erstellen Sie nun unsere erste Aktion, mit der die Tests ausgeführt werden. Gemäß der Dokumentation sollten alle Aktionen in einem speziellen Verzeichnis gespeichert werden:
$ mkdir -p .github/workflows $ touch .github/workflows/test_on_push.yaml
Ich möchte, dass diese Aktion bei jedem Push-Ereignis in jedem Zweig ausgeführt wird, mit Ausnahme von Releases (Tags, da separate Tests durchgeführt werden):
on: push: tags: - '!refs/tags/*' branches: - '*'
Dann erstellen wir eine Aufgabe, die in der neuesten verfügbaren Version von Ubuntu ausgeführt wird:
jobs: run_tests: runs-on: [ubuntu-latest]
In Schritten überprüfen wir den Code, installieren Python, installieren Abhängigkeiten, führen Tests durch und zeigen die Abdeckung an:
steps: # - uses: actions/checkout@master # python - uses: actions/setup-python@v1 with: python-version: '3.8' architecture: 'x64' - name: Install requirements # run: pip install -r requirements.txt - name: Run tests run: coverage run src/tests.py - name: Tests report run: coverage report
Alle zusammen name: Run tests on any Push event # push , . # on: push: tags: - '!refs/tags/*' branches: - '*' jobs: run_tests: runs-on: [ubuntu-latest] steps: # - uses: actions/checkout@master # python - uses: actions/setup-python@v1 with: python-version: '3.8' architecture: 'x64' - name: Install requirements # run: pip install -r requirements.txt - name: Run tests run: coverage run src/tests.py - name: Tests report run: coverage report
Versuchen wir, ein Commit zu erstellen und zu sehen, wie unsere Aktion funktioniert.

Bestehen von Tests in der Aktionsoberfläche
Hurra, wir haben es geschafft, die erste Aktion zu erstellen und auszuführen! Versuchen wir einen Test zu unterbrechen und schauen uns die Ausgabe an:

Testet den Absturz in der Aktionsoberfläche
Die Tests sind fehlgeschlagen. Die Anzeige wurde rot und erhielt sogar eine Benachrichtigung per E-Mail. Was du brauchst! 3 der 8 Punkte kann eine Zielschaltung konfiguriert betrachtet werden. Versuchen Sie nun mit der Montage, um herauszufinden, halten unsere Docker Bilder.
Hinweis! Als nächstes benötigen wir ein Docker- Konto
Zuerst schreiben wir ein einfaches Dockerfile, in dem unsere Anwendung ausgeführt wird.
Dockerfile # FROM python:3.8-alpine # /app COPY ./ /app # RUN apk update && pip install -r /app/requirements.txt --no-cache-dir # ( Distutils) RUN pip install -e /app # EXPOSE 8080 # CMD web_server # distutils #CMD python /app/src/app.py
Um den Container an den Hub zu senden, muss man sich beim Docker anmelden. Da ich aber nicht möchte, dass die ganze Welt das Kennwort für das Konto erfährt, verwende ich die in GitHub integrierten Geheimnisse. Im Allgemeinen können nur Passwörter in Geheimnisse gesteckt werden, und der Rest wird in * .yaml fest codiert, und dies wird funktionieren. Aber ich möchte meine Aktionen ohne Änderungen kopieren und alle spezifischen Informationen aus den Geheimnissen herausholen.

Github-Geheimnisse
DOCKER_LOGIN - Anmeldung in hub.docker.com
DOCKER_PWD - Passwort
DOCKER_NAME - Name des Docker-Repositorys für dieses Projekt (muss im Voraus erstellt werden)
Okay, die Vorbereitung ist abgeschlossen, jetzt erstellen wir unsere zweite Aktion:
$ touch .github/workflows/pub_on_release.yaml
Wir kopieren die Tests von der vorherigen Aktion, mit Ausnahme des Starttriggers (ich konnte die Aktionen nicht importieren). Wir ersetzen es durch "Launch on Release":
on: release: types: [published]
WICHTIG! Es ist sehr wichtig, die richtige Bedingung für das Ereignis zu erfüllen
Wenn on.release beispielsweise keine Typen angibt, löst dieses Ereignis mindestens zwei Ereignisse aus: veröffentlicht und erstellt. Dass es wird einmal 2 des Montageprozesses gestartet werden.
Nun werden wir in der gleichen Datei eine weitere Aufgabe ausführen, abhängig von der ersten:
build_and_pub: needs: [run_tests]
needs - sagt, dass diese Task erst startet, wenn run_tests endet
WICHTIG! Wenn Sie über mehrere Aktionsdateien und mehrere Aufgaben verfügen, werden diese gleichzeitig in verschiedenen Umgebungen ausgeführt. Für jede Aufgabe wird unabhängig von anderen Aufgaben eine separate Umgebung erstellt. Wenn Sie keine Anforderungen angeben, werden die Testaufgabe und die Assemblys gleichzeitig und unabhängig voneinander gestartet.
Fügen Sie Umgebungsvariablen hinzu, in denen unsere Geheimnisse sein werden:
env: LOGIN: ${{ secrets.DOCKER_LOGIN }} NAME: ${{ secrets.DOCKER_NAME }}
In diesen Schritten müssen wir uns beim Docker anmelden, den Container sammeln und in der Registrierung veröffentlichen:
steps: - name: Login to docker.io run: echo ${{ secrets.DOCKER_PWD }} | docker login -u ${{ secrets.DOCKER_LOGIN }} --password-stdin - uses: actions/checkout@master - name: Build image run: docker build -t $LOGIN/$NAME:${GITHUB_REF:11} -f Dockerfile . - name: Push image to docker.io run: docker push $LOGIN/$NAME:${GITHUB_REF:11}
$ {GITHUB_REF: 11} ist eine Github-Variable, die eine Zeile mit einem Verweis auf das Ereignis speichert, für das der Trigger ausgeführt wurde (Zweigname, Tag usw.). Wenn wir Tags im Format "v0.0.0" haben, müssen Sie die ersten 11 abschneiden Zeichen, dann bleibt "0.0.0".
Gib den Code ein und erstelle ein neues Tag. Und wir sehen, dass unser Container erfolgreich zusammengestellt und an die Registrierung gesendet wurde, und wir haben unser Passwort nirgendwo aufleuchten lassen.

Erstellen und versenden Sie Container in der Aktionsoberfläche
Überprüfen Sie den Hub:

Container in Docker-Hub gespeichert
Alles funktioniert, aber die schwierigste Aufgabe bleibt - Bereitstellung. Hier benötigen Sie bereits einen VPS und eine weiße IP-Adresse, wo wir den Container bereitstellen und wo Sie den Hook senden können. Theoretisch können Sie auf der Seite des VPS oder des Heimservers ein Skript auf der Krone ausführen, das bei einem neuen Image ausgelöst wird, oder auf irgendeine Weise mit dem Telegramm-Bot spielen. Es gibt sicherlich unzählige Möglichkeiten, dies zu tun. Aber ich werde mit externer ip arbeiten. Um nicht schlau zu sein, habe ich einen kleinen Webservice in Flask mit einer einfachen API geschrieben.
Kurz gesagt, es gibt 1 Endpunkt „/“.
Eine GET-Anforderung gibt json mit allen aktiven Containern auf dem Host zurück.
POST - empfängt die Daten im Format:
{ "owner": " ", "repository": " ", "tag": "v0.0.1", # "ports": {"8080": 8080, “443”: 443} # }
Was passiert auf dem Host:
- vom empfangenen json wird der name des neuen bildes abgeholt
- ein neues Bild ist gewölbt
- Wenn das Bild heruntergeladen wurde, wird der aktuelle Container angehalten und gelöscht
- ein neuer Container wird gestartet, mit der Veröffentlichung von Ports (-p Flag)
Alle Arbeiten mit Docker werden mit der Docker-Py- Bibliothek durchgeführt
Es wäre sehr falsch, einen solchen Dienst ohne minimalen Schutz im Internet zu veröffentlichen, und ich habe einen Anschein von API KEY gemacht, der Dienst liest das Token aus den Umgebungsvariablen und vergleicht es dann mit dem Header {Authorization: CI_TOKEN}
Webserverliste für die Bereitstellung Für diese Anwendung habe ich auch setup.py verwendet, um sie auf dem System zu installieren. Sie können es installieren mit:
$ python3 setup.sy install
vorausgesetzt, Sie haben die Dateien heruntergeladen und befinden sich im Verzeichnis dieser Anwendung.
Nach der Installation müssen Sie die Anwendung als Dienst aktivieren, damit sie sich beim Neustart des Servers von selbst startet. Hierzu verwenden wir systemd
Hier ist eine Beispielkonfigurationsdatei:
[Unit] Description=Deployment web server After=network-online.target [Service] Type=simple RestartSec=3 ExecStart=/usr/local/bin/ci_example Environment=CI_TOKEN=#<I generate it with $(openssl rand -hex 20)> [Install] WantedBy=multi-user.target
Es bleibt nur zu laufen:
$ sudo systemctl daemon-reload $ sudo systemctl enable ci_example.service $ sudo systemctl start ci_example.service
Mit dem Befehl können Sie das Protokoll des Web Delivery-Servers anzeigen
$ sudo systemctl status ci_example.service
Der Serverteil ist fertig, es bleibt nur noch ein Hook zu unserer Aktion hinzuzufügen. Fügen Sie dazu die Geheimnisse der IP-Adresse unseres Servers und die CI_TOKEN hinzu, die wir bei der Installation der Anwendung generiert haben.
Zuerst wollte ich eine bereit, Maßnahmen zu locken verwenden, um von marketpleysa Github, aber leider, entfernt er die Zitate aus der POST-Anfrage Körper, dass es unmöglich zu analysieren json macht. Dies passte offensichtlich nicht zu mir und ich entschied mich für die eingebaute Locke in Ubuntu (auf der ich Container sammle), was sich im Übrigen positiv auf die Leistung auswirkte, da keine Montage eines zusätzlichen Containers erforderlich ist:
deploy: needs: [build_and_pub] runs-on: [ubuntu-latest] steps: - name: Set tag to env run: echo ::set-env name=TAG::$(echo ${GITHUB_REF:11}) - name: Send webhook for deploy run: "curl --silent --show-error --fail -X POST ${{ secrets.DEPLOYMENT_SERVER }} -H 'Authorization: ${{ secrets.DEPLOYMENT_TOKEN }}' -H 'Content-Type: application/json' -d '{\"owner\": \"${{ secrets.DOCKER_LOGIN }}\", \"repository\": \"${{ secrets.DOCKER_NAME }}\", \"tag\": \"${{ env.TAG }}\", \"ports\": {\"8080\": 8080}}'"
Hinweis: Es ist sehr wichtig, den Schalter --fail anzugeben, da andernfalls jede Anforderung erfolgreich ist, auch wenn als Antwort ein Fehler empfangen wurde.
Beachten Sie auch, dass die Variablen in der Abfrage verwendet wird, nicht tatsächlich Variablen und spezielle Funktionsaufrufe außer GITHUB_REF aufgrund dessen, was ich für eine lange Zeit haben, konnte nicht verstehen, warum die Anforderung nicht richtig funktioniert. Aber nachdem gab sie alles passiert ist.
Action wird erstellt und bereitgestellt name: Publish on Docker Hub and Deploy on: release: types: [published] # jobs: run_tests: # runs-on: [ubuntu-latest] steps: # - uses: actions/checkout@master # python - uses: actions/setup-python@v1 with: python-version: '3.8' architecture: 'x64' - name: Install requirements # run: pip install -r requirements.txt - name: Run tests # run: coverage run src/tests.py - name: Tests report run: coverage report build_and_pub: # needs: [run_tests] runs-on: [ubuntu-latest] env: LOGIN: ${{ secrets.DOCKER_LOGIN }} NAME: ${{ secrets.DOCKER_NAME }} steps: - name: Login to docker.io # docker.io run: echo ${{ secrets.DOCKER_PWD }} | docker login -u ${{ secrets.DOCKER_LOGIN }} --password-stdin # - uses: actions/checkout@master - name: Build image # image hub.docker .. login/repository:version run: docker build -t $LOGIN/$NAME:${GITHUB_REF:11} -f Dockerfile . - name: Push image to docker.io # registry run: docker push $LOGIN/$NAME:${GITHUB_REF:11} deploy: # registry, # curl needs: [build_and_pub] runs-on: [ubuntu-latest] steps: - name: Set tag to env run: echo ::set-env name=RELEASE_VERSION::$(echo ${GITHUB_REF:11}) - name: Send webhook for deploy run: "curl --silent --show-error --fail -X POST ${{ secrets.DEPLOYMENT_SERVER }} -H 'Authorization: ${{ secrets.DEPLOYMENT_TOKEN }}' -H 'Content-Type: application/json' -d '{\"owner\": \"${{ secrets.DOCKER_LOGIN }}\", \"repository\": \"${{ secrets.DOCKER_NAME }}\", \"tag\": \"${{ env.RELEASE_VERSION }}\", \"ports\": {\"8080\": 8080}}'"
$ NAME: $ {GITHUB_REF: name: Publish on Docker Hub and Deploy on: release: types: [published] # jobs: run_tests: # runs-on: [ubuntu-latest] steps: # - uses: actions/checkout@master # python - uses: actions/setup-python@v1 with: python-version: '3.8' architecture: 'x64' - name: Install requirements # run: pip install -r requirements.txt - name: Run tests # run: coverage run src/tests.py - name: Tests report run: coverage report build_and_pub: # needs: [run_tests] runs-on: [ubuntu-latest] env: LOGIN: ${{ secrets.DOCKER_LOGIN }} NAME: ${{ secrets.DOCKER_NAME }} steps: - name: Login to docker.io # docker.io run: echo ${{ secrets.DOCKER_PWD }} | docker login -u ${{ secrets.DOCKER_LOGIN }} --password-stdin # - uses: actions/checkout@master - name: Build image # image hub.docker .. login/repository:version run: docker build -t $LOGIN/$NAME:${GITHUB_REF:11} -f Dockerfile . - name: Push image to docker.io # registry run: docker push $LOGIN/$NAME:${GITHUB_REF:11} deploy: # registry, # curl needs: [build_and_pub] runs-on: [ubuntu-latest] steps: - name: Set tag to env run: echo ::set-env name=RELEASE_VERSION::$(echo ${GITHUB_REF:11}) - name: Send webhook for deploy run: "curl --silent --show-error --fail -X POST ${{ secrets.DEPLOYMENT_SERVER }} -H 'Authorization: ${{ secrets.DEPLOYMENT_TOKEN }}' -H 'Content-Type: application/json' -d '{\"owner\": \"${{ secrets.DOCKER_LOGIN }}\", \"repository\": \"${{ secrets.DOCKER_NAME }}\", \"tag\": \"${{ env.RELEASE_VERSION }}\", \"ports\": {\"8080\": 8080}}'"
Okay, wir haben alles zusammengestellt. Jetzt erstellen wir ein neues Release und sehen uns die Aktionen an.

Webhook zur Aktionsschnittstelle der Bereitstellungsanwendung
Es stellte sich heraus, dass wir eine GET-Anfrage an den Bereitstellungsservice senden (zeigt alle aktiven Container auf dem Host an):

Wird auf dem VPS-Container bereitgestellt
Jetzt senden wir Anforderungen an unsere bereitgestellte Anwendung:

GET-Anforderung an implementierten Container

POST-Anforderung an implementierten Container
Schlussfolgerungen
GitHub Aktionen sind sehr bequem und flexibles Werkzeug, das verwendet werden kann, eine Menge Dinge zu tun, die das Leben erheblich vereinfachen kann. Es hängt alles von der Vorstellungskraft ab.
Sie unterstützen Integrationstests mit Services.
Als logische Fortsetzung dieses Projekts können Sie webhook die Möglichkeit hinzufügen, benutzerdefinierte Parameter zum Starten des Containers zu übergeben.
In Zukunft werde ich versuchen, dieses Projekt als Grundlage für den Einsatz von Steuerkarten zu nehmen, wenn ich mit k8s studiere und experimentiere
Wenn Sie ein Home-Projekt haben, können GitHub-Aktionen die Arbeit mit dem Repository erheblich vereinfachen.
Syntaxdetails finden Sie hier.
Alle Projektquellen