本日も乙

ただの自己満足な備忘録。

CloudFront + nginx(http_image_filter_module) + S3 を使って画像変換サーバを構築する

珍しく(初めて?)AWSネタです。
仕事でAWSVPCを構築しているのですが、画像を取り扱うサーバが欲しいとの要望がありました。
画像を表示するだけならCloudFront+S3だけで強力な画像(CDN)サーバを構築することができるのですが、今回は画像サイズもリアルタイムで変換して表示してほしいとのことでした。

幸いにも社内で以前、nginxのhttp_image_filter_moduleを使って画像変換している実例(実サーバ)があるのでそれを参考に構築してみました。

構成図

簡単な図です。

diagram

今回やりたかったこと

  • ユーザが独自ドメインマッピングされたCloudFrontにアクセスすると、CloudFrontに画像がキャッシュされていればその画像を返します
  • もし画像がキャッシュされていなければELB・EC2インスタンス経由でS3にある画像を取得します
  • EC2インスタンスhttp_image_filter_moduleが組み込まれたnginxがインストールされており、画像サイズをQueryString(width, heightなど)で指定すると、S3に保存している画像をリサイズして返します

なぜ、ELBを設置しているかと、以下の2つの理由があります。

  1. 画像変換サーバ(EC2インスタンス)の負荷対策
    • CloudFrontに画像がキャッシュされていれば頻繁にアクセスされず負荷が上がることがないと思いますが、最低限の負荷対策はしておきたいです。ELB配下に置いておくことでAutoScalingにも対応できます。
  2. 画像変換サーバ(EC2インスタンス)をPrivateSubnetに置きたかった
    • セキュリティ的観点からEC2インスタンスをPublicSubnet(RouteTablesでInternet Gatewayに出ていけるように設定しているSubnet)に置くことで外部から攻撃されるのを考慮しました
    • 外部からアクセスできる入り口をELBのみにし、EC2インスタンスはELBからのアクセス(http,https)のみを許容することで安全にサーバを運用することができると考えています

環境

前提条件

  • VPC,Subnet,SecurityGroup,NATインスタンスなどの作成・設定は省略しています
  • aws-cliの初期設定も省略しています

VPC内にELBを作成・設定する

aws-cliを使ってみます。

$ aws elb create-load-balancer --load-balancer-name elb-image-filter \
--listeners Protocol=HTTP,LoadBalancerPort=80,InstanceProtocol=HTTP,InstancePort=80 \
--subnets subnet-xxxxxxxxx \
--security-groups sg-xxxxxxxxx \
--output text --query 'DNSName'

elb-image-filterという名前で作成しました。SubnetはPublicSubnetを指定します。

次にELBのHealthCheckを設定します。

$ aws elb configure-health-check --load-balancer-name elb-image-filter \
--health-check Target=TCP:80,Interval=30,UnhealthyThreshold=2,HealthyThreshold=2,Timeout=5

IntervalHealthyThresholdなどはお好みで設定してください。

VPC内にEC2インスタンスを作成する

こちらもaws-cliを使って作成しました。

$ aws ec2 run-instances --image-id ami-xxxxxxxx \
--key-name xxxxxxxx \
--security-group-ids sg-xxxxxxxx \
--instance-type t2.micro \
--subnet-id subnet-xxxxxxxx \
--count 1 \
--output text --query 'Instances[].InstanceId'

作成したEC2インスタンスにNameタグを付けます。

$ aws ec2 create-tags --resources i-xxxxxxxx --tags Key=Name,Value=vpc-image-filter

先ほど作成したEC2インスタンス(i-xxxxxxxx)にvpc-image-filterというNameタグを付けました。

ELB配下にEC2インスタンスを追加します。

$ aws elb register-instances-with-load-balancer --load-balancer-name elb-image-filter --instances i-xxxxxxxx

この時点ではまだnginxをインストールしていない(80ポートは開いていない)ため、ELBのHealthCheckに失敗(OutOfService)しています。

EC2インスタンスにnginxをインストール・設定する

インストール方法は、nginx(1.4.7)をソースからインストールするを参照してほしいのですが、今回インストールするときの相違点を以下に挙げます。

  • nginxのバージョンについて
    • 何でも良いのですが、現時点(2014/08/18)でStableである1.6.1でインストールします
  • パッケージのインストールについて
    • http_image_filter_moduleをインストールするには、gdが必要なので予めインストールしておきます。
$ sudo yum install gd gd-devel
  • configureのオプションについて
    • http_image_filter_moduleをインストールするには、configureのオプションに--with-http_image_filter_moduleを付ける必要があります
$ ./configure --with-http_image_filter_module

nginxをインストールしたら起動します。

$ sudo service nginx start

ELBのHealthCheckがInServiceになるのを待ちます。先ほど設定したHealthCheckだと約1分程度でInServiceになるはずです。

$ aws elb describe-instance-health --load-balancer-name vpc-image-filter --output text --query 'InstanceStates[]'
N/A     i-xxxxxxxx      N/A     InService

もしずっと待ってもOutOfServiceになっていたら、VPCのネットワーク設定(NetworkACL,SecurityGroup)やnginx,ELBの設定を見直してください。

InServiceになったら、ブラウザからELBのDNSName(http://elb-image-filter-xxxxxxxx.ap-northeast-1.elb.amazonaws.com/)を確認して、nginxのページ(Welcome to nginx!)が表示されることを確認してください。

S3にバケット作成&画像アップロードする

S3のバケット作成と画像アップロードはManagementConsoleから行いました。
バケット名はimage-filer、画像ファイル名はtest.jpgにしました。

画像をアップロードしてもそのままではブラウザから確認できないため、パーミッションを変更します。
アップグレードした画像に対して、下図のようにパーミッションを設定します。GranteeEveryoneを選択し、Open/Downloadにチェックを入れてください。

s3_permission

https://s3-ap-northeast-1.amazonaws.com/image-filter/test.jpgか、https://image-filter.s3-ap-northeast-1.amazonaws.com/test.jpg にアクセスして画像が表示されればOKです。

S3にある画像のサイズを変換させて表示させる

ここでようやく、nginxのhttp_image_filter_moduleを使った設定を行います。
/etc/nginx/nginx.conf に記述していますが、別ファイルに記述してIncludeしても良いです。
設定内容は、簡単!リアルタイム画像変換をNginxだけで行う方法 | cloudrop を参考にしました。

user  nginx nginx;
worker_processes  2;

error_log  /var/log/nginx/error.log debug;

pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    set_real_ip_from 10.0.0.0/16;
    real_ip_header X-Forwarded-For;
    real_ip_recursive off;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    gzip on;
    merge_slashes on;

    gzip_static off;

    server_tokens off;

    server {
        listen 80;
        server_name localhost;
        root /etc/nginx/html;

        access_log /var/log/nginx/image-filter-access.log main;
        error_log  /var/log/nginx/image-filter-error_log;

        resolver 8.8.8.8 valid=30s;

        image_filter_buffer 5M;

        location ~ /(.*\.jpg|png|gif)$ {
            set $s3host s3-ap-northeast-1.amazonaws.com/image-filter;
            set $file $1;
            set $width 150;
            set $height 150;
            set $quality 100;

            if ($query_string !~ .*=.*) {
                rewrite ^ /s3_original last;
            }

            if ($arg_width ~ (\d*)) {
                set $width $1;
            }

            if ($arg_height ~ (\d*)) {
                set $height $1;
            }

            if ($arg_quality = (100|[1-9][0-9]|[1-9])) {
                set $quality $1;
            }

            if ($arg_type = "resize") {
                rewrite ^ /s3_resize last;
            }

            rewrite ^ /s3_crop last;
        }

        location /s3_original {
            internal;
            proxy_pass https://$s3host/$file;
        }

        location /s3_resize {
            internal;
            proxy_pass https://$s3host/$file;
            image_filter resize $width $height;
            image_filter_jpeg_quality $quality;
            error_page 415 = @empty;
        }

        location /s3_crop {
            internal;
            proxy_pass https://$s3host/$file;
            image_filter crop $width $height;
            image_filter_jpeg_quality $quality;
            error_page 415 = @empty;
        }

        location @empty {
            empty_gif;
        }
    }
}

複雑そうな設定に見えますが、画像サイズなどのパラメータにしたがって、nginx -> S3へのリバースプロキシを設定しているだけです。

nginxを再起動(ORリロード)します

$ sudo service nginx reload

http://elb-image-filter-xxxxxxxx.ap-northeast-1.elb.amazonaws.com/test.jpg?width=300&height=500&type=resize などheight, weightのパラメータ値を設定し、ブラウザからアクセスしてリサイズされた画像を表示されればOKです。

CloudFrontの設定を行う

CloudFrontのCDN作成は、ManagementConsoleで、Create DistributionsからCloudFrontを作成します。
下図のように、Origin Domain Nameには、ELBのDNSNameを選択してください。OriginIDは自動的に入力されるので変更しなくても良いです。

create_distribution

Default Cache Behavior Settingsでは、Forward Query StringsをYesに設定するだけであとはデフォルト設定で良いです。
Distribution Settingsにある、Alternate Domain Names(CNAMEs)は後で設定します。

default_cache_behaivor_settings

設定するとCloudFront Distributionsの一覧に追加されますのでStatusDeployedになるまで待ちます。

Deployedになったら、ブラウザで、http://xxxxxxxxxxxxxx.cloudfront.net/test.jpg?width=300&height=500&type=resize にアクセスして画像が表示されればOKです。
xxxxxxxxxxxxx.cloudfront.net は作成されたCloudFrontのDomanNameです。

画像変換サーバのnginxのアクセスログを確認すると、初回アクセスのみアクセスされ、同じリクエストをするとCloudFront側でキャッシュされるため画像変換サーバにアクセスが来なくなることが分かります。異なる画像サイズを指定する度に画像変換サーバにアクセスされますが、CloudFront側にもリサイズされた画像がキャッシュされます。

独自ドメインの設定

ここまでで画像変換サーバの構築は終わっているのですが、URLがhttp://xxxxxxxxxxxxxx.cloudfront.net〜のようになっていますので、独自ドメインで画像を表示できるようにします。
ドメインimg.example.comとして設定します。

独自ドメインの設定には、CloudFrontとRoute53の設定が必要です。また、必要に応じてnginxの設定も変更します。

CloudFrontの設定

下図のようにAlternate Domain Names(CNAMEs)独自ドメインを設定します。

cloudfront_cname

設定完了後、CloudFront DistributionsにてCNAMEaの列にドメインが表示されればOKです。

cloudfront_list

Route53の設定

下図のようにALIASレコードを設定します。

route53_alias

CNAMEレコードでも良いのですが、ALIASレコードの方が余計なリクエストをしなくて済むみたいです。
参考:Amazon Route 53のALIASレコード利用のススメ

設定が完了したら、digコマンドでDNSレコードを確認してみます。

$ dig img.example.com

; <<>> DiG 9.8.3-P1 <<>> img.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 30409
;; flags: qr rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 0

;; QUESTION SECTION:
;img.example.com. IN   A

;; ANSWER SECTION:
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx
img.example.com. 59 IN A       54.230.xxx.xxx

;; Query time: 112 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Thu Aug 14 19:15:53 2014
;; MSG SIZE  rcvd: 178

複数IPアドレスが返ってくるようになるはずです。

nginxの設定

ServerNameを変更します。localhsot(デフォルトサーバ)でも良いのですが、複数のVirtualHostを設定したい場合はServerNameをELBのDNS名に変更します。
また、ELBのDNS名が長すぎてnginx: [emerg] could not build the server_names_hash, you should increase server_names_hash_bucket_size: 64というエラーが出るため、server_names_hash_bucket_size 128;というパラメータを設定しています。

下記がnginxの設定ファイルの最終版です。

user  nginx nginx;
worker_processes  2;

error_log  /var/log/nginx/error.log debug;

pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    set_real_ip_from 10.0.0.0/16;
    real_ip_header X-Forwarded-For;
    real_ip_recursive off;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    gzip on;
    merge_slashes on;

    gzip_static off;

    server_tokens off;

    server_names_hash_bucket_size  128;

    server {
        listen 80;
        server_name  elb-image-filter-xxxxxxxx.ap-northeast-1.elb.amazonaws.com;
        root /etc/nginx/html;

        access_log /var/log/nginx/image-filter-access.log main;
        error_log  /var/log/nginx/image-filter-error_log;

        resolver 8.8.8.8 valid=30s;

        image_filter_buffer 5M;

        location ~ /(.*\.jpg|png|gif)$ {
            set $s3host s3-ap-northeast-1.amazonaws.com/image-filter;
            set $file $1;
            set $width 150;
            set $height 150;
            set $quality 100;

            if ($query_string !~ .*=.*) {
                rewrite ^ /s3_original last;
            }

            if ($arg_width ~ (\d*)) {
                set $width $1;
            }

            if ($arg_height ~ (\d*)) {
                set $height $1;
            }

            if ($arg_quality = (100|[1-9][0-9]|[1-9])) {
                set $quality $1;
            }

            if ($arg_type = "resize") {
                rewrite ^ /s3_resize last;
            }

            rewrite ^ /s3_crop last;
        }

        location /s3_original {
            internal;
            proxy_pass https://$s3host/$file;
        }

        location /s3_resize {
            internal;
            proxy_pass https://$s3host/$file;
            image_filter resize $width $height;
            image_filter_jpeg_quality $quality;
            error_page 415 = @empty;
        }

        location /s3_crop {
            internal;
            proxy_pass https://$s3host/$file;
            image_filter crop $width $height;
            image_filter_jpeg_quality $quality;
            error_page 415 = @empty;
        }

        location @empty {
            empty_gif;
        }
    }
}

nginxを再起動(ORリロード)します。

$ sudo service nginx reload

全て設定完了後、ブラウザから http://img.example.com/test.jpg?width=500&height=650&type=resize にアクセスして画像が表示されることを確認してください。

もし画像が表示されない場合は

キャッシュを削除すると表示されることがあります。

  • PCのDNSキャッシュ

Mac OS Xの場合

$ sudo killall -HUP mDNSResponder
  • ブラウザのキャッシュ
    • スーパーリロードする

最後に

CloudFrontと組み合わせた画像変換サーバを構築してみました。