長生村本郷Engineers'Blog

千葉県長生村本郷育ちのエンジニアが書いているブログ

ffmpeg + Nginx + RTMP で 音声付き動画配信 on RaspberryPI

f:id:kenzo0107:20180815181451p:plain

概要

RaspberryPI 上で rtmp モジュール付きの nginx をビルドし
WebCamera で撮影した 動画+音声付き を HLS 配信する際の手順をまとめました。

経緯

はじめは外出中にペットのうさぎ用に mjpeg-streamer でモニターしていました。
子供が生まれると、子供が元気にしてるかな、とふとモニターするようになりました。

ですが、うさぎは鳴きませんが、子供は泣き叫びます。
mjpeg-streamer では表情こそわかりますが、我が子の声が聞こえてきません。

その為、動画+音声付きで低負荷で 動画+音声 配信ができないものかと探していた際に ffmpeg に出会いました。*1

購入したもの

LOGICOOL HDウェブカム フルHD動画対応 C615

LOGICOOL HDウェブカム フルHD動画対応 C615

Nginx

http://nginx.org/en/download.html で現時点で最新バージョンをダウンロードしました。

各種ダウンロード

以下ダウンロードしています。
nginx-http-auth-digest は Nginx で Digest 認証すべくモジュール追加しました。

  • Nginx
  • nginx-rtmp-module
  • openssl
  • nginx-http-auth-digest
  • ffmpeg
// home ディレクトリで作業するとします。
pi$ cd ~

// nginx
pi$ wget http://nginx.org/download/nginx-1.15.2.tar.gz

// nginx-rtmp-module
pi$ wget -O rtmp.zip https://github.com/arut/nginx-rtmp-module/archive/master.zip
pi$ wget -O ssl.zip https://github.com/openssl/openssl/archive/master.zip

// nginx-http-auth-digest
pi$ git clone https://github.com/samizdatco/nginx-http-auth-digest.git
pi$ cd nginx-http-auth-digest
pi$ git clone https://gist.github.com/frah/3921741
pi$ patch -u < 3921741/patch-ngx_http_auth_digest_module.diff

// ffmpeg
pi$ git clone git://source.ffmpeg.org/ffmpeg.git
pi$ wget ftp://ftp.alsa-project.org/pub/lib/alsa-lib-1.1.6.tar.bz2

解凍

pi$ tar xvzf nginx-1.15.2.tar.gz
pi$ unzip rtmp.zip
pi$ unzip ssl.zip
pi$ tar xjvf alsa-lib-1.1.6.tar.bz2

Nginx ビルド

pi$ cd nginx-1.15.2/
pi$ sudo ./configure --with-http_ssl_module --with-http_realip_module --add-module=../nginx-rtmp-module-master --with-openssl=../openssl-master --add-module=../nginx-http-auth-digest
pi$ sudo make
pi$ sudo make install

Nginx Version 確認

pi$ /usr/local/nginx/sbin/nginx -V

nginx version: nginx/1.15.2
built by gcc 4.9.2 (Raspbian 4.9.2-10+deb8u1)
built with OpenSSL 1.1.1-pre9-dev  xx XXX xxxx
TLS SNI support enabled
configure arguments: --with-http_ssl_module --with-http_realip_module --add-module=../nginx-rtmp-module-master --with-openssl=../openssl-master --add-module=../nginx-http-auth-digest

Nginx をシンボリックリンクでパスが通っている場所から参照できるようにする。

pi$ sudo ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx

pi$ which nginx

ffmpeg ビルド

pi$ sudo apt-get install libomxil-bellagio-dev

pi$ cd alsa-lib-1.1.6
pi$ ./configure --prefix=/home/pi/ffmpeg
pi$ sudo make
pi$ sudo make install

pi$ cd /home/pi/ffmpeg
pi$ sudo ./configure  --enable-gpl  --enable-nonfree --enable-mmal --enable-omx-rpi --enable-omx --extra-cflags="-I/home/pi/ffmpeg/include" --extra-ldflags="-L/home/pi/ffmpeg/lib" --extra-libs=-ldl
pi$ sudo make -j4
pi4 sudo make install

sudo apt-get install libomxil-bellagio-dev を実行していない場合に以下のエラーが出ました。

ERROR: OMX_Core.h not found

録音してみる

動画撮影デバイス一覧確認

pi$ v4l2-ctl --list-device

HD Webcam C615 (usb-3f980000.usb-1.3):
        /dev/video0

上記コマンド実行時に以下のようなエラーが出る時は、

Failed to open /dev/video0: No such file or directory

以下コマンドを試してください。

pi$ sudo pkill /dev/video0

音声入力デバイス一覧確認

自分の場合は
カード 1 は WebCam
カード 2 は マイク、
です。

pi$ arecord -l

**** ハードウェアデバイス CAPTURE のリスト ****
カード 1: C615 [HD Webcam C615], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0
カード 2: Device [USB PnP Sound Device], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

いざ録音

カード 2 が入力デバイスである為、 hw:2 としました。

pi$ ffmpeg -f alsa -ac 1 -i hw:2 -f v4l2 -s 640x480 -i /dev/video0 output.mpg

生成された output.mpg ファイルを mac 上にダウンロードし再生を試してみてください。

これで再生されれば、ffmpeg が問題なく動作していることを確認できたことになります。

次は配信する為の設定です。

Nginx 設定

設定ファイル格納用ディレクトリ作成

pi$ sudo mkdir -p /usr/local/nginx/conf.d

HLS ファイル生成用ディレクトリ作成

pi$ sudo mkdir -p /var/www/html/live/hls

HLS 配信用 index.html

  • hls.min.js 取得
pi$ cd /var/www/html
pi$ wget https://cdn.jsdelivr.net/hls.js/latest/hls.min.js
  • /var/www/html/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8"/>
  <script src="./hls.min.js"></script>
</head>

<body>
  <video id="video"></video>
  <script>
    if(Hls.isSupported()) {
      var video = document.getElementById('video');
      var hls = new Hls();
      hls.loadSource('/live/hls/stream.m3u8');
      hls.attachMedia(video);
      hls.on(Hls.Events.MANIFEST_PARSED,function() {
      video.play();
    });
   }
  </script>
</body>
</html>

Digest 認証設定

pi$ cd /var/www
pi$ sudo htdigest -c .htdigest 'digest AuthNginx' hoge
password: <enter password>

各種設定ファイル配置

  • /usr/local/nginx/conf.d/default.conf
server {
    listen 8090;
    proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
    access_log /var/log/nginx/access.log combined;
    error_log /var/log/nginx/error.log warn;

    location = /favicon.ico {
        access_log off;
        empty_gif;
        expires 30d;
    }

    location / {
        auth_digest "digest AuthNginx";
        auth_digest_user_file /var/www/.htdigest;

        root /var/www/html;
        index index.html;
        set_real_ip_from    127.0.0.1;
        real_ip_header      X-Forwarded-For;
    }
}
  • /usr/local/nginx/conf.d/rtmp
rtmp {
    server {
        listen 1935;
        chunk_size 4096;
        allow play all;
        access_log /var/log/nginx/rtmp_access.log;

        application live {
            live on;
            hls on;
            record off;
            hls_path /var/www/html/live/hls;
            hls_fragment 1s;
            hls_type live;
        }
    }
}
  • /usr/local/nginx/conf/nginx.conf
user  www-data;
worker_processes  1;

pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include           mime.types;
    default_type      application/octet-stream;
    sendfile          on;
    keepalive_timeout 65;
    include /usr/local/nginx/conf.d/*.conf;
}

include /usr/local/nginx/conf.d/rtmp;

Nginx 起動設定ファイル

  • /lib/systemd/system/nginx.service
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Nginx 起動

pi$ sudo systemctl daemon-reload
pi$ sudo systemctl start nginx
pi$ sudo systemctl status nginx

この時はまだ HLS ファイルが生成されていませんので
http://<RaspberryPI IP>:8090 にアクセスしても HLS 配信されていません。

f:id:kenzo0107:20180815172215p:plain

ffmpeg を起動することで /var/www/html/live/hls/ ディレクトリ以下に stream.m3u8 が生成されます。

ffmpeg 起動

pi$ sudo ffmpeg \
-f alsa -ac 1 -thread_queue_size 8192 -i hw:2 \
-f v4l2 -thread_queue_size 8192 -input_format yuyv422 -video_size 432x240 -framerate 30 -i /dev/video0 \
-c:v h264_omx -b:v 768k -bufsize 768k -vsync 1 -g 16  \
-c:a aac -b:a 128k -ar 44100 \
-af "volume=30dB" \
-f flv rtmp://localhost/live/stream;

アクセスしてみる

http://<RaspberryPI IP>:8090 にアクセスしてみます。 Digest 認証を問われるので設定した ID/PW を入力します。

f:id:kenzo0107:20180815172119p:plain

HLS 配信されていることを確認できました!

f:id:kenzo0107:20180815173337p:plain

負荷状況としては CPU 25 - 30% 程度に収まっています。

まとめ

ffmpeg + Nginx + RTMP で HLS 配信を RaspberryPI 上に構築できました。

実際の運用では、常時起動してはおらず、見たいときだけ起動するような仕組みにしており、負荷は極力抑えています。 監視は Mackerel の無料プランで今のところ十分です。

育児ハックの一環として参考になれば何よりです。

*1:勿論のことですが、家族の了承を得た上で設定しています。

Flask+Service Worker on Heroku で PWA チュートリアル

f:id:kenzo0107:20180814131355p:plain

概要

自分にとっては dev.to でバズった Service Worker。 その概要と機能性をなぞってみようとチュートリアル的に学んだ内容をまとめました。

掲題の通り、Flask + Service Worker を Heroku で動作させ、PWA(Progressive Web Apps) してみました。

lit-wildwood-62785.herokuapp.com

経緯

以前、Python の軽量 FW、Flask で Web アプリケーションを作りました。

これまでは Docker コンテナをデプロイ出来るプラットフォームを試す為に以下試してました。

上記プラットフォームでは、月数百円程度ですが、費用が発生します。

2018年8月7日、Heroku の free プランが 月 1500 時間無料とあったので アプリ×2 つ動かしても無料でいける!ということで Heroku にしてみました。

sleep 対策として以下を参照しました。 casualdevelopers.com

さらなる sleep 対策として Service Worker があればオフラインでもサービス動作させられるし、sleep し続けてもいいのでは?と思い、導入してみました。

Flask に Service Worker 導入

ソースは git にあります。

github.com

簡単に導入時のポイント

  • app.py というメインスクリプト/sw.js へのアクセスできるようにします。
@app.route('/sw.js', methods=['GET'])
def sw():
    return app.send_static_file('sw.js')

基本、上記 2 step をしてから Service Worker の各処理を実装していきます。

Install

以下の install イベントでは、指定したキャッシュさせたいファイルパスを全てキャッシュさせています。 挙動のイメージとしては、トップページにアクセスした際に Service Worker がブラウザに導入(install)されるイベントの発生時にキャッシュを生成しています。

var urlsToCache = [
  '/',
  '/static/img/favicon.ico',
  '/static/img/logo.png',
  '/static/css/bootstrap.min.css',
  '/static/css/flickity.org.css',
  '/static/js/async_set_circuit.js',
  '/static/js/bootstrap.min.js',
  '/static/js/flickity.pkgd.min.js',
  '/static/js/jquery-3.1.0.min.js',
  '/static/js/jquery.countdown.min.js',
  '/static/js/superagent.js',
  '/static/js/tether.min.js'
]

self.addEventListener('install', event => {
  console.log('install')
  event.waitUntil(
    caches.open(cacheName)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache)
      })
  )
})

Chrome > Developer Tool > Application > Cache Storage を見るとキャッシュされているのがわかります。

f:id:kenzo0107:20180814123700p:plain

fetch

以下処理は、fetch イベントでブラウザでキャッシュしたファイルを呼び出しています。

self.addEventListener('fetch', function(event) {
  console.log('fetch')
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        if (response) {
          return response;
        }
        return fetch(event.request)
      }
    )
  );
});

activate

Service Worker は active 状態になってもすぐにブラウザ上のリソースを操作できず、 もう一度ページにアクセスした際にできるようになっています。

その為、一度しかアクセスしないユーザにとっては Service Worker によるパフォーマンスの向上を体験できないことになります。

その為、 以下 activate イベントによって直ちに操作できるようにします。

self.addEventListener('activate', event => {
  console.log('activate')
  event.waitUntil(self.clients.claim());
})

基本、以上の設定で Service Worker 導入完了でした。

前後を比較すると Waterfall で見る、リソースのロードタイムがキュッと縮んでいるのがわかります。

Before f:id:kenzo0107:20180814124810p:plain

After f:id:kenzo0107:20180814124826p:plain

まとめ

Service Worker で一度キャッシュさせた後はオフラインでも動作するような仕組みが作れました。 オフラインでも動作する、というのは魅力的♪

ただし、クエリパラメータのパターンの多い URL がある場合などは キャッシュされにくく、この場合のキャッシュ戦略としては、ひとまず静的ファイルのみキャッシュするなどで対応するのが良いのか、 等考えさせられるところがありました。

例)

/ts?circuit_id=1&station_id=1 はアクセスしたけど  
/ts?circuit_id=1&station_id=2 はアクセスしてない  
という場合はオフラインにしたら /ts?circuit_id=1&station_id=2 は閲覧できなくなる

また、POST method は Service Worker は未対応で issue が上がっているようです。 Workaround として以下提案がされているブログがありましたが、実装が複雑で、まだこの辺りは開発の余地がある印象です。

medium.com

以下 Service Worker 導入時の苦労した点があり、涙無くして見られない内容でした。 日経電子版 サイト高速化とPWA対応

他趣味アプリで Workbox を利用していますが、 こちらも書いていきたいと思います。

参考

stackoverflow.com app.codegrid.net

超速! Webページ速度改善ガイド ── 使いやすさは「速さ」から始まる (WEB+DB PRESS plus)

超速! Webページ速度改善ガイド ── 使いやすさは「速さ」から始まる (WEB+DB PRESS plus)

Datadog NTP 監視でアラート鳴りまくり対応

f:id:kenzo0107:20180730133759j:plain

概要

サーバ時刻の監視を Datadog で実施する際、標準時刻の参照先が異なることで 不要なアラートが発生する事象がありました。

Datadog はデフォルトで pool.ntp.org を参照しています。

AWS EC2 に設定した Chrony ではデフォルトで ntp.nict.jp を参照する様にしていた為、ある日突然アラートがなりまくる事象がありました。

この対策として、 Datadog と Chrony の参照先を統一して管理する様に設定しました。

タイムサーバホストを統一する

今回は、AWS を利用しており、 AWS にも NTP サーバがある為、そちらを参照することとしました。

AWS Time Sync Service のホストは 169.254.169.123 です。

169.254.169.123 のリンクローカル IP アドレスを介してアクセス可能な為、プライベートサブネットからでもアクセス可能です。 ip アドレスという辺りがある日変更されたとかあると辛いので怖いですが、今の所、そういうことはないです。

init_config:

instances:
  - offset_threshold: 60
    host: 169.254.169.123 # 追加
# server ntp.nict.jp minpoll 4 maxpoll 4  # コメントアウト
server 169.254.169.123 prefer iburst # 追加

上記設定後、リスタート

$ sudo systemctl restart chrony
$ sudo systemctl restart datadog-agent

上記によりアラート解消されました。

参照

Docker 不要リソースお掃除 compose

f:id:kenzo0107:20180711135636p:plain

概要

ECS EC2 で一部コンテナが 起動開始→失敗→起動開始→失敗 を繰り返し
サーバが容量不足 no space left に陥る事象がありました。

その時の対応をまとめました。

docker 不要リソース削除

docker 不要リソース削除処理コマンドは以下の様なものを実行します。

// コンテナ削除
$ docker ps -aq | xargs docker rm

// イメージ削除
$ docker images -aq | xargs docker rmi

// タグ無しイメージ一括削除
$ docker volume ls -qf dangling=true | xargs docker volume rm

dangling ... ぶら下がる、 ぶらぶら揺れる

Spotify のお掃除イメージを使う

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /etc:/etc spotify/docker-gc
$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker:/var/lib/docker martin/docker-cleanup-volumes

docker-compose.yml にしてみる

docker-compose.yml フォーマットにすることでイメージ管理が容易になります。

version: '2'

services:
  docker-gc:
    image: spotify/docker-gc
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /etc:/etc

  docker-cleanup-volumes:
    image: martin/docker-cleanup-volumes
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker:/var/lib/docker

ECS EC2 で one-off Container 実行してお掃除

ecs-cli compose \
        --debug \
        --project-name <project name> \
        --file docker-compose.yml \
    run \
        --cluster <cluster name> \ 
        --region ap-northeast-1 \
        --aws-profile <profile>

ToDo

初めて利用する Public Container に対しての脆弱性をチェックする仕組みを導入する必要があるかと思います。
この辺りまとめてまた執筆したいと思います。

ElastiCache メンテナンス対応 ~2018年梅雨~

2018年6月頃 AWS ElastiCache のメンテナンス通知が大量発生した時の備忘録です。

メンテ時に参照したリンク

Redis 用 Amazon ElastiCache

基本、ノードのリプレイスが必要です。

以下、手動実行する時の手順です。

node = 1 の場合

スタンドアローンな ElastiCache の場合、

  1. バックアップをとる
  2. リードレプリカを追加
  3. レプリカを昇格しプライマリにする
  4. レプリカとなった元ノードを削除

これでノードのリプレイスが完了しました。

node = N (>=2)

複数ノードある場合、

  1. バックアップをとる
  2. フェールオーバー実施

AWS Support 曰く、

フェールオーバー API は障害をシミュレートするので、フェイルオーバー後にノードの置き換えも行われます。

とのこと

メンテナンス完了の確認方法

正確にステータス確認するにはサポートに確認する以外はないかなと思います。

イベントには操作ログが残りメンテが実施されたというログは一切残りません。

また、アラートもすぐさま消えません。正確にはわかりませんが、数時間程度経過したら消えていました。

手動実行せず放っておいた場合

メンテナンスはメンテナンスウィンドウで指定した時間帯に実施されました。

ちなみに、放っておいた時のイベントログは以下のようになっていました。

n = 1 f:id:kenzo0107:20180624220417p:plain

n = 2 f:id:kenzo0107:20180624220358p:plain

n= 2 でフェールオーバーが自動実行されているのがわかります。

メンテナンス自体の経過時間はデータ量にもよるので一概には言えませんので
本番前に一度リハーサルして概算とっておくのが良いかなと思います。

以上 ご参考になれば幸いです。

ecs-cli バージョン指定してインストール

完全な備忘録です。

経緯

6月に入って数日、
ecs-cli の latest をインストールすると latest が 1.6.0 となり
ecs-cli compose ... を実行すると以下のようなエラーが出るようになりました。

level=error msg="Unable to open ECS Compose Project" error="Volume driver is not supported"

1.4.0 では問題なかったタスク定義でしたが
1.6.0 では Volume driver is not supported となったそうで処理がこけるようになりました。

その対応として 1.4.0 にバージョン固定した設定です。

結論

latest の部分を <version> に変更することで指定のバージョンでインストールが可能です。

curl -s https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-linux-amd64-v1.4.0 -o /usr/bin/ecs-cli && chmod +x /usr/bin/ecs-cli

AWS Documentation では

最新の ecs-cli のみの説明の為、若干ハマりました。

docs.aws.amazon.com

GitHub には Download specific version とあるのでこちらの情報を追っておくと良かったです。

github.com

以上 本当に備忘録でした。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

Rails × Redis でスレッドセーフなアクセス数ランキング実装

f:id:kenzo0107:20180606115540j:plain

概要

メディアサイトで記事ページへアクセス数ランキングを実装しました。

  • Rails 5.1
  • Redis (AWS ElastiCache 3.2.10)

その際にマルチスレッド環境を考慮してスレッドセーフな実装を心がけました。

スレッドセーフとは

スレッドセーフとは複数のスレッドが同時並行的に実行しても問題が発生しないことを意味します。 スレッドセーフでない場合は、あるスレッドで変更した共有データが、他のスレッドによって上書きされてしまう可能性があります。

Webサーバーやデータベースなどのサーバー用ソフトウェアは、マルチスレッド(マルチプロセス)で動作しているので、サーバー向けアプリケーションを開発するときは、マルチスレッドで動作するように実装することが望ましいです。

参照

スレッドセーフ - Wikipedia

JavaのThreadLocalとスレッドセーフについて

仕様

メディアサイトで記事詳細ページへアクセスした際に その記事 ID に対して閲覧数を +1 インクリメントします。

そして、 その閲覧数 TOP 10 のランキングを表示する、 というものです。

その際の Rails, Redis の設定についてまとめました。

実装方法検討

config/initializers/redis.rb*1 で Redis の初期設定の実装方法を検討しました。

global 変数として設定

require 'redis'

REDIS = Redis.new(host: host, port: port)

上記の場合、 グローバルで Redis クライアントを持っており マルチスレッド環境では、複数のスレッドが上書きされる可能性があります。

Thread.current

require 'redis'

def redis
  Thread.current[:redis] ||= Redis.new(host: host, port: port)
end

現在実行中のスレッドを取得しスレッド毎のデータを担保します。

が、以下 2 点の問題があります。

  1. 他人が上書いてしまう
  2. 構造化されていない

ActiveSupport::PerThreadRegistry

require 'redis'

class RedisRegistry
  extend ActiveSupport::PerThreadRegistry
  attr_accessor :redis, :current_permissions
end

def redis
  RedisRegistry.redis ||= Redis.new(host: host, port: port)
end

redis をスレッドローカル変数として定義し、そのアクセスをカプセル化し上書きされるのを防止しています。

ですが、 Rails 5.2 で deprecated となっておりました (TへT)

thread_mattr_accessor

以下を見てみると thread_mattr_accessor の挙動が Fix していました。 Fix thread_mattr_accessor share variable superclass with subclass

thread_mattr_accessor を利用して書き換えます。

  • config/initializers/redis.rb
require 'redis'

class RedisRegistry
  thread_mattr_accessor :redis
end

def redis
  RedisRegistry.redis ||= Redis.new(host: host, port: port)
end

アクセス数インクリメント

rescue 設定は Redis に接続できなくなった場合でもサイト自体が落ちることはなく、ランキングだけが表示されなくなる様にする為に設定しました。

    def increment_access_count(id)
      redis.zincrby "entries/daily/#{Time.zone.today}", 1, id
    rescue SocketError => e
      logger.error e
    end

アクセスランキング取得

Redis の zrevrangebyscore によりスコアの大きい順に 10 個、ID を取得します。 もし取得できなかった場合、 [] を返します。 decorate で体良く整形して View に渡します。 *2

    def access_ranking
      limit = 10
      ids = redis.zrevrangebyscore "entries/daily/#{Time.zone.today}", '+inf', 0, limit: [0, limit]
      if ids.any?
        where(id: ids).order(['field(id, ?)', ids]).limit(limit).decorate
      else
        []
      end
    rescue SocketError => e
      logger.error e
      []
    end

以上です。

Redis入門 インメモリKVSによる高速データ管理

Redis入門 インメモリKVSによる高速データ管理

*1:host, port は secrets.yml なり ENV で設定してください

*2:decorate のコードは省略してます