Le Blog Utux

HTTP 200 GET /

De Kubernetes à NixOS

Rédigé par uTux 2 commentaires

Il y a un an et demi, j'annonçais avoir migré mon blog sur Kubernetes (K3s). Bien que je sois globalement satisfait du résultat et que cela m'a permis de beaucoup apprendre, je rencontre tout de même des désagréments :

  • Kubernetes, c'est lourd. Comptez 1 GB de RAM à vide. Pour être à l'aise, j'ai du prendre un VPS avec 4GB de RAM, là où 2 suffiraient largement sans Kubernetes.
  • Absence de support d'IPv6. En 2022 c'est franchement difficile à justifier.
  • La gestion des Ingress est complexe. J'utilise Traefik mais c'est compliqué, il m'a fallu quasiment 2 jours pour faire fonctionner les certificats ACME, et les montées de version ne se passent pas toujours très bien.
  • Comme prévu, les mises jour à demandent un peu plus d'efforts car les composants ne sont pas gérés par la distribution. Il faut donc:
    • Mettre K3s à jour régulièrement.
    • Reconstruire et livrer les images même si l'application n'a pas changé, juste pour être sûr de ne pas trimbaler de failles de sécurité dans les librairies.
    • Mettre à jour Traefik régulièrement.

Bref, même si je trouve que Kubernetes est une super solution qui a mis en valeur les containers et l'écosystème Linux, c'est clairement overkill pour moi et je souhaite me simplifier la vie. Et si au passage je peux me former à de nouvelles technos, c'est un plus.

La solution de facilité aurait été d'installer Debian et de coder quelques roles Ansible mais l'inconvénient est que je n'apprendrais rien de nouveau. Je suis donc parti sur NixOS. C'est une distribution où la configuration s'effectue de manière déclarative via le langage Nix, avec gestion des états et possibilité de rollback.

En effet lorsque vous gérez votre distribution Linux avec Ansible, Puppet ou Salt, vous pouvez déployer une configuration et des applications, en revanche le retour en arrière n'est pas géré. Par exemple si vous avez déployé Apache et que vous voulez changer pour Nginx, c'est à vous de coder la manière dont Ansible va désinstaller Apache, sinon il sera toujours présent. Avec NixOS, si vous retirez toute référence à httpd dans votre configuration, il ne sera plus du tout présent à la prochaine génération du système. C'est un fonctionnement atomic et stateful.

Pour l'installation, j'ai utilisé nixos-infect, un script à lancer sur la plupart des distributions Linux afin de les remplacer par NixOS. Associé à un cloud-init, il permet de faire le déploiement via Terraform de manière complètement automatique, ce qui est vraiment cool il faut le dire. Et pour les gens qui préfèrent une installation à la main, sachez que Hetzner proposer une ISO NixOS et un accès console distante même pour les VPS, c'est donc totalement possible :)

Pour la partie hébergement web, voici mon fichier de configuration /etc/nixos/web.nix :

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

let
  defaultVirtualHost = {
    documentRoot = "/var/www/html/blank";
    addSSL = true;
    forceSSL = false;
    sslServerKey = "/var/www/ssl/snakeoil.key";
    sslServerCert = "/var/www/ssl/snakeoil.pem";
    locations."/" = {
      index = "index.html";
    };
  };
  makeVirtualHost = webroot:
    { documentRoot = "${webroot}";
      forceSSL = true;
      enableACME = true;
      locations."/" = {
        index = "index.php";
      };
      extraConfig = ''
        <Directory "${webroot}">
          AllowOverride All
        </Directory>
      '';
    };
in {
  security = {
    acme = {
      email = "hostmaster@example.org";
      acceptTerms = true;
    };
  };
  services = {
    httpd = {
      enable = true;
      mpm = "prefork";
      adminAddr = "hostmaster@example.org";
      enablePHP = true;
      phpPackage = pkgs.php80;
      phpOptions = "display_errors = off";
      extraConfig = ''
        SetOutputFilter DEFLATE
        ServerSignature Off
        ServerTokens prod
        <FilesMatch ".(ico|pdf|jpg|jpeg|JPEG|png|gif|js|css)$">
          Header set Cache-Control "max-age=3600, public"
        </FilesMatch>
      '';
      extraModules = [
        "deflate"
      ];
      virtualHosts =
        {"_default_" = defaultVirtualHost;
         "utux.fr"   = (makeVirtualHost "/var/www/html/utux.fr");
        };
    };
  };
}

Explications : j'ai d'autres vhosts non mentionnés ici, et à chaque fois leur configuration est identique. Pour éviter d'avoir à répéter du code, j'utilise let (permet de définir des variables et des fonctions) ainsi que in (pour les appliquer). Je ne connais pas de moyen plus propre de faire des loop pour le moment, car c'est le but. Étant donné que j'utilise PluXml, il ne faut pas oublier le AllowOverride All qui permet aux .htaccess de fonctionner, sinon les fichiers XML qui contiennent les data deviennent accessible, ce qui fait fuiter pas mal d'infos sensibles.

Pour le moment, ça marche plutôt bien. Le serveur consomme 119 Mo de RAM, soit quasiment 10x moins que Kubernetes :) Et en prime le support de l'IPv6 est revenu.

En conclusion, je suis content de pouvoir enfin tester NixOS en production. Bien que je ne sois pas encore familier avec le langage Nix, et que je le trouve bien trop compliqué par rapport à un bon vieux Ansible, j'estime que c'est l'avenir de Linux sur serveur. Les distributions devront être atomiques, et leur configuration centralisée, versionnée et reproductible. J'espère que ceci est le début d'une aventure positive.

Thank you debops (advanced Ansible tricks)

Rédigé par uTux Aucun commentaire

I think debops developers are really good and I learn a lot of advanced stuff when I read their roles. I think they should be the reference for Ansible best practises. Here is 3 useful tricks that I did not know.

if in variables

From debops/ansible-docker:

docker__upstream: '{{ True
                      if (docker__distribution_release == "stretch")
                      else False }}'

Self explanatory.

Variables mapping

From debops/ansible-docker::

docker__upstream_arch_map:
  'x86_64': 'amd64'
  'armhf': 'armhf'

docker__upstream_repository: '{{ "deb [arch="
        + docker__upstream_arch_map[ansible_architecture]
        + "] https://download.docker.com/linux/" + docker__distribution|lower + " "
        + docker__distribution_release + " " + docker__upstream_channel }}'

The fact ansible_architecture is x86_64, so the variable docker__upstream_arch_map[ansible_architecture] will be resolve to amd64.

YAML to json j2

From debops/ansible-docker:

{{ docker__tpl_options | to_nice_json }}

In a variable file, simply set a YAML dictionnary, ie /defaults/main.yml:

docker__tpl_options:
  data-root: /var/lib/docker
  storage-driver: overlay2

In templates/daemon.j2:

{{ docker__tpl_options | to_nice_json }}

Thank you debops :)

Ansible filter by Azure tag

Rédigé par uTux Aucun commentaire

It took me some time to figure out how to use Azure tags as a filter for Ansible. Well it's not that hard, azure_rm.py will automatically generate groups based on tags with this pattern:

  • Tag: role:webserver
  • Becomes group: role_webserver

Yes, that's the trick, ":" becomes "_". Let's say you want to run a playbook only on machines tagged with role:webserver, just use the following command:

$ ansible-playbook -i azure_rm.py playbook.yml --limit role_webserver

Ansible Jinja loop all groups except

Rédigé par uTux Aucun commentaire

I want to generate a /usr/local/etc/backuppc/hosts with all hosts from the ansible inventory, but exclude a group.

Inventory:

[dbservers]
db1
db2
db3

[DisableBackup]
db3

hosts.j2:

# {{ ansible_managed }}

{% for hosts in groups['all'] | difference(groups['DisableBackup']) %}
{{ hosts }}
{% endfor %}

tasks/generate_hosts.yml:

  - name: Generate hosts file
    template:
      src: hosts.j2
      dest: "{{ backuppc__confdir }}/hosts"
      owner: "{{ backuppc__user }}"
      group: "{{ backuppc__group }}"
      mode: 0644
      force: yes

Run:

ansible-playbook -i inventory install_backuppc.yml

Result:

cat /usr/local/etc/backuppc/hosts
# Ansible managed
db1
db2

Source.

Ansible: récupérer les clés SSH depuis BitBucket

Rédigé par uTux Aucun commentaire

Je m'attaque aux API REST et en particulier celle de BitBucket pour y récupérer mes clés SSH que je déploie ensuite sur mes serveurs, le tout avec Ansible. Voilà un petit bout de code sur lequel j'ai passé une bonne soirée car autant c'est facile d'interroger l'API, autant ça l'est un peu moins de faire rentrer les bons champs dans des variables ;)

Pré requis:

  • Sur votre compte BitBucket
  • Settings > App passwords
  • Créez un password avec l'autorisation Account / read.

Allons-y (à mettre dans votre tasks/main.yml par exemple):

---

  - name: GET authorized_keys from REST API
    uri:
      url: "{{ authorized_keys.api_url }}"
      method: "{{ authorized_keys.api_method }}"
      user: "{{ authorized_keys.api_user }}"
      password: "{{ authorized_keys.api_password }}"
      force_basic_auth: "{{ authorized_keys.api_force_basic }}"
    register: authorized_keys__json_response
    delegate_to: localhost
    run_once: True
    # Delegate to localhost and run 1 time because
    # we don't need to query the API from each remote host

  - name: Set Keys as a Var
    set_fact:
      authorized_keys__keys: "{{ authorized_keys__json_response.json | json_query('values[*].key') }}"
    delegate_to: localhost
    run_once: True
    # Extract the fields 'key' in a variable

  - name: Concatenate Keys
    set_fact:
      authorized_keys__keys_concatenate: "{{ authorized_keys__keys | join('\n') }}"
    delegate_to: localhost
    run_once: True
    # Ansible authorized_keys module with 'exclusive' option requires all keys in one batch
    # separated by a newline \n

La variable authorized_keys__keys contient la liste des clés publiques. La variable authorized_keys__keys_concatenate n'est pas une liste mais un champ contenant toutes les clés séparées par un retour à la ligne et elle est utile si vous souhaitez utiliser le module authorized_keys avec exclusive: yes car ce dernier ne marche pas bien avec une liste, il faut donc lui fournir 1 seul "batch".

Complétez ensuite vos variables, par exemple:

# group_vars/all/vars.yml

authorized_keys:
  api_user: john
  api_url: "https://api.bitbucket.org/2.0/users/john/ssh-keys"
  api_method: GET
  api_force_basic: yes
  api_password: password créé dans l'interface bitbucket

Bien sûr en vrai on mettra le password dans un vault ;)

Et voilà, vous pouvez utiliser authorized_keys__keys ou authorized_keys__keys_concatenate pour la suite!

Fil RSS des articles de ce mot clé