Contraintes de la plateforme¶
Les mécanismes qui suivent sont tous documentés dans la spécification CWL, mais on ne pense que rarement à les utiliser d'emblée. Ce sont pourtant les causes d'échec les plus fréquentes sur la plateforme, et elles ont un point commun déroutant : le workflow reste valide, il échoue tard, et le message d'erreur ne désigne presque jamais la vraie cause.
AntFlow ne sait pas alimenter une entrée File¶
Entrées File¶
Un workflow dont une entrée est de type File est
du CWL parfaitement valide ; simplement, l'interface ne propose pas de téléverser un
fichier. Le workflow n'est donc pas lançable, alors que rien, ni dans le CWL ni à la
validation, ne signale quoi que ce soit.
La parade tient en un motif, le stage-in : l'entrée du workflow devient une string - un chemin S3, qu'AntFlow sait très bien transmettre - et une première étape se charge de la transformer en fichiers locaux.
inputs:
entree_path:
type: string
default: "s3://<votre-bucket>/donnees/entree.tif"
steps:
stage_in: # string -> Directory
run: stage_in.cwl
traitement: # consomme le répertoire
in:
entree: stage_in/donnees
Le CommandLineTool du traitement reçoit un vrai répertoire : la contrainte est absorbée
par l'étape de stage-in, sans contaminer le reste du workflow.
Exemple de script de stage-in¶
Le script copie un fichier (s3://bucket/dossier/entree.tif) ou tout un
répertoire (s3://bucket/dossier/) dans le répertoire de sortie de l'étape.
#!/usr/bin/env python3
"""Stage-in : copie un fichier ou un répertoire S3 dans un répertoire local."""
import argparse
import json
from pathlib import Path
from urllib.parse import urlparse
def make_client(config_path):
import boto3 # importé ici : le reste du script reste testable sans boto3
# JSON aux clés du client boto3 : endpoint_url, region_name, aws_access_key_id…
cfg = json.loads(Path(config_path).read_text()) if config_path else {}
print(f"s3 endpoint={cfg.get('endpoint_url')}") # jamais le secret
return boto3.client("s3", **cfg)
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--entree_path", required=True, help="s3://bucket/fichier ou s3://bucket/dossier/")
p.add_argument("--s3-config", type=Path, help="fichier JSON de config S3")
p.add_argument("--output-dir", default="donnees")
a = p.parse_args()
u = urlparse(a.entree_path)
bucket, key = u.netloc, u.path.lstrip("/")
s3 = make_client(a.s3_config)
# Répertoire : toutes les clés sous le préfixe. Fichier : la clé elle-même.
prefix = key.rstrip("/") + "/"
pages = s3.get_paginator("list_objects_v2").paginate(Bucket=bucket, Prefix=prefix)
keys = [o["Key"] for page in pages for o in page.get("Contents", [])] or [key]
for k in keys:
dst = Path(a.output_dir) / (k[len(prefix):] if k.startswith(prefix) else Path(k).name)
dst.parent.mkdir(parents=True, exist_ok=True)
s3.download_file(bucket, k, str(dst))
print(f"ok s3://{bucket}/{k} -> {dst}")
if __name__ == "__main__":
main()
Plusieurs choix d'écriture le rendent exploitable par la plateforme, et méritent d'être repris dans vos propres scripts :
| Choix | Pourquoi |
|---|---|
Sortie dans --output-dir relatif |
Le CWL récupère les sorties dans le répertoire de travail de l'étape ; un chemin absolu écrirait en dehors et le fichier serait perdu. |
| Aucune erreur S3 rattrapée | Un téléchargement qui échoue arrête le script avec un code de retour non nul : c'est le seul signal d'échec que voit le moteur. |
| Endpoint affiché, secret jamais | Distingue « mauvais endpoint » de « mauvaise clé » dans les journaux du conteneur. |
| Config lue depuis un JSON | Le JSON est déposé par InitialWorkDirRequirement depuis un secret. |
import boto3 dans la fonction |
Le reste du script reste testable sans dépendance S3. |
Reste à décrire ce script en CWL. Le tool déclare en sortie le répertoire entier : un fichier seul, ou un format multi-fichiers comme le shapefile et ses annexes, y arrivent ensemble.
cwlVersion: v1.2
class: CommandLineTool
label: Stage-in depuis S3
baseCommand: [python3, /app/stage_in.py]
requirements:
DockerRequirement:
dockerPull: registry.gitlab.com/<groupe>/<projet>:0.1.0
NetworkAccess:
networkAccess: true # indispensable : l'étape sort du conteneur
InitialWorkDirRequirement:
listing:
- entryname: s3_config.json
entry: $(inputs.s3_config)
ResourceRequirement:
ramMin: 1024
ramMax: 1024
coresMin: 0.5 # attend surtout le réseau
coresMax: 1
inputs:
entree_path:
type: string
inputBinding:
prefix: --entree_path
s3_config:
type: string # alimenté par un secret
arguments:
- prefix: --s3-config
valueFrom: s3_config.json
- prefix: --output-dir
valueFrom: donnees
outputs:
donnees:
type: Directory
outputBinding:
glob: donnees
Si l'étape suivante attend un fichier unique, déclarez plutôt la sortie en type: File
avec glob: donnees/*.
Les secrets (S3 ou autre)¶
Une chaîne a souvent besoin d'identifiants : clés S3, jeton d'API, mot de passe. Ils ne doivent apparaître ni dans le code, ni dans l'image, ni dans le dépôt, ni dans une entrée ordinaire du workflow, qui les afficherait dans le formulaire et dans les journaux. AntFlow propose deux façons de les fournir :
| Coffre Keycloak | Variables CI/CD GitLab | |
|---|---|---|
| Qui crée le secret | chaque utilisateur, dans son profil AntFlow | le mainteneur du projet GitLab |
| Portée | les exécutions lancées par cet utilisateur | toutes les exécutions du projet |
| Arrivée dans le workflow | valeur d'une entrée déclarée secrète (cwltool:Secrets) |
variable d'environnement des étapes |
| Convient à | des identifiants propres à chaque utilisateur | des identifiants de service, communs au projet |
Depuis le coffre Keycloak : une entrée secrète¶
- Créez le secret dans votre coffre, depuis votre profil AntFlow (voir Manage workflow secrets).
-
Déclarez l'entrée comme secrète avec l'extension
cwltool:Secrets, au niveau du workflow : -
Au lancement, le formulaire propose pour cette entrée la liste des secrets de votre coffre. Vous choisissez un nom : AntFlow transmet la valeur correspondante au workflow, sans qu'elle apparaisse dans le formulaire ni dans les journaux.
$namespaces indispensable
Sans lui, cwltool:Secrets n'est pas résolu. À la validation locale, cwltool signale
Unknown hint http://commonwl.org/cwltool#Secrets : c'est attendu et sans
conséquence, le hint étant interprété par la plateforme.
Depuis GitLab : une variable d'environnement¶
- Créez une variable CI/CD dans le projet GitLab de la chaîne
(
Settings → CI/CD → Variables), avec un nom qui commence parCWL__(deux tirets bas) :CWL__AWS_CONFIG. - AntFlow retire le préfixe et l'injecte dans l'environnement de chaque étape :
CWL__AWS_CONFIGdevientAWS_CONFIG. Les variables sans ce préfixe restent dans GitLab. -
Lisez-la dans le script, comme n'importe quelle variable d'environnement :
En local, cwltool ne transmet pas l'environnement aux étapes : précisez-le avec
cwltool --preserve-environment AWS_CONFIG ….
Écrire un secret dans un fichier¶
Beaucoup de scripts attendent un fichier (s3_config.json) plutôt qu'une valeur. Deux
façons de le produire :
| Façon | Quand la choisir |
|---|---|
| Dans l'étape qui le consomme | une seule étape utilise le secret, qui arrive par une entrée |
Dans une étape dédiée, qui sort le fichier en File |
plusieurs étapes utilisent le secret, ou il arrive par une variable d'environnement |
Dans l'étape qui le consomme¶
InitialWorkDirRequirement écrit le fichier dans le répertoire de travail de l'étape,
juste avant son exécution, à partir de l'entrée secrète :
requirements:
InitialWorkDirRequirement:
listing:
- entryname: s3_config.json
entry: $(inputs.s3_config)
Rien ne circule entre les étapes : c'est la façon la plus simple.
Dans une étape dédiée¶
Une petite étape écrit le secret dans un fichier et le déclare en sortie File. Les
étapes suivantes le reçoivent comme n'importe quel fichier :
- id: make_secret
class: CommandLineTool
label: Écrit le secret S3 dans un fichier
requirements:
InitialWorkDirRequirement:
listing:
- entryname: s3_config.json
entry: $(inputs.s3_config)
baseCommand: ["true"] # le fichier est déjà écrit, rien à exécuter
inputs:
s3_config:
type: string # entrée déclarée dans cwltool:Secrets
outputs:
s3_config_file:
type: File
outputBinding:
glob: s3_config.json
- id: make_secret
class: CommandLineTool
label: Écrit la variable AWS_CONFIG dans un fichier
baseCommand: ["sh", "-c"]
arguments:
# échoue si la variable est absente, plutôt que de produire un fichier vide ;
# la valeur n'est jamais affichée
- valueFrom: >-
[ -n "$AWS_CONFIG" ] || { echo "AWS_CONFIG absente" >&2; exit 1; };
printf '%s' "$AWS_CONFIG" > s3_config.json
inputs: []
outputs:
s3_config_file:
type: File
outputBinding:
glob: s3_config.json
Dans le workflow, chaque étape qui en a besoin branche cette sortie :
steps:
make_secret:
run: "#make_secret"
in: {} # ou { s3_config: s3_config } depuis le coffre
out: [s3_config_file]
traitement:
run: "#traitement"
in:
s3_config: make_secret/s3_config_file
out: [produit]
Chaque étape tourne dans un pod distinct : le fichier passe donc par le volume partagé du workflow. Ne le déclarez jamais en sortie du workflow, sans quoi il serait publié avec les résultats.
Bonnes pratiques¶
- Jamais de secret en clair : ni dans le code, ni dans l'image, ni dans un
defaultdu CWL, ni dans une sortie du workflow. - Un secret par usage, avec un nom explicite :
s3-cs-group,oidc-catalogue. - Affichez le contexte, jamais la valeur : l'endpoint S3 dans les journaux, pas la clé.
- Le secret protège la valeur en amont (formulaire, journaux) ; il ne chiffre pas le fichier une fois déposé dans l'étape.
Dans la documentation AntFlow
- Manage workflow secrets : enregistrer un secret dans votre coffre et le choisir à l'exécution
- Use a secret input : déclarer et utiliser une entrée secrète dans un workflow
ResourceRequirement - mémoire et CPU¶
Si votre traitement charge des données volumineuses en mémoire, cette section est sans doute la plus utile de la page. Faute de déclaration, l'étape hérite de la limite par défaut du cluster, souvent basse ; dès que le traitement la dépasse, le noyau tue le pod sans sommation (OOMKilled).
Le symptôme a de quoi égarer : aucune trace Python, aucun message applicatif, juste une étape qui se termine sur un code non nul. Rien n'indique que la mémoire est en cause, et le même traitement passe sans difficulté sur votre poste, où il dispose de toute la RAM de la machine.
requirements:
ResourceRequirement:
ramMin: 1024 # Mo - mémoire requise pour démarrer
ramMax: 3026 # Mo - plafond alloué à l'étape
coresMin: 1
coresMax: 2
| Champ | Rôle |
|---|---|
Min |
Conditionne la planification : la plateforme attend un nœud avec au moins cela de libre. Le fixer haut retarde le démarrage sans rien accélérer. |
Max |
Plafond alloué. C'est lui qui protège de l'OOM - et lui qu'il faut relever quand un pod est tué. |
D'où l'asymétrie à viser : un Min modeste pour être planifié rapidement, un Max
confortable pour ne pas être tué en cours de route. Notez que les cœurs acceptent des
valeurs fractionnaires (coresMin: 0.5), ce qui est tout indiqué pour une étape qui passe
l'essentiel de son temps à attendre le réseau.
Comment choisir ces valeurs. Ce sont des ordres de grandeur, pas une mesure. La règle
empirique : comptez plusieurs fois la taille du jeu de données
sur disque. Les données compressées ou sérialisées occupent couramment 5 à 10 fois leur
poids en fichier une fois chargées en mémoire (raster décompressé, géométries vectorielles
désérialisées), et le traitement lui-même demande sa propre marge. En cas de doute, voyez
large sur le Max : sur-réserver coûte peu, sous-réserver fait tuer le pod.
Pour mesurer plutôt qu'estimer :
docker run --rm -v "$PWD/data:/data:ro" <image> \
/usr/bin/time -v python3 /app/mon_traitement.py /data/entree.tif sortie.tif \
2>&1 | grep 'Maximum resident'
Maximum resident set size donne le pic réel en kilo-octets ; prenez environ le double
pour ramMax.