DevOps & Scaling

Intégrer CaptchaAI à Azure Functions pour une résolution CAPTCHA cloud

Résoudre des CAPTCHA sans faire tourner un serveur en permanence : c'est le cas d'usage idéal d'Azure Functions. Vous exposez la résolution comme un endpoint HTTP facturé à l'invocation, la clé API CaptchaAI reste dans Key Vault et Queue Storage absorbe les pics de volume — le tout dans la région Azure la plus proche de vos utilisateurs (France Central, West Europe).

L'intérêt est double : aucun microservice dédié à maintenir juste pour appeler l'API de résolution, et une supervision au même endroit que le reste de votre stack, dans Application Insights.


L'architecture en trois briques

Avant le code, gardez en tête les trois composants que vous allez assembler :

  • Une fonction HTTP qui reçoit la demande, appelle CaptchaAI et renvoie le token.
  • Key Vault pour stocker la clé API hors du code et des fichiers de configuration.
  • Queue Storage pour découpler la soumission de la résolution quand le volume monte.

Côté prérequis : un compte Azure actif, la CLI az et les Azure Functions Core Tools, un runtime Python 3.11, et une clé API CaptchaAI valide. Ni conteneur, ni VM à provisionner.


Exposer la résolution via un endpoint HTTP

La brique de base est une fonction déclenchée par une requête HTTP POST. Elle lit la méthode et ses paramètres dans le corps JSON, appelle l'API CaptchaAI, puis renvoie le token résolu. Toute la logique — soumission de la tâche puis interrogation du résultat (polling) — tient dans une fonction solve réutilisable que vous partagerez ensuite avec le déclencheur de file.

Concrètement, la fonction :

  • valide le corps de la requête et renvoie une erreur 400 si le JSON est absent ;
  • lit la clé API depuis une variable d'environnement, jamais en dur ;
  • interroge le résultat toutes les 5 secondes jusqu'à obtention du token ou expiration du délai.
# function_app.py
import json
import time
import os
import logging
import urllib.request
import urllib.parse
import azure.functions as func

app = func.FunctionApp()


@app.route(route="solve", methods=["POST"])
def solve_captcha(req: func.HttpRequest) -> func.HttpResponse:
    """HTTP trigger for CAPTCHA solving."""
    try:
        body = req.get_json()
    except ValueError:
        return func.HttpResponse(
            json.dumps({"error": "JSON body required"}),
            status_code=400,
            mimetype="application/json",
        )

    method = body.get("method", "userrecaptcha")
    params = body.get("params", {})
    api_key = os.environ["CAPTCHAAI_KEY"]

    try:
        token = solve(api_key, method, params)
        return func.HttpResponse(
            json.dumps({"token": token}),
            mimetype="application/json",
        )
    except Exception as e:
        logging.error(f"Solve failed: {e}")
        return func.HttpResponse(
            json.dumps({"error": str(e)}),
            status_code=500,
            mimetype="application/json",
        )


def solve(api_key, method, params, timeout=90):
    """Solve CAPTCHA via CaptchaAI API."""
    submit_data = urllib.parse.urlencode({
        "key": api_key,
        "method": method,
        "json": 1,
        **params,
    }).encode()

    req = urllib.request.Request(
        "https://ocr.captchaai.com/in.php",
        data=submit_data,
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        result = json.loads(resp.read())

    if result.get("status") != 1:
        raise RuntimeError(f"Submit error: {result.get('request')}")

    task_id = result["request"]

    start = time.time()
    while time.time() - start < timeout:
        time.sleep(5)
        poll_url = (
            f"https://ocr.captchaai.com/res.php"
            f"?key={api_key}&action=get&id={task_id}&json=1"
        )
        with urllib.request.urlopen(poll_url, timeout=15) as resp:
            data = json.loads(resp.read())

        if data["request"] != "CAPCHA_NOT_READY":
            if data.get("status") == 1:
                return data["request"]
            raise RuntimeError(f"Solve error: {data['request']}")

    raise TimeoutError("Solve timeout")

Le paramètre method accepte les valeurs habituelles de l'API : userrecaptcha pour reCAPTCHA v2 et v3, turnstile pour Cloudflare Turnstile, ou post pour un CAPTCHA image. La fonction reste donc générique quel que soit le type à résoudre.


Protéger la clé API avec Key Vault

Votre clé API ne doit jamais vivre dans le code ni dans un fichier de configuration versionné. Dans Azure, Key Vault est l'endroit prévu pour ça. La marche à suivre tient en trois gestes :

  • stocker le secret dans le coffre ;
  • activer une identité managée sur la fonction ;
  • accorder à cette identité la seule permission get.
# Create Key Vault
az keyvault create \
  --name captchaai-vault \
  --resource-group myResourceGroup

# Store secret
az keyvault secret set \
  --vault-name captchaai-vault \
  --name CaptchaAIKey \
  --value "YOUR_API_KEY"

# Grant function access
az webapp identity assign \
  --name my-captcha-function \
  --resource-group myResourceGroup

az keyvault set-policy \
  --name captchaai-vault \
  --object-id <principal-id> \
  --secret-permissions get

La référence @Microsoft.KeyVault(...) se pose ensuite directement dans les paramètres d'application. La fonction lit la clé au démarrage, sans que le secret n'apparaisse jamais en clair dans le portail ni dans vos logs :

CAPTCHAAI_KEY=@Microsoft.KeyVault(SecretUri=https://captchaai-vault.vault.azure.net/secrets/CaptchaAIKey/)

Absorber les pics de volume avec Queue Storage

Pour les charges importantes, découplez la soumission de la résolution. Une file Azure Queue Storage encaisse les tâches instantanément, et une fonction déclenchée par la file les traite à son rythme, sans jamais saturer l'endpoint HTTP ni bloquer l'appelant.

@app.queue_trigger(
    arg_name="msg",
    queue_name="captcha-tasks",
    connection="AzureWebJobsStorage",
)
def process_queue_task(msg: func.QueueMessage):
    """Process CAPTCHA task from queue."""
    task = json.loads(msg.get_body().decode())
    api_key = os.environ["CAPTCHAAI_KEY"]

    try:
        token = solve(api_key, task["method"], task["params"])
        logging.info(f"Task {task['id']} solved")

        # Store result in Table Storage or return queue
        _store_result(task["id"], "success", token)

    except Exception as e:
        logging.error(f"Task {task['id']} failed: {e}")
        _store_result(task["id"], "error", str(e))


def _store_result(task_id, status, value):
    """Store result (simplified — use Table Storage in production)."""
    logging.info(f"Result: {task_id} = {status}")

Un point à garder en tête pour dimensionner ce traitement : la facturation CaptchaAI se fait par thread simultané, pas par résolution. Le parallélisme réel est plafonné par votre allocation de threads — 5 avec le plan BASIC ($15/mois, 5 threads). Multiplier les instances Azure au-delà n'accélère rien.

Côté conformité, ne faites transiter dans la file que le strict nécessaire (sitekey, URL de page). Minimiser les données personnelles journalisées est une bonne pratique RGPD, surtout si vos traces Application Insights sont conservées longtemps.


Organiser les fichiers du projet

Un projet Azure Functions en Python reste volontairement plat. Quatre fichiers suffisent à décrire l'application :

captcha-function/
├── function_app.py
├── requirements.txt
├── host.json
└── local.settings.json

requirements.txt ne contient que la dépendance du runtime :

azure-functions

host.json fixe le comportement global. Le functionTimeout doit couvrir votre résolution la plus lente, polling compris ; deux minutes est un point de départ raisonnable :

{
  "version": "2.0",
  "functionTimeout": "00:02:00",
  "logging": {
    "logLevel": {
      "default": "Information"
    }
  }
}

local.settings.json ne sert qu'au développement local. Utilisez-y une clé de test distincte de votre clé de production :

{
  "IsEncrypted": false,
  "Values": {
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "CAPTCHAAI_KEY": "YOUR_API_KEY_FOR_LOCAL_DEV"
  }
}

Déployer la fonction sur Azure

Trois commandes suffisent : créer l'application, la publier, puis la tester avec un appel curl. Adaptez la région --consumption-plan-location à votre audience (francecentral ou westeurope) plutôt que de garder la valeur de l'exemple.

# Create function app
az functionapp create \
  --resource-group myResourceGroup \
  --consumption-plan-location westus2 \
  --runtime python \
  --runtime-version 3.11 \
  --functions-version 4 \
  --name my-captcha-solver \
  --storage-account mystorageaccount

# Deploy
func azure functionapp publish my-captcha-solver

# Test
curl -X POST https://my-captcha-solver.azurewebsites.net/api/solve \
  -H "Content-Type: application/json" \
  -d '{
    "method": "userrecaptcha",
    "params": {
      "googlekey": "SITE_KEY",
      "pageurl": "https://example.com"
    }
  }'

Une réponse contenant un champ token confirme que la chaîne fonctionne de bout en bout.


Alimenter la file d'attente

Depuis n'importe quel client Python, envoyez vos tâches dans la file. Chaque message est un petit JSON décrivant la méthode et ses paramètres ; la fonction de traitement s'occupe du reste.

from azure.storage.queue import QueueClient
import json

queue = QueueClient.from_connection_string(
    conn_str="YOUR_STORAGE_CONNECTION_STRING",
    queue_name="captcha-tasks",
)

# Submit batch
for i in range(10):
    task = {
        "id": f"task-{i}",
        "method": "userrecaptcha",
        "params": {
            "googlekey": "SITE_KEY",
            "pageurl": f"https://example.com/page{i}",
        },
    }
    queue.send_message(json.dumps(task))
    print(f"Queued task-{i}")

Consumption ou Premium : quel plan d'hébergement ?

Le choix du plan d'hébergement Azure conditionne votre latence et votre facture :

  • Consumption — facturation à l'usage, idéal pour les volumes faibles ou irréguliers. Contrepartie : des cold starts de quelques secondes après une période d'inactivité.
  • Premium — instances chaudes en permanence, cold starts éliminés et intégration VNET possible. Plus cher, mais justifié dès que la latence de la première requête compte.

Pour un solveur déclenché ponctuellement, Consumption suffit souvent. Basculez sur Premium si vos SLA n'admettent pas les cold starts ou si vous traitez un flux continu.


Dépannage

Problème Cause probable Correctif
La fonction expire avant la fin de la résolution functionTimeout trop court Augmentez functionTimeout dans host.json
Key Vault renvoie une valeur vide Identité managée ou policy absente Activez l'identité et accordez la permission get
Les messages sont rejoués indéfiniment Exception non gérée Interceptez les erreurs connues et journalisez
Le cold start ralentit les premières requêtes Plan Consumption + runtime Python Passez en Premium ou augmentez les workers

Questions fréquentes

Où placer la clé API CaptchaAI dans Azure ?

Dans Key Vault, jamais dans le code. La fonction lit le secret au démarrage via son identité managée, grâce à la référence @Microsoft.KeyVault(...) posée dans les paramètres d'application.

Comment limiter l'effet du cold start sur la résolution ?

Le cold start ajoute quelques secondes à la première invocation après une inactivité — c'est le compromis du plan Consumption. Le plan Premium garde des instances chaudes en continu et supprime ce délai.

Peut-on résoudre plusieurs CAPTCHA en parallèle ?

Oui. Durable Functions gère les schémas fan-out/fan-in : plusieurs résolutions en parallèle, puis agrégation des tokens. Le débit réel reste borné par votre nombre de threads CaptchaAI.

Quels types de CAPTCHA cette fonction prend-elle en charge ?

Tous ceux exposés par l'API : il suffit de changer le champ method. CaptchaAI couvre reCAPTCHA v2 et v3, Cloudflare Turnstile et Challenge, GeeTest v3, ainsi que les CAPTCHA image et grille.

Cette architecture est-elle compatible avec le RGPD ?

Par construction, elle ne manipule que des sitekeys et des URL de page, pas de données personnelles. Restez attentif à ce que vous journalisez dans Application Insights et appliquez la minimisation des données à vos propres charges.


Guides connexes


Déployez sur Azure, puis récupérez votre clé CaptchaAI pour brancher votre fonction serverless à vos workflows.

Les commentaires sont désactivés pour cet article.