NixOS で PHP 環境作ったった

# なにこれ?

[NixOS](https://nixos.org/nixos/) でPHPアプリケーションサーバを作ってみました。

NixOS はパッケージマネージャ [Nix](https://nixos.org/nix/) を利用した Linux ディストリビューションで、宣言的な記述でシステム環境を構築することが可能で、インストールパッケージの細かい制御が可能な点が魅力です。
イメージとしては Ansible が組み込まれた Linux といったところでしょうか。
またパッケージマネージャ Nix は既存の Linux 環境に導入が可能です。

NixOS には[独自のコンテナ機能](https://nixos.org/nixos/manual/#ch-containers)があったり、クラウドへのデプロイまで済ませられる [NixOps](https://nixos.org/nixops/) コマンドを提供していたり、色々と魅力的に思えます。

本稿では 基本的な設定方法とハマりやすい点について説明し、最後に設定ファイルを紹介します。

# インスタンスを立ち上げる

まずは AWS で EC2 インスタンスを立ち上げます。[公式が AWS 向けの AMI を公開しています](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/ec2-amis.nix)ので、それを使用できます。
インスタンスタイプは、今回の構成では t2.micro でも十分です。ただしディスクスペースをかなり使うので、最低でも 10 GiB は取っておいたほうが良さそうです。

インスタンスが立ち上がったらログインしましょう。初期ユーザは root です。

# NixOS の環境設定の基本

続いて環境設定ファイルを記述しましょう。環境設定ファイルは /etc/nixos/configuration.nix です。後述する nixos-rebuild コマンドを実行すると、このファイルに基づいた環境が構築されます。デフォルトでは以下のような記述になっているかと思います。

~~~
{
  imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];
  ec2.hvm = true;
}
~~~

configuration.nix は [nix 言語](https://nixos.org/nix/manual/)で記述されています。 nix 言語をインタラクティブに実行することの出来る [nix-shell](https://nixos.org/nix/manual/#sec-nix-shell) という処理系もありますが、今回は使用しません。

変数 imports にはファイルパスのリストが束縛されます。 nix 言語では複数のファイルに分割して設定を行っていくことが可能で、 imports に設定されたリスト中のパスのファイルを読み込んでいき、それを評価してオプションに値を束縛をしてゆきます。これはモジュールなどと呼ばれます。

[ec2.hvm](https://nixos.wiki/wiki/Install_NixOS_on_Amazon_EC2) は EC2 インスタンスを使用するときのオプションの一つで、 HVM 仮想化を利用する際に設定する必要がある値です。 NixOS はこういったオプションに値を束縛していくことで設定を行います。 NixOS は[オプションの検索ページ](https://nixos.org/nixos/options.html#)を用意しており、大半のオプションが検索可能です。

以下のコマンドを実行することで環境のテストビルドをできます。

~~~
nixos-rebuild -v test
~~~

``-v`` オプションは冗長出力オプションです。ビルド中、様々なファイルが評価されていることが見て取れるはずです。
引数の test は nixos-rebuild のサブコマンドで、テストビルドを行う事を指定しています。テストビルドの場合は再起動するともとの環境に戻ります。
ブートローダにもインストールして、起動時から立ち上げたい場合は nixos-rebuild switch を実行しましょう。

## 特定のパッケージを使用する

まずは使用するパッケージコレクションのリビジョンを固定しましょう。
NixOS のパッケージコレクションは、[nixpkgs](https://github.com/nixos/nixpkgs) という名前で github で公開されています。
この nixpkgs レポジトリのリビジョンを固定することで、インストールされるパッケージのバージョンを固定することができます。
また、リビジョンの他にブランチを指定することも可能です。この場合はリリース毎のブランチを指定することである程度パッケージのバージョンを固定しつつ、セキュリティパッチだけあてるなどが出来そうです。

今回は利用するブランチを、 "release-19.03" に固定しましょう。
configuration.nix に以下のパッチを当てます。

~~~
0a1,7
> let
>   pkgsSrc = (builtins.fetchGit {
>     url = https://github.com/nixos/nixpkgs;
>     ref = "release-19.03";
>   });
>   pkgs = import pkgsSrc {};
> in
~~~

パッチの内容を cat などを使ってファイル、 patch などに書き出し、 ``patch configuration.nix patch`` などとすることでパッチを当てることが出来ます。

[builtins.fetchGit](https://nixos.org/nix/manual/#builtin-fetchGit) は git レポジトリを取得する [nix 組み込み関数](https://nixos.org/nix/manual/#ssec-builtins)で、 ``{ url = ... }`` は fetchGit に渡す引数です。 pkgsSrc にダウンロードしたレポジトリが束縛され、これを import して引数なしで評価して pkgs に束縛しています。

fetchGit は git コマンドを利用しますが、 NixOS にはデフォルトでは git コマンドがインストールされていないので、このまま nixos-rebuild を実行しても途中で失敗してしまいます。以下のコマンドを実行して git をインストールしましょう。

~~~
nix-env -iA nixos.git
~~~

## 基本的なパッケージのインストール

パッケージコレクションを指定したので、今度はこのコレクションからパッケージをインストールしてみましょう。
NixOS はデフォルトでは git はもちろん、 vim エディタすら入っていません(syslog デーモンも見当たりませんが、 systemd 付属の journald がログデーモンとして用意されています)。試しに file コマンドを実行して、インストールされていないことをみましょう。

続いて今回使用するパッケージをインストールしましょう。
configuration.nix に以下のパッチを当てます。

~~~
10a11,14
>
>   environment.systemPackages = with pkgs; [
>     rsyslog git vim file nginx php73
>   ];
~~~

オプション environment.systemPackages に、インストールしたいパッケージのリストを束縛します。右辺の冒頭で ``with pkgs;`` とすることで、リスト内のパッケージは全て先ほど指定したパッケージコレクションの名前空間から選択されます。

nixos-rebuild を実行すると、パッケージがインストールされた環境が構築されます。試しにまた file コマンドを実行してみましょう。
利用可能なパッケージは ``nix search `` コマンドで検索をすることができます。

# 環境設定時の Tips

## オプション束縛値の上書き

ここまで見たように NixOS ではオプションに値を設定して環境を定義していきます。NixOS は[オプションの検索ページ](https://nixos.org/nixos/options.html#)を用意しており、大半のオプションが検索可能です。

モジュールは import することでいくつもの値を設定してくれるので便利なのですが、そういったオプションの値を変更したい場合は以下のような書き方をする必要があります。

~~~
  services.openssh = pkgs.lib.mkForce {
    permitRootLogin = "no";
    passwordAuthentication = false;
  };
~~~

値を一度 [mkForce](https://nixos.org/nixos/manual/#sec-option-definitions-setting-priorities) で処理してから services.openssh に束縛しています。

NixOS の束縛は全て優先度が設定されています。デフォルトの優先度は 1000 です。
複数の束縛がある場合、優先度が最も高いものが採用されます。
mkForce は優先度を 50 に設定する関数です。具体的に優先度を設定したい場合は、 [mkOverride](https://nixos.org/nixos/manual/#sec-option-definitions-setting-priorities) が使えます。

services.openssh.permitRootLogin には、モジュール``amazon-image.nix``で値が束縛されているので、 mkForce を利用する必要があります。

``pkgs.lib`` を書く手間を減らしたい場合は、 ``with`` を用いた書き方が可能です。詳しくは [configuration.nix ファイル](#configuration.nix ファイル)を見てください。

## モジュールの引数

今回はファイル自体で設定を完結させるため、自分でパッケージコレクションを指定しました。しかし NixOS がデフォルトで提供するものを使用することも可能です。
configuration.nix はそれ自体モジュールであり、 nixos-rebuid は pkg, lib, config などの引数を与えて評価します。
configuration.nix の冒頭を、 ``let ... in`` で始めるのではなく、以下のように記述することで簡潔に記述をすることが可能になります。

~~~
{ pkgs, lib, ... }:
~~~

## EC2 ユーザデータとして configuration.nix を渡す

今回作成した configuration.nix は、 EC2 起動時にユーザデータとして渡すことで、初めから指定の構成の環境を立ち上げることが可能です。ただし、 git を利用した記述は、デフォルトの構成では git が含まれていないため動きません。モジュール引数を用いた記述を行いましょう。

# configuration.nix ファイル

最終的な configuration.nix ファイルの内容は以下のようになります。
主要な設定項目は以下の通りです。

+ パッケージコレクションの指定とパッケージのインストール
+ rsyslog の設定
+ ユーザの追加、および sudo, sshd など権限まわりの設定
+ nginx, phpfpm, ファイアウォールの設定

この記述を利用する場合、ユーザ名と SSH 公開鍵については、自分自身で書き換えて使用してください。``!TODO!`` とコメントにある行がそれです。

~~~
let 
  pkgsSrc = (builtins.fetchGit {
    url = https://github.com/nixos/nixpkgs;
    ref = "release-19.03";
  });
  pkgs = import pkgsSrc {};
  lib = pkgs.lib;
in
with lib;
{
  imports = [ <nixpkgs/nixos/modules/virtualisation/amazon-image.nix> ];
  ec2.hvm = true;

  nix.gc = {
    automatic = true;
    dates = "19:30";
  };

  environment.systemPackages = with pkgs; [
    rsyslog
    git vim file screen shellcheck
    nginx php73
  ];
 
  services.rsyslogd = {
    enable = true;
    extraConfig = ''
      *.*;		-/var/log/all
    '';
  };

  ################################################
  services.openssh = mkForce {
    permitRootLogin = "no";
    passwordAuthentication = false;
  };

  security.sudo.wheelNeedsPassword = false; 

  users.users.username = {           # !TODO! username にユーザ名をセット
    isNormalUser = true;
    extraGroups = [ "wheel" ];
    openssh.authorizedKeys.keys = [
      "ssh-rsa ..." ];               # !TODO! 使用する SSH 公開鍵をセット
  };

  ################################################
  networking.firewall.allowedTCPPorts = [ 80 443 ];

  services.phpfpm = {
    poolConfigs."default" = ''
        listen = /var/run/default-phpfpm.sock
        user = nginx
        group = nginx
        pm = dynamic
        pm.max_children = 32
        pm.max_requests = 500
        pm.start_servers = 2
        pm.min_spare_servers = 2
        pm.max_spare_servers = 5
        listen.owner = nginx
        listen.group = nginx
        php_admin_value[error_log] = 'stderr'
        php_admin_flag[log_errors] = on
        env[PATH] = ${lib.makeBinPath [ pkgs.php ]}
        catch_workers_output = yes
        ;; security.limit_extensions =  ;; to accept *.html file
    '';
  };

  services.nginx = {
    enable = true;
    virtualHosts."_".locations."/" = {
      root = "/var/www/default";
      extraConfig = ''
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_index index.php;
        fastcgi_pass unix:/var/run/default-phpfpm.sock;
        include ${pkgs.nginx}/conf/fastcgi_params;
        include ${pkgs.nginx}/conf/fastcgi.conf;
      '';
    };
  };
}
~~~