Azure Functionsでサーバーサイドアプリを動かす

When

2016/12/02

At

[Gotanda.js #6 in Oisix](https://gotandajs.connpass.com/event/42328/)

---

# 今回のGitHubリポジトリ

[ovrmrw/qiita-advent-calendar-2016-microservices-1](https://github.com/ovrmrw/qiita-advent-calendar-2016-microservices-1)

現在更新中:innocent:

===

話す内容

- Azure Functionsの基本。
- Azure Functionsでサーバーサイドアプリを動かす。

---

## 自己紹介

ちきさん
(Tomohiro Noguchi)

Twitter: [@ovrmrw](https://twitter.com/ovrmrw)

ただのSIer。

Angular Japan User Group (ng-japan)スタッフ。

![3a2512bb-aa72-4515-af42-1f1721252f39.jpg](https://qiita-image-store.s3.amazonaws.com/0/74793/c753b39c-1581-ea1d-87d1-181f37bda60d.jpeg)

---

### アカウントIDの由来

1. the day after tomorrow 
1. overmorrow(俗語)
1. 略して
1. ovrmrw
1. 「先を見据えて」よりさらに先を、みたいな感じです。

(よく聞かれるので:innocent:)

---

(ここから本編)

---

# Azure Functionsの基本から

- AWS Lambda + API Gatewayみたいなもの。
- (Herokuみたいな)Web Appsと同じ仕組みのVMを起動するのでAWS Lambdaより遅い。(多分)
- ストレージIOがとても遅いので**モジュールを多数importする場合はWebpackでバンドルすべき**。
- Node.js以外にもC#とかF#とかでも書けるらしい。

---

## Rule1. サブフォルダの名前がエンドポイントになる。

```
root
|--function1
|  |--function.json
|  |--main.ts
|--function2
   |--function.json
   |--main.ts
```

生成されるエンドポイント。

- `https://my-azure-functions.azurewebsites.net/api/function1`
- `https://my-azure-functions.azurewebsites.net/api/function2`

---

## Rule2. ファンクションフォルダにはfunction.jsonが必須。

HTTPエンドポイントの場合はこのように書く。
`function.json`が存在しないとファンクションと見なされない。

```json:function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "disabled": false
}
```

---

## Rule3. contextを引数に取る関数をexportする。

最高にシンプルなファンクションはこんな感じ。

```ts:main.ts
export function azureFunction(context, req) {
  context.res = {
    status: 200,
    body: {
      message: 'Hello world.',
    }
  };
  context.done();
}
```

Rules

- exportする関数は最低限`context`を引数に取ること。
- `context.res`に`status`と`body`をセットして返すこと。
- 最後に`context.done()`に呼ぶこと。(あるいはPromiseを返すこと)


```ts:main.ts(Promise)
export async function azureFunction(context, req) {
  context.res = {
    status: 200,
    body: {
      message: 'Hello world.',
    }
  };
  // context.done(); <-- Promiseを返す場合はdone()は呼ばなくていい。
}
```

---

## Rule3-2. 複数ファイルで構成される場合はindex.tsを用意しておくとトラブルがない。

例えばこのような状況では`index.ts`を作っておかないと**Not Found沼**に嵌ることがある。

```
root
|--function1
   |--function.json
   |--index.ts
   |--main.ts
   |--server.ts
```

`index.ts`はファンクションをimportして`export default`でexportするだけで良い。

```ts:index.ts
import { azureFunction } from './main';

export default azureFunction;
```

---

(ここまでが基本編)

---

### Azure Functionsのエンドポイントでサーバーサイドアプリ(Express, Hapi等)を動かすことができる。

---

# Server-side app on Azure Functions

```
root
|--hapi-simple
   |--function.json
   |--index.ts
   |--main.ts
   |--server.ts
```

(example)

- `https://my-azure-functions.azurewebsites.net/api/hapi-simple`
    - `{ message: 'Hello world'}`
- `https://my-azure-functions.azurewebsites.net/api/hapi-simple/Gotanda.js`
    - `{ message: 'Hello world, Gotanda.js'}`

---

## function.jsonにちょっと細工

`"route":"hapi-simple/{*segments}"`を書くことでルーティングが可能になる。

```json:function.json
{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "route":"hapi-simple/{*segments}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "disabled": false
}
```

---

## server.ts

サーバーサイド(Hapi)のコードをそのまま持ち込んで、URIを生成する関数をPromiseでexportする。

```ts:server.ts
import * as Hapi from 'hapi';

const server = new Hapi.Server();
server.connection({
  host: 'localhost'
});

server.route([
  {
    method: 'GET',
    path: '/',
    handler: (req, reply) => {
      const message = 'Hello world.';
      reply({ message });
    }
  },
  {
    method: 'GET',
    path: '/{name}',
    handler: (req, reply) => {
      const message = 'Hello world, ' + req.params['name'];
      reply({ message });
    }
  }
]);


let uri: string | undefined;

export function createUri(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    if (uri) {
      resolve(uri);
    } else {
      server.start((err) => {
        if (err) { reject(err); }
        uri = server.info.uri;
        console.log('Server running at:', uri);
        resolve(uri);
      });
    }
  });
}
```

---

## main.ts

Hapiサーバー起動後のURIを取得して、ファンクションに入ってきたrequestオブジェクトをリレーしてHapiサーバーにfetchする。

```ts:main.ts
import { customFetch } from '../lib/utils';
import { createUri } from './server';

export async function azureFunction(context, req) {
  try {
    const uri: string = await createUri();
    const res = await customFetch(uri, req);

    if (res.status === 200) {
      context.res = {
        status: res.status,
        body: res.myBody,
        headers: res.myHeaders,
      };
    } else { // Not Found
      context.res = {
        status: res.status,
        body: res.statusText,
      }
    }
  } catch (err) {
    context.res = {
      status: 500,
      body: err,
    };
  }
}
```

---

## index.ts

`main.ts`で定義したファンクションを`export default`でexportしているだけ。

```ts:index.ts
import { azureFunction } from './main';

export default azureFunction;
```

---

1ドメインでこんなことも可能です。

```
root
|--express1
|  |--function.json
|  |--index.ts
|  |--main.ts
|  |--server.ts
|--express2
|  |--function.json
|  |--index.ts
|  |--main.ts
|  |--server.ts
|--express3
   |--function.json
   |--index.ts
   |--main.ts
   |--server.ts
```

生成されるエンドポイント。

- `https://my-azure-functions.azurewebsites.net/api/express1`
- `https://my-azure-functions.azurewebsites.net/api/express2`
- `https://my-azure-functions.azurewebsites.net/api/express3`

---

イメージ図
![Server-side app on Azure Functions.png](https://qiita-image-store.s3.amazonaws.com/0/74793/4baa98ae-b598-41fe-b3ca-77402a774b5e.png)

---

(動作デモ)

- /api/hapi-simple
    - [/api/hapi-simple/](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/hapi-simple/)
    - [/api/hapi-simple/Gotanda](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/hapi-simple/Gotanda)
- /api/express1
    - [/api/express1/](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/express1/)
    - [/api/express1/hello?name=Gotanda](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/express1/hello?name=Gotanda)
    - [/api/express1/public](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/express1/public)
- /api/hapi1
    - [/api/hapi1/](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/hapi1/)
    - [/api/hapi1/hello?name=Gotanda](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/hapi1/hello?name=Gotanda)
    - [/api/hapi1/public](https://qiita-advent-calendar-2016-microservices.azurewebsites.net/api/hapi1/public)

---

- まだJSONを返すAPIしか試作してないけど、HTMLを返すこともできるのでは？
- つまりAngular SSR on Azure Functionsをやりたい。
- ここ数日トライし続けているが光が見えない。
- 誰か作れたら教えてください。

===

- (最新情報) HTMLは返せた。でも画像が返せない。もうAzureの仕様なのでは…
- 画像は全て別サーバーから取得するようにしたらイケるかもしれない。

---

### Thanks!