Gérer une infrastructure avec Terraform, CDKTF et NixOS

Vincent Bernat

Il y a quelques an­nées, j’ai ré­duit mon in­fra­struc­ture per­son­nelle au strict mi­ni­mum. Jusqu’en 2018, elle re­pré­sen­tait une dou­zaine de conte­neurs tour­nant sur un seul ser­veur Hetz­ner1. J’ai mi­gré mon cour­riel vers Fast­mail et mes zones DNS vers Gandi. Il ne me res­tait plus que mon blog à hé­ber­ger. À ce jour, ma pe­tite in­fra­struc­ture est com­po­sée de 4 ma­chines vir­tuelles exé­cu­tant NixOS sur Hetz­ner Cloud et Vultr, d’une poi­gnée de zones DNS sur Gandi et Route 53, et de quelques dis­tri­bu­tions Cloud­Front. Elle est gé­rée par CDK pour Ter­ra­form (CDKTF), tan­dis que les dé­ploie­ments de NixOS sont gé­rés par NixOps.

Dans cet ar­ticle, je pré­sente briè­ve­ment Ter­ra­form, CDKTF et l’éco­sys­tème Nix. J’ex­plique éga­le­ment com­ment uti­li­ser Nix pour ac­cé­der à ces ou­tils dans votre shell afin de les uti­li­ser ra­pidement.

Mise à jour (10.2025)

HashiCorp a opté pour la Bu­si­ness Source Li­cense pour tous ces lo­gi­ciels. C’est une dé­cep­tion, no­tam­ment pour Ter­ra­form qui est un com­po­sant clé pour au­to­ma­ti­ser les in­fra­struc­tures et qui a bé­né­fi­cié d’une large com­mu­nauté. Il existe dé­sor­mais un fork com­mu­nau­taire, OpenTofu, mais ce­lui-ci ne couvre pas CDKTF. Le pro­jet a in­di­qué qu’il n’avait pas le temps pour tra­vailler sur la com­pa­ti­bi­lité avec CDKTF.

Mise à jour (12.2025)

Le dé­ve­lop­pe­ment de CDKTF s’est ar­rêté. Cela m’agace pro­di­gieu­se­ment. Je sup­pose que je vais de­voir re­gar­der de nou­veau vers Pulumi.

Mise à jour (10.2026)

Fi­na­le­ment, je suis passé à OpenTofu et à CDK Ter­rain, un fork com­mu­nau­taire de CDKTF.

CDKTF : infrastructure en tant que code#

Ter­ra­form est un ou­til d’« in­fra­struc­ture en tant que code ». Vous pou­vez dé­fi­nir votre in­fra­struc­ture en dé­cla­rant des res­sources avec le lan­gage HCL. Ce der­nier pos­sède quelques fonc­tion­na­li­tés sup­plé­men­taires, comme des boucles per­met­tant de dé­cla­rer plu­sieurs res­sources à par­tir d’une liste, des fonc­tions in­té­grées que vous pou­vez ap­pe­ler dans les ex­pres­sions et l’ex­pan­sion de va­riables dans les chaînes de ca­rac­tères. Ter­ra­form s’ap­puie sur un large en­semble de four­nis­seurs pour gé­rer les res­sources.

Gérer des serveurs#

Voici un court exemple uti­li­sant le four­nis­seur pour Hetz­ner Cloud pour créer une ma­chine virtuelle :

variable "hcloud_token" {
  sensitive = true
}
provider "hcloud" {
  token = var.hcloud_token
}

resource "hcloud_server" "web03" {
  name = "web03"
  server_type = "cpx11"
  image = "debian-11"
  datacenter = "nbg1-dc3"
}

resource "hcloud_rdns" "rdns4-web03" {
  server_id = hcloud_server.web03.id
  ip_address = hcloud_server.web03.ipv4_address
  dns_ptr = "web03.luffy.cx"
}

resource "hcloud_rdns" "rdns6-web03" {
  server_id = hcloud_server.web03.id
  ip_address = hcloud_server.web03.ipv6_address
  dns_ptr = "web03.luffy.cx"
}

L’ex­pres­si­vité de HCL est as­sez li­mi­tée et je trouve qu’un lan­gage gé­né­ra­liste est plus pra­tique pour dé­crire les res­sources. C’est là qu’in­ter­vient CDK pour Ter­ra­form : vous pou­vez gé­rer votre in­fra­struc­ture à l’aide de votre lan­gage de pro­gram­ma­tion pré­féré, no­tam­ment Type­Script, Go et Py­thon. Voici l’exemple pré­cé­dent uti­li­sant CDKTF et TypeScript :

import { App, TerraformStack, Fn } from "cdktf";
import { HcloudProvider } from "./.gen/providers/hcloud/provider";
import * as hcloud from "./.gen/providers/hcloud";

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    const hcloudToken = new TerraformVariable(this, "hcloudToken", {
      type: "string",
      sensitive: true,
    });
    const hcloudProvider = new HcloudProvider(this, "hcloud", {
      token: hcloudToken.value,
    });

    const web03 = new hcloud.server.Server(this, "web03", {
      name: "web03",
      serverType: "cpx11",
      image: "debian-11",
      datacenter: "nbg1-dc3",
      provider: hcloudProvider,
    });
    new hcloud.rdns.Rdns(this, "rdns4-web03", {
      serverId: Fn.tonumber(web03.id),
      ipAddress: web03.ipv4Address,
      dnsPtr: "web03.luffy.cx",
      provider: hcloudProvider,
    });
    new hcloud.rdns.Rdns(this, "rdns6-web03", {
      serverId: Fn.tonumber(web03.id),
      ipAddress: web03.ipv6Address,
      dnsPtr: "web03.luffy.cx",
      provider: hcloudProvider,
    });
  }
}

const app = new App();
new MyStack(app, "cdktf-take1");
app.synth();

La com­mande de cdktf synth gé­nère un fi­chier de confi­gu­ra­tion pour Ter­ra­form, terraform plan pré­vi­sua­lise les chan­ge­ments et terraform apply les ap­plique. Main­te­nant que vous dis­po­sez d’un lan­gage gé­né­ra­liste, vous pou­vez uti­li­ser des fonctions.

Gérer des enregistrements DNS#

Si l’uti­li­sa­tion de CDKTF pour 4 ser­veurs web peut sem­bler un peu exa­gé­rée, il en va tout au­tre­ment lorsqu’il s’agit de gé­rer quelques zones DNS. Avec DNSControl, qui uti­lise Java­Script comme lan­gage, j’ai pu dé­fi­nir la zone bernat.ch avec ce bout de code :

D("bernat.ch", REG_NONE, DnsProvider(DNS_BIND, 0), DnsProvider(DNS_GANDI),
  DefaultTTL('2h'),
  FastMailMX('bernat.ch', {subdomains: ['vincent']}),
  WebServers('@'),
  WebServers('vincent');

Cela pro­duit 38 en­re­gis­tre­ments. Avec CDKTF, j’écris :

new Route53Zone(this, "bernat.ch", providers.aws)
  .sign(dnsCMK)
  .registrar(providers.gandiVB)
  .www("@", servers)
  .www("vincent", servers)
  .www("media", servers)
  .fastmailMX(["vincent"]);

Toute la ma­gie est si­tuée dans les fonc­tions ap­pe­lées. Vous pou­vez re­gar­der le fi­chier dns.ts dans le dé­pôt cdktf-take1 pour com­prendre com­ment cela fonc­tionne. Ra­pidement :

  • Route53Zone() crée une zone sur Route 53 ;
  • sign() signe la zone avec une clé de si­gnature ;
  • registrar() ins­crit la zone au­près du re­gistre de do­maines et confi­gure DNSSEC ;
  • www() crée les en­re­gis­tre­ments A et AAAA pour les ser­veurs web ;
  • fastmailMX() crée les en­re­gis­tre­ments MX et d’autres en­re­gis­tre­ments liés pour confi­gu­rer Fast­mail en tant que four­nis­seur de courriel.

Voici le contenu de la fonc­tion fastmailMX(). Elle gé­nère quelques en­re­gis­tre­ments et re­tourne la zone en cours pour fa­ci­li­ter le chaînage :

fastmailMX(subdomains?: string[]) {
  (subdomains ?? [])
    .concat(["@", "*"])
    .forEach((subdomain) =>
      this.MX(subdomain, [
        "10 in1-smtp.messagingengine.com.",
        "20 in2-smtp.messagingengine.com.",
      ])
    );
  this.TXT("@", "v=spf1 include:spf.messagingengine.com ~all");
  ["mesmtp", "fm1", "fm2", "fm3"].forEach((dk) =>
    this.CNAME(`${dk}._domainkey`, `${dk}.${this.name}.dkim.fmhosted.com.`)
  );
  this.TXT("_dmarc", "v=DMARC1; p=none; sp=none");
  return this;
}

Je vous en­cou­rage à par­cou­rir le dé­pôt pour plus de détails !

À propos de Pulumi#

Ma pre­mière ten­ta­tive au­tour de Ter­ra­form a été d’uti­li­ser Pu­lumi. Vous pou­vez trou­ver cette ten­ta­tive sur GitHub. C’est as­sez si­mi­laire à ce que je fais ac­tuel­le­ment avec CDKTF. La prin­ci­pale dif­fé­rence est que j’uti­lise Py­thon au lieu de Type­Script2 car ce der­nier ne m’était pas fa­mi­lier à l’époque.

Pu­lumi est an­té­rieur à CDKTF et il uti­lise une ap­proche lé­gè­re­ment dif­fé­rente. CDKTF gé­nère une confi­gu­ra­tion pour Ter­ra­form (au for­mat JSON au lieu de HCL), lais­sant la pla­ni­fi­ca­tion, la ges­tion des états et le dé­ploie­ment à ce der­nier. Il est donc lié aux li­mites de ce qui peut être ex­primé par Ter­ra­form, no­tam­ment lorsque vous de­vez trans­for­mer des don­nées ob­te­nues d’une res­source à une autre3. Pu­lumi a be­soin de four­nis­seurs spé­ci­fiques pour chaque res­source. De nom­breux four­nis­seurs en­cap­sulent des four­nis­seurs Terraform.

Bien que Pu­lumi offre une bonne ex­pé­rience uti­li­sa­teur, je suis passé à CDKTF car écrire des four­nis­seurs pour Pu­lumi est une cor­vée. CDKTF ne né­ces­site pas de pas­ser par cette étape. En de­hors des grands ac­teurs (AWS, Azure et Google Cloud), l’exis­tence, la qua­lité et la fraî­cheur des four­nis­seurs Pu­lumi sont in­égales. La plu­part des four­nis­seurs s’ap­puient sur un four­nis­seur Ter­ra­form et il se peut qu’ils soient en re­tard de quelques ver­sions, qu’il leur manque quelques res­sources ou qu’ils pré­sentent des bogues qui leur sont propres.

Lorsqu’un four­nis­seur n’existe pas, vous pou­vez en écrire un à l’aide de la bi­blio­thèque pu­lumi-ter­ra­form-bridge. Le pro­jet Pu­lumi four­nit un mo­dèle à cet ef­fet. J’ai eu une mau­vaise ex­pé­rience avec ce­lui-ci lors de l’écri­ture de four­nis­seurs pour Gandi et Vultr : le Makefile ins­talle au­to­ma­ti­que­ment Pu­lumi en uti­li­sant curl | sh et ne fonc­tionne pas avec /bin/sh. Il y a un manque d’in­té­rêt pour les contri­bu­tions com­mu­nau­taires4 ou même pour les four­nis­seurs pour les ac­teurs tiers.

NixOS & NixOps#

Nix est un lan­gage de pro­gram­ma­tion pu­re­ment fonc­tion­nel. Nix est aussi le nom du ges­tion­naire de pa­quets qui est construit au-des­sus du lan­gage Nix. Il per­met aux uti­li­sa­teurs d’ins­tal­ler des pa­quets de ma­nière dé­cla­ra­tive. nixpkgs est un dé­pôt de pa­quets. Vous pou­vez ins­tal­ler Nix au-des­sus d’une dis­tri­bu­tion Li­nux or­di­naire. Si vous vou­lez plus de dé­tails, une bonne res­source est le site of­fi­ciel, no­tam­ment la sec­tion « Ap­prendre ». La courbe d’ap­pren­tis­sage est rude, mais la ré­com­pense est grande.

NixOS : distribution Linux déclarative#

NixOS est une dis­tri­bu­tion Li­nux construite au-des­sus du ges­tion­naire de pa­quets Nix. Voici un bout de confi­gu­ra­tion pour ajou­ter quelques paquets :

environment.systemPackages = with pkgs;
  [
    bat
    htop
    liboping
    mg
    mtr
    ncdu
    tmux
  ];

Il est pos­sible de mo­di­fier une dé­ri­va­tion5 exis­tante pour uti­li­ser une ver­sion dif­fé­rente, ac­ti­ver une fonc­tion­na­lité spé­ci­fique ou ap­pli­quer un cor­rec­tif. Voici com­ment j’ac­tive et confi­gure Nginx pour désac­ti­ver le mo­dule stream, ajou­ter le mo­dule de com­pres­sion Brotli et ajou­ter le mo­dule d’ano­ny­mi­sa­tion des adresses IP. De plus, au lieu d’uti­li­ser OpenSSL 3, je conti­nue à uti­li­ser OpenSSL 1.16.

services.nginx = {
  enable = true;

  package = (pkgs.nginxStable.override {
    withStream = false;
    modules = with pkgs.nginxModules; [
      brotli
      ipscrub
    ];
    openssl = pkgs.openssl_1_1;
  });

Si vous avez be­soin d’ajou­ter cer­taines mo­di­fi­ca­tions, c’est éga­le­ment pos­sible. À titre d’exemple, voici com­ment j’ai cor­rigé en avance les failles de sé­cu­rité dé­cou­vertes en 2019 dans Nginx en at­ten­dant que cela soit cor­rigé dans NixOS7 :

services.nginx.package = pkgs.nginxStable.overrideAttrs (old: {
  patches = old.patches ++ [
    # HTTP/2: reject zero length headers with PROTOCOL_ERROR.
    (pkgs.fetchpatch {
      url = https://github.com/nginx/nginx/commit/dbdd[…].patch;
      sha256 = "a48190[…]";
    })
    # HTTP/2: limited number of DATA frames.
    (pkgs.fetchpatch {
      url = https://github.com/nginx/nginx/commit/94c5[…].patch;
      sha256 = "af591a[…]";
    })
    #  HTTP/2: limited number of PRIORITY frames.
    (pkgs.fetchpatch {
      url = https://github.com/nginx/nginx/commit/39bb[…].patch;
      sha256 = "1ad8fe[…]";
    })
  ];
});

Si cela vous in­té­resse, je­tez un coup d’œil à ma confi­gu­ra­tion re­la­ti­ve­ment ré­duite : common.​nix contient la confi­gu­ra­tion à ap­pli­quer sur tous les ser­veurs (SSH, uti­li­sa­teurs, pa­quets com­muns), web.​nix contient la confi­gu­ra­tion pour les ser­veurs web et isso.​nix exé­cute Isso dans un conte­neur systemd.

NixOps : outil de déploiement pour NixOS#

Sur un seul nœud, la confi­gu­ra­tion de NixOS se trouve dans le fi­chier /etc/​nixos/​configuration.​nix. Après l’avoir mo­di­fiée, vous de­vez exé­cu­ter la com­mande nixos-rebuild switch. Nix va cher­cher toutes les dé­pen­dances pos­sibles dans le cache bi­naire et construit le reste. Il crée une nou­velle en­trée dans le menu du char­geur de dé­mar­rage et ac­tive la nou­velle confi­guration.

Pour gé­rer plu­sieurs nœuds, il existe plu­sieurs op­tions, dont NixOps, de­ploy-rs, Col­mena et morph. Je ne les connais pas toutes, mais de mon point de vue, les dif­fé­rences ne sont pas si im­por­tantes. Il est éga­le­ment pos­sible de construire un tel ou­til soi-même car Nix four­nit les blocs de construc­tion les plus im­por­tants : nix build et nix copy. NixOps est l’un des pre­miers ou­tils pu­bliés mais je vous en­cou­rage à ex­plo­rer les al­ter­natives.

La confi­gu­ra­tion de NixOps est écrite avec le lan­gage Nix. Voici une confi­gu­ra­tion sim­pli­fiée pour dé­ployer znc01.​luffy.cx, web01.​luffy.cx et web02.​luffy.cx, à l’aide des fonc­tions server et web :

let
  server = hardware: name: imports: {
    deployment.targetHost = "${name}.luffy.cx";
    networking.hostName = name;
    networking.domain = "luffy.cx";
    imports = [ (./hardware/. + "/${hardware}.nix") ] ++ imports;
  };
  web = hardware: idx: imports:
    server hardware "web${lib.fixedWidthNumber 2 idx}" ([ ./web.nix ] ++ imports);
in {
  network.description = "Luffy infrastructure";
  network.enableRollback = true;
  defaults = import ./common.nix;
  znc01 = server "exoscale" [ ./znc.nix ];
  web01 = web "hetzner" 1 [ ./isso.nix ];
  web02 = web "hetzner" 2 [];
}

Connecter le tout avec Nix#

L’éco­sys­tème Nix est une so­lu­tion uni­fiée aux dif­fé­rents pro­blèmes liés à la ges­tion des lo­gi­ciels et des confi­gu­ra­tions. Les en­vi­ron­ne­ments de dé­ve­lop­pe­ment dé­cla­ra­tifs et re­pro­duc­tibles en consti­tuent une ca­rac­té­ris­tique très in­té­res­sante. Cela res­semble aux en­vi­ron­ne­ments vir­tuels de Py­thon, mais ils ne sont pas spé­ci­fiques à un langage.

Courte introduction aux « flakes » Nix#

J’uti­lise les « flakes », une nou­velle fonc­tion­na­lité de Nix qui amé­liore la re­pro­duc­ti­bi­lité en fixant toutes les dé­pen­dances et en iso­lant le pro­ces­sus de construc­tion. Bien que cette fonc­tion­na­lité soit mar­quée comme ex­pé­ri­men­tale8, elle est de plus en plus po­pu­laire et vous pou­vez trou­ver flake.​nix et flake.​lock à la ra­cine de cer­tains dépôts.

A titre d’exemple, voici le contenu du flake.​nix li­vré avec Snimpy, un ou­til SNMP in­ter­ac­tif pour Py­thon re­po­sant sur libsmi, une bi­blio­thèque C :

{
  inputs = {
    nixpkgs.url = "nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, ... }@inputs:
    inputs.flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = inputs.nixpkgs.legacyPackages."${system}";
      in
      {
        # nix build
        packages.default = pkgs.python3Packages.buildPythonPackage {
          name = "snimpy";
          src = self;
          preConfigure = ''echo "1.0.0-0-000000000000" > version.txt'';
          checkPhase = "pytest";
          checkInputs = with pkgs.python3Packages; [ pytest mock coverage ];
          propagatedBuildInputs = with pkgs.python3Packages; [ cffi pysnmp ipython ];
          buildInputs = [ pkgs.libsmi ];
        };
        # nix run + nix shell
        apps.default = { 
          type = "app";
          program = "${self.packages."${system}".default}/bin/snimpy";
        };
        # nix develop
        devShells.default = pkgs.mkShell {
          name = "snimpy-dev";
          buildInputs = [
            self.packages."${system}".default.inputDerivation
            pkgs.python3Packages.ipython
          ];
        };
      });
}

Si Nix est ins­tallé sur votre système :

  • nix run github:​vincentbernat/​snimpy exé­cute Snimpy ;
  • nix shell github:​vincentbernat/​snimpy four­nit un shell avec Snimpy prêt à être utilisé ;
  • nix build github:​vincentbernat/​snimpy construit le pa­quet Python ;
  • nix develop . four­nit un shell pour dé­ve­lop­per au­tour de Snimpy de­puis un clône du dépôt9.

Pour plus d’in­for­ma­tions sur les flakes, re­gar­dez le tu­to­riel de Tweag.

Nix et CDKTF#

A la ra­cine du dé­pôt que j’uti­lise pour CDKTF, il y a un fi­chier flake.​nix pour confi­gu­rer un shell avec Ter­ra­form et CDKTF ins­tal­lés et avec les va­riables d’en­vi­ron­ne­ment né­ces­saires pour au­to­ma­ti­ser mon in­fra­structure.

Ter­ra­form est déjà pré­sent dans nixpkgs, mais je dois ap­pli­quer une rus­tine sur le four­nis­seur Gandi. Ce n’est pas un pro­blème avec Nix !

terraform = pkgs.terraform.withPlugins (p: [
  p.aws
  p.hcloud
  p.vultr
  (p.gandi.overrideAttrs
    (old: {
      src = pkgs.fetchFromGitHub {
        owner = "vincentbernat";
        repo = "terraform-provider-gandi";
        rev = "feature/livedns-key";
        hash = "sha256-V16BIjo5/rloQ1xTQrdd0snoq1OPuDh3fQNW7kiv/kQ=";
      };
    }))
]);

CDKTF est écrit en Type­Script. J’ai un fi­chier package.​json avec toutes les dé­pen­dances né­ces­saires, y com­pris celles pour uti­li­ser Type­Script comme lan­gage cible :

{
  "name": "cdktf-take1",
  "version": "1.0.0",
  "main": "main.js",
  "types": "main.ts",
  "private": true,
  "dependencies": {
    "@types/node": "^14.18.30",
    "cdktf": "^0.13.3",
    "cdktf-cli": "^0.13.3",
    "constructs": "^10.1.151",
    "eslint": "^8.27.0",
    "prettier": "^2.7.1",
    "ts-node": "^10.9.1",
    "typescript": "^3.9.10",
    "typescript-language-server": "^2.1.0"
  }
}

J’uti­lise Yarn pour ob­te­nir un fi­chier yarn.​lock qui peut en­suite être uti­lisé di­rec­te­ment pour construire la dé­ri­va­tion conte­nant toutes les dé­pendances :

nodeEnv = pkgs.mkYarnModules {
  pname = "cdktf-take1-js-modules";
  version = "1.0.0";
  packageJSON = ./package.json;
  yarnLock = ./yarn.lock;
};

L’étape sui­vant est de gé­né­rer les four­nis­seurs CDKTF à par­tir des four­nis­seurs Ter­ra­form et de les in­clure dans une dé­rivation :

cdktfProviders = pkgs.stdenvNoCC.mkDerivation {
  name = "cdktf-providers";
  nativeBuildInputs = [
    pkgs.nodejs
    terraform
  ];
  src = nix-filter {
    root = ./.;
    include = [ ./cdktf.json ./tsconfig.json ];
  };
  buildPhase = ''
    export HOME=$(mktemp -d)
    export CHECKPOINT_DISABLE=1
    export DISABLE_VERSION_CHECK=1
    export PATH=${nodeEnv}/node_modules/.bin:$PATH
    ln -nsf ${nodeEnv}/node_modules node_modules

    # Build all providers we have in terraform
    for provider in $(cd ${terraform}/libexec/terraform-providers; echo */*/*/*); do
      version=''${provider##*/}
      provider=''${provider%/*}
      echo "Build $provider@$version"
      cdktf provider add --force-local $provider@$version | cat
    done
    echo "Compile TS → JS"
    tsc
  '';
  installPhase = ''
    mv .gen $out
    ln -nsf ${nodeEnv}/node_modules $out/node_modules
  '';
};

En­fin, nous dé­fi­nis­sons l’en­vi­ron­ne­ment de dé­ve­loppement :

devShells.default = pkgs.mkShell {
  name = "cdktf-take1";
  buildInputs = [
    pkgs.nodejs
    pkgs.yarn
    terraform
  ];
  shellHook = ''
    # No telemetry
    export CHECKPOINT_DISABLE=1
    # No autoinstall of plugins
    export CDKTF_DISABLE_PLUGIN_CACHE_ENV=1
    # Do not check version
    export DISABLE_VERSION_CHECK=1
    # Access to node modules
    export PATH=$PWD/node_modules/.bin:$PATH
    ln -nsf ${nodeEnv}/node_modules node_modules
    ln -nsf ${cdktfProviders} .gen

    # Credentials
    for p in \
      njf.nznmba.pbz/Nqzvavfgengbe \
      urgmare.pbz/ivaprag@oreang.pu \
      ihyge.pbz/ihyge@ivaprag.oreang.pu; do
        eval $(pass show $(echo $p | tr 'A-Za-z' 'N-ZA-Mn-za-m') | grep '^export')
    done
    eval $(pass show personal/cdktf/secrets | grep '^export')
    export TF_VAR_hcloudToken="$HCLOUD_TOKEN"
    export TF_VAR_vultrApiKey="$VULTR_API_KEY"
    unset VULTR_API_KEY HCLOUD_TOKEN
  '';
};

Les dé­ri­va­tions lis­tées dans buildInputs sont dis­po­nibles dans le shell fourni. Le contenu de shellHook est exé­cuté lors du dé­mar­rage du shell. Il éta­blit des liens sym­bo­liques pour rendre dis­po­nible l’en­vi­ron­ne­ment Java­Script construit à une étape pré­cé­dente, ainsi que les four­nis­seurs CDKTF gé­né­rés. Il ex­porte éga­le­ment toutes les in­for­ma­tions d’iden­ti­fi­cation10.

J’uti­lise éga­le­ment di­renv avec un fi­chier .envrc pour pas­ser au­to­ma­ti­que­ment dans l’en­vi­ron­ne­ment de dé­ve­lop­pe­ment. Cela per­met éga­le­ment à ce der­nier d’être dis­po­nible de­puis Emacs, no­tam­ment lors de l’uti­li­sa­tion de lsp-mode pour ob­te­nir les com­plé­tions. nix develop . per­met aussi d’ac­ti­ver ma­nuel­le­ment l’en­vi­ronnement.

J’uti­lise les com­mandes sui­vantes pour dé­ployer11 :

$ cdktf synth
$ cd cdktf.out/stacks/cdktf-take1
$ terraform plan --out plan
$ terraform apply plan
$ terraform output -json > ~-automation/nixops-take1/cdktf.json

La der­nière com­mande pro­duit un fi­chier JSON conte­nant les don­nées né­ces­saires pour fi­nir le dé­ploie­ment avec NixOps.

NixOps#

Le fi­chier JSON ex­porté par Ter­ra­form contient la liste des ser­veurs avec quelques at­tributs :

{
  "hardware": "hetzner",
  "ipv4Address": "5.161.44.145",
  "ipv6Address": "2a01:4ff:f0:b91::1",
  "name": "web05.luffy.cx",
  "tags": [
    "web",
    "continent:NA",
    "continent:SA"
  ]
}

Dans le fi­chier network.​nix, cette liste est im­por­tée et trans­for­mée en un en­semble d’at­tri­buts dé­cri­vant les ser­veurs. Une ver­sion sim­pli­fiée res­semble à cela :

let
  lib = inputs.nixpkgs.lib;
  shortName = name: builtins.elemAt (lib.splitString "." name) 0;
  domainName = name: lib.concatStringsSep "." (builtins.tail (lib.splitString "." name));
  server = hardware: name: imports: {
    networking = {
      hostName = shortName name;
      domain = domainName name;
    };
    deployment.targetHost = name;
    imports = [ (./hardware/. + "/${hardware}.nix") ] ++ imports;
  };
  cdktf-servers-json = (lib.importJSON ./cdktf.json).servers.value;
  cdktf-servers = map
    (s:
      let
        tags-maybe-import = map (t: ./. + "/${t}.nix") s.tags;
        tags-import = builtins.filter (t: builtins.pathExists t) tags-maybe-import;
      in
      {
        name = shortName s.name;
        value = server s.hardware s.name tags-import;
      })
    cdktf-servers-json;
in
{
  // […]
} // builtins.listToAttrs cdktf-servers

Pour web05, on ob­tient ceci :

web05 = {
  networking = {
    hostName = "web05";
    domainName = "luffy.cx";
  };
  deployment.targetHost = "web05.luffy.cx";
  imports = [ ./hardware/hetzner.nix ./web.nix ];
};

Comme pour CDKTF, à la ra­cine du dé­pôt que j’uti­lise pour NixOps, il y a un fi­chier flake.​nix pour four­nir un shell avec NixOps confi­guré. Comme NixOps ne sup­porte pas les dé­ploie­ments pro­gres­sifs, j’uti­lise gé­né­ra­le­ment ces com­mandes pour dé­ployer sur un unique serveur12 :

$ nix flake update
$ nixops deploy --include=web04
$ ./tests web04.luffy.cx

Si les tests se dé­roulent sans sou­cis, je dé­ploie les autres nœuds un par un avec la com­mande suivante :

$ (set -e; for h in web{03..06}; do nixops deploy --include=$h; done)

La com­mande nixops deploy dé­ploie tous les ser­veurs en pa­ral­lèle et peut donc pro­vo­quer une panne si tous les ser­veurs Nginx sont in­dis­po­nibles au même moment.


Cet ar­ticle est en chan­tier de­puis trois ans. Le contenu a été mis à jour et af­finé au fur et à me­sure de mes ex­pé­ri­men­ta­tions. Il y a en­core beau­coup à ex­plo­rer13, mais j’es­time que le contenu est dé­sor­mais suf­fi­sant pour être publié ! 🎄