きっかけ
もう20年も前だろうか。あの頃はみんなホームページというものを持っていた。htmlを手打ちする者もいれば、ガッツリCGIな人もいて、中身は日記だったり(ああ、出始めた頃はブログといったな)、研究成果だったり、お気持ちの表明だったり、それはそれはいろんなページがあったものだ。
かくゆうワイ将も、固定IPとドメインとSSL証明書をとって20年ぐらいサイトを運営(というほど大層なものではないが)してきた。
だが時代は移り変わり、個人のお心を述べたり、日記を公開したり、日常のTipsを語ったり、そういう何かを発信するのはSNSが肩代わりするようになってしまった。うーん、こういう時代にわざわざ自前でサイトを持つ意味はあるのだろうか。
- 普段のお気持ち表明はTwitterでええやろ(フォロワーの集まりで形成されたコミュニティで発信する、という意味で)
- 技術的な発信はやっぱりQiitaだねガハハハ
- だがたまに動画や音声の配信をしたい、ンまァそういうサービスもあるが、24時間公開できるサイトは少ない。たとえば自宅の庭にカメラしかけて動画配信するには向かない。
- たまーに、バンド活動でファイルをやりとりする事があるけど、なんとなーくGoogle DriveやDropbox以外でやりたい。んまぁGoogle Driveの15GBを使い切るのはいつになるかわからないが、なんとなーく情報は自前で持ちたい。
- WordPressは触ってみたけど、カスタマイズの幅が多すぎるというか、そこまでの機能はいらないんだよなぁ。
などなど自分の欲する要件を色々考えた結果、結局のところ自分のサイトの立ち位置としては、色んなSNSから自分の情報をとってきて表示する名刺的なもので良いのではないかと思い、今回の10年ぶりの大幅リニューアルとなった。
基本的な仕組みの変更
これまでのサイトはだいたいこんな感じだった。
cgiはrubyで組んで、テンプレートにはerbを使ってた。コンテンツ(ブログ)のデータは日付別に区切られたフォルダをクロールして最新のデータを表示するようにしてた。ブログの元データは投稿用メールアカウントにメールが届くとスクリプトを起動してメール本文からhtmlを生成して、日付別に区切られたフォルダに格納するようにしていた。今じゃあんまり推奨されない方式だけども、昔はよくそういう「メールが届いたらコマンド実行」という機能を使っていた。メールとhtmlの変換やページ内の部品の生成には自前のmarkdown to htmlなエンジンを使っていた。
今回はこれをリニューアルして、このようにした。
cgiはpythonに切り替え。まず、メインのコンテンツはTwitter/Qiita/NoteのAPIで取ってきてタイムスタンプでソートして表示することにした。QiitaやNoteはコンテンツがでかいので、タイトルと本文の最初だけとってくる。メールによる更新は今回廃止(他サイトから取ってくるからいらないよね)、ページ内のコンテンツの生成は自前のmarkdownをやめてyamlで部品を読み込んで、jinja2でテンプレートに突っ込む方式になった。
今流行りのクライアントサイドを重くして、Web APIだのGraphQLだのクライアントサイドのjavascriptで全部済ます、というアプローチもあったが、うーん、自分にはちょっと思い浮かばなかった。古い人間なのでコードが外から参照されるのに抵抗があるのかもしれない。あと、SSR極めるなら、cgiにする必要もなかったかもしれない。
ansibleによる準備
今回webサーバをapacheからnginxに切り替えた、理由はなんとなく。ンまァワイのような弱小サークルじゃぁC10K問題なんて夢のような話だけども。
とりあえず、インストール〜コンフィグファイル配信〜コンテンツ配信までをansibleで自動化してみた。まぁWebサーバは一台で運用してるから、自動化の恩恵はあまりないけども、たとえばWebサーバをRaspberry Piに移動するとか、k8sに移動するとか、そういう場面ではこれから使えるかなぁ、というアレ。
- hosts: web-servers
become: yes
become_user: root
become_method: sudo
tasks:
- name: Install nginx
apt:
name:
- nginx-full
- fcgiwrap
state: present
- name: Install python library for CGI
apt:
name:
- python3-jinja2
- python3-yaml
- python3-orderedattrdict
- python3-python3-requests-oauthlib
state: present
- hosts: web-servers
become: yes
become_user: root
become_method: sudo
tasks:
- name: copy config to /etc
ansible.builtin.copy:
src: "etc/nginx/"
dest: "/etc/nginx/"
- name: create symbolic lynk
file:
src: /etc/nginx/sites-available/{{ item }}
dest: /etc/nginx/sites-enabled/{{ item }}
state: link
force: yes
loop:
- default
- fugumaniacs.com
- name: restart-services
service: name=nginx state=restarted enabled=yes
- hosts: web-servers
become: yes
become_user: root
become_method: sudo
tasks:
- name: update contents(do rsync)
synchronize:
src: "var/www/"
dest: "/var/www/"
delete: yes
- name: change file owner to www-data
file:
path: "/var/www/fugumaniacs.com/"
state: directory
group: www-data
owner: www-data
recurse: yes
- name: enable executable index.cgi
file:
path: "/var/www/fugumaniacs.com/html/index.cgi"
state: file
owner: www-data
mode: "0544"
#
ちなみにansibleでsyncronizeを使うには、操作対象のサーバにansible操作ユーザがパスなしでsudoできなければいけないらしいので/etc/sudoersをちょこちょこっと書き換えないといけない。
- name: enable ansible-remoteuser to nopass sudo.
lineinfile:
path: /etc/sudoers
state: present
regexp: '^{{ansible_user}}\s'
line: "{{ansible_user}} ALL=(ALL) NOPASSWD:ALL"
ページの作成
ページの生成はjinja2を使って実装とコンテンツ部分を分けた。
たとえば以下のようなコンテンツ本体とテンプレートを用意しておいて
title: "フグタシステムズ"
subtitle: "なんか色々とアレな人のステータスモニタ 2021-"
...
(中略)
...
<div id="block-top">
<div> <a href="/" id="icon"> <img src="/icons/mecha-fugu-v01-black.png"> </
a><div id="title">{{page.title}}</div></div>
<div id="subtitle">{{page.subtitle}}</div>
{{page.top}}
</div>
...
...
cgi本体でこれを読み込んで処理する。
(中略)
page_params = {}
(中略:ここでpage_paramsのデフォルト値を読み込む)
with open("ページのパラメータ.yml") as fd:
p = orderedattrdict.AttrDict(yaml.safe_load(fd))
page_params.update(p)
(中略)
j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(PAGE_PARAMS_DIR,encoding='utf8'))
templ = j2_env.get_template('template.html.j2')
html = templ.render(page=page_params)
(中略)
print('Content-Type: text/html')
print('')
print(html)
orderedattrticって便利なんだけど、名前なんとかならんか、デフォルトで入ってていい機能だと思うけれども・・・・。
過去コンテンツの扱い
過去のコンテンツは、幸いにも日付別のフォルダにindex.htmlを置いてあったので、簡単なスクリプトで各日付のindex.htmlへのindex.htmlを生成して、あとは知らないぜ、となった。
#!/usr/bin/bash
echo '<html lang="ja"> <meta charset="utf-8"/><head> <link rel="stylesheet" href="/default.css"> <link rel="icon" type="image/x-icon" href="/favicon.ico"></head>
<body> <ul>
'
ls | grep -v `basename $0` | grep -v index.html | while read line; do echo "<li><
a href='${line}' target='_blank'>${line}</li>"; done
echo ' </ul></body></html> '
icecast2まわりの設定
やったぜ、とりあえず静的コンテンツはできたぜ、となったのだが、つまんねぇので、音声配信、いわゆるインターネットラジオのインスタンスを立ててみることにした。
まずは、icecast2とliquidsoapをインストールする。
- hosts: icecast2-servers
become: yes
become_user: root
become_method: sudo
tasks:
- name: Install icecast and liquidsoap
apt:
name:
- icecast2
- liquidsoap
- psmisc
- ffmpeg
- faad
- flac
update_cache: yes
state: present
##############################################
- name: create liquidsoap dirs
file:
path: "{{ item }}"
state: directory
owner: icecast2
group: icecast
mode: 0660
loop:
- /var/log/liquidsoap
- /etc/liquidsoap
- /var/run/liquidsoap
- /var/lib/liquidsoap
- name: upload contents
synchronize:
src: "{{ item }}"
dest: /var/lib/liquidsoap/
copy_links: yes
delete: yes
loop:
- 2019-aokgf
- 2019-varisrts
- 2020-genpei
- 2021-emoemonoscar
- playlist.txt
- name: upload liquidsoap conf
template:
src: main.liq.j2
dest: /etc/liquidsoap/main.liq
owner: icecast2
group: icecast
mode: 0660
- name: upload liquidsoap startup
template:
src: liquidsoap.service.j2
dest: /etc/systemd/system/liquidsoap.service
owner: icecast2
group: icecast
mode: 0660
- name: do daemon-reload
command:
cmd: "systemctl daemon-reload"
- service: name=icecast2 enabled=yes state=stopped
- service: name=icecast2 enabled=yes state=started
##############################################
- name: apply icecast2 confs
template:
src: icecast.xml.j2
dest: /etc/icecast2/icecast.xml
owner: icecast2
group: icecast
mode: 0660
- service: name=icecast2 enabled=yes state=restarted
liquidsoapはデフォルトでサービスとして起動しないので、以下のファイルを作ってsystemdの管理下にする。一度作ったらsystemctl daemon-reload
しないと反映されないので注意。
[Unit]
Description=Liquidsoap daemon
Documentation=http://liquidsoap.fm/
After=network.target
[Service]
Type=simple
User=root
Group=liquidsoap
PIDFile=/run/liquidsoap/liquidsoap.pid
ExecStart=/usr/bin/liquidsoap /etc/liquidsoap/main.liq
ExecStop=/usr/bin/killall liquidsoap
[Install]
WantedBy=multi-user.target
ちなliquidsoapのコンフィグファイルがね、クセありすぎじゃぁと思った。
set("init.allow_root",true)
set("log.file",true)
set("log.file.path","/var/log/liquidsoap/liquidsoap.log")
set("log.file.append",false)
set("log.level",0)
set("init.daemon.pidfile.path", "/run/liquidsoap/liquidsoap.pid")
playlist(mode="normal","/var/lib/liquidsoap/playlist.txt")
my_music = playlist("/var/lib/liquidsoap/playlist.txt")
my_stream = fallback(
[
request.queue(),
switch([
({0h-24h}, my_music)
])
]
)
output.icecast(
%mp3( bitrate=24, samplerate=22050, id3v2=true),
host="localhost",
port=8000,
mount="/stream",
password = "んーーなにかパスワード的なものソースの何かアレ",
name="fugumaniacs icecast",
description = "game music from fugumaniacs",
fallible=true,
my_stream
)
liquidsoapは%mp3のセクションに不要なオプションを設定すると起動しなかったりして本当に混乱した。エラーログにナニが悪いんだか全然書いてねぇとかひでぇな。
ほいでもって、icecast2のサーバが経ったら、Webサーバのnginxにリバースプロキシの設定して、Webサーバにicecast2サーバの配信をひっぱってくるように設定する。ここもansibleのtemplate使っとるけども、とりあえず一部だけ:
(中略)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
proxy_max_temp_file_size 0;
location /app/icecast2/stream {
rewrite /api/icecast2/stream01 /stream permanent;
proxy_pass http://ローカルネットにあるicecast2サーバ:8000/stream;
}
(中略)
liquidsoapはv4lからの配信もOKらしいので、そのうち庭の動画でも配信するようにしようかと思う。
できあがり
できあがった結果がこれだ。
感想
今回やってみて思うのは、ンまァわかんない事はGoogle様に聞けばだいたいわかる、というのと、今はライブラリとか方法論が確立されてて、簡単なテンプレート適用だけだったら簡単にできるな、という事でした。なんでDjango使わなかったの?と言われると「知らんかったから」としか言えないが、ルーティングするほどたくさんコンテンツやモードがあるわけじゃないので、デカいフレームワークはいらないかな、っていう。
ンマーセキュリティ大丈夫か?とかそういう問題はあるんだが、どうするかなー、cgiやめてSSR極めるっつーのはあるかもね。
おまけ
昔ながらのインフラの仕事がなくなりつつあるので、誰か仕事くださいっっつ。