Rails で created_by と updated_by をいい感じにセットしたい

はじめまして!

8月にインゲージに入社したエンジニア1年生の @shutooike です!(保険)

突然ですが、弊社サービスの設定系のテーブルにはその設定を誰が作成・更新したものかを記録するために created_by, updated_by というログインユーザーのIDを入れるカラムがあります。

その created_by, updated_by にログインユーザーIDをセットする処理は、単に save(updated_by: current_user.id) としているところもあれば、before_save などのコールバックでセットしているところもありモデルごとにバラバラでした。

そこらへんいい感じにしてよと @ishiyu に言われたので、負債返却週間 *1で処理の共通化をした話をします!

実装①:コールバック編 🤙

方針before_validation コールバックでログインユーザー(操作ユーザー)のIDをセットする

require 'active_support/concern'
module Concerns::Userstamp
  extend ActiveSupport::Concern
  
  #----------------------------------------
  # Included
  #----------------------------------------
  included do
    # CALLBACKS
    before_validation :set_operator_id

    # VALIDATIONS
    validates :created_by, presence: true
    validates :updated_by, presence: true
  end

  private
  
  def set_operator_id
    self.created_by = operator_id if self.new_record?
    self.updated_by = operator_id 
  end

  def operator_id
    User.current.id # User.current は devise でいうところの `current_user` を呼び出せるようなメソッド
  end
end

この Userstamp モジュールをモデルに include することで、作成・更新時は created_by, updated_by のことを考えなくてよくなりました。

※ コールバックが走らないメソッドは別です。

レビューにて 📝

@shutooike:

できましたレビューお願いします〜

@ishiyu:

お!いい感じだね〜
ただ、access_token のような内部で自動更新する場合は updated_by 変えたくないんだよね〜
save(touch: false) ってしたら updated_at 更新されないからそのノリで使いたい

@shutooike:

たしかに 🦀

実装②:黒魔術編 🔮

方針:ActiveRecord の Timestamp モジュールのメソッドをオーバーライドして save(touch: false)updated_by の更新しないようにする

rails/activerecord/lib/active_record/timestamp.rb github.com

require 'active_support/concern'
module Concerns::Userstamp
  extend ActiveSupport::Concern

  private

  def _create_record
    self.created_by = operator_id
    self.updated_by = operator_id

    super
  end

  # Rails6 では引数がなくなるので、バージョンアップ時に修正する必要あり
  def _update_record(*args, touch: true, **options)
    if touch && self.has_changes_to_save?
      self.updated_by = operator_id
    end

    super(*args, touch: touch, **options)
  end

  def operator_id
    User.current.id # User.current は devise でいうところの `current_user` を呼び出せるようなメソッド
  end

end

private メソッドをオーバーライドするという若干トリッキーなことをしていますが、これで save(touch: false) とした時は updated_at と同様に updated_by を更新しないようになりました!

ちなみにこのコードは Rails 5 系でしか動かないので注意してください。

さいごに

大いなる力には、大いなる責任が伴うことを忘れてはいけない

とりあえずいつものやつ置いときます 😎

他にいいやり方があればぜひ教えてください!

ではまた 🙋‍♂️🙋‍♂️🙋‍♂️

*1:弊社で毎月第1週に行っている名前通り負債を返却する週間

HTML/CSS レイアウトの書き方

こんにちは。石田( @ishiyu )です。

今回はフロントエンジニアとして、HTML/CSS レイアウトについて説明したいと思います。
弊社は、自分以外にフロントエンジニアと呼べる人がいないため、失敗例も交えて説明できればと思います。

TL;DR

思ったより長くなってしまったので先に要約をざっくり書いておきます。

  • 全体レイアウトを grid レイアウトを使用する
  • その他のレイアウトでは flex レイアウトを使用する
  • ウインドウサイズに合わせて、回り込ませたい場合に float レイアウトを使用する(flex レイアウトでも回り込みの設定は可能だが、ネストが深くなりがちで手間がかかるため)

この3つを守ることで、構造が分かりやすいレイアウトになります。

また、極力 positiondisplay を指定したレイアウト構成はしないようにしましょう。

レイアウトの種類

知っている人も多いと思いますが、まずはCSSでのレイアウト方法について簡単におさらいしましょう。
現在、よく知られているものとしては以下の4つがあります。

  • table レイアウト
  • float レイアウト
  • flex レイアウト
  • grid レイアウト

それぞれ、以下のデザインでレイアウトを組んでみました。

f:id:ishiyu1125:20201120172526p:plain
レイアウト

table レイアウト

IT業界に10年以上いる人にとっては懐かしいやつですね。
昔はこれがスタンダードでした。

試しにレイアウトを組んでみると、このようになります。

See the Pen Tabel Layout by Yu Ishida (@ishiyu) on CodePen.

ブラウザに左右されずにレイアウトされるため、わりと嬉しかった印象なのですが、久々に組んでみると、ネストが深すぎて非常にややこしくて辛いです。

今となっては、このレイアウトで組むのはHTMLメールぐらいじゃないですかね。

float レイアウト

自分は、float で全体レイアウトを組んでいるシステムを作ったことはないですが、コンポーネントレベルでは未だによくお世話になります。 IE6 でマージンが2倍になるバグがあったので、流行った当時(2000年代前後)では好んで使うことはなかったです。

レイアウトを組んでみると、このようになります。

See the Pen Float Layout by Yu Ishida (@ishiyu) on CodePen.

テーブルレイアウトと比べると、ぐっと見やすくなりました。

弱点としては widthheight をきちんと設定していかないとうまく回り込んでくれないこと、 clear を設定し忘れて回り込みが解除されずにレイアウトが崩れることですね。

flex レイアウト

CSS3が主流になって、使われだしたレイアウトです。 今でもかなり使われている印象を持っています。(注意は必要ですが)IE11でもきちんとレイアウトできるので非常にありがたいですね。

レイアウトを組んでみると、このようになります。

See the Pen Flex Layout by Yu Ishida (@ishiyu) on CodePen.

float レイアウトと比べると、レイアウトのための style 設定は増えましたが、width の意識が減るのがありがたいです。回り込みの意識も必要がなくなるので楽ですね。

grid レイアウト

flexレイアウトだけでも、ほぼすべてに対応できるのですが、flexは縦か横の一方向に対してしか柔軟ではありません。 そこで登場したのが grid レイアウトです。

レイアウトを組んでみると、このようになります。

See the Pen Grid Layout by Yu Ishida (@ishiyu) on CodePen.

ページ全体レイアウトを組むときには非常に重宝するレイアウトで、grid レイアウト内で完結するため、widthheight の指定が不要になるのが特徴です。

レイアウトするときに守るべき考え方

紹介した4つを使用してHTML/CSS のレイアウトしていくことで問題の起こりにくくなると思います。(table レイアウトを使うことはないと思いますが)

つまり

  • 全体レイアウトを grid レイアウトを使用する
  • その他のレイアウトでは flex レイアウトを使用する
  • ウインドウサイズに合わせて、回り込ませたい場合に float レイアウトを使用する(flex レイアウトでも回り込みの設定は可能だが、ネストが深くなりがちで手間がかかるため)

この3つを守ることで、構造が分かりやすいレイアウトになります。

よく見る失敗例

とりあえず、小手先で解決しようとしたけど、結局 ウインドウサイズやズーム設定を変えると崩れたりしやすいことが多いものです。

絶対位置指定での配置をする

<div style="position: relative;">
  <img src="hoge.jpg" style="position: absolute; left: 3px;">
  <span style="position: absolute; left: 120px;">hogehoge</span>
</div>

上記のようなタイプです。 このような場合、ウインドウサイズが超えると切れてしまう見づらくなります。 オンラインマークなどデザインを重ねる以外で、position: absolute; を使用するのは控えたほうが無難です。

display を変えて、無理やりレイアウト調整する

<div>
  <img src="hoge.jpg">
  <div style="display: inline-block;">hogehoge</div>
</div>

このような場合はデザインに問題はありませんが、読み手側が理解しにくいという問題が発生します。 今回の例ではHTMLに直接CSSを設定していますがファイルが分かれることも多く、多用されると結構ツライことが多いです。

まとめ

今回は、HTML/CSSのレイアウトについて、まとめてみました。
エンジニアの方々はプログラミングする際にはネストの数やパフォーマンスなどを意識して書いていると思います。そこには、よく書くパターンのようなものがあるはずです。
同じようにHTMLについても、同じようによく使うパターンがあるため、それを使うことで簡単で他の人にも読みやすくものになります。

ぜひとも、デザインが崩れなかったらいいやぐらいに適当に書くのではなく、読みやすいものを意識して書いてもらえると嬉しいです。

Exchange Online の先進認証に対応しました(1)

f:id:kizashi1122:20201110142108j:plain

id:kizashi1122 です。

弊社が開発・運営している Re:lation でも遅ればせながら Exchange Online の先進認証に対応しました。 Microsoft が 2020/10/13 にExchange Online の基本認証を廃止するというアナウンスをしたためです。

support.microsoft.com

このエントリでは以下のような流れで説明を進めます。

基本認証と先進認証

Microsoft のドキュメントを見ると基本認証は英語で Basic Authentication であり、先進認証は Modern Authentication であることがわかります。。

基本認証と言っていますがいわゆるHTTP上のBASIC認証のことではありません。

ユーザ名とパスワードを使った認証のことを指しています。 つまり今までは、SMTP/POP3/IMAP を利用する際に、Office365 のユーザIDとパスワードを使う必要がありました。しかしセキュリティ上好ましくないとのことで、この基本認証が廃止になり、先進認証に置き換わることになりました。

ちなみに「廃止」と書きましたが、コロナの影響によりこの基本認証を利用していたアカウントについては、2021年の後半までは使えるようです。

https://techcommunity.microsoft.com/t5/exchange-team-blog/basic-authentication-and-exchange-online-april-2020-update/ba-p/1275508

そして先進認証ですが、これはいわゆる OAuth 2.0 を指しています。

SMTP/POP3/IMAP を利用する際に、ユーザ名とパスワードではなく、ユーザ名と OAuth 2.0 で取得したアクセストークンを使って認証することになります。

やること

さて先進認証の対応と一言で言っても、やるべきことはたくさんあります。

  1. OAuth 2.0 の設定(Azure AD側)
  2. OAuth 2.0 の設定(サービス側)
  3. アクセストークンをつかった SMTP/POP3 の認証
  4. リフレッシュトークンを使ったアクセストークンの更新

細かいことは他にもありますがおおまかに言うとこんな感じになるでしょうか。 実はこの先進認証対応についてですが、 Re:lation では G Suite については対応済でした。このときは技術的な部分ではなく、Google とのやりとりでかなり苦戦しました。そのあたりのことはすでにこちらに記載しています。

Gmail API(Google OAuth)利用承認取得記 - 思ったより大変でした - インゲージ開発者ブログ

この G Suite での経験もあり、今回の Exchange Online での対応はさくっと終わると予想していましたが、その読みは甘く G Suite とは別の意味で苦戦しました。 その苦戦した内容を何回かにエントリをわけて解説していきたいと思います。

本エントリでは「OAuth 2.0 の設定(Azure AD側)」まで書こうと思います。

用語の整理

Azure AD の画面では「アプリ」という言葉が出てきます。

当然「アプリケーション」の略ですが、アプリケーションというとサービス側と勘違いしてしまいそうにもなるのでここで用語の整理をしておきたいと思います。 OAuth まわりでは、きちんとした定義はあるかと思いますが、このエントリでは以下のように定義し以降はこの用語を使います。

一般化した場合 Re:lationの場合
サービス利用者 リソースオーナー ユーザ
アクセストークン発行側 認可サーバ Office 365
アクセストークン利用側 サービス Re:lation

OAuth 2.0 の設定(Azure AD側)

前提として、Azure AD にアカウントを作っておく必要があります。 弊社では Office 365 のアカウントをすでに持っていたので同じアカウントでログインできました。

アプリの登録

次に、Azure AD の管理画面からアプリの登録をします。アプリという言葉を使っていますがあくまで Azure AD で作る設定となります。

f:id:kizashi1122:20201112174947p:plain

[新規登録] よりアプリを作成します。

リダイレクトURIの設定

f:id:kizashi1122:20201111220746p:plain

アプリに関する情報を登録します。名前は何でもいいです。何でもいいですが、この名称は認可画面で表示されることにご注意ください。

ここですね。

次に、 リダイレクト URI を登録します。これはいわゆるコールバックURLのことです。 OAuth で認可画面でユーザIDとパスワードを入力後に戻るリダイレクト先(サービス側)のURLとなります。

リダイレクト URI は開発環境では http://localhost を使うことができます。 それ以外では https である必要があります。

認可サーバーによっては、開発環境用でも https を利用が必須の場合があるので、Office365 はその意味では楽でした。

Re:lation ではユーザはマルチテナント型のサービスのためそれぞれ固有のサブドメインを持つという特有の事情があります。 そのため、リダイレクト URI は全ユーザ共通のURLを発行する必要があり、その共通URLに遷移後、各自のサブドメイン側のURLにリダイレクトする仕組みを自作しています。

シークレットトークンの発行

証明書とシークレット メニューをクリックしましょう。

シークレットトークンの発行は、私の知る限り、認可サーバ側で設定を新規作成した時点で発行されてることが多いと思うのですが、Azure AD では明示的に発行する必要があります。期限は選べますが「なし」を選んでおけばよいでしょう。「なし」を選んだとしても有効期限はちゃんと設定されるようで 2299/12/31 となるようです。

f:id:kizashi1122:20201112172322p:plain

f:id:kizashi1122:20201112172409p:plain

通常、シークレットトークンはクライアントIDとペアで使います。そのクライアントIDはどこにあるのでしょうか? こちらは 概要 メニューのところにあります。

f:id:kizashi1122:20201112172609p:plain

スコープの設定

あとはスコープの設定です。何の権限を与えるか?という話です。

APIのアクセス許可 メニューをクリックします。

f:id:kizashi1122:20201112172937p:plain

User.Read はデフォルトで選択されています。

ここでは SMTP と POP3 を使った送受信をユーザの代わりにサービスがおこないたいという要件になります。 アクセス許可の追加 から Microsoft Graph を選択し、

  • offline_access
  • SMTP.Send
  • POP.AccessAsUser.All

を選択して保存します。

f:id:kizashi1122:20201112173336p:plain

選択後はこのような画面となります。

画面にある API/アクセス許可の名前 はサービス側からも設定する必要があり、その場合は scope というパラメータに設定することになります。

Teams と連携したい場合は Teams を選択すればよいと思いますし、ここはやりたいことによってアクセス許可の内容も変わってきます。

おさらい

サービス側では設定に必要になるのは

  • クライアントID
  • シークレットトークン
  • スコープ(画面では API/アクセス許可の名前

になります。

あとは簡単じゃない?と思いたいですが、スコープでハマることになります。

が、今回はここまで。

squid を使ってみた

こんにちは masm11 です。

systemd シリーズの途中ですが、ここで別のネタを挟みたいと思います。 タイトルのとおり、今回は squid です。

個人的に squid をインストールして使ってみましたので、説明してみたいと思います。

squid とは / 使用目的

squid は、プロキシサーバです。http だけでなく https にも ftp にも、知る人ぞ知る gopher にだって対応しています。

個人的に使うだけなので、簡単なものが良かったのですが、ほぼデファクトスタンダードの ようなので、これを使ってみました。

Web サービスを開発・運営していると、「時々おかしい!」というメッセージとともに ブラウザのスクリーンショットが送られてきて、 そこに "squid" という文字列が見えることが、時々あります。 そういったことからも「デファクトスタンダードなんだなぁ」と感じます。

さて、私は Linux マシンを 3台持っています。全て ArchLinux です。 ArchLinux はローリングアップデートを採用しています。 「OS のバージョンというものがなく、パッケージがそれぞれのタイミングで更新されていく」 というと解りやすいでしょうか。 例えば、一昨日は GNOME が更新され、昨日は GCC が更新され、今日は binutils が、 といった感じです。

わりと頻繁にアップデートしていても、ダウンロードサイズが 100MB を超えることは普通にありますし、 しばらく放置していて久しぶりにアップデートしようと思ったらダウンロードサイズが 1GB 超だった! なんてこともあります。

こんなマシンを 3台持っていると、「ギガが減る」わけです。せめて、3台を同じタイミングで 更新する時くらいは、パッケージファイルをキャッシュしたいな、と思うわけです。

そこで squid を使ってみることにしました。

インストールする

Linux 系 OS なら、おそらくパッケージが用意されていると思います。 私は ArchLinux なので、

sudo pacman -S squid

でインストールできました。

設定する

設定ファイルは /etc/squid/squid.conf にあります。 このファイルは最小構成だそうで、ドキュメントは /etc/squid/squid.conf.documented にあります。 (ArchLinux 以外でも同様なのかはわかりませんが)

acl 設定

上から順に見ていきます。

acl localnet src 0.0.0.1-0.255.255.255  # RFC 1122 "this" network (LAN)
acl localnet src 10.0.0.0/8             # RFC 1918 local private network (LAN)
acl localnet src 100.64.0.0/10          # RFC 6598 shared address space (CGN)
acl localnet src 169.254.0.0/16         # RFC 3927 link-local (directly plugged) machines
acl localnet src 172.16.0.0/12          # RFC 1918 local private network (LAN)
acl localnet src 192.168.0.0/16         # RFC 1918 local private network (LAN)
acl localnet src fc00::/7               # RFC 4193 local private network range
acl localnet src fe80::/10              # RFC 4291 link-local (directly plugged) machines

ここでは、「localnet とは」を定義しています。 設定は十分なように見えて、実は自分が使っている IPv6 アドレスは当然含まれていませんので、 追加しておきます。

acl localnet src 2001::/16

私のネットワークの IPv6 アドレスは頻繁に変わるため、広く設定しました。 ファイアウォールで外部からのアクセスはありませんので、これでも大丈夫だと思います。

acl SSL_ports port 443

次に、「SSL_ports とは」を定義しています。SSL ポートは 443、つまり HTTPS ですね。

acl Safe_ports port 80          # http
acl Safe_ports port 443         # https

これは「Safe_ports とは」を定義しています。これは Web サーバのポート番号で、80 と 443 です。 他にもデフォルトで ftp や gopher やその他たくさん設定がありましたが、コメントアウトしました。

acl CONNECT method CONNECT

これは「CONNECT とは」を定義しています。ブラウザ-プロキシ間の通信に使う CONNECT メソッドである、 という設定です。

アクセスコントロール設定

さて、ここまでは、「localnet とは」「SSL_ports とは」といった定義をしてきました。 ここからは、これらを使ったアクセス制御を設定していきます。

http_access deny !Safe_ports

「Safe_ports でなかったら」deny、つまり即エラーを返します。

http_access deny CONNECT !SSL_ports

こちらは条件を2つ書いています。「CONNECT メソッドである」かつ「SSL_ports でない」場合です。 CONNECT なのに接続先が 443 でなかったらエラーとしています。

http_access allow localnet
http_access allow localhost

localnet (先程定義したものです) や localhost は許可しています。

http_access deny all

ここまでのいずれにも該当しなければ、エラーとしています。

プロキシ受け付けポートの設定

次にプロキシ受け付けポートの設定をします。

http_port 8443 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=4MB tls-cert=/etc/squid/pki/ica_crt.pem tls-key=/etc/squid/pki/ica_key.pem
sslcrtd_program /usr/lib64/squid/security_file_certgen -s /var/local/squid/ssl_db -M 4MB
sslcrtd_children 5
ssl_bump bump all
sslproxy_cert_error allow all

通常、HTTPS のプロキシは、トンネルをするだけです。プロキシには暗号化されたデータが通るので、 プロキシは通信内容を知ることができません。それでは ArchLinux のパッケージファイルをキャッシュ することができません。通信内容を知る必要があります。

squid の世界では ssl-bump と呼ぶようです。squid がブラウザと TLS 通信し、サーバとも TLS 通信します。こうすれば通信内容を知ることができます。

ただし、ここには問題があります。ブラウザは www.google.com と通信しようとしているのに、 サーバ証明書が masm11.me ではブラウザが拒否します。 かといって、www.google.com の証明書を (あくまで合法的に) もらってきても、秘密鍵がないため、 TLS 通信することができません。 そこで、これを解決するために、squid が www.google.com の秘密鍵とオレオレ証明書を自動生成します。 もちろん、これだけではブラウザが拒否しますので、オレオレルート CA 証明書を ブラウザに登録して信用させておく必要があります。

この辺りの事情を設定したのが、上記設定内容の1行目になります。 ブラウザから受け付けるポート番号は 8443 で、ssl-bump モードで、 サーバ証明書は自動生成し、 証明書はいくつかメモリにキャッシュしておき、 中間 CA 証明書と秘密鍵を設定しています。

ここに設定した中間 CA 証明書と秘密鍵は、今回自分で作成しましたが、作成手順を書こうとすると、 それだけで記事1本書けてしまいますので、ここでは割愛します。 以下のページを参考に作成しました。

https://qiita.com/bashaway/items/ac5ece9618a613f37ce5

生成されたサーバ証明書は、ディスクに持っておき、再利用します。 格納先が2行目にある /var/local/squid/ssl_db で、 /usr/lib64/squid/security_file_certgen というプログラムが証明書を生成して 格納するようです。

なお、/var/local/squid/ssl_db というディレクトリは初期化が必要です。

/usr/lib64/squid/security_file_certgen -c -s /var/local/squid/ssl_db

を実行することで、初期化することができます。

3行目は、このプログラムを5つまで起動するのでしょう。

4行目、

ssl_bump bump all

については、1行目で ssl-bump モードにはしたものの、全てそれでいいかと言うと、 そうでない場合もあり得ます。例えば接続元 IP アドレスによっては ssl-bump したくない、 ということもありそうです。

上記では、全て bump する設定にしてあります。

全く未確認の設定で恐縮ですが、おそらく、

ssl_bump bump localnet
ssl_bump splice all

と設定すれば、localnet からの接続なら bump し、それ以外は splice (つまりトンネリング) する ものと思われます。

キャッシュ格納先の設定

次にキャッシュを格納するディレクトリについての設定です。

cache_dir ufs /var/cache/squid 4096 16 256

/var/cache/squid に置きます。4096 は MB 単位で、4GBになります。 あとの数値2つは、キャッシュディレクトリの構造を指定するもので、 だいたい固定のようです。

キャッシュの有効期間の設定

最後に、キャッシュの有効期間についての設定です。

refresh_pattern ^ftp:        1440    20%     10080
  • /^ftp:/ の場合は
  • 最小キャッシュ時間 1440分 (=1日)
  • 鮮度しきい値 20%
  • 最大キャッシュ時間 10080分 (=1週間)

という設定です。

最小キャッシュ時間は、キャッシュに入れてからこの時間が経っていない場合、キャッシュから返します。 最大キャッシュ時間は、キャッシュに入れてからこの時間が経っている場合、改めてサーバから取得して返します。

また、鮮度という概念があります。鮮度は以下の式で計算されます。

(そのファイルがキャッシュに入ってから今までの時間) / (そのファイルがサーバに作成されてから今までの時間)

これが 20% 未満だったら、キャッシュから返し、20% 以上だったらサーバから取得して返します。

具体的にどういう感じなのか、よく解らないですね。

(now - cache_create) / (now - server_create) = 0.2

式変形すると、以下のようになります。

(now - cache_create) = (now - server_create) / 5

キャッシュの age がサーバ上の age の 1/5 になったら expire です。

いまいち意味がつかみづらいので、もう少し変形してみると、以下のようになります。

(now - cache_create) = (cache_create - server_create) / 4

キャッシュ作成時におけるサーバファイルの age の 1/4 が経過したら expire、ですね。

ただし、「最小キャッシュ時間」「最大キャッシュ時間」のパラメータもありますし、 HTTP ヘッダでの expire 設定もあり、これらの方が優先度は上です。

頻繁に更新されるページはいつまでも持ってるわけにはいかないけど、 長いこと放置されてるページは少々長く持ってても大丈夫、という考えで、 このパラメータを使うと、そういう設定ができるのでしょう。

しかし、ちょっと複雑すぎますね。

結局私は、もともとの「パッケージファイルをしばらくキャッシュしたい」 という目的に立ち返り、以下のように設定しました。

refresh_pattern \.pkg\.tar\.zst$        0       100%    4320
refresh_pattern .                       0       0%      0

.pkg.tar.zst は、パッケージファイルの末尾です。その場合は最大3日間キャッシュします。 それ以外の場合は、キャッシュしません。

使ってみる

まず、squid を起動します。

sudo systemctl start squid

ちゃんと起動したか確認します。

● squid.service - Squid Web Proxy Server
     Loaded: loaded (/usr/lib/systemd/system/squid.service; enabled; vendor preset: disabled)
     Active: active (running) since Sat 2020-11-07 01:32:28 JST; 1min 16s ago
       Docs: man:squid(8)
   Main PID: 831014 (squid)
      Tasks: 9 (limit: 18973)
     Memory: 24.2M
     CGroup: /system.slice/squid.service
             ├─831014 /usr/sbin/squid --foreground -sYC
             ├─831024 (squid-1) --kid squid-1 --foreground -sYC
             ├─831030 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831031 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831032 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831033 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831034 (security_file_certgen) -s /var/local/squid/ssl_db -M 4MB
             ├─831046 (logfile-daemon) /var/log/squid/access.log
             └─831047 (unlinkd)

1107 01:32:28 mike squid[831024]:         0 Objects cancelled.
1107 01:32:28 mike squid[831024]:         0 Duplicate URLs purged.
1107 01:32:28 mike squid[831024]:         0 Swapfile clashes avoided.
1107 01:32:28 mike squid[831024]:   Took 0.06 seconds (5449.45 objects/sec).
1107 01:32:28 mike squid[831024]: Beginning Validation Procedure
1107 01:32:28 mike squid[831024]:   Completed Validation Procedure
1107 01:32:28 mike squid[831024]:   Validated 319 Entries
1107 01:32:28 mike squid[831024]:   store_swap_size = 230576.00 KB
1107 01:32:29 mike squid[831024]: storeLateRelease: released 0 objects
1107 01:32:38 mike systemd[1]: /usr/lib/systemd/system/squid.service:15: PIDFile= references a path below legacy directory /var/run/, ...

大丈夫そうです。次の OS 再起動時にも自動起動するように設定しておきます。

sudo systemctl enable squid

wget を使ってプロキシ経由の通信を試みてみます。

luna:~ % http_proxy=mike.local:843 \
 https_proxy=mike.local:8443 \
 /usr/bin/wget --ca-certificate=/etc/pacman.d/RootCA_crt.pem \
 https://www.google.com/
--2020-11-07 01:37:03--  https://www.google.com/
CA証明書 '/etc/pacman.d/RootCA_crt.pem' をロードしました
mike.local (mike.local) をDNSに問いあわせています... IPv6アドレス, IPv6アドレス
mike.local (mike.local)|IPv6アドレス|:8443 に接続しています... 接続しました。
Proxy による接続要求を送信しました、応答を待っています... 200 OK
長さ: 特定できません [text/html]
`index.html.1' に保存中

index.html.1                         [ <=>                                                       ]  13.74K  --.-KB/s 時間 0.01s    

2020-11-07 01:37:04 (1.34 MB/s) - `index.html.1' へ保存終了 [14068]

luna:~ % 

なんだかうまくいってそうです。

なお、ご存知かもしれませんが、プロキシは、HTTP と HTTPS を同じポートで待ち受けることができます。 最初に生で CONNECT が送られて来たら、そこから TLS handshake が始まります。 最初に生で GET 等が送られて来たら、それは生の HTTP です。 簡単に区別できるんですね。

では、ArchLinux のパッケージマネージャ pacman に設定します。

/etc/pacman.d/RootCA_crt.pem にオレオレルート CA 証明書を置いています。 その上で、/etc/pacman.conf には以下のように書きます。

XferCommand = /usr/bin/env https_proxy=mike.local:8443 http_proxy=mike.local:8443 /usr/bin/wget --ca-certificate=/etc/pacman.d/RootCA_crt.pem -c -O %o %u

これで

sudo pacman -Syu

を実行すると、更新されたパッケージがプロキシ経由でダウンロードされてきます。

squid の通信ログは /var/log/squid/access.log にあります。その中から引用してみます。

1604669997.994      2 クライアントIPv6アドレス NONE/200 0 CONNECT ftp.jaist.ac.jp:443 - HIER_NONE/- -
1604669998.326    303 クライアントIPv6アドレス TCP_MISS/200 635208 GET https://ftp.jaist.ac.jp/pub/Linux/ArchLinux/community/os/x86_64/iwd-1.9-1-x86_64.pkg.tar.zst - HIER_DIRECT/2001:df0:2ed:feed::feed application/x-tar

1行目は CONNECT の処理ですね。NONE/200 の 200 の部分が、クライアントに http 200 を返したことを示しています。 2行目はパッケージの GET の処理です。キャッシュになかった (TCP_MISS) ので、直接ダウンロードした (HIER_DIRECT) ことがわかります。

もう一箇所引用します。

1604669997.505      3 クライアントIPv6アドレス NONE/200 0 CONNECT ftp.jaist.ac.jp:443 - HIER_NONE/- -
1604669997.592     68 クライアントIPv6アドレス TCP_HIT/200 1378946 GET https://ftp.jaist.ac.jp/pub/Linux/ArchLinux/extra/os/x86_64/libxml2-2.9.10-3-x86_64.pkg.tar.zst - HIER_NONE/- application/x-tar

1行目は先程と同様です。 2行目はキャッシュにあった (TCP_HIT) ため、ダウンロードしなかった (HIER_NONE) ことがわかります。

なお、TCP_MISS, TCP_HIT などと、「TCP」という語が見えますが、TCP/IP の「TCP」とは全く無関係のようです。

まとめ

以上、squid の導入から pacman を使った確認まで、一気にご説明しました。

ふ~疲れました。

しかし、デファクトスタンダードたる squid の底力はこんなものではないはずです。 せっかくインストールしてみたので、この機にいろいろ試してみたいと思います。 特に websocket がどうなってるのか、とか。

ではまた!!

systemd - unit ファイルの編集編

こんにちは、masm11 です。 systemd シリーズの第3回です。

blog.ingage.jp

blog.ingage.jp

前回は .service の書き方を説明しました。 unit ファイルには他にも種類がありますが、 ここで unit ファイルの書き換え方を説明したいと思います。

まるっと置き換える

OS のパッケージをインストールすると、unit ファイルは (/usr)/lib/systemd/system/ に置かれると思います。foo.service というファイル名だとしましょう。 この foo.service に手を加えたいとします。

その際、この (/usr)/lib/systemd/system/foo.service に直接手を加えてはいけません。パッケージマネージャが手動による編集を検出して、パッケージの更新ができなくなってしまいます。

代わりに、この foo.service を /etc/systemd/system/foo.service に コピーし、編集します。

こうすることで、/usr/lib/systemd/system/foo.service は無効になり、 代わりに /etc/systemd/system/foo.service が参照されるようになります。

drop-in ファイルを使う

/etc/systemd/system/foo.service にコピーして編集する方法では、 /usr/lib/systemd/system/foo.service が無視されます。 パッケージの更新によって /usr/lib/systemd/system/foo.service が 更新されても、無視され、/etc/systemd/system/foo.service が 使われ続けるのです。

これはあまり嬉しくありません。そこで、drop-in という機構を使います。

drop-in の場合はファイルのコピーはしません。 /etc/systemd/system/foo.service.d/ というディレクトリを作り、 その中に .conf で終わる適当なファイル名で差分のみを書きます。

…よくわからないと思うので、具体例で説明します。

/usr/lib/systemd/system/systemd-journal-flush.service:

[Unit]
Description=Flush Journal to Persistent Storage
Documentation=man:systemd-journald.service(8) man:journald.conf(5)
DefaultDependencies=no
Requires=systemd-journald.service
After=systemd-journald.service systemd-remount-fs.service
Before=systemd-tmpfiles-setup.service
RequiresMountsFor=/var/log/journal

[Service]
ExecStart=journalctl --flush
ExecStop=journalctl --smart-relinquish-var
Type=oneshot
RemainAfterExit=yes
TimeoutSec=90s

この中の After=Requires= に依存関係を追加したいとします。 その場合、

/etc/systemd/system/systemd-journal-flush.service.d/dependencies.conf:

[Unit]
Requires=zfs-mount.service
After=zfs-mount.service

と書きます。こうすることで、/usr/lib/systemd/system/systemd-journal-flush.service/etc/systemd/system/systemd-journal-flush.service.d/dependencies.conf の内容が融合され、解釈されるわけです。

この方法なら、パッケージの更新で /usr/lib/systemd/system/systemd-journal-flush.service が更新されても、ちゃんと反映されます。

まとめ

今回は unit ファイルの書き換え方を説明しました。

drop-in ファイルは便利です。/etc/systemd/*.conf も drop-in に対応しています。こちらも systemd の機能追加に従って項目が増えていきます。 直接編集すると、systemd パッケージの更新がうまくいかず、手間がかかるのですが、drop-in なら問題ありません。

個人的には /etc/passwd も drop-in に対応しないかな、と思っていたのですが、 いつの間にか systemd-homed なんてものができてたんですね。 これも使ってみたいと思います。

ではまた!

systemd - service 編

こんにちは、masm11 です。

前回から始まった systemd シリーズ。今回は .service ファイルの書き方です。

.service ファイルとは

このファイルにサービスの定義を記述します。

sysvinit 等であれば /etc/init.d//etc/rc.d/ に置いていた script の代わりとなるものです。

ファイルの置き場所としては、OS のパッケージに含まれる場合は /usr/lib/systemd/system//lib/systemd/system/ にある場合が多いと思いますが、 自分で作る場合は /etc/systemd/system/ に置きます。

[Unit] 部分

.service ファイルは、まず [Unit] という行から始まります。 [Unit] 部分は以下のような感じになっています。

[Unit]
Description=ZFS replication service
After=network.target
Requires=network.target

Description はこのサービスの説明です。あまりたいした意味はないのですが、 以下のように、ログに出力される時などに使われます。

10月 08 02:00:00 luna systemd[1]: Started ZFS replication service.

AfterRequires は依存関係を記述します。 Requires を書いておくと、network.target が起動に失敗した場合は この unit は起動しなくなります。

依存関係を記述する方法は、他にも WantsConflicts などいろいろありますが、 AfterRequires を使っていればわりとなんとかなっています。

[Service] 部分

.service ファイルには [Service] 部分があります。 以下のような感じになっています。

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/etc/systemd/lo-setup start
ExecStop=/etc/systemd/lo-setup stop
Type

simple, exec, forking, oneshot, dbus, notify, idle の 中から選択して指定します。よく使うのは oneshot, simple, forking です。

  • oneshot: 一度実行してすぐ終了するタイプに指定します。
  • simple: デフォルトです。 起動したプロセスがサービスを提供する場合に指定します。
  • fork: 起動したプロセスが起動時に fork する場合に指定します。
ExecStart / ExecStop / ExecStartPre

ExecStartsystemctl start した時に実行するコマンドを指定します。

ExecStopsystemctl stop した時にサービスを停止するコマンドを指定します。 省略することもよくあります。省略した場合は systemd がプロセスにシグナルを 送って止めます。

ExecStartPreExecStart の前に実行しておきたいコマンドを指定します。 ExecStartPre は複数指定することができます。

マニュアルには

If the command is not a full (absolute) path, it will be resolved to a full path using a fixed search path determinted at compilation time.

と、コマンドがフルパスでなくても大丈夫そうなことが書いてありますが、 経験的にはフルパスでないと動いてくれません。

コマンドの前に - を書いておくと、エラーを無視してくれるようです。 ExecStartPre でちょっと先に実行しておきたいけど、エラーになる場合もある、 というような場合に便利そうです。

RemainAfterExit
RemainAfterExit=yes

と指定しておくと、プロセスが終了しても、異常扱いになりません。

Restart / RestartSec
Restart=always

と指定しておくと、プロセスが終了した場合に、もう一度起動してくれます。

ただ、すぐに起動してしまうので、間隔をあけたい場合は、追加で

RestartSec=5

と指定しておけば、5秒待ってくれます。

User

プロセスのユーザを指定します。

User=masm

聞くところによると、存在しないユーザを指定すると root になってしまう 仕様だそうです。User を指定する際にはご注意ください。

StandardOutput / StandardError

これらは、プロセスの標準出力と標準エラー出力をどうするかを指定します。

私は大抵 journal と指定しています。この場合、systemd の journal に 出力され、journalctl コマンドで見ることができます。

他にも、ファイルへ出力したり、tty に出力したり、いろいろな指定ができるようです。

[Install] 部分

これを書いておくと、systemctl enable できるようになります。

[Install]
WantedBy=multi-user.target

multi-user で起動する際には、この .service も start する、 という指定です。

大抵の場合は multi-user.target で大丈夫です。

@%i

.service ファイルのファイル名に @ を含めることができます。 例えば zfsnap@.service という感じです。

この場合、systemctl enablesystemctl start の際には、

systemctl start zfsnap@2w.service

というように指定します。@ の後ろに 2w と付けました。 この 2w.service ファイルの中で、%i を使って受け取れます。 具体的には以下のように使います。

[Service]
Type=oneshot
ExecStart=/usr/bin/zfsnap snapshot -a %i zbak/android zbak/svc zbak/tama

この場合だと、%i2w に置き換わって、

/usr/bin/zfsnap snapshot -a 2w zbak/android zbak/svc zbak/tama

が実行されるわけです。

この zfsnap@.service ファイル一つで、zfsnap@2w.service, zfsnap@3d.service など いろんな使い方ができます。

あまり使わないですが、たまに便利なことがある機能です。

まとめ

今回は .service ファイルの書き方を経験を交えながら淡々と説明しました。 前回同様、私が使うものだけに絞っています。

.service ファイルはそれだけで使うこともできますが、 他の unit ファイルと組み合わせて使うこともできる、便利なものです。 組み合わせ方は次々回以降に説明しますのでご期待下さい。

ではまた!