コンテンツにスキップ

Heroku上でWallarmを実行する

WallarmはHerokuクラウドプラットフォームにデプロイされたWebアプリケーションとAPIを保護できます。本ガイドでは、トラフィックをリアルタイムに解析するためにHeroku上でWallarmノードを実行する手順を説明します。

現時点では、Wallarmから提供するHeroku向けの公式Dockerイメージはありません。そのため、本ガイドではall-in-oneインストーラーを使用して独自のイメージを作成・実行する方法を説明します。

要件

  • ホストシステムにDockerがインストールされていること

  • Heroku用Wallarm DockerイメージをプッシュするためのDockerアカウント

  • ホストシステムにHeroku CLIがインストールされていること

  • Herokuのweb dyno上で動作するアプリケーションまたはAPI

  • US CloudまたはEU CloudのWallarm Consoleへの管理者アクセス

  • Wallarmのall-in-oneインストーラーをダウンロードするためのhttps://meganode.wallarm.comへのアクセス

  • USのWallarm Cloudを利用するためのhttps://us1.api.wallarm.comまたはEUのWallarm Cloudを利用するためのhttps://api.wallarm.comへのアクセス

  • 攻撃検出ルールやAPI仕様の更新をダウンロードし、さらに許可リスト、拒否リスト、グレーリストの国、地域、またはデータセンターの正確なIPを取得するために、以下のIPアドレスへのアクセス

    node-data0.us1.wallarm.com - 34.96.64.17
    node-data1.us1.wallarm.com - 34.110.183.149
    us1.api.wallarm.com - 35.235.66.155
    34.102.90.100
    34.94.156.115
    35.235.115.105
    
    node-data1.eu1.wallarm.com - 34.160.38.183
    node-data0.eu1.wallarm.com - 34.144.227.90
    api.wallarm.com - 34.90.110.226
    

ステップ1: WallarmのDocker設定を準備する

HerokuにWallarmのDockerイメージをデプロイするには、まずイメージのビルドプロセスに必要な設定ファイルを作成します。以下の手順に従ってください。

  1. ローカルシステムにWallarmのDocker設定用ディレクトリを作成し、そのディレクトリに移動します。

  2. NGINXとWallarmの設定を含むnginx.confファイルを作成します。DockerイメージはNGINX互換のall-in-oneインストーラーを基にするため、NGINXが適切に設定されていることを確認してください。

    以下は、Wallarmノードをmonitoringモードで動作させる基本構成のテンプレートです。

    daemon off;
    worker_processes auto;
    load_module /usr/lib/nginx/modules/ngx_http_wallarm_module.so;
    pid /tmp/nginx.pid;
    include /etc/nginx/modules-enabled/*.conf;
    
    events {
      worker_connections 768;
      use epoll;
      accept_mutex on;
    }
    
    http {
      gzip on;
      gzip_comp_level 2;
      gzip_min_length 512;
      gzip_proxied any; # HerokuルーターはViaヘッダーを送信します
    
      proxy_temp_path /tmp/proxy_temp;
      client_body_temp_path /tmp/client_temp;
      fastcgi_temp_path /tmp/fastcgi_temp;
      uwsgi_temp_path /tmp/uwsgi_temp;
      scgi_temp_path /tmp/scgi_temp;
    
      sendfile on;
      tcp_nopush on;
      tcp_nodelay on;
      keepalive_timeout 65;
      types_hash_max_size 2048;
      server_tokens off;
    
      # server_names_hash_bucket_size 64;
      # server_name_in_redirect off;
    
      include /etc/nginx/mime.types;
      default_type application/octet-stream;
    
      access_log /var/log/nginx/access.log;
      error_log /var/log/nginx/error.log;
    
      # メインのHerokuアプリ
      server {
        listen $PORT default_server;
        server_name _;
        wallarm_mode monitoring;
    
        location / {
          proxy_pass http://unix:/tmp/nginx.socket;
    
          # Herokuアプリは常にロードバランサーの背後にあるため、すべてのIPを信頼します
          set_real_ip_from 0.0.0.0/0;
          real_ip_header X-Forwarded-For;
          real_ip_recursive off;
          proxy_redirect off;
          proxy_set_header Host $http_host;
          proxy_set_header "Connection" "";
        }
    
        error_page 403 /403.html;
        location = /403.html {
            root /usr/share/nginx/html;
            internal;
        }
      }
    
      # Wallarmステータスのヘルパー(localhostのみ)
      server {
        listen 127.0.0.8:$PORT;
        server_name localhost;
        allow 127.0.0.0/8;
        deny all;
        wallarm_mode off;
        disable_acl "on";
        access_log off;
        location ~/wallarm-status$ {
          wallarm_status on;
        }
      }
    }
    
  3. WallarmのDockerイメージ用の指示を含む以下のentrypoint.shファイルを作成します。

    #!/bin/bash
    
    set -e
    
    log() {
        local msg="$1"
        local level="$2"
        if [ -z "$level" ]; then
            level="INFO"
        fi
        echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $msg"
    }
    
    log "Script execution started."
    
    # 必要なディレクトリが存在することを確認します
    log "Ensuring necessary directories exist for supervisord."
    mkdir -p /opt/wallarm/var/log/wallarm
    mkdir -p /opt/wallarm/run/supervisor
    
    if [ ! -z "$WALLARM_API_TOKEN" ]; then
        log "WALLARM_API_TOKEN is set, checking configuration."
        if [[ $DYNO == web.* ]]; then
            log "Heroku dyno type [$DYNO] is 'web', proceeding with Wallarm configuration."
            # 環境変数を引き継ぎます
            log "Propagating environment variables from /opt/wallarm/env.list."
            set -a
            source /opt/wallarm/env.list
            if [ -s /etc/wallarm-override/env.list ]; then
                log "Propagating environment variables from /etc/wallarm-override/env.list."
                source /etc/wallarm-override/env.list
            fi
            set +a
    
            # CloudにWallarmノードを登録します
            log "Registering Wallarm node in the cloud."
            /opt/wallarm/register-node
    
            # nginxの設定でPORTを構成します
            log "Replacing \$PORT in Nginx configuration with value $PORT."
            sed -i "s/\$PORT/${PORT}/g" /etc/nginx/nginx.conf
    
            # PORTが正しく置換されたことを確認します
            log "Checking if the PORT in Nginx configuration was successfully replaced."
            if cat /etc/nginx/nginx.conf | grep -q "listen ${PORT}"; then
                    log "Successfully replaced PORT in Nginx configuration with value $PORT."
            else
                    log "Failed to replace PORT in Nginx configuration!" "ERROR"
                    exit 1
            fi
    
            # $PORTを$NGINX_PORTとしてエクスポートします(`export-metrics`スクリプトに必要)
            log "Exporting PORT as NGINX_PORT for Wallarm metrics."
            export NGINX_PORT="$PORT"
    
            # supervisordの下でWallarmサービスとNGINXをすべて起動します
            log "Starting all Wallarm services and NGINX under supervisord."
            /opt/wallarm/usr/bin/python3.10 /opt/wallarm/usr/bin/supervisord -c /opt/wallarm/etc/supervisord.conf --loglevel=debug
            # supervisordが正常に起動したか確認します
            log "Checking if supervisord process is running."
            if pgrep -f "supervisord" > /dev/null; then
                log "supervisord process started successfully."
            else
                log "Failed to start supervisord process!" "ERROR"
                exit 1
            fi
    
            # supervisordで管理されるサービスの状態を確認します
            log "Checking the status of all services managed by supervisord every 10s during 3 minutes."
            timeout=0
    
            while (/opt/wallarm/usr/bin/supervisorctl -c /opt/wallarm/etc/supervisord.conf status | grep -qv "RUNNING");
            do
                log "One or more services failed to start!" "ERROR"
                log "Waiting 10s and check it again"
                sleep 10s
                timeout=$(( timeout + 10 ))
    
                if [ $timeout -ge 180 ];
                then
                  log "One or more services failed to start!" "ERROR"
                  /opt/wallarm/usr/bin/supervisorctl -c /opt/wallarm/etc/supervisord.conf status
                  exit 1
                fi
            done
    
            log "All services are running successfully."
            log "Wallarm configuration completed."
    
        else
            log "Heroku dyno type [$DYNO] is not 'web', skipping Wallarm configuration."
        fi
    else
        log "WALLARM_API_TOKEN is not set, executing CMD."
    fi
    
    # CMDコマンドを実行します
    log "Executing command: $@"
    exec "$@"
    log "Script execution finished."
    
  4. 次のコマンドを実行してentrypoint.shの権限を-rwxr-xr-xに設定します。

    chmod 755 entrypoint.sh
    
  5. Wallarmがブロックするリクエストに対して、整ったページを表示する403.htmlファイルを作成します。次をコピーできます。

    <!doctype html> <html> <head> <meta charset=utf-8> <meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" name=viewport> <title>Forbidden</title> <link rel="shortcut icon" type="image/x-icon" href="https://www.herokucdn.com/favicon.ico"> <style>html, body {
      font-family: sans-serif;
      -ms-text-size-adjust: 100%;
      -webkit-text-size-adjust: 100%;
      background-color: #F7F8FB;
      height: 100%;
      -webkit-font-smoothing: antialiased;
    }
    
    body {
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }
    
    .message {
      text-align: center;
      align-self: center;
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 0px 20px;
      max-width: 450px;
    }
    
    .message__title {
      font-size: 22px;
      font-weight: 100;
      margin-top: 15px;
      color: #47494E;
      margin-bottom: 8px;
    }
    
    p {
      -webkit-margin-after: 0px;
      -webkit-margin-before: 0px;
      font-size: 15px;
      color: #7F828B;
      line-height: 21px;
      margin-bottom: 4px;
    }
    
    .btn {
      text-decoration: none;
      padding: 8px 15px;
      border-radius: 4px;
      margin-top: 10px;
      font-size: 14px;
      color: #7F828B;
      border: 1px solid #7F828B;
    }
    
    .hk-logo, .app-icon {
      fill: #DBE1EC;
    }
    
    .info {
      fill: #9FABBC;
    }
    
    body.friendly {
      background: -webkit-linear-gradient(-45deg, #8363a1 0%, #74a8c3 100%);
      background: linear-gradient(135deg, #8363a1 0%, #74a8c3 100%);
    }
    
    body.friendly .message__title {
      color: #fff;
    }
    
    body.friendly p {
      color: rgba(255, 255, 255, 0.6);
    }
    
    body.friendly .hk-logo, body.friendly .app-icon {
      fill: rgba(255, 255, 255, 0.9);
    }
    
    body.friendly .info {
      fill: rgba(255, 255, 255, 0.9);
    }
    
    body.friendly .btn {
      color: #fff;
      border: 1px solid rgba(255, 255, 255, 0.9);
    }
    
    .info_area {
      position: fixed;
      right: 12px;
      bottom: 12px;
    }
    
    .logo {
      position: fixed;
      left: 12px;
      bottom: 12px;
    }
    
    </style> <base target=_parent /> </head> <body> <div class=spacer></div> <div class=message> <svg width=49 height=51 xmlns="http://www.w3.org/2000/svg"><path d="M3.9468 10.0288L20.5548.995c2.4433-1.3267 5.45-1.3267 7.8936 0l16.6078 9.0338C47.4966 11.3585 49 13.8102 49 16.4666V34.534c0 2.6537-1.5034 5.1082-3.9438 6.438l-16.6078 9.0307c-2.4435 1.3297-5.4503 1.3297-7.8937 0L3.9467 40.972C1.5035 39.642 0 37.1876 0 34.534V16.4667c0-2.6564 1.5034-5.108 3.9468-6.4378z" class=app-icon fill-rule=evenodd /></svg> <div class=message__title> Your request has been blocked </div> <p> If you think this is a mistake, please get in touch with the app's support team. </p> </div> <div class=logo> <svg width=85 height=24 xmlns="http://www.w3.org/2000/svg"><g class=info fill-rule=evenodd><path d="M27.8866 16.836h2.373v-3.504h2.919v3.504h2.373V8.164h-2.373v3.2227h-2.919V8.164h-2.373v8.672zm10.4888 0h6.4666V14.949h-4.0935v-1.6054h2.7764v-1.8282h-2.7765v-1.4062h3.8918V8.164h-6.265v8.672zm8.8396 0h2.3256V13.824h.6526L51.89 16.836h2.5154l-1.863-3.3165c1.151-.3867 1.7325-1.1718 1.7325-2.5312 0-2.086-1.3765-2.8242-3.631-2.8242h-3.429v8.672zm2.3256-4.793v-1.9805h1.0204c.973 0 1.4.2578 1.4.9844 0 .7264-.427.996-1.4.996h-1.0204zM60.8363 17c2.112 0 4.307-1.3242 4.307-4.5 0-3.1758-2.195-4.5-4.307-4.5-2.124 0-4.319 1.3242-4.319 4.5 0 3.1758 2.195 4.5 4.319 4.5zm0-1.875c-1.2458 0-1.946-1.0313-1.946-2.625 0-1.5938.7002-2.5664 1.946-2.5664 1.234 0 1.934.9726 1.934 2.5664 0 1.5938-.7 2.625-1.934 2.625zm6.7157 1.711h2.373v-2.6954l.6764-.7734 2.0764 3.4687h2.6816l-3.2155-5.25 2.9543-3.422h-2.7527l-2.4205 3.1407V8.164h-2.373v8.672zm13.4552.1288c2.563 0 3.6782-1.3125 3.6782-3.6093V8.164H82.36v5.1798c0 1.1953-.3798 1.7343-1.329 1.7343-.9493 0-1.3408-.539-1.3408-1.7342V8.164h-2.373v5.1915c0 2.2968 1.127 3.6093 3.69 3.6093zM2.4444 0C.9214 0 0 .8883 0 2.3226v19.3548C0 23.1068.9215 24 2.4444 24h17.1112C21.0736 24 22 23.1117 22 21.6774V2.3226C21.995.8883 21.0735 0 19.5556 0H2.4444zm16.8973 1.9c.4025.0045.7583.3483.7583.7214v18.7572c0 .3776-.3558.7214-.7583.7214H2.6583c-.4025 0-.7583-.3438-.7583-.7214V2.6214c0-.3777.3558-.7214.7583-.7214h16.6834z"/><path d="M16.43 20h-2.2527v-6.8048c0-.619-.1917-.838-.3786-.9666-1.131-.7667-4.3855-.0334-6.3458.7333l-1.553.6475L5.9048 4h2.2814v6.3333c.4314-.1333.973-.2714 1.524-.3857 2.4206-.5143 4.1987-.3762 5.3586.4048.6375.4286 1.3612 1.2714 1.3612 2.8428V20zM11.57 8h2.6675c1.4042-1.75 1.9732-3.35 2.1925-4h-2.6623c-.3967.95-1.1223 2.55-2.1977 4zM5.9 20v-5.6l2.43 2.8L5.9 20z"/></g></svg> </div> </body> </html>
    
  6. WallarmのDockerイメージのビルドプロセスを記述するDockerfileを作成します。

    FROM ubuntu:22.04
    
    ARG VERSION="6.4.1"
    
    ENV PORT=3000
    ENV WALLARM_LABELS="group=heroku"
    ENV WALLARM_API_TOKEN=
    ENV WALLARM_API_HOST="us1.api.wallarm.com"
    
    RUN apt-get -qqy update && apt-get -qqy install nginx curl && apt-get clean
    
    # Wallarmのall-in-oneインストーラーをダウンロードして展開します
    RUN curl -o /install.sh "https://meganode.wallarm.com/$(echo "$VERSION" | cut -d '.' -f 1-2)/wallarm-$VERSION.x86_64-glibc.sh" \
            && chmod +x /install.sh \
            && /install.sh --noexec --target /opt/wallarm \
            && rm -f /install.sh \
            && cd /opt/wallarm \
            && chmod +x pick-module.sh \
            && SELECTED_MODULE="$(./pick-module.sh)" \
            && echo "Selected module => $SELECTED_MODULE" \
            # wlrm-moduleをNGINXのモジュールディレクトリにコピーします
            && cp "$SELECTED_MODULE" /usr/lib/nginx/modules/ngx_http_wallarm_module.so \
            && mkdir -p /usr/local/lib \
            && mv /opt/wallarm/modules/libwallarm.so* -t "/usr/local/lib/" \
            && rm -rf /opt/wallarm/modules
    
    # supervisordをバックグラウンドで実行します。フォアグラウンドのプロセスはHerokuアプリ本体です
    RUN sed -i '/nodaemon=true/d' /opt/wallarm/etc/supervisord.conf
    
    # supervisordにNGINXを追加します
    RUN printf "\n\n[program:nginx]\ncommand=/usr/sbin/nginx\nautorestart=true\nstartretries=4294967295\n" | tee -a /opt/wallarm/etc/supervisord.conf
    
    # Herokuは権限のないユーザー(dyno:dyno)で実行するため、Wallarmディレクトリへのアクセス権を付与する必要があります
    RUN find /opt/wallarm -type d -exec chmod 777 {} \;
    
    # NGINX設定をコピーします
    COPY nginx.conf /etc/nginx/nginx.conf
    
    # Heroku風の403エラーページ
    COPY 403.html /usr/share/nginx/html/403.html
    
    # entrypoint.shを追加します
    COPY entrypoint.sh /entrypoint.sh
    
    # entrypointにdyno:dynoの下で設定を変更させ、NGINXログをコンソールへリダイレクトします
    RUN chmod +x /entrypoint.sh \
            && chmod 666 /etc/nginx/nginx.conf \
            && chmod 777 /etc/nginx/ \
            && ln -sf /dev/stdout /var/log/nginx/access.log \
            && ln -sf /dev/stderr /var/log/nginx/error.log
    
    ENTRYPOINT ["/entrypoint.sh"]
    

ステップ2: Heroku用のWallarm Dockerイメージをビルドする

先ほど作成したディレクトリ内で次のコマンドを実行します。

docker build -t wallarm-heroku:6.4.1 .
docker login
docker tag wallarm-heroku:6.4.1 <DOCKERHUB_USERNAME>/wallarm-heroku:6.4.1
docker push <DOCKERHUB_USERNAME>/wallarm-heroku:6.4.1

ステップ3: ビルドしたDockerイメージをHerokuで実行する

イメージをHerokuにデプロイするには:

  1. 以降の操作を行うため、アプリケーションディレクトリのルートに移動します。

  2. アプリのランタイムに固有の必要な依存関係のインストールを含むDockerfileを作成します。Node.jsアプリケーションの場合は、次のテンプレートを使用します。

    FROM <DOCKERHUB_USERNAME>/wallarm-heroku:6.4.1
    
    ENV NODE_MAJOR=20
    
    # NodeSourceからNodeJS v20をインストールします
    RUN apt-get update \
        && apt-get install -qqy ca-certificates curl gnupg \
        && mkdir -p /etc/apt/keyrings \
        && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
        && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \
        && apt-get update \
        && apt-get install nodejs -qqy \
        && apt-get clean
    
    ADD . /opt/webapp
    WORKDIR /opt/webapp
    
    # 依存関係をインストールしてアプリをビルドします
    RUN npm install --omit=dev 
    ENV npm_config_prefix /opt/webapp
    
    # private spacesではheroku.ymlの`run`セクションは無視されることに注意してください
    # 参照: https://devcenter.heroku.com/articles/build-docker-images-heroku-yml#known-issues-and-limitations
    CMD ["npm", "run", "start"]
    
  3. 次の内容でheroku.yml構成ファイルを作成します。

    build:
      docker:
        web: Dockerfile
    
  4. NGINXが$PORTを使用するため、アプリケーションが$PORTではなく/tmp/nginx.socketで待ち受けるように調整します。例えば、設定は次のようになります。

    // app.js
    const app = require('express')()
    
    let port = process.env.PORT || 3000 // Wallarmが未設定の場合は$PORTで待ち受けます
    if(process.env.WALLARM_API_TOKEN) port = '/tmp/nginx.socket' // Wallarmが設定されています
    
    app.listen(port, (err) => {
        if (err) throw err
        console.log(`> App is listening on ${port}`)
    })
    
    app.get('/', (req, res) => {
        res.send('This app is protected by Wallarm')
    })
    
  5. Wallarm CloudにWallarmノードインスタンスをリンクするため、適切な種類のフィルタリングノードトークンを生成します。

    1. Wallarm Console → SettingsAPI tokensUS CloudまたはEU Cloudで開きます。
    2. 使用タイプがNode deployment/DeploymentのAPI tokenを探すか作成します。
    3. このトークンをコピーします。
    4. 次の環境変数で、Wallarmノードを追加するノードグループ名を指定します。
    heroku config:set WALLARM_LABELS=group=<NODE_GROUP_NAME>
    
    1. US CloudまたはEU CloudでWallarm Console → Nodesを開きます。
    2. Wallarm nodeタイプのフィルタリングノードを作成し、生成されたトークンをコピーします。
  6. ノードをCloudに接続するためのパラメータを、該当する変数に設定します。

    heroku config:set WALLARM_API_TOKEN=<NODE_TOKEN>
    
    heroku config:set WALLARM_API_HOST=api.wallarm.com
    heroku config:set WALLARM_API_TOKEN=<NODE_TOKEN>
    
  7. アプリケーションをプッシュして再起動をトリガーし、Wallarmノードをデプロイします。

    git add Dockerfile heroku.yml app.js
    git commit -m "Add Wallarm Docker"
    heroku stack:set container
    git push heroku <BRANCH_NAME>
    

ステップ4: デプロイをテストする

デプロイが機能していることを確認するため、Path Traversalのエクスプロイトを用いてテスト攻撃を実行します。

curl http://<HEROKU_APP_DOMAIN>/etc/passwd

ノードはデフォルトでフィルタリングモードのうちmonitoringで動作するため、Wallarmノードは攻撃をブロックせず記録します。攻撃が記録されたことを確認するには、Wallarm Console → Attacksに進みます。

インターフェースのAttacks

デバッグ

WallarmのベースDockerイメージで問題が発生した場合は、エラーメッセージがないかHerokuのログを確認します。

heroku logs --tail

デプロイ中に支援が必要な場合は、Wallarmサポートチームにお問い合わせください。