人に運勢があるなら、HTTPリクエストにも運勢があるはず（謎

本記事は[富士通クラウドテクノロジーズ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/fjct) 21日目の記事です。
昨日の記事は@ometora_sateyanの[Python初学者がToDoリストリマインダーbotを作ってみた](https://qiita.com/ometora_sateyan/items/da75b9f8c90d8b9d5452)でした。
個人的にこういったBotのトリガーにGitLabのPipeline Scheduleを利用するのはおもしろい発想だなーと思いました。軽いBotであれば、Pipeline Schedule -> Gitlab-CI -> Incoming Webhook -> Slackでシュッと作れてますね。

---

## TL;DR

HTTPリクエストのヘッダーに一定の確率でGoodを設定するTraefikのPluginをシュッと作りました。
- https://github.com/ystkfujii/traefik-plugin-fortune

おわり

## はじめに

[Traefik](https://github.com/traefik/traefik)はEdge Routerと呼ばれるものであり、リバースプロキシーやロードバランサーとして利用することができるOSSです。詳細は下記を参照してください。

- https://doc.traefik.io/traefik/#welcome
- https://zenn.dev/pitekusu/books/traefik-pitekusu/viewer/introduction

![](https://doc.traefik.io/traefik/assets/img/traefik-architecture.png)

また、Traefikでは、ユーザの要件に合わせてPluginを作成し組み込むことも可能です。
いくつか[Plugin Catalog](https://plugins.traefik.io/plugins)で公開されているので、眺めてみると役に立つPluginがあるかもしれません。

今回はこのPluginをシュッと作りたいと思います。

## Pluginの作成

HTTPリクエストの運勢をHeaderに差し込むPluginを作りたいと思います。
作成するPluginの処理としては以下になります。

- HeaderにKey:`X-FORTUNE`で運勢を追加する
- 運勢はGoodまたはBadになる
- 確率RateでGoodになる。それ以外はBadになる

はい、シュッと

```golang
// Config the plugin configuration.
type Config struct {
	Rate int `json:"rate,omitempty"`
}

// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
	return &Config{
		Rate: 0,
	}
}

// Lottery.
type Lottery struct {
	next     http.Handler
	rate     int
	name     string
	template *template.Template
}

// New created a new Demo plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
	return &Lottery{
		rate:     config.Rate,
		next:     next,
		name:     name,
		template: template.New("lottery").Delims("[[", "]]"),
	}, nil
}

func (l *Lottery) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	rand.Seed(time.Now().UnixNano())

	fortune := rand.Intn(100) + 1
	os.Stdout.WriteString(fmt.Sprintf("fortune/Rate: %v/%v \n", fortune, l.rate))
	if l.rate < fortune {
		req.Header.Set("X-FORTUNE", "Bad")
	} else {
		req.Header.Set("X-FORTUNE", "Good")
	}

	l.next.ServeHTTP(rw, req)
}
```

[Defining a Plugin](https://github.com/traefik/plugindemo/blob/8a77aea29f9038903ab44059e2aa42a37ff52752/readme.md#defining-a-plugin)を参照するにPluginで作成する必要があるものは下記の3種類見たいです。

- `type Config struct { ... }`
  - Dynamic Configurationで設定したい項目（後述）
- `func CreateConfig() *Config`
  - Configの作成
- `func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error)`
  - Pluginで行う処理
  - X-FORTUNEヘッダーを挿入する処理


:::note info
__ログ出力に関して__

[Logs](https://github.com/traefik/plugindemo/blob/8a77aea29f9038903ab44059e2aa42a37ff52752/readme.md#logs)曰く、Log出力する方法は`os.Stdout.WriteString("...")`または`os.Stderr.WriteString("...")`を使用するしかないみたいです。
:::

## traefik.yaml

Pluginの実態以外に、`.traefik.yaml`が必要になります。
作成しない場合、TraefikでPluginを読み込む際に下記のエラーが発生します。

```text
msg="Plugins are disabled because an error has occurred." error="1 error occurred:\n\t* failed to open the plugin manifest plugins-local/src/github.com/ystkfujii/traefik-plugin-fortune/.traefik.yml: open plugins-local/src/github.com/ystkfujii/traefik-plugin-fortune/.traefik.yml: no such file or directory\n\n"
```

`.traefik.yaml`の設定項目は下記になります。
内容は[Manifest](https://github.com/traefik/plugindemo/blob/8a77aea29f9038903ab44059e2aa42a37ff52752/readme.md#manifest)の内容をコピペして日本語訳したものになります。

```yaml
#【必須】Plugins Catalog web UIで表示する名前
displayName: Name of your plugin

#【必須】現在は`middleware`のみ設定可能
type: middleware

#【必須】PluginのImport Path
import: github.com/username/my-plugin

#【必須】Pluginの簡単な説明
summary: Description of what my plugin is doing

#【任意】Pluginに関連するメディア
iconPath: foo/icon.png
bannerPath: foo/banner.png

# 【必須】
# Pluginの確認用のデータ
# Plugins Catalogは起動時の有効性のテストのひとつとして、下記のデータでPluginをテストする
testData:
  Headers:
    Foo: Bar
```

:::note info
__pluginのパッケージ名について__

最初はパッケージ名をmainに設定して作成してたんですが、TraefikでPluginを読み込む際に下記エラーが発生してうまく読み込めませんでした。

> Plugins are disabled because an error has occurred." error="failed to eval New: 1:28: undefined: traefik_plugin_fortune"

下記を参照しパッケージ名をPlugin名にすることで解決しました。

https://github.com/traefik/plugindemo/issues/15
:::

## Dockerfile

上記で作成したPluginを読み込んで動作確認するためのdocker composeファイルは以下になります。
今回はRateを30に設定しているので、30%でX-FORTUNEヘッダーが"Good"になります。

```compose
services:
  traefik:
    image: "traefik:v2.10"
    container_name: "traefik"
    command:
      #- "--log.level=DEBUG"
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      # Plugin Catalogから読み込む際にコメントアウト
      #- "--experimental.plugins.lottery.modulename=github.com/ystkfujii/traefik-plugin-fortune"
      #- "--experimental.plugins.lottery.version=v0.0.1"
      # ローカルから読み込む
      - "--experimental.localPlugins.lottery.moduleName=github.com/ystkfujii/traefik-plugin-fortune"
    ports:
      - "80:80"
    volumes:
      - ".:/plugins-local/src/github.com/ystkfujii/traefik-plugin-fortune"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  whoami:
    image: "traefik/whoami"
    container_name: "simple-service"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=PathPrefix(`/who`)"
      - "traefik.http.routers.whoami.entrypoints=web"
      # traefik-plugin-fortuneの設定
      - "traefik.http.routers.whoami.middlewares=my-plugin"
      - "traefik.http.middlewares.my-plugin.plugin.lottery.Rate=30"
```

## 動作確認

dockerイメージの起動

```shell
docker compose up
```

アクセス確認！
30%の確率でX-Fortune: Goodになります。

```console
$ curl localhost/who
Hostname: a3c72290d619
IP: 127.0.0.1
IP: 192.168.144.3
RemoteAddr: 192.168.144.2:39244
GET /who HTTP/1.1
Host: localhost
User-Agent: curl/7.81.0
Accept: */*
Accept-Encoding: gzip
X-Fortune: Good
X-Forwarded-For: 192.168.144.1
X-Forwarded-Host: localhost
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: 6733240255a7
X-Real-Ip: 192.168.144.1
```

おわり！

---

明日は@unecchiの「Notion APIでWeb API操作を勉強してみた」です。
お楽しみに！