How to setup auto deactivate license for MD Core container in AWS ECS with Lambda function?

Info:

This article apply for MD Core container in AWS ECS

Background

There are situations in ECS deployment that the deployment-ids within a license key get zombie as the container is terminated but it could not properly deactivate that deployment-id.

Goal:

When an MD Core task starts, record its deployment ID; when the task stops (scale-in, crash, OOM, deploy), deactivate that deployment reliably—even if the container can’t run its own shutdown hook.

1 - Architecture (at a glance)

  • Trigger: EventBridge rule on ECS Task State Change (we filter by desiredStatus for speed).

  • Lambda (single function):

    • On desiredStatus=RUNNING → wait until lastStatus=RUNNING, call MD Core Admin APIGET http://<task-private-ip>:8008/admin/license (header apikey: <MDCORE_APIKEY>) → read deployment.

    • Store mapping { task_arn → deployment } in DynamoDB (TTL’d).

    • On desiredStatus=STOPPED → wait until lastStatus=STOPPED, read activation key + deployment → call GET https://activation.dl.opswat.com/deactivation?key=<activation-key>&deployment=<deployment>.

  • Secrets: Activation key and Admin API key live in AWS Secrets Manager; they are injected into the Task Definition as environment secrets (not plain env).

  • Networking: Lambda is placed in the same VPC as ECS and allowed to reach the task’s private IP on TCP/8008.

2 - Prerequisites

  • ECS Fargate service for MD Core (container name recommended: mdcore).

  • VPC with private subnets and either a NAT gateway (or VPC endpoints for Secrets Manager & DynamoDB).

  • Basic AWS permissions to create Lambda, EventBridge rules, DynamoDB table, and Secrets.

3 - Store secrets in AWS Secrets Manager

Create two secrets (names are examples; choose your own):

# 1) MD Core activation key (for deactivation calls)
aws secretsmanager create-secret \
--name mdcore/license-key \
--secret-string '<YOUR_ACTIVATION_KEY>'
# 2) MD Core Admin API key (for /admin/license)
aws secretsmanager create-secret \
--name mdcore/admin-apikey \
--secret-string '<STRONG_RANDOM_APIKEY>'

4 - Update the Task Definition (container mdcore)

Add environment secrets:

  • MDCORE_LICENSE_KEY → value from Secrets Manager mdcore/license-key

  • MDCORE_APIKEY → value from Secrets Manager mdcore/admin-apikey

Tip: You do not need to expose port 8008 externally. Lambda will call the task’s private IP on port 8008.

5 - Create the DynamoDB table (idempotency & mapping)

aws dynamodb create-table \
--table-name mdcore-deployments \
--attribute-definitions AttributeName=task_arn,AttributeType=S \
--key-schema AttributeName=task_arn,KeyType=HASH \
--billing-mode PAY_PER_REQUEST

Then enable TTL on attribute ttl (Console → DynamoDB → Table → TTL).


6 - Create the Lambda function


A. Basics

  • Author from scratch → Name: mdcore-license-handler

  • Runtime: Python 3.11, Architecture: x86_64

  • Create.

B. VPC

  • Configuration → VPC → Edit

    • Select the same VPC and private subnets as ECS.

    • Security group for Lambda: allow egress to the ECS task SG.

  • ECS task SG: allow inbound TCP 8008 from the Lambda SG.

C. Environment variables

DDB_TABLE = mdcore-deployments
MDCORE_CONTAINER_NAME = mdcore
MDCORE_APIKEY_NAME = MDCORE_APIKEY
MDCORE_ACTIVATION_KEY_NAME = MDCORE_LICENSE_KEY
MDCORE_ADMIN_PORT = 8008
WAIT_RUNNING_SEC = 120
WAIT_STOPPED_SEC = 90
DEACTIVATE_URL = https://activation.dl.opswat.com/deactivation

D. Permissions (IAM)

From Lambda → Configuration → Permissions → click the Role name → Add permissions → Create inline policy → JSON:

{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action":["ecs:DescribeTasks","ecs:DescribeTaskDefinition"], "Resource": "*" },
{ "Effect": "Allow", "Action":["secretsmanager:GetSecretValue"], "Resource": "*" },
{ "Effect": "Allow", "Action":["dynamodb:GetItem","dynamodb:PutItem","dynamodb:UpdateItem"], "Resource": "*" },
{ "Effect": "Allow", "Action": ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"], "Resource": "*" }
]
}

Later, tighten Resource to specific ARNs (table + secrets). If secrets use a CMK, also grant kms:Decrypt.

E. Code (paste into lambda_function.py, handler = lambda_function.handler)

import os, time, json, urllib.request, urllib.parse
import boto3
from botocore.exceptions import ClientError

ecs = boto3.client('ecs')
secrets = boto3.client('secretsmanager')
ddb = boto3.resource('dynamodb')
table = ddb.Table(os.environ['DDB_TABLE'])

MDCORE_CONTAINER = os.environ.get('MDCORE_CONTAINER_NAME', 'mdcore')
APIKEY_ENV = os.environ.get('MDCORE_APIKEY_NAME', 'MDCORE_APIKEY')
ACTKEY_ENV = os.environ.get('MDCORE_ACTIVATION_KEY_NAME', 'MDCORE_LICENSE_KEY')
ADMIN_PORT = int(os.environ.get('MDCORE_ADMIN_PORT', '8008'))
DEACTIVATE_URL = os.environ.get('DEACTIVATE_URL', 'https://activation.dl.opswat.com/deactivation')
WAIT_RUNNING_SEC = int(os.environ.get('WAIT_RUNNING_SEC', '120'))
WAIT_STOPPED_SEC = int(os.environ.get('WAIT_STOPPED_SEC', '90'))

def _get_secret(maybe_arn):
if not maybe_arn:

7 - Create the EventBridge rule

Console → EventBridge → Rules → Create rule → Rule with an event pattern → AWS services → Service: Elastic Container Service (ECS) → Event type: ECS Task State Change → Switch to JSON editor and paste:

{
"source": ["aws.ecs"],
"detail-type": ["ECS Task State Change"],
"detail": {
"clusterArn": ["arn:aws:ecs:<region>:<acct>:cluster/<your-cluster>"],
"group": ["service:<your-mdcore-service>"],
"desiredStatus": ["RUNNING","STOPPED"]
}
}

Target: Lambda function → select mdcore-license-handler. In Additional settings: enable a DLQ (SQS) and keep default retries.


8 - Test & validate

  1. Scale service to 0 and back to 1:

  • On scale-up, CloudWatch Logs should show: status=mapped ... deployment=<id>. Verify DynamoDB row { task_arn, deployment, status=ACTIVE }.

  • On scale-in/stop, Logs should show: status=deactivated. Row updated to status=DEACTIVATED.

  1. Manual curl (optional)

  • Get deployment from inside the task:

curl -H "apikey: $MDCORE_APIKEY" http://localhost:8008/admin/license
  • Expected JSON contains "deployment": "<MSCL...>".

  1. Security Group check

  • If mapping fails, ensure Lambda SG - ECS task SG allows TCP/8008

9 - Troubleshooting quick list

  • EventBridge rule not firing: wrong region/bus; pattern mismatch (check clusterArn/group); use CloudWatch “Metrics for matched events”.

  • Mapping fails (RUNNING): Lambda not in VPC / no IP on task yet / SG not allowing 8008 / wrong MDCORE_APIKEY_NAME.

  • Deactivation fails (STOPPED): No DDB mapping (task died before RUNNING), wrong MDCORE_LICENSE_KEY secret, outbound internet blocked (need NAT for the activation URL) or corporate proxy.

  • Timeouts: Increase WAIT_RUNNING_SEC / WAIT_STOPPED_SEC; keep EventBridge target retry window ≥ 1h.

10 - Hardening & Ops

  • Least privilege: restrict IAM to specific ARNs; add kms:Decrypt if secrets use a CMK.

  • Observability: add CloudWatch metric filters for "Deactivate failed" and alarm.

  • Idempotency: deactivation endpoint should be safe to call multiple times; DDB state prevents repeats.

  • Cost: all serverless; negligible under normal loads.

11 - Alternative "sidecar register" (if opening 8008 to Lambda is hard)

Add a tiny sidecar container that waits for MD Core to be ready, calls /admin/license, and writes the mapping directly to DynamoDB (using the task role). Then the Lambda only needs to handle STOPPED events to deactivate. This avoids Lambda→8008 traffic.

Support:

If Further Assistance is required, please proceed to log a support case or chatting with our support engineer.