/ Azure

Azure Functions - Teil 2 - Visual Studio Code + TypeScript

Im vorigen Artikel Azure Functions - Teil 1 habe ich versuchte Azure Functions in Microsofts Cloud Strategie einzugliedern und kurz zu erklären, was Azure Function sind.
Außerdem war mir wichtig ein paar Tipps für den Start zu geben. Wobei sich das vor allem auf die Links zur sehr guten Dokumentation beschränkte.

Doch in der Doku findet man noch nicht viel über das, wie man eine Applikation als Projekt anlegt. Die "Entwicklerstory" fehlt noch. Sicher wird sich da noch viel tun (Visual Studio Integration, usw.), doch wenn man jetzt schon starten will...

Wie man Azure Functions in TypeScript und mit Hilfe von Visual Studio Code entwickelt, das will ich in diesem Artikel zeigen.

Es kann durchaus sein, dass die TypeScript Unterstützung verbessert wird und mein vorgestellter Weg überflüssig wird, schließlich sind wir noch im Preview Stadium von Azure Functions.

Dieser Artikel ist Teil der Serie:
Let's Code Artikel

Get started with Azure Functions


1. Lokales Projekt

Wie bekommen wir nun das Projekt nach Visual Studio Code.

Man muss dazu die Continuous Integration aktivieren. Dadurch definiert man, wo der Code liegen kann. Im einfachsten Fall erstellt man ein "lokales" Git Repository in Azure, das man sich dann auf den lokalen Rechner klont.
Eine andere Möglichkeit wäre GitHub zu verbinden. Checkt man in GitHub etwas ein, wird Azure automatisch (via WebHooks) informiert und holt sich den neuesten Code von dort.

VS Code (Visual Studio Code) kann man dann hervorragend dazu verwenden das Projekt zu strukturieren und mit dem Sourcecode Repository zu synchronisieren.

Natürlich klappt das auch mit C#, anderen Sprachen und anderen Editoren. Hier in dieser Serie geht es aber um die Verwendung von TypeScript und VS Code.

Bibliotheken, die man mit npm einbindet, werden bei einem Update von Azure automatisch aktualisiert. Das ist eine Funktion der Azure App Services und funktioniert dadurch auch bei Azure Functions (Kudu heißt das Deployment/Tools System).

Continuous Integration

Der Link "Function app settings", rechts über den Tabs, führt auf eine Seite, in der man die Configure Continuous Integration aktivieren kann.

Function App Settings

Bereitstellungsquelle auswählen
  • "Setup" klicken
  • "Erforderliche Einstellungen konfigurieren" klicken
  • "Lokales Git-Repository" auswählen und mit Ok abschließen.

Wir wollen hier also in Azure ein git repository erstellen und nicht auf github.

Bereitstellungsquelle auswählen

Der Code der einzelnen Azure Funktionen ist nun im Portal nicht mehr veränderbar (auf "read only" gesetzt).
(Bemerkung: Muss man später doch nochmal im Portal an der Function etwas ändern, dann kann man die Verbindung trennen in Function app settings / Configure Continuous Integration. Nach der Änderung kann/muss man wieder ein neues repository anlegen.)

Clone

In den Function app settings / Go to App Service Settings findet man wie in den App Services üblich die GIT-Klon-URL.

Bemerkung: Dieser Vorgang ist nicht Azure Functions spezifisch, auch wenn man z.B. eine WebApp erstellt, kann man so vorgehen.

Nun ein neues Verzeichnis erstellen und in der PowerShell dann das Repository holen

mkdir hlc.azure.functions
cd .\hlc.azure.functions\
git clone https://myadmin@hlcfunctions3.scm.azurewebsites.net:443/hlcfunctions3.git

Der Git Credential Manager für Windows meldet sich (falls installiert) und erwartet Benutzername/Passwort.

Und damit ist der aktuelle Sourcecode auch schon auf der lokalen Festplatte.

Nähere Infos zur Verwendung von Git und GitHib in Zusammenhang mit VS Code kann man in den Videos Let's Code 6/7: VS Code + Git + GitHub erfahren.

2. TypeScript

Es gibt noch keinen von Microsoft vorgeschlagenen Weg, wie man TypeScript schlau einsetzen soll, somit machen wir es uns vorerst einfach. Man könnte sich auch überlegen Interfaces für den Einstiegspunkts zu definieren, damit man bei der Implementierung der Azure Function besser unterstützt wird und so context, req, arguments, ... typsicher machen (typischer Einstiegspunkt eines HTTPTriggers: module.exports = function myfunction(context, req) ). Ich habe damit zwar begonnen, aber der Nutzen ist mir zu gering für den Aufwand. Mir ist es wichtiger, dass ich diesen Einstiegspunkt möglichst so belasse, wie er vom Generator bei der Erstellung der Azure Function vorgegeben wurde und die Logik in eine eigene Klasse auslagere.

Das hat den Vorteil, dass ich diese Logik dann auch ohne zu deployen auf meinem Rechner testen kann und dass sich die Azure Functions noch ändern können, ohne dass ich einen großen Aufwand dann hätte.

Mir gefällt dieser Template Ansatz, wie ich ihn im Eintrag TypeScript - Node.js - Start Template gezeigt habe, noch immer sehr gut, daher habe ich mir auch für eine Azure Function diesen Vorgang notiert.

Es liegt jetzt also der generierte Code lokal auf meinem Rechner und nun will ich ein Projekt daraus erzeugen, eine JavaScript Bibliothek (lodash wird wieder als Beispiel verwendet) hinzugefügt, TypeScript Definition Files inkludiert und eine TypeScript Klasse erstellt, die dann von unserer Azure Function verwendet wird.

Durch diesen Start hat man, meiner Meinung nach schon viele Unsicherheiten gelöst und kann sich bei der Implementierung der Funktion dann daran anhalten (neues definition file oder eine library dazufügen und diese richtig importieren z.B.).

Eine Function App (in unserem Beispiel: hlc.azure.functions) kann mehrere Azure Functions enthalten. Man könnte jede Function ihre eigene Umgebung einrichten (libraries, ...). Da ich aber nicht vorhabe in einer Function App unterschiedliche Technologien einzusetzen (node.js und nicht mische mit .Net) erstelle ich im Basis Ordner die grundlegenden Sachen. (Hat man nur eine Azure Function, ist es wohl einfacher, das Projekt in der Function selbst zu erstellen.)

Achtung:

  1. In das durch git clone erstellte
    Verzeichnis wechseln (in meinem Fall hlcfunction3).
  2. Bei npm init werden Eingaben erwartet, einfach mal mit return alle bestätigen (Kann man später immer noch im package.json file ändern.).
    cd .\hlcfunctions3
    tsc --init
    tsd init

    tsd install node --save
    tsd install lodash --save
    New-Item .gitignore

    npm init
    npm install lodash --save
    New-Item common -ItemType Directory
    New-Item common\utils.ts

   code .

Dadurch öffnet sich VS Code.

Und dort nun als erstes die einzelnen Azure Function index.js Files in Typescript Files verwandeln (Man findet sie in den Azure Function Ordnern, wie HttpTriggerNodeJS1, QueueTriggerNodeJS1, ... - je nachdem, wie man sie benannt hat.):

index.js Dateien in index.ts umbenennen

Die Vorgangsweise ist im Gegensatz zum oben erwähnten TypeScript - Node.js - Start Template etwas vereinfacht, weil wir auf die Trennung von Src und Dist Verzeichnis verzichten, die erzeugten Maps und JavaScript files werden einfach in dasselbe Verzeichnis wie das TypeScript Verzeichnis gelegt.
Dadurch ersparen wir uns eine komplexere Task Runner Konfiguration.

Damit die *.js und *.js.map Files nicht verwirren und wir plötzlich doch in einem JavaScript File editieren und das verlieren beim nächsten Kompilieraufruf, blenden wir diese Dateien mit der Hilfe von Visual Studio Code allerdings aus:

Settings:
File / Preferences /Workspace settings

{
    "files.exclude": {
		"**/*.js.map": true,
		"**/*.js": true
	}
}

Nun folgende Files füllen:

  • src/common/utils.ts:
import * as _ from "lodash";
export class Utils {   
    public static kebapStyle(input: string): string {
        return _.kebabCase(input);
    }
}

//.gitignore

node_modules
typings
.vscode

//HttpTriggerNodeJS1/index.ts

Nun verwenden wir eine HttpTrigger Azure Funktion, die ein wenig geändert wird, damit sie die Utils Methode verwendet.

Die index.ts files wurden schon gefüllt, da wir sie durch Umbenennen erzeugt haben.

(Bemerkung: um das js file wieder zu sehen, einfach kurz im .vscode/settings.json die zuerst eingefügten Zeilen auskommentieren und speichern.)

Das index.ts file jetzt noch kurz anpassen:

import {Utils} from "../common/utils";

module.exports = function(context, req) {
    context.log('Node.js HTTP trigger function processed a request. RequestUri=%s', req.originalUrl);

//    if (req.query.name || (req.body && req.body.name)) {
      let input = (req.query && req.query.name) || (req.body && req.body.name);
      if (input) {
        let output = Utils.kebapStyle(input);

        context.res = {
            // status: 200, /* Defaults to 200 */
//            body: "Hello " + (req.query.name || req.body.name)
            body: output
        };
    }
    else {
        context.res = {
            status: 400,
            body: "Please pass a name on the query string or in the request body"
        };
    }
    context.done();
};

Im tsconfig.json ändern wir zwei Werte:

"target": "es6",
"sourceMap": true

Alles speichern.

3. Deployment

Build

Nun Builden, damit ein Task Runner config angelegt wird:

Ctrl + Shift + B
- Configure Task Runner:
  - TypeScript - tsconfig.json

Und nun nochmals builden (Ctrl + Shift + B).

Man sieht auch, dass sich dadurch die Anzahl der Files ändert (durch die Anzeige im Git symbol). Dies ist deshalb, da die TypeScript Map und JavaScript Files durch das Compilieren erstellt werden.

Deploy

Wenn der Build Vorgang erfolgreich war, dann muss man nur mehr den Sourcecode committen und dann mit dem Azure git repository syncen.

Das geht alles aus VS Code:

  • In der linken ViewBar - Git
  • Changes auf + klicken
  • Message eintippen.
  • Commit all (Hackerl, Häkchen, Check mark oder wie das heißt)
  • More (drei Punkte) + Sync

VS Code Git ViewBar

Bemerkung: Ich committe auch die ts und js.map files, obwohl die zur Ausführung nicht nötig wären.
Wenn man das nicht will, kann man zu .gitignore noch folgendes dazufügen:

**/*.ts
**/*.js.map

Das dauert nun eine Weile, bis die Synchronisation mit dem entfernten git repository fertig gestellt ist. Und noch länger dauert das Aktualisieren der App, da die node_modules von Kudu installiert werden müssen.

Doch man kann inzwischen schon am Portal testen, bis der Fehler, dass die Bibliothek lodash unbekannt ist, nicht mehr erscheint und das erwartete Ergebnis zurückkommt.

Testen im Portal

Aufruf der Funktion im Portal

Oder auch im Browser indem man den Parameter name anhängt:
Aufruf der Funktion im Browser

Testen - lokal

Die jetzige Implementierung der Function ist zwar nicht wirklich repräsentativ für eine Business Logik, aber das Prinzip ist gleich. Wir wollen in diesem Fall unsere Utils.kebapStyle Methode testen.

Eine neue Datei erstellen:
// tests/testKebap.ts

import {Utils} from "../common/utils";
class TestKebap {
    public static async main() {
        let input = "Zwiebel,Tomate";
        let output = Utils.kebapStyle(input);
        console.log(output);
    }
}
TestKebap.main();

Dann wie im Eintrag
TypeScript - Node.js - Start Template unter Debuggen beschrieben, vorgehen.

Im file launch.json aktiviere ich jetzt meist zusätzlich externalConsole, denn im output Fenster innerhalb Visual Studio Codes wird nicht immer alles angezeigt (Vielleicht ein Bug).

"program": "${workspaceRoot}/tests/testKebap.ts",
...
"externalConsole": true,
"sourceMaps": true,
"outDir": "${workspaceRoot}/tests"

Man sollte eigentlich gleich Unit Tests schreiben. Ich muss mir das in TypeScript/JavaScript aber erst erarbeiten.


Zusammenfassung

Auf diese Weise habe ich nun wieder ein Template geschaffen, dass ich mir erstelle, wenn ich eine Azure Function machen will: ein Node.JS TypeScript Projekt, eine Library dazugefügt, TypeScript definition Files, via GIT synchronisiert.

Die Logik der Funktion wird in eigene TypeScript Klassen ausgelagert und können so von einer Test Funktion lokal, ohne Azure Deployment, getestet werden.

Treten Fehler in der "deployten" Version auf, dann sieht man das in der Konsole im Azure Portal der jeweiligen Azure Function.
Will man auch eingreifen, z.B. eine Library via npm zum Testen installieren oder Files ansehen, dann eignet sich dafür die Kudu Konsole. Erreichbar über die
Function App settings - Go to App Service Settings - Tools - Kudu - Gehe zu
Es öffnet sich ein neuer Browser Tab
Debug Console - cmd
Einfacher geht es indem man "scm" an der geeigneten Stelle in der Service URL einfügt. z.B.:
https://hlcfunctions3.scm.azurewebsites.net/

Vielleicht sollte ich zu Kudu mal einen eigenen Artikel verfassen.


Artikel in diesem Blog


Weitere Let's Code Artikel.