Nginx リバースプロキシの実行環境としてApp Runnerを利用してみた

月額利用料金は数ドル台、転送先のオリジンを環境変数で設定できる、汎用のリバースプロキシ環境として AWS App Runner を利用してみました。
2023.06.08

オリジンを環境変数で指定、汎用のリバースプロキシとして動作する Nginxのコンテナイメージを Amazon Linux 2023 で作成し、 AWS App Runner を リバースプロキシの実行環境として利用する機会がありましたので、 紹介させて頂きます。

作業環境(EC2)

AMIは Amazon Linux 2023 (x86_64)。 Dockerは 標準リポジトリのパッケージを利用しました。

ECR、App Runnerを AWS CLIで操作するため、EC2ロールとして AdministratorAccessポリシーを付与したものを利用しました。

インストール

sudo yum install docker jq -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -a -G docker ec2-user
sudo su - ec2-user

バージョン情報

$ docker --version
Docker version 20.10.23, build 7155243

$ cat /etc/os-release
NAME="Amazon Linux"
VERSION="2023"
ID="amzn"
ID_LIKE="fedora"
VERSION_ID="2023"
PLATFORM_ID="platform:al2023"
PRETTY_NAME="Amazon Linux 2023"
ANSI_COLOR="0;33"
CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2023"
HOME_URL="https://aws.amazon.com/linux/"
BUG_REPORT_URL="https://github.com/amazonlinux/amazon-linux-2023"
SUPPORT_END="2028-03-01"

コンテナイメージ作成

Docker設定

IMAGE='docker-nginx'
mkdir -p ~/${IMAGE}
cd ~/${IMAGE}

## Dockerfile
cat << "EoL" > Dockerfile
FROM nginx:latest
COPY ./default.conf.template /etc/nginx/templates/default.conf.template
COPY ./healthcheck.html /var/www/healthcheck.html
EoL

## default.conf.template
cat << "EoL" > default.conf.template
server {
  listen 8080 default_server;
  server_name localhost;
  set $backend ${PROXY_HOST};
  resolver 169.254.169.253 valid=5s;
  location = /healthcheck.html {
    access_log off;
    root /var/www;
  }
  location / {
    proxy_ssl_server_name on;
    proxy_set_header Host $backend;
    proxy_pass https://$backend;
  }
}
EoL

## healthcheck.html
cat << "EoL" > healthcheck.html
<html>ok</html>
EoL

Nginx設定

環境変数対応

環境変数をNginx設定に反映するため、Nginx 1.19 よりデフォルトサポートされた envsubst を利用しました。

環境変数を利用する設定項目、${PROXY_HOST} をとテンプレートファイルを /etc/nginx/templates/default.conf.template に配置。

テンプレートファイル中の ${PROXY_HOST}を、環境変数で指定した値に置換したファイルを、Nginxの設定ファイルとして利用しました。

Using environment variables in nginx configuration (new in 1.19)

By default, this function reads template files in /etc/nginx/templates/*.template and outputs the result of executing envsubst to /etc/nginx/conf.d.

Official build of Nginx.

名前解決

オリジン(リバースプロキシ先)のIPアドレスが変更される場合に備え、 Nginxのワークアラウンドとして知られる、proxy_pass を変数で定義する設定を行いました。

resolver には Route 53 Resolver のIPv4アドレス (169.254.169.25) を指定、 一定頻度 (TTL5秒)でオリジンの名前解決が行われる設定としました。

proxy_pass

ヘルスチェック

  • App Runnerのヘルスチェック用にローカルファイルを配置しました。
  • ヘルスチェックのアクセスログ出力は抑制、CloudWatch Logs 費用の節約を図りました。

SNI

  • HTTPS接続 に SNIを利用する CloudFront、API Gateway などを オリジンで利用可能とするため、proxy_ssl_server_name on を設定しました。

コンテナイメージ作成

IMAGE='docker-nginx'
cd ~/${IMAGE}
docker build -t ${IMAGE} .

起動

  • 環境変数で オリジンとなるFQDN、弊社コーポレートサイト(CloudFront)を指定して、イメージを起動しました。
IMAGE='docker-nginx'
cd ~/${IMAGE}

## run
docker run --name ${IMAGE} --rm -p 8080:8080 --env "PROXY_HOST=classmethod.jp" -d ${IMAGE}
環境変数確認

Nginxの設定ファイルに指定したオリジンのFQDNが反映されている事を確認しました。

$ docker exec -it ${IMAGE} cat /etc/nginx/conf.d/default.conf | grep 'set $backend'
  set $backend classmethod.jp;
疎通確認

curlコマンドを利用した疎通確認を試みました。

$ curl -v localhost:8080/healthcheck.html
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /healthcheck.html HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.25.0
< Date: Tue, 06 Jun 2023 02:24:59 GMT
< Content-Type: text/html
< Content-Length: 16
< Last-Modified: Tue, 06 Jun 2023 02:16:17 GMT
< Connection: keep-alive
< ETag: "647e96f1-10"
< Accept-Ranges: bytes

$ curl -s localhost:8080/healthcheck.html
<html>ok</html>

$ curl -s localhost:8080/ | head -n 4
<!doctype html>
<html lang="ja">
<head>
  <script>

イメージPush

ECRにプライベートリポジトリを作成、「latest 」タグを付与したイメージを Pushしました。

ECRのURL要素に含まれるアカウントIDはSTSの「get-caller-identity」、 リージョンはメタデータ(IMDSv2)より取得しました

## Docker Image Name
IMAGE='docker-nginx'
## Account ID
ACCOUNT=`aws sts get-caller-identity | jq -r .Account`
## REGION
TOKEN_IMDSv2=`curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600"`
REGION=`curl -s -H "X-aws-ec2-metadata-token: $TOKEN_IMDSv2" http://169.254.169.254/latest/meta-data/placement/availability-zone | sed -e 's/.$//'`

## Create Repository
aws ecr create-repository --repository-name ${IMAGE} --region ${REGION}

## ECR URI
ECR_URI="${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${IMAGE}"
echo "ECR_URI: ${ECR_URI}"

docker tag ${IMAGE}:latest ${ECR_URI}
aws --region ${REGION} ecr get-login-password | docker login --username AWS --password-stdin "${ECR_URI}:latest"
docker push ${ECR_URI}

App Runner環境作成

設定ファイル作成

今回、CLIで App Runner 環境を作成を試みました。

Nginx設定用の環境変数や、スペック、ヘルスチェックで設定などを反映したJSONファイルを用意しました。

cat << EoL > app-runner.json
{
  "ServiceName": "nginx-app",
  "SourceConfiguration": {
    "ImageRepository": {
      "ImageIdentifier": "${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${IMAGE}:latest",
      "ImageRepositoryType": "ECR",
      "ImageConfiguration": {
        "RuntimeEnvironmentVariables": {
          "PROXY_HOST": "classmethod.jp"
        },
        "Port": "8080"
      }
    },
    "AutoDeploymentsEnabled": false,
    "AuthenticationConfiguration": {
      "AccessRoleArn": "arn:aws:iam::${ACCOUNT}:role/service-role/AppRunnerECRAccessRole"
    }
  },
  "InstanceConfiguration": {
    "Cpu": "0.25 vCPU",
    "Memory": "0.5 GB"
  },
  "HealthCheckConfiguration": {
    "Protocol": "HTTP",
    "Path": "/healthcheck.html",
    "Interval": 20,
    "Timeout": 2,
    "HealthyThreshold": 1,
    "UnhealthyThreshold": 5
  }
}
EoL

cat app-runner.json | jq .
$ aws apprunner create-service --cli-input-json file://app-runner.json
{
    "Service": {
        "ServiceName": "nginx-app",
        "ServiceId": "00000000000000000000000000000000",
        "ServiceArn": "arn:aws:apprunner:ap-northeast-1:000000000000:service/nginx-app/000000000000",
        "ServiceUrl": "0000000000.ap-northeast-1.awsapprunner.com",
        "CreatedAt": "2023-05-27T15:42:27.605000+00:00",
        "UpdatedAt": "2023-05-27T15:42:27.605000+00:00",
        "Status": "OPERATION_IN_PROGRESS",
        "SourceConfiguration": {
            "ImageRepository": {
                "ImageIdentifier": "000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/docker-nginx:latest",
                "ImageConfiguration": {
                    "RuntimeEnvironmentVariables": {
                        "PROXY_HOST": "classmethod.jp"
                    },
                    "Port": "8080"
                },
                "ImageRepositoryType": "ECR"
            },
            "AutoDeploymentsEnabled": false,
            "AuthenticationConfiguration": {
                "AccessRoleArn": "arn:aws:iam::000000000000:role/service-role/AppRunnerECRAccessRole"
            }
        },
        "InstanceConfiguration": {
            "Cpu": "256",
            "Memory": "1024"
        },
        "HealthCheckConfiguration": {
            "Protocol": "HTTP",
            "Path": "/healthcheck.html",
            "Interval": 20,
            "Timeout": 2,
            "HealthyThreshold": 1,
            "UnhealthyThreshold": 5
        },
        "AutoScalingConfigurationSummary": {
            "AutoScalingConfigurationArn": "arn:aws:apprunner:ap-northeast-1:000000000000:autoscalingconfiguration/DefaultConfiguration/1/000000000000",
            "AutoScalingConfigurationName": "DefaultConfiguration",
            "AutoScalingConfigurationRevision": 1
        },
        "NetworkConfiguration": {
            "EgressConfiguration": {
                "EgressType": "DEFAULT"
            },
            "IngressConfiguration": {
                "IsPubliclyAccessible": true
            }
        }
    },
    "OperationId": "000000000000"
}

動作確認

App RunnerのサービスURLを取得、curlコマンドで App Runner の リバースプロキシを経由して、オリジンのコンテンツを表示できる事を確認しました。

APP_URL="https://`aws apprunner list-services --output text --query 'ServiceSummaryList[?ServiceName==\`nginx-app\`].ServiceUrl'`"
echo ${APP_URL}
curl -s ${APP_URL}
  • 実行結果
$ curl -s ${APP_URL} | head -n 3
<!doctype html>
<html lang="ja">
<head>

利用例

App Runner

Node.jsのコンテナ実行環境 として App Runnerを利用する場合、 前段 に Nginxコンテナの App Runnerを配置する事で、Webサーバ経由で Node.jsの実行環境を公開する利用が可能です。

キャッシュ用ヘッダをNginxで設定、CloudFrontのキャッシュ制御などが可能となります。

Nginxでルーティング、特定パスを別サーバなどに転送するパスルーティングなどが可能になります。

アプリケーション実行環境のApp Runnerをプライベートアクセス用に設定し、 VPC Endpoint 経由で Nginx に公開する事で、Nginxを用いた認証利用も可能になります。

※VPCエンドポイントを利用する場合、AZ毎の維持、通信費用が発生する点にはご注意ください。

CloudFront切替

CloudFront、ディストリビューション間のCNAME重複が禁止されているため、 無停止でCloudFrontディストリビューションを交換する場合、AWSサポートへの依頼や、同一アカウントであれば AssociateAlias APIを利用する必要がありました。

CloudFront ディストリビューション のCNAME交換作業中、 Nginxを公開環境として利用する事で、任意の時間帯にサービスダウンの無いメンテナンスが可能となります。

ただし、CloudFrontの前段に Nginxを配置する利用は、CDNの配信性能としては大きく低下します。 Nginx が ボトルネックとならない事や、性能低下が許容できるレベルである事を事前に確認頂いた上で、 極力短期間の利用に留める事をお勧めします。

  • 移行前

  • 移行中

  • 移行後

まとめ

App Runner、CPU負荷が発生しない状態であれば、プロビジョニングされたコンテナインスタンス 0.007 USD/GBのメモリ費用に応じた費用で利用できます。

北米リージョンで、メモリ割り当て設定を最小の「0.5GB」とした場合、App Runnerの月額費用は 2.5ドル (0.007 USD x 0.5(GB) x 24(h) x 30(d)) の維持費と、CPU負荷に応じて発生する従量課金で利用できます。

Nginxの実行環境として、AWSではFargate(ECS)、EC2なども利用可能ですが、 HTTPS提供を必要とする場合、Let's Encrypt の設定や、ELBを利用する場合そのコストが発生します。

シンプルな要件、スモールスタートで Nginxを利用する場合、App Runner もお試しください。