slides.ivabus.dev/ru/nixos-not-a-luxury/slides.md

10 KiB
Raw Blame History

NixOS

Не роскошь, а новый подход к построению дистрибутивов Linux

Иван Бущик "ivabus", 2023

Что такое NixOS?

Декларативный, атомарный и воспроизводимый дистрибутив Linux

Создан на базе менеджера пакетов Nix и коллекции Nixpkgs

--

Что такое Nix?

Пакетный менеджер? Функциональный язык?

Всё и сразу!

--

О Nixpkgs

  • Две ветки (unstable и release)
  • 80000+ пакетов
  • Содержит "суть" NixOS - основные модули и опции
  • Не только Linux (Darwin)

--

Каналы Nixpkgs

  • Стабильные / нестабильные (23.11, 23.04, unstable)
  • По платформе (NixOS - nixos-*, Darwin - nixpkgs-*-darwin)
  • По времени обновления (обычные, small)

Nix как язык программирования

  • Полностью функциональный
  • Чистый и ленивый

--

Пример синтаксиса функций

add_a_b = { a ? 1, b ? 2 }: a + b
add_a_b { } # 3
add_a_b { a=5; } # 7

(x: x * 2) 3 # 6

Nix как пакетный менеджер

  • Использует Nixpkgs как "репозиторий"
  • При необходимости собирает пакет на месте
  • Является интерпретатором языка Nix
  • Написан на C++

--

Установка без установки

nix-shell -p <package>


О конфигурации NixOS

--

Система целиком описывается конфигурационным файлом(-ами).

Множество разных систем могут быть заданы через единую конфигурацию.

--

Что можно описывать конфигурацией

  • Установленное ПО
  • Настройки служб
  • Настройки сети
  • Параметры загрузчика / ядра
  • Пространство пользователя (home-manager)

--

Воспроизводимость

  • Система может быть собрано побитно идентично (почти) в разное время
  • Каждый пакет и инструкции к его сборке хешированы (/nix/store/zz....s1-curl-8.1.1)

--

Минимальная конфигурация NixOS

{
  boot.loader.grub.device = "/dev/sda";
  fileSystems."/".device = "/dev/sda1";
}

--

А если не минимально?

{ config, pkgs, ... }:
{
  imports = [ ./hardware-configuration.nix ];
  environment.systemPackages = with pkgs; [
    firefox
  ];
  services.xserver = {
    enable = true;
    displayManager.sddm.enable = true;
    desktopManager.plasma5.enable = true;
  };
  services.sshd.enable = true;
  users.mutableUsers = false;
  users.users.root = {
    hashedPassword = null;
    openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAaC1l....VfR user@host" ];
  };
}

--

Сеть

networking = {
  hostname = "hostname";
  useDHCP = true;
  nameservers =
    [ "1.0.0.1#cloudflare-dns.com" "8.8.8.8#dns.google" ];
};

services.resolved = {
  enable = true;
  dnssec = "false";
  extraConfig = ''
    DNSOverTLS=yes
  '';
};

services.avahi = {
  enable = true;
  nssmdns = true;
}

Модульность конфигурации

{ config, lib, pkgs, ... }:
let cfg = config.my.roles.server.nginx;
in {
  options.my.roles.server.nginx.enable =
    lib.mkEnableOption "Initial nginx setup";
  config = lib.mkIf (cfg.enable) {
    services.nginx = {
      enable = true;
      package = pkgs.nginxQuic;
      recommendedGzipSettings = true;
      recommendedOptimisation = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;
    };
    security.acme = { acceptTerms = true; defaults.email = "user@host"; };
    networking.firewall.allowedTCPPorts = [ 80 443 ];
    networking.firewall.allowedUDPPorts = [ 80 443 ];
  };
}

--

{ config, lib, pkgs, ... }:
let cfg = config.my.roles.server.ivabus-dev;
in {
  options.my.roles.server.ivabus-dev.enable =
    lib.mkEnableOption "Serve ivabus.dev";
  config = lib.mkIf (cfg.enable) {
    my.roles.server.nginx.enable = true;
    services.nginx = {
      virtualHosts."ivabus.dev" = {
        forceSSL = true;
        enableACME = true;
        http3 = true;
        root = pkgs.callPackage ../../pkgs/ivabus-dev.nix { };
        extraConfig = ''
          error_page 404 /404.html;
        '';
        serverAliases = [ "www.ivabus.dev" ];
      };
    };
  };
}

--

Корень конфигурации

{ config, pkgs, lib, secrets, ... }:

let
  my = import ../..;
in {
  imports = [ my.modules ./hardware.nix ];

  my.roles.server.ivabus-dev.enable = true;

  my.features.secrets = true;

  services.nginx = {
    virtualHosts."music.ivabus.dev" = {
      locations."/".proxyPass = "http://${secrets.maas-address}:4533";
      enableACME = true;
      forceSSL = true;
      http3 = true;
    };
  };
}


Упаковка сервиса на Rust

{ pkgs ? import <nixpkgs> { system = builtins.currentSystem; },
lib ? pkgs.lib, rustPlatform ? pkgs.rustPlatform,
fetchCrate ? pkgs.fetchCrate }:

rustPlatform.buildRustPackage rec {
  pname = "urouter";
  version = "0.6.0";

  src = fetchCrate {
    inherit pname version;
    sha256 = "sha256-KfoZ9NinD6PCL4M3U4sB8GHdbDLeRW7uFeQpGxmzJ90=";
  };

  cargoSha256 = "sha256-VfoF4hzWf5j2QtXyS/jFYCMfowl47YcAjxs2PV9C6oo=";

  nativeBuildInputs = [ pkgs.pkg-config ];
}

--

Создание удобных опций

{ config, lib, pkgs, ... }:
let
  cfg = config.my.roles.server.urouter;
  aliasFormat = pkgs.formats.json { };
in {
  options.my.roles.server.urouter = {
    enable = lib.mkEnableOption "Enable urouter";

    settings = lib.mkOption rec {
      type = aliasFormat.type;
      apply = lib.recursiveUpdate default;
      default = { alias = [ ]; };
      example = {
        alias = [
          {
            uri = "/";
            alias = { url = "https://someurl"; };
          }
          {
            uri = "/";
            alias = { file = "some_file"; };
            agent = { regex = "^curl/[0-9].[0-9].[0-9]$"; };
          }
        ];
      };
      description = lib.mdDoc ''
        alias.json configuration in Nix format.
      '';
    };

    dir = lib.mkOption {
      type = lib.types.str;
      default = "/var/urouter";
      example = "/home/user/urouter";
    };

    address = lib.mkOption {
      type = lib.types.str;
      default = "0.0.0.0";
      example = "0200::1";
    };

    port = lib.mkOption {
      type = lib.types.ints.u16;
      default = 8080;
      example = 80;
    };

    openFirewall = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = lib.mdDoc "Open TCP port in the firewall";
    };
  };
  config = lib.mkIf (cfg.enable) {
    networking.firewall.allowedTCPPorts =
      lib.mkIf cfg.openFirewall [ cfg.port ];

    systemd.services.urouter = {
      description = "urouter HTTP Service";
      after = [ "network.target" ];
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        ExecStart = ''
          ${
            pkgs.callPackage ../../pkgs/urouter.nix { }
          }/bin/urouter --alias-file-is-set-not-a-list --alias-file ${
            aliasFormat.generate "alias.json" cfg.settings
          } --dir ${cfg.dir} --address ${cfg.address} --port ${
            builtins.toString cfg.port
          }
        '';
        BindReadOnlyPaths = [ cfg.dir ];
      };
    };
  };
}

--

Использование

{ config, pkgs, lib, ... }:
rec {
  my.server.urouter = {
    enable = true;
    settings = {
      alias = [
        {
          uri = "/";
          alias = { url = "https://ivabus.dev"; };
        }
        {
          uri = "/";
          alias = { file = "dotfiles"; };
          agent = { regex = "^curl/[0-9].[0-9].[0-9]$"; };
        }
        {
          uri = "d";
          alias = { file = "dotfiles"; };
        }
      ];
    };
    dir = "/var/urouter";
    port = 8090;
    address = "127.0.0.1";
  };
  my.roles.server.nginx.enable = true;
  services.nginx.virtualHosts."iva.bz" = {
    locations."/".proxyPass = "http://${
      config.my.server.urouter.address}:${config.my.server.urouter.port}";
    enableACME = true;
    addSSL = true;
    http3 = true;
    serverAliases = [ "www.iva.bz" ];
  };
}

Хранение секретиков

{ config, ... }:
let
  canaryHash = builtins.hashFile "sha256" ./secrets/canary;
  expectedHash =
    "bc6f38a927602241c5e0996b61ebd3a90d5356ca76dc968ec14df3cd45c6612c";
in if (canaryHash != expectedHash && config.my.features.secrets) then
  abort
    "Secrets are enabled and not readable. Have you run `git-crypt unlock`?"
else {
  maas-address = builtins.readFile ./secrets/maas-address;
}

--

Взаимодействие с секретиками

{ config, lib, secrets, ... }:
let cfg = config.my.roles.yggdrasil-peer;
in {
  options.my.roles.yggdrasil-peer.enable = lib.mkEnableOption "Enable ...";
  config = lib.mkIf (cfg.enable) {
    my.features.secrets = lib.mkForce true;
    my.roles.yggdrasil-client.enable = true;
    services.yggdrasil = {
      settings = {
        Peers = lib.mkForce [];
        Listen = [
          "quic://[::]:60003?password=${secrets.yggdrasil-password}"
          "tls://[::]:60002?password=${secrets.yggdrasil-password}"
        ];
      };
    };
    networking.firewall.allowedTCPPorts = [ 60002 ];
    networking.firewall.allowedUDPPorts = [ 60003 ];
  };
}


Спасибо за внимание

Моя конфигурация

https://ивабус.рф/nixos