* 当サイトはアフィリエイト広告を利用しています。

DevOps Webプログラミング

Traefikでデプロイ & Let's EncryptでTLS対応させる

今回はTraefikで実際にデプロイ & TLS対応について記事にしたいと思います。

少し前にTraefik入門の記事を書きましたが、そのプロジェクトをベースにLet's EncryptでTLS対応を行っていきます。

Traefik入門の記事はこちらから。

↓↓↓

まえがき

この記事の前提

この記事はTraefikの入門記事の続きになります。前回のTraefik記事のソースコードはこちらから。

今回の記事の完成版ソースコードはこちらから。

プロジェクトのコードは大きく分けて3つに分かれており、Traefikによるリバースプロキシ、Expressによる簡単なAPIサーバー(backend)、Reactによるフロントエンド(frontend)という構成です。

本記事ではフロントエンド、バックエンド、Traefikの管理画面(ダッシュボード)をTLS対応させていきます。

TraefikでのLet's Encryptについて

Traefikを使っている場合Let's EncryptによるTLS対応は非常に簡単です。

「Certificate Resolvers」(以下「リゾルバー」と呼ぶ)というものを設定すると、証明書の発行や自動更新など全てTraefikがやってくれます。

やるべきことは大きく分けて以下の2つです。

  • エントリーポイントとリゾルバーを設定
  • それらをサービスに割り当てる

たったこれだけです。エントリーポイントは前回の記事で書いた通り、Traefikにおける「入り口(もしくは扉)」のようなものです。http通信用の入り口とは別にhttps用の「入り口」も用意するというイメージです。

以上を簡単にコードで示すと以下のようになります。(要点だけ抽出してあります)

entryPoints:
  web:
    address: ':80'
  websecure:
    address: ':443'

certificatesResolvers:
  myresolver:
    acme:
      email: 'test@test.com'
      storage: '/letsencrypt/acme.json'
      tlsChallenge: {}
      

このようにエントリーポイント(websecure)とリゾルバー(myresolver)を設定します。これをサービス側に割り当てます。

サービス側 (docker-compose.yml)

services:
  some-app:
    (中略)
    labels:
      - traefik.enable=true
      - traefik.http.routers.frontend-nginx-secured.entrypoints=websecure
      - traefik.http.routers.frontend-nginx-secured.rule=Host(`frontend.example.com`)
      - traefik.http.routers.frontend-nginx-secured.tls.certresolver=myresolver

labelsの箇所でエントリーポイント、リゾルバーを割り当てています。ここでドメインの指定も行います。

これだけでLet's EncryptによるTLS対応が出来てしまいます。非常に簡単であることが分かっていただけると思います。

補足

ここからは前回の記事で作成したMERNプロジェクトをデプロイしていきます。TLS対応自体は簡単なのですが、管理画面(ダッシュボード)の認証機能などのため少し長い記事になってしまいました。

お急ぎの方は記事冒頭の「目次」をご活用ください。

実践

それでは実際にデプロイに向けて準備を進めていきます。

 全体の流れ

全体の流れは以下のようになります。

  • 準備
  • Traefik側の設定
  • サービス側の設定
  • サーバー上で実行

準備

確認事項

事前にドメインとサーバーのセットアップが必要です。

  • nslookupコマンドでドメイン名が解決出来るか確認
  • サーバーでdocker, docker-composeが実行できるか確認

ドメインはフロントエンド、バックエンド、Traefikの管理画面(Dashboard)用に3つ必要になります。

ここからは前回のプロジェクトを元に必要な変更をしていきます。最初に準備として以下の対応をします。

  • Dashboard機能をオフにする
  • 各サービスで環境変数を使うように準備

Dashboardをオフにする

ローカル環境ではinsecure: trueを指定することでDashboardを有効化していました。しかしそのままでは誰でも見ることが出来てしまうので、実際にサーバーに展開する際は必ず認証を追加する必要があります

ここでは一旦Dashboardをオフにして、認証は後で追加していきます。

reverse-proxy > config > traefik.yml

# 以下を削除
api:
  insecure: true

Dashboard用のポートも削除しておきます。

reverse-proxy > docker-compose.yml

ports:
      - '80:80' # http
      - '8080:8080' # Dashboard用 <= 削除

各サービスで環境変数を使うように準備 (読み飛ばし推奨)

次に前回のプロジェクトでハードコードしていた情報を環境変数から読み取るように変更しておきます。

Traefikに直接関係する話ではないため、読み飛ばして構いません

バックエンド側

前回のプロジェクトではローカルで動かすだけだったので、MongoDBのパスワードはハードコードしてしまっていました。今回は.envファイルから読み取るように変更します。

.envファイルから読み取るためにdotenvパッケージをインストールします。

yarn add dotenv

ソースコード上のMongoDBと接続する箇所を変更します。

backend > app.js

require('dotenv').config();
var express = require('express');

...(略)

// mongoose接続設定
const mongoose = require('mongoose');
const uri = `mongodb://${process.env.MONGO_USERNAME}:${process.env.MONGO_PASSWORD}@mongo-example/traefik-test?authSource=admin`;
const options = {
  useNewUrlParser: true,
  useUnifiedTopology: true,
};

...(略)

これで.envファイルからMongoDBのユーザ名・パスワードを読み込むようになりました。

MongoDBサービス

MongoDBのユーザ名・パスワードもバックエンド側同様に環境変数から読み取るようにします。MongoDBサービスはreverse-proxyフォルダのdocker-compose.ymlで設定してあります。

reverse-proxy > docker-compose.yml

services:
  reverse-proxy:
    略

  mongo-db:
    image: mongo:5.0.3-focal
    container_name: mongo-example
    restart: always
    volumes:
      - mongo-example:/data/db
    environment:
      MONGO_INITDB_ROOT_USERNAME: ${MONGO_USERNAME}
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
    networks:
      # Traefikで管理しないのでweb-servicesネットワークを指定する必要はない
      - backend-db

docker composeでは自動で.envファイルを読み込んでくれますので、.envファイルにMONGO_USERNAME、MONGO_PASSWORDを設定します。

フロントエンド側

フロントエンド側では通信するバックエンドAPIのURLを環境変数として指定します。

React (create react app環境)では予め.envファイルを読み込む仕組みが用意されていますので、追加でパッケージのインストールや設定は必要ありません。

.envファイルに環境変数REACT_APP_BASE_URLとしてバックエンドのURLを指定しましょう。環境変数の名前はREACT_APP_から始めなくてはならないので注意してください。

frontend > src > App.js

function App() {
  const [message, setMessage] = useState('');
  useEffect(() => {
    fetch(process.env.REACT_APP_BASE_URL, {
      method: 'GET',
            
(以下略 ...

create-react-app環境では環境に応じて適した.envファイルを自動で読み込んでくれます。例えばプロダクション環境では.env.productionファイルを読み込んでくれますので、上手く活用しましょう。

Reactの環境変数については以下の記事を参考にしてください。 ↓↓

これで準備は完了です。ここから実際にデプロイ & TLS対応の設定をしていきます。

Traefikの設定

Traefik側でやることは以下の通りです。

  • エントリーポイントを追加し、443ポートを割り当て
  • resolverの設定
  • http => httpsへのリダイレクトを設定
  • Dashboardに認証機能を追加

それではそれぞれ解説していきます。

エントリーポイントを追加・443ポート割り当て

https通信用のエントリーポイントを作成し、443ポートを割り当てます。入門記事でも述べたように、エントリーポイントの設定はstatic configurationに書きます。

reverse-proxy > config > traefik.yml

entryPoints:
  web:
    address: ':80'
  websecure: # エントリーポイント名。好きな名前でOK
    address: ':443'

そしてcomposeファイルにも443ポートの設定を追加します。

reverse-proxy > docker-compose.yml

  reverse-proxy:
    image: traefik:v2.5.3
    restart: always
    ports:
      - '80:80' # http
      - '443:443' # https
    
    (以下略 ...

エントリーポイントの設定はこれだけです。

resolver設定

次にTLS対応のためのリゾルバーを設定していきます。ドキュメントに従って書いていきます。

注意点

Let's Encryptにはレート制限がありますので、まずは「ステージング環境」を利用してテストをするのが一般的です。

Traefikではそのためのオプションが用意されているので、活用しましょう。

下記のcaserverのオプションを追加して動作確認をします。

なおメールアドレスは自分のアドレスを設定してください。

certificatesResolvers:
  myresolver: # リゾルバー名。好きな名前でOK
    acme:
      email: 'test@test.com' # 自分のメールアドレスを指定
      storage: '/letsencrypt/acme.json' # 認証情報の置き場所を指定。
      tlsChallenge: {} # 「TLS Challenge」を使う。443ポートを開けておく必要がある。
      # Let's Encryptにはレート制限があります。
      # 制限にかからないようにするため、デバッグ時には以下のオプションを追加します。
      caserver: 'https://acme-staging-v02.api.letsencrypt.org/directory'

ここで指定したstorageにACME証明書であるacme.jsonが配置されます。このフォルダはtraefikコンテナ内のフォルダですので、ファイル永続化のためホスト側との共有(マウント)が必要になります。

composeファイルのvolume設定を変更します。

reverse-proxy > docker-compose.yml

volumes:
      - /var/run/docker.sock:/var/run/docker.sock # 必須
      - ./config:/etc/traefik/ # 設定ファイルをマウントするため
      - ./letsencrypt:/letsencrypt # 自動的にフォルダが作られる。acme.jsonが置かれる

reverse-proxy > letsencryptフォルダにacme.jsonファイルが置かれることになります。フォルダの作成は自動で行われるので、手動でフォルダを作成する必要はありません。

補足

今回は「チャレンジ」の種類としてtlsChallengeを使っています。他にもhttpChallengeやdnsChallengeといったチャレンジがあります。tlsChallengeは80番ポートを使わず、TLSレイヤーで実行されるという特徴があります。

それぞれの違いについては下記のリンクを参考にしてください。

http => httpsへのリダイレクトを設定

通信をhttpsで一本化するため、httpリクエストをhttpsへとリダイレクトさせたいはずです。

Traefikではこれも簡単に設定できます。

エントリーポイントを以下のように設定します。

reverse-proxy > config > traefik.yml

entryPoints:
  web:
    address: ':80'
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https

  websecure:
    address: ':443'

これだけです。簡単!

基本的にはTraefik側の設定はこれで終了なのですが、Dashboardの利用のためにはもうひと手間が必要です。

Dashboardに認証を追加する

ローカル環境ではinsecure設定をすることでDashboardを使用できていましたが、実際のサーバーで動かす場合はセキュリティ上この方法では出来ません。

TraefikではMiddlewareを設定することで認証機能を追加することが出来ますので、DashboardにはBasic認証をかけることにします。

やることは大きく分けて以下の通り

  • Fileプロバイダーを設定(Dynamic Configurationを読み込むため)
  • Dashboard用のルーターを設定(ドメイン・パスなど)
  • 認証のミドルウェアを追加
  • Basic認証用にユーザ・パスワードを用意する(サーバー上で)

前回の記事でも述べたとおり、ルーター・ミドルウェアはDynamic Configurationで設定します。ここまではDynamic Configurationは各サービスのdocker-compose.ymlに書いていましたが、今回はファイルから読み込む形で設定していきます。

補足

今回の設定をreverse-proxy > docker-compose.yml内に書くことも出来ますが、再利用性や可読性の観点からファイルから読み取る形にしてあります。

まずはファイルを読み込むため、Fileプロバイダーを設定する必要があります。プロバイダーの設定はStatic Configurationで行います。

reverse-proxy > config > traefik.yml

providers:
  docker:
    exposedByDefault: false
    network: web-services

  file:
    directory: /etc/traefik/dynamic # <= 追加

これで指定したフォルダに置かれた設定ファイルを読み込んでくれます。

それでは肝心の設定ファイル(Dynamic Configuration)を作成していきます。大きく分けてルーターの設定とミドルウェアの設定に分かれています。

reverse-proxy > config > dynamic > dynamic_conf.yml

# Dynamic Configuration
http:
  routers:
    dashboard:
      # ドメインを変更すること
      rule: Host(`traefik.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
      tls:
        certResolver: myresolver
      service: api@internal
      middlewares:
        - auth

  middlewares:
    auth:
      basicAuth:
        usersFile: /etc/traefik/.htpasswd # /etc/traefik は reverse-proxy/configにマウントされる(docker-composeを参照)

Dashboard用にルーターを作成し、ドメイン、パスを割り当てています。httpsで通信するためにリゾルバーも指定しています。

また認証用のミドルウェアを作成し、Dashboardのルーターに割り当てています。

Basic認証のユーザ・パスワードは別ファイルから読み取る形にしてあります。reverse-proxy > config > .htpasswdファイルに以下のような形で記述します。

test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/
test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0

といってもこれを手動で書く必要はありません。以下のセクションを参考にしてください。

パスワードの生成について

パスワードの生成には以下のhtpasswdコマンドを使います。user passwordの箇所を自分の設定したいユーザ名、パスワードに置き換えて実行します。

$ echo $(htpasswd -nb user password)

htpasswdが使えなければパッケージをインストールしてください。

このコマンドの出力を.htpasswdファイルにそのまま出力します。例えばユーザ名をadminUser、パスワードをadminPasswordにしたい場合は次のようなコマンドになります。

コマンド例:

$ echo $(htpasswd -nb adminUser adminPassword) > ./reverse-proxy/config/.htpasswd

このコマンドをデプロイする際にサーバー上で実行してください。

少しややこしくなってしまいましたが、以下のようなフォルダ構成になります。(.envにはMongoDBのユーザ名・パスワードが書かれています)

参考:

長くなりましたが、以上でTraefik側の設定は終了です。ここからはサービス側の設定になります。

サービス側の設定

サービス側ではTraefikで設定したエントリーポイント、リゾルバーを各サービスに割り当て、ドメインの設定をします。

それぞれ見ていきましょう。

バックエンド側

backend > docker-compose.yml

version: '3.8'

services:
  backend-api:
    image: dummy-backend
    build:
      context: .
    networks:
      - web-services
      - backend-db
    labels:
      - traefik.enable=true
      - traefik.http.routers.backend-api-secured.entrypoints=websecure # traefik.ymlで指定したhttps用のエントリーポイント
      - traefik.http.routers.backend-api-secured.rule=Host(`api.example.com`) # 用意したドメインを指定
      - traefik.http.routers.backend-api-secured.tls.certresolver=myresolver # traefik.ymlで指定したリゾルバー名

networks:
  web-services:
    external: true
  backend-db:
    external: true

labelsにエントリーポイント、リゾルバーを設定し、ruleでドメインの指定を行っています。

websecuremyresolverはtraefik.ymlで指定したエントリーポイント名、リゾルバー名にしてください。

これだけでOKです。

フロントエンド

フロントエンド側もバックエンドと同様です。

version: '3.8'

services:
  frontend-app:
    image: dummy-frontend
    build:
      context: .
    networks:
      - web-services
    labels:
      - traefik.enable=true
      - traefik.http.routers.frontend-nginx-secured.entrypoints=websecure # traefik.ymlで指定したhttps用のエントリーポイント
      - traefik.http.routers.frontend-nginx-secured.rule=Host(`front.example.com`) # 用意したドメインを指定
      - traefik.http.routers.frontend-nginx-secured.tls.certresolver=myresolver # traefik.ymlで指定したリゾルバー名

networks:
  web-services:
    external: true

なおこれに加えて、ソースコード内のバックエンドAPIの通信先を変更しておいてください。

上のセクションで対応したように、通信先を環境変数から読み取るようにしている場合は.env.productionに記述しておけばOKです。

例(.env.production):

REACT_APP_BASE_URL=https://api.example.com

以上で全ての設定は終了です。以下はサーバー上で実行した結果を画像とともに載せておきます。

実行

まずはLet's Encryptのステージング環境でテストを行います。

reverse-proxy > config > traefik.ymlでcaserverの箇所が指定されていることを確認してdocker-compose up します。

dashboardへアクセスすると「証明書が無効です」と表示されます。失敗していそうですが、問題ありません。証明書の内容を見てみます。

 

証明証を見てみると「STAGING」と表示されているので(たぶん)OKです。

各サービスも立ち上げてみて、同じようにステージング用の仮の証明書が発行されているか確認してみてください。

問題なければ、一度acme.jsonを削除します。reverse-proxy > letsencryptフォルダに保存されているはずです。

reverse-proxy > config > traefik.ymlでcaserverの行をコメントアウトし、再度サービスを立ち上げてください。

DashboardのURLにアクセスすると、ユーザ名・パスワードを求められるはずです。入力すると、以下のようにTraefikのDashboardが表示されます。

ここからバックエンド、フロントエンドのサービスを立ち上げていくと、以下のようにルーターとサービスの数も変わっていきます。

各サービスも正常に動作しているか確認してみてください。

バックエンドAPI ↓↓

フロントエンド ↓↓

これで全て終了です。

補足

証明書の更新

Let's Encryptを使っている場合、証明書の期限が30日以内になると自動更新してくれるようです。

まとめ

結局何が言いたかったか

非常に長い記事になってしまいましたが、何が言いたいかと言うと、「TraefikではLet's EncryptでTLS対応させることは非常に簡単だ」ということです。

記事でも述べた通り、エントリーポイントとリゾルバーを設定し、それを各サービスに割り当てるだけで後は全てTraefikがやってくれます

それだけの話なのですが、実際に動くプロジェクトを書こうとするとこれだけ長い記事になってしまいました。

追加調査

追加で調査が必要なこととして、サポートされているメトリクスの活用方法、Kubernetesでの運用などが挙げられると思います。

(当ブログでは記事にする予定はありません)

以上です。

-DevOps, Webプログラミング
-,