{"slug": "swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws", "title": "Swap Your LLM Without a Deploy: Dynamic Model Routing on Bedrock with AWS AppConfig", "summary": "A developer built a model router on AWS AppConfig and Amazon Bedrock that lets users swap large language models without redeploying code. The router uses feature flags to map routing keys like 'fast', 'cheap', and 'open' to model IDs, enabling instant rollbacks and config-only deployments. The implementation includes a Lambda behind API Gateway that reads flags at runtime and routes requests to Claude Haiku, Amazon Nova Micro, or Meta Llama.", "body_md": "Bedrock model IDs have a short shelf life. New Claude, Nova, and Llama versions land every few months, pricing changes, and yesterday's best pick becomes today's legacy model. If the model ID is hardcoded in your Lambda, every swap is a code change, a review, and a redeploy — and if the new model misbehaves, rolling back is another deploy.\n\nIn this hands-on, we'll fix that by building a model router where the model choice lives in AWS AppConfig feature flags, not in code. A Lambda behind API Gateway reads the flags at runtime and routes each request to Claude Haiku, Amazon Nova Micro, or Meta Llama. Swapping a model becomes a config deployment: no code change, no redeploy, instant rollback.\n\nPrefer video? This entire hands-on is also on YouTube:\n\n```\ncurl ?model=fast ──▶ API Gateway ──▶ Lambda ──▶ Amazon Bedrock (Converse)\n                                      │              ▲\n                                      │   which model_id?\n                                      ▼              │\n                               AWS AppConfig ────────┘\n                               feature flags:\n                                 fast  → Claude Haiku\n                                 cheap → Nova Micro\n                                 open  → Llama\n```\n\nThe client asks for a routing *key* (`fast`\n\n, `cheap`\n\n, `open`\n\n) — never a model ID. What each key means is decided by whoever controls the AppConfig deployment.\n\nAWS AppConfig is a managed feature-flag and configuration service. Three properties make it a good fit for LLM routing:\n\n`us-east-1`\n\n`jq`\n\ninstalledBedrock models are updated frequently — list what's currently available and use the latest versions, not the ones printed in this article.\n\n```\n# Anthropic Haiku family\naws bedrock list-inference-profiles \\\n  --region us-east-1 \\\n  --query 'inferenceProfileSummaries[?contains(inferenceProfileId, `haiku`)].inferenceProfileId' \\\n  --output table\n\n# Amazon Nova Micro\naws bedrock list-inference-profiles \\\n  --region us-east-1 \\\n  --query 'inferenceProfileSummaries[?contains(inferenceProfileId, `nova-micro`)].inferenceProfileId' \\\n  --output table\n\n# Meta Llama\naws bedrock list-inference-profiles \\\n  --region us-east-1 \\\n  --query 'inferenceProfileSummaries[?contains(inferenceProfileId, `llama`)].inferenceProfileId' \\\n  --output table\n```\n\nExport the ones you'll route between (replace with the versions listed in your account):\n\n```\nexport CLAUDE_MODEL=\"us.anthropic.claude-haiku-4-5-20251001-v1:0\"\nexport NOVA_MODEL=\"us.amazon.nova-micro-v1:0\"\nexport LLAMA_MODEL=\"us.meta.llama4-scout-17b-instruct-v1:0\"\n\necho \"Claude: $CLAUDE_MODEL\"\necho \"Nova  : $NOVA_MODEL\"\necho \"Llama : $LLAMA_MODEL\"\n```\n\nAppConfig has a small hierarchy: an application contains environments (dev, prod, …) and configuration profiles (the config itself). We create one of each, then a feature-flag document with three flags — each carrying a `model_id`\n\nattribute.\n\n```\nREGION=us-east-1\n\n# 1. Application\nAPP_ID=$(aws appconfig create-application --region $REGION \\\n  --name bedrock-router --query Id --output text)\n\n# 2. Environment\nENV_ID=$(aws appconfig create-environment --region $REGION \\\n  --application-id $APP_ID --name dev --query Id --output text)\n\n# 3. Configuration Profile (feature flag type)\nPROFILE_ID=$(aws appconfig create-configuration-profile --region $REGION \\\n  --application-id $APP_ID --name model-router \\\n  --location-uri hosted --type \"AWS.AppConfig.FeatureFlags\" \\\n  --query Id --output text)\n\n# 4. Feature flags\njq -n \\\n  --arg c \"$CLAUDE_MODEL\" --arg n \"$NOVA_MODEL\" --arg l \"$LLAMA_MODEL\" \\\n'{\n  flags: {\n    fast:  {name:\"fast\",  attributes:{model_id:{constraints:{type:\"string\"}}}},\n    cheap: {name:\"cheap\", attributes:{model_id:{constraints:{type:\"string\"}}}},\n    open:  {name:\"open\",  attributes:{model_id:{constraints:{type:\"string\"}}}}\n  },\n  values: {\n    fast:  {enabled:true, model_id:$c},\n    cheap: {enabled:true, model_id:$n},\n    open:  {enabled:true, model_id:$l}\n  },\n  version: \"1\"\n}' > /tmp/flags.json\n\naws appconfig create-hosted-configuration-version --region $REGION \\\n  --application-id $APP_ID --configuration-profile-id $PROFILE_ID \\\n  --content-type \"application/json\" \\\n  --content fileb:///tmp/flags.json \\\n  /dev/null\n\n# 5. Deploy (using the AWS predefined strategy AppConfig.AllAtOnce)\naws appconfig start-deployment --region $REGION \\\n  --application-id $APP_ID --environment-id $ENV_ID \\\n  --deployment-strategy-id AppConfig.AllAtOnce \\\n  --configuration-profile-id $PROFILE_ID \\\n  --configuration-version 1\n\necho \"APP_ID=$APP_ID\"\necho \"ENV_ID=$ENV_ID\"\necho \"PROFILE_ID=$PROFILE_ID\"\n```\n\n`AppConfig.AllAtOnce`\n\nis fine for a dev environment. In production you'd pick a gradual strategy (linear or canary) with a CloudWatch alarm attached, so a bad config rolls back automatically.\n\nVerify what's deployed:\n\n```\naws appconfig get-hosted-configuration-version --region $REGION \\\n  --application-id $APP_ID --configuration-profile-id $PROFILE_ID \\\n  --version-number 1 \\\n  /tmp/flags_out.json > /dev/null\n\ncat /tmp/flags_out.json | jq\n```\n\nCreate a Lambda (Python, name it `bedrock-router`\n\n) in the console, then add this inline policy to its execution role (IAM → the role → Add permissions → Create inline policy → JSON):\n\n```\n{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [\n    {\n      \"Sid\": \"BedrockConverse\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"bedrock:InvokeModel\",\n        \"bedrock:InvokeModelWithResponseStream\"\n      ],\n      \"Resource\": \"*\"\n    },\n    {\n      \"Sid\": \"AppConfigRead\",\n      \"Effect\": \"Allow\",\n      \"Action\": [\n        \"appconfig:StartConfigurationSession\",\n        \"appconfig:GetLatestConfiguration\"\n      ],\n      \"Resource\": \"*\"\n    }\n  ]\n}\n```\n\n(For production, scope `Resource`\n\ndown to your specific models and AppConfig ARNs.)\n\n``` python\nimport json\nimport os\nimport time\nimport boto3\n\nREGION = os.environ[\"AWS_REGION\"]\nAPP_ID = os.environ[\"APPCONFIG_APP_ID\"]\nENV_ID = os.environ[\"APPCONFIG_ENV_ID\"]\nPROFILE_ID = os.environ[\"APPCONFIG_PROFILE_ID\"]\n\nappconfigdata = boto3.client(\"appconfigdata\", region_name=REGION)\nbedrock = boto3.client(\"bedrock-runtime\", region_name=REGION)\n\n# Simple cache to reduce AppConfig calls when the container is reused\n_cache = {\"config\": None, \"token\": None, \"expires_at\": 0}\nCACHE_TTL_SEC = 30\n\ndef _load_config():\n    now = time.time()\n\n    # Return the cached config while it is still valid\n    if _cache[\"config\"] is not None and now < _cache[\"expires_at\"]:\n        return _cache[\"config\"]\n\n    # Start a session only on the first call\n    if _cache[\"token\"] is None:\n        session = appconfigdata.start_configuration_session(\n            ApplicationIdentifier=APP_ID,\n            EnvironmentIdentifier=ENV_ID,\n            ConfigurationProfileIdentifier=PROFILE_ID,\n        )\n        _cache[\"token\"] = session[\"InitialConfigurationToken\"]\n\n    resp = appconfigdata.get_latest_configuration(\n        ConfigurationToken=_cache[\"token\"]\n    )\n    _cache[\"token\"] = resp[\"NextPollConfigurationToken\"]\n\n    content = resp[\"Configuration\"].read()\n    if content:\n        # Replace only when there is an update. Keep the current cache if the content is empty\n        _cache[\"config\"] = json.loads(content)\n\n    _cache[\"expires_at\"] = now + CACHE_TTL_SEC\n    return _cache[\"config\"]\n\ndef lambda_handler(event, context):\n    try:\n        flags = _load_config()  # Feature flag value map: {\"fast\":{\"enabled\":true,\"model_id\":\"...\"}, ...}\n\n        qs = event.get(\"queryStringParameters\") or {}\n        model_key = qs.get(\"model\", \"fast\")  # Default is fast\n        prompt = qs.get(\"prompt\", \"Hello. Please introduce yourself in one sentence.\")\n\n        flag = flags.get(model_key)\n        if not flag or not flag.get(\"enabled\"):\n            return {\n                \"statusCode\": 400,\n                \"headers\": {\"Content-Type\": \"application/json; charset=utf-8\"},\n                \"body\": json.dumps(\n                    {\n                        \"error\": f\"model key not available: {model_key}\",\n                        \"available_keys\": [k for k, v in flags.items() if v.get(\"enabled\")],\n                    },\n                    ensure_ascii=False,\n                ),\n            }\n\n        model_id = flag[\"model_id\"]\n\n        resp = bedrock.converse(\n            modelId=model_id,\n            messages=[{\"role\": \"user\", \"content\": [{\"text\": prompt}]}],\n            inferenceConfig={\"maxTokens\": 300, \"temperature\": 0.5},\n        )\n\n        text = resp[\"output\"][\"message\"][\"content\"][0][\"text\"]\n\n        return {\n            \"statusCode\": 200,\n            \"headers\": {\"Content-Type\": \"application/json; charset=utf-8\"},\n            \"body\": json.dumps(\n                {\n                    \"model_key\": model_key,\n                    \"model_id\": model_id,\n                    \"prompt\": prompt,\n                    \"response\": text,\n                    \"usage\": resp.get(\"usage\", {}),\n                },\n                ensure_ascii=False,\n            ),\n        }\n\n    except Exception as e:\n        return {\n            \"statusCode\": 500,\n            \"headers\": {\"Content-Type\": \"application/json; charset=utf-8\"},\n            \"body\": json.dumps(\n                {\"error\": type(e).__name__, \"message\": str(e)},\n                ensure_ascii=False,\n            ),\n        }\n```\n\nThree details worth reading twice:\n\n`appconfigdata`\n\nworks as a polling session: `start_configuration_session`\n\nonce, then `get_latest_configuration`\n\nwith a token that gets replaced on every call. If nothing changed since the last poll, the response body is `if content:`\n\n.`_cache`\n\nsurvives between invocations. You get near-instant responses and at most one AppConfig poll per 30 seconds per container.Set the Lambda's environment variables (Configuration tab → Environment variables → Edit) with the values printed in Step 2:\n\n```\necho \"APPCONFIG_APP_ID    = $APP_ID\"\necho \"APPCONFIG_ENV_ID    = $ENV_ID\"\necho \"APPCONFIG_PROFILE_ID= $PROFILE_ID\"\n```\n\nQuick unit test (Test tab → Event name: test1 → Event JSON):\n\n```\n{\n  \"queryStringParameters\": {\n    \"model\": \"cheap\",\n    \"prompt\": \"Please introduce yourself in three lines\"\n  }\n}\n```\n\nCreate an HTTP API (name it `bedrock-router-api`\n\n) with a `/chat`\n\nroute integrated with the Lambda, then:\n\n```\n# Change the URL below to match your environment\nexport API_URL=\"https://abc123xyz.execute-api.us-east-1.amazonaws.com\"\necho \"$API_URL/chat\"\n```\n\nRoute to each model by key:\n\n```\n# Call Claude Haiku (fast)\ncurl -s -G \"$API_URL/chat\" -d \"model=fast\" --data-urlencode \"prompt=What is generative AI, in three lines\" | jq\n\n# Call Nova Micro (cheap)\ncurl -s -G \"$API_URL/chat\" -d \"model=cheap\" --data-urlencode \"prompt=What is generative AI, in three lines\" | jq\n\n# Call Llama (open)\ncurl -s -G \"$API_URL/chat\" -d \"model=open\" --data-urlencode \"prompt=What is generative AI, in three lines\" | jq\n\n# If no key is specified, the default (fast) is used\ncurl -s -G \"$API_URL/chat\" --data-urlencode \"prompt=Hello\" | jq '.model_key, .model_id'\n\n# Check error handling for an invalid key\ncurl -s -G \"$API_URL/chat\" -d \"model=unknown\" --data-urlencode \"prompt=test\" | jq\n```\n\nSame endpoint, three different models, chosen by a query parameter.\n\nNow the payoff. Suppose Claude Haiku is overkill for the `fast`\n\nroute and you want Nova Micro there too. Create version 2 of the flags — note `fast`\n\nnow carries `$n`\n\n— and deploy it:\n\n```\nREGION=us-east-1\n\njq -n \\\n  --arg c \"$CLAUDE_MODEL\" --arg n \"$NOVA_MODEL\" --arg l \"$LLAMA_MODEL\" \\\n'{\n  flags: {\n    fast:  {name:\"fast\",  attributes:{model_id:{constraints:{type:\"string\"}}}},\n    cheap: {name:\"cheap\", attributes:{model_id:{constraints:{type:\"string\"}}}},\n    open:  {name:\"open\",  attributes:{model_id:{constraints:{type:\"string\"}}}}\n  },\n  values: {\n    fast:  {enabled:true, model_id:$n},\n    cheap: {enabled:true, model_id:$n},\n    open:  {enabled:true, model_id:$l}\n  },\n  version: \"1\"\n}' > /tmp/flags_v2.json\n\naws appconfig create-hosted-configuration-version --region $REGION \\\n  --application-id $APP_ID --configuration-profile-id $PROFILE_ID \\\n  --content-type \"application/json\" \\\n  --content fileb:///tmp/flags_v2.json \\\n  /dev/null\n\naws appconfig start-deployment --region $REGION \\\n  --application-id $APP_ID --environment-id $ENV_ID \\\n  --deployment-strategy-id AppConfig.AllAtOnce \\\n  --configuration-profile-id $PROFILE_ID \\\n  --configuration-version 2\n```\n\nWait for the cache TTL (up to ~30 seconds), then:\n\n```\n# fast should now be Nova Micro\ncurl -s -G \"$API_URL/chat\" -d \"model=fast\" --data-urlencode \"prompt=Introduce yourself\" | jq '.model_key, .model_id'\n```\n\nThe Lambda never changed. No deploy, no cold start, no release process — the model behind `fast`\n\nis now a different one, and deploying version 1 again would roll it back just as fast.\n\n```\n# API Gateway (look up the API ID and delete)\nAPI_ID=$(aws apigatewayv2 get-apis --region us-east-1 \\\n  --query 'Items[?Name==`bedrock-router-api`].ApiId' --output text 2>/dev/null)\nif [ -n \"$API_ID\" ] && [ \"$API_ID\" != \"None\" ]; then\n  aws apigatewayv2 delete-api --region us-east-1 --api-id \"$API_ID\"\nfi\n\n# Lambda\naws lambda delete-function --region us-east-1 --function-name bedrock-router 2>/dev/null\n\n# IAM role (inline policy first, then the role itself)\naws iam delete-role-policy \\\n  --role-name bedrock-router-role \\\n  --policy-name bedrock-router-inline 2>/dev/null\naws iam detach-role-policy \\\n  --role-name bedrock-router-role \\\n  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 2>/dev/null\naws iam delete-role --role-name bedrock-router-role 2>/dev/null\n\n# AppConfig (child resources first)\nif [ -n \"$APP_ID\" ]; then\n  aws appconfig delete-environment --region us-east-1 \\\n    --application-id \"$APP_ID\" --environment-id \"$ENV_ID\" 2>/dev/null\n  aws appconfig delete-configuration-profile --region us-east-1 \\\n    --application-id \"$APP_ID\" --configuration-profile-id \"$PROFILE_ID\" 2>/dev/null\n  aws appconfig delete-application --region us-east-1 --application-id \"$APP_ID\" 2>/dev/null\nfi\n```\n\nSeparating \"which model\" from \"the code that calls it\" is not a convenience — it's how you keep an LLM application operable in a market where models are replaced every few months. AppConfig gives that separation deployment strategies, version history, and rollback for free. If your Bedrock model IDs live in code today, I recommend trying this pattern in your own environment.\n\n**About the author**\n\nMaruchin Tech — 12x AWS Certified | Cloud & AI for manufacturing and supply chain (AWS / Google Cloud / Azure) | Udemy instructor (100K+ students)\n\n🎥 Video version of this hands-on:\n\n[https://youtu.be/6k2lO4_fA7o](https://youtu.be/6k2lO4_fA7o)\n\n📚 Full course — AWS Certified Generative AI Developer Professional (AIP-C01) Exam Prep:\n\n[https://www.udemy.com/course/aws-certified-generative-ai-developer-professional-exam-prep/](https://www.udemy.com/course/aws-certified-generative-ai-developer-professional-exam-prep/)\n\n👨🏫 All my courses (Udemy profile):\n\n[https://www.udemy.com/user/maruchin-tech-2/](https://www.udemy.com/user/maruchin-tech-2/)\n\n🎫 Monthly discount coupons:\n\n[https://www.youtube.com/@MaruchinTech-cloud/posts](https://www.youtube.com/@MaruchinTech-cloud/posts)", "url": "https://wpnews.pro/news/swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws", "canonical_source": "https://dev.to/maruchin_tech_555/swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws-appconfig-48bn", "published_at": "2026-08-22 07:32:11+00:00", "updated_at": "2026-08-22 07:43:21.248808+00:00", "lang": "en", "topics": ["large-language-models", "ai-infrastructure", "developer-tools", "mlops"], "entities": ["AWS AppConfig", "Amazon Bedrock", "Lambda", "API Gateway", "Claude Haiku", "Amazon Nova Micro", "Meta Llama"], "alternates": {"html": "https://wpnews.pro/news/swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws", "markdown": "https://wpnews.pro/news/swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws.md", "text": "https://wpnews.pro/news/swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws.txt", "jsonld": "https://wpnews.pro/news/swap-your-llm-without-a-deploy-dynamic-model-routing-on-bedrock-with-aws.jsonld"}}