Guide · AWS Batch · Compute Environments & Job Definitions

MCP Server AWS Batch — CreateComputeEnvironment, CreateJobQueue, RegisterJobDefinition

AWS Batch lets MCP tools trigger containerized compute jobs without managing EC2 instances. You provision a compute environment (the underlying fleet — EC2, Spot, or Fargate), attach one or more job queues that route jobs to environments by priority, register job definitions that describe the container and resource requirements, then call SubmitJobCommand from any tool handler. The three architectural decisions that determine everything are: EC2/Spot vs Fargate (Fargate is simpler but costs more per vCPU-hour; EC2 Spot is cheapest but requires Spot interruption handling), job definition versioning (each RegisterJobDefinitionCommand creates a new revision — always pin to name:revision in production), and compute environment scaling (Batch scales minvCpus to maxvCpus automatically, but cold-start for a new instance fleet adds 60–120 seconds to job start time).

TL;DR

Install @aws-sdk/client-batch. Create a managed compute environment with CreateComputeEnvironmentCommand (choose EC2 or FARGATE type, set maxvCpus, provide subnets and security groups). Create a job queue with CreateJobQueueCommand pointing to the compute environment with priority 1. Register a job definition with RegisterJobDefinitionCommand specifying your container image and resourceRequirements. Submit jobs with SubmitJobCommand{jobName, jobQueue, jobDefinition}. Poll DescribeJobsCommand until status is SUCCEEDED or FAILED.

Client setup

All Batch resources are regional. Construct BatchClient with the same region as your VPC subnets — a compute environment must reference subnets in the same region, and cross-region API calls will fail.

import {
  BatchClient,
  CreateComputeEnvironmentCommand,
  CreateJobQueueCommand,
  RegisterJobDefinitionCommand,
  SubmitJobCommand,
  DescribeJobsCommand,
} from '@aws-sdk/client-batch';

const batch = new BatchClient({ region: 'us-east-1' });

Install the package with npm install @aws-sdk/client-batch. The Batch client package is separate from @aws-sdk/client-ec2 — Batch uses its own service endpoint (batch.us-east-1.amazonaws.com).

Compute environments

A compute environment defines the underlying fleet that runs your jobs. MANAGED environments let Batch handle instance provisioning and scaling; UNMANAGED environments require you to register your own ECS container instances. Use MANAGED unless you have a specific reason to manage instances yourself.

The type inside computeResources controls the fleet model:

typeUnderlying fleetWhen to use
EC2On-demand EC2Predictable workloads, no interruption tolerance
SPOTEC2 Spot InstancesCost-sensitive workloads that can checkpoint and restart
FARGATEFargate on-demandBurst workloads with no cold start tolerance; simpler IAM
FARGATE_SPOTFargate SpotNon-critical Fargate workloads at ~70% cost reduction
import { type CreateComputeEnvironmentCommandInput } from '@aws-sdk/client-batch';

export async function createFargateComputeEnv(params: {
  name: string;
  maxvCpus: number;
  subnetIds: string[];
  securityGroupIds: string[];
  serviceRoleArn?: string;
}) {
  const input: CreateComputeEnvironmentCommandInput = {
    computeEnvironmentName: params.name,
    type: 'MANAGED',
    state: 'ENABLED',
    computeResources: {
      type: 'FARGATE',
      maxvCpus: params.maxvCpus,
      subnets: params.subnetIds,
      securityGroupIds: params.securityGroupIds,
    },
    // serviceRole omitted → Batch uses the AWSServiceRoleForBatch service-linked role
    ...(params.serviceRoleArn ? { serviceRole: params.serviceRoleArn } : {}),
  };

  const response = await batch.send(new CreateComputeEnvironmentCommand(input));
  return response.computeEnvironmentArn!;
}

export async function createEc2ComputeEnv(params: {
  name: string;
  minvCpus: number;      // keep 0 to scale to zero (scales down when idle)
  maxvCpus: number;
  instanceTypes: string[];  // e.g. ['m5', 'r5'] or ['optimal']
  subnetIds: string[];
  securityGroupIds: string[];
  instanceRole: string;  // ARN of instance profile (AmazonEC2ContainerServiceforEC2Role)
}) {
  const response = await batch.send(new CreateComputeEnvironmentCommand({
    computeEnvironmentName: params.name,
    type: 'MANAGED',
    state: 'ENABLED',
    computeResources: {
      type: 'EC2',
      minvCpus: params.minvCpus,
      maxvCpus: params.maxvCpus,
      instanceTypes: params.instanceTypes,
      subnets: params.subnetIds,
      securityGroupIds: params.securityGroupIds,
      // instanceRole must be an INSTANCE PROFILE ARN (arn:aws:iam::...:instance-profile/...)
      // not the role ARN itself — this is a common mistake
      instanceRole: params.instanceRole,
    },
  }));
  return response.computeEnvironmentArn!;
}

When using instanceTypes: ['optimal'], Batch selects instance families from C4, M4, and R4 (or newer generations) based on current Spot pricing and availability. If you need specific hardware (e.g. GPU instances for ML), specify exact types like ['p3.2xlarge', 'g4dn.xlarge'].

After creation, the compute environment enters CREATING state. Wait for status to become VALID before attaching it to a job queue. Poll with DescribeComputeEnvironmentsCommand:

import {
  DescribeComputeEnvironmentsCommand,
} from '@aws-sdk/client-batch';

export async function waitForComputeEnvValid(name: string): Promise<void> {
  for (let i = 0; i < 60; i++) {
    const response = await batch.send(new DescribeComputeEnvironmentsCommand({
      computeEnvironments: [name],
    }));
    const env = response.computeEnvironments?.[0];
    if (env?.status === 'VALID') return;
    if (env?.status === 'INVALID') {
      throw new Error(`Compute env invalid: ${env.statusReason}`);
    }
    await new Promise(r => setTimeout(r, 5_000));
  }
  throw new Error('Compute environment did not become VALID within 5 minutes');
}

Job queues

A job queue connects to one or more compute environments with a priority ordering. When Batch schedules a job from this queue, it tries the highest-priority compute environment first; if that environment is at capacity, it falls over to lower-priority ones. Priority is an integer — higher number means higher priority.

import { type CreateJobQueueCommandInput } from '@aws-sdk/client-batch';

export async function createJobQueue(params: {
  name: string;
  computeEnvironmentArn: string;
  priority?: number;  // defaults to 1; higher number = higher priority
  schedulingPolicyArn?: string;
}) {
  const input: CreateJobQueueCommandInput = {
    jobQueueName: params.name,
    state: 'ENABLED',
    priority: params.priority ?? 1,
    computeEnvironmentOrder: [
      {
        order: 1,         // order within the queue; multiple envs allowed
        computeEnvironment: params.computeEnvironmentArn,
      },
    ],
    ...(params.schedulingPolicyArn
      ? { schedulingPolicyArn: params.schedulingPolicyArn }
      : {}),
  };

  const response = await batch.send(new CreateJobQueueCommand(input));
  return response.jobQueueArn!;
}

Multi-environment queues are useful when you want a primary Spot environment with an on-demand fallback:

// Spot first, on-demand fallback
await batch.send(new CreateJobQueueCommand({
  jobQueueName: 'ml-training-queue',
  state: 'ENABLED',
  priority: 10,
  computeEnvironmentOrder: [
    { order: 1, computeEnvironment: spotComputeEnvArn },    // tried first
    { order: 2, computeEnvironment: onDemandComputeEnvArn }, // fallback
  ],
}));

Job definitions

RegisterJobDefinitionCommand creates a versioned template for a job. Each call creates a new revision number (name:1, name:2, etc.). The job definition specifies the container image, resource requirements, command, environment variables, and IAM role for the job container.

import { type RegisterJobDefinitionCommandInput } from '@aws-sdk/client-batch';

export async function registerJobDefinition(params: {
  name: string;
  image: string;
  vcpus: number;      // e.g. 1 (Fargate minimum 0.25, max 16)
  memoryMiB: number;  // e.g. 2048 (Fargate minimum 512)
  command?: string[]; // override container CMD; e.g. ['python', 'train.py']
  jobRoleArn?: string;         // permissions your job code needs
  executionRoleArn?: string;   // Fargate infrastructure role (required for Fargate)
  useFargate?: boolean;
}) {
  const input: RegisterJobDefinitionCommandInput = {
    jobDefinitionName: params.name,
    type: 'container',
    containerProperties: {
      image: params.image,
      resourceRequirements: [
        { type: 'VCPU',   value: String(params.vcpus) },
        { type: 'MEMORY', value: String(params.memoryMiB) },
      ],
      ...(params.command ? { command: params.command } : {}),
      ...(params.jobRoleArn ? { jobRoleArn: params.jobRoleArn } : {}),
      ...(params.executionRoleArn ? { executionRoleArn: params.executionRoleArn } : {}),
      ...(params.useFargate ? {
        fargatePlatformConfiguration: { platformVersion: 'LATEST' },
        networkConfiguration: { assignPublicIp: 'ENABLED' },
      } : {}),
      logConfiguration: {
        logDriver: 'awslogs',
        options: {
          'awslogs-group':          '/aws/batch/job',
          'awslogs-region':         'us-east-1',
          'awslogs-stream-prefix':  params.name,
        },
      },
    },
    // retryStrategy defines how many times to retry on failure
    retryStrategy: { attempts: 3 },
    // platformCapabilities required when using Fargate
    ...(params.useFargate
      ? { platformCapabilities: ['FARGATE'] }
      : { platformCapabilities: ['EC2'] }),
  };

  const response = await batch.send(new RegisterJobDefinitionCommand(input));
  // Returns e.g. 'my-job-def:5' — the name:revision string
  return {
    arn:      response.jobDefinitionArn!,
    revision: response.revision!,
    // pin to this exact revision in production SubmitJob calls
    nameRevision: `${params.name}:${response.revision}`,
  };
}

Always store the nameRevision string and pass it to SubmitJobCommand. If you pass just the name (no revision), Batch uses the latest active revision, which can change unexpectedly when you update a definition.

Submitting a job

Once your infrastructure is set up, submitting a job is straightforward. The jobName must be 1–128 characters ([a-zA-Z0-9_-]); make it descriptive because it appears in CloudWatch log stream names.

export async function submitJob(params: {
  jobName: string;
  jobQueue: string;         // name or ARN
  jobDefinition: string;    // 'name:revision' e.g. 'my-job-def:5'
  environment?: Record<string, string>;  // override env vars for this run
  command?: string[];       // override container command for this run
  vcpuOverride?: number;
  memoryMiBOverride?: number;
}) {
  const response = await batch.send(new SubmitJobCommand({
    jobName: params.jobName,
    jobQueue: params.jobQueue,
    jobDefinition: params.jobDefinition,
    ...(params.environment || params.command || params.vcpuOverride ? {
      containerOverrides: {
        ...(params.environment ? {
          environment: Object.entries(params.environment).map(
            ([name, value]) => ({ name, value })
          ),
        } : {}),
        ...(params.command ? { command: params.command } : {}),
        ...(params.vcpuOverride || params.memoryMiBOverride ? {
          resourceRequirements: [
            ...(params.vcpuOverride
              ? [{ type: 'VCPU' as const, value: String(params.vcpuOverride) }]
              : []),
            ...(params.memoryMiBOverride
              ? [{ type: 'MEMORY' as const, value: String(params.memoryMiBOverride) }]
              : []),
          ],
        } : {}),
      },
    } : {}),
  }));

  return {
    jobId:   response.jobId!,
    jobName: response.jobName!,
  };
}

IAM permissions

Three separate IAM principals are involved in an AWS Batch workflow. Keeping them distinct avoids over-permission:

PrincipalRoleKey permissions
MCP server (caller)Your Lambda/EC2 instance rolebatch:SubmitJob, batch:DescribeJobs, batch:ListJobs, batch:TerminateJob, batch:CancelJob
Job containerjobRoleArnWhatever your batch job needs (S3, DynamoDB, etc.)
Fargate infrastructureexecutionRoleArnecr:GetAuthorizationToken, ecr:BatchGetImage, logs:CreateLogStream, logs:PutLogEvents
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "BatchSubmitAndMonitor",
      "Effect": "Allow",
      "Action": [
        "batch:SubmitJob",
        "batch:DescribeJobs",
        "batch:ListJobs",
        "batch:TerminateJob",
        "batch:CancelJob",
        "batch:DescribeJobQueues",
        "batch:DescribeJobDefinitions",
        "batch:DescribeComputeEnvironments"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PassRoleToJob",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::123456789012:role/batch-job-role",
      "Condition": {
        "StringEquals": { "iam:PassedToService": "batch.amazonaws.com" }
      }
    }
  ]
}

The iam:PassRole statement is required when RegisterJobDefinitionCommand or SubmitJobCommand specifies a jobRoleArn. Without it, the API call returns AccessDenied: User is not authorized to pass a role.

Related pages