Deploy Inventory on ECS Express Mode

Deploy a race-safe inventory API on ECS Express Mode with MongoDB Atlas.

Introduction

30 Second Summary

A stock counter can look correct until two customers reserve the same item at once. Putting it online adds another challenge because the product data lives somewhere else.

In this project, you will deploy your existing race-safe inventory service with Amazon ECS Express Mode. You'll expose it through public HTTPS before proving its stock stays non-negative under competing reservations.

What You'll Build

You'll open an AWS-provided HTTPS URL to watch your deployed service reject one of two competing oversized reservations while its stock stays above zero.

By the end of this project, you'll have:

  • A public inventory service that reports its health through the browser. The inventory view shows five keyboard units from MongoDB Atlas.
  • A remote concurrency proof where two oversized reservations compete for five units. You'll see one request succeed while the other conflicts. The final stock remains 1.
  • An operational trail that lets you inspect the immutable ARM64 image in Amazon ECR. AWS Secrets Manager supplies the Atlas URI at runtime. Amazon CloudWatch shows startup and reservation events.
  • Secret Mission: An optional challenge to replace the stored database password with temporary workload credentials from an ECS task role.

Do I need any experience before starting?

You need the completed Build a Race-Safe Inventory Service with Mongoose project on macOS.

It must include package.json, package-lock.json, compose.yaml, product.js, app.js, and race.js alongside Node.js 24.22.0, Docker Desktop, and Visual Studio Code.

Before We Start

This is your moment to commit to deploying the race-safe inventory service you already built. Locking in that goal keeps the project focused on proving its existing behavior through public HTTPS.

Create Guardrailed Cloud Environments

Local services stop when you shut down your laptop. Resources in Amazon Web Services can keep running after you close the terminal. Those resources can consume credits independently.

MongoDB Atlas will host the inventory database outside your Mac. Before the application leaves your laptop, you will protect both cloud environments with access controls plus spending alerts.

In this step, get ready to:
  • Add AWS account guardrails.
  • Authenticate the AWS CLI with temporary credentials.
  • Create a restricted Atlas database environment.
Secure the AWS account with spending guardrails

The AWS root user controls the entire account. Multi-factor authentication protects that access before any deployment resources exist.

Add Guardrails Before Resources

Creating a cloud account can feel like a financial commitment. An eligible AWS Free plan prevents cash charges while it remains active.

Later resources still consume finite AWS credits. Usage alerts plus budgets give you early warning.

  • Create an AWS Free plan account if you are eligible.
  • Complete the required account verification prompts.
  • Sign in to the AWS Management Console as the root user.

You will see the AWS Management Console home page for your new account.

  • Set the console Region selector to us-east-1.

The console header now shows us-east-1 as the active Region.

  • Use the account menu to open the root-user security credentials page.

The page shows the current MFA registration state for the root user.

  • Start the root MFA registration flow.
  • Choose a passkey or security key as the device type.

Your device presents a verification prompt for the phishing-resistant credential.

  • Complete the device verification prompt.
  • Confirm that root MFA now shows as registered.

Your most powerful AWS identity now requires a second proof of access.

  • Check the root access key section.
  • Leave root access keys uncreated.

The root user now has MFA protection without permanent command-line credentials.

Make Spending Visible

Usage alerts identify Free plan consumption. Budgets provide separate notifications as costs approach your chosen thresholds.

  • Open Billing and Cost Management from the AWS console.
  • Select Billing preferences.

The page shows the account alert preferences.

  • Enable Receive AWS Free Tier usage alerts under Alert preferences.

The Free Tier alert preference now shows as enabled.

  • Save the billing preferences.

The enabled preference now persists on the page.

  • Open AWS Budgets from Billing and Cost Management.
  • Select Create budget.

The budget creation page presents the available setup methods.

  • Choose Use a template (simplified).
  • Select Zero spend budget.

The budget summary now shows a zero-spend alert template.

  • Enter your account email as the alert recipient.
  • Create the zero-spend budget.

The AWS Budgets page now lists your zero-spend guardrail.

  • Start creating a second budget.
  • Choose the Monthly cost budget option.

The second budget form now accepts a monthly spending amount.

  • Set the monthly amount to USD 5.
  • Enter your account email as the alert recipient.

The budget summary now shows a monthly limit of USD 5.

  • Create the monthly cost budget.

The AWS Budgets page now lists both spending guardrails.

Install and authenticate the AWS CLI

The AWS Command Line Interface lets you create cloud resources from the terminal. Version 2.32.0 or later supports aws login with temporary credentials.

  • Check the installed AWS CLI version by running this command:
aws --version

What Does This Check?

The command reports the installed AWS CLI release. The next tabs cover every common result from this check.

✔️ I see version 2.32.0 or higher

Your AWS CLI meets the minimum version required for temporary console authentication.

  • Continue to the temporary authentication flow below.

ⓧ I see an older version

Your installed AWS CLI is below 2.32.0. Install the current release before signing in.

  • Install the current AWS CLI for your macOS user by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash

What Does This Command Do?

The command downloads the official macOS installer script. It runs the installer for your user account.

  • Verify the updated AWS CLI version by running this command:
aws --version

What Should You See?

The reported release should now be 2.32.0 or higher.

Still Seeing the Older Version?

  • Close the terminal after the installer finishes.
  • Open a fresh terminal through macOS search.
  • Run the version check again.

If the older release still appears, help me identify which AWS CLI installation my terminal is using.

ⓧ Command not found

The AWS CLI is not available in your terminal. Install the current release with the official macOS script.

  • Install the current AWS CLI for your macOS user by running this command:
curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash

What Does This Command Do?

The command downloads the official macOS installer script. It runs the installer for your user account.

  • Confirm that the AWS CLI is available by running this command:
aws --version

What Should You See?

The terminal should report AWS CLI version 2.32.0 or higher.

Is the Command Still Unavailable?

  • Close the terminal after the installer finishes.
  • Open a fresh terminal through macOS search.
  • Run the version check again.

If the command is still unavailable, help me diagnose my AWS CLI installation on macOS.

The sign-in flow uses your browser to authorize temporary credentials. It also asks you to confirm us-east-1 as the default Region.

  • Start the temporary AWS sign-in flow by running this command:
aws login

What Does This Command Do?

The command connects the CLI to your console identity. The resulting temporary credentials refresh automatically during the authenticated session.

  • Complete the browser authorization with your AWS account.
  • Accept us-east-1 as the default Region.

The terminal returns control after the authenticated session is ready.

Does the Sign-In Flow Stall?

  • Return to the terminal to check for an authorization prompt.
  • Confirm that the browser is signed in to the intended AWS account.
  • Restart the sign-in flow after completing any pending browser prompt.

For account-specific help, help me diagnose my AWS CLI login flow.

Create the Atlas database boundary

Atlas keeps the MongoDB database available after the local Docker Compose service stops. A Free cluster provides enough capacity for this inventory demonstration at $0 per hour.

  • Create a MongoDB Atlas account.
  • Sign in to Atlas.

You will see the Atlas projects page for your account.

  • Start the Atlas project creation flow.
  • Enter race-safe-inventory as the project name.

The project summary now shows race-safe-inventory as the new project name.

  • Create the Atlas project.
  • Start creating a database deployment inside race-safe-inventory.

The deployment page presents the available cluster tiers.

  • Select the Free cluster option.
  • Choose AWS as the cloud provider.

The deployment summary now shows the Free tier on AWS.

  • Select US_EAST_1 as the AWS Region.

The deployment summary now shows AWS with US_EAST_1.

  • Create the Free cluster.

This is the slowest screen in the step. Provisioning may remain in progress for several minutes.

The cluster status changes to running when Atlas finishes provisioning it.

  • Open the database access controls for race-safe-inventory.
  • Select Create Database User.

The user form lets you define application authentication separately from your Atlas account.

Protect the Database Credential

The SCRAM password is a live database credential. The project never displays it in code or validation screenshots.

  • Choose SCRAM password authentication.
  • Enter a dedicated application username.

The application identity is now separate from your Atlas account identity.

  • Generate a strong password in your password manager.
  • Paste the generated password into the Atlas password field.

Atlas masks the credential inside the user form.

  • Set the database privilege to readWrite.
  • Set the privilege database to inventory_lab.

The privilege summary now grants write access only within inventory_lab.

  • Restrict the user to the inventory cluster.
  • Create the database user.

The database access page now lists the application user with its cluster restriction.

  • Open the project IP Access List.
  • Select Add Your Current IP Address.

Atlas fills the entry with your Mac's current public IP address.

  • Save the current-IP access entry.

The IP Access List now contains an entry for your Mac.

  • Return to the running cluster.
  • Select Connect.

The connection flow checks for both a database user plus an allowed client IP.

  • Select Choose Your Connection Method.
  • Choose Drivers.

Atlas now shows the SRV URI for an application connection.

  • Copy the SRV URI.
  • Paste the copied URI into a secure note in your password manager.

The secure note keeps the connection string outside the project files.

  • Change the database path in the URI to /inventory_lab.
  • Replace the URI password placeholder with the SCRAM password inside the secure note.

The full URI now authenticates the application user against the inventory_lab database.

  • Save the completed URI in your password manager.

Before you verify, predict whether the CLI can identify the same AWS account you authorized in the browser.

  • Verify the temporary AWS identity by running this command:
aws sts get-caller-identity

What Does This Verify?

The command returns the AWS account plus the current authenticated principal. A successful response proves that the CLI can make authenticated AWS requests.

You will see your AWS account identity in the response. Your temporary CLI session is ready for the deployment commands ahead.

  • Switch back to the race-safe-inventory Atlas project from earlier.
  • Confirm that the Free cluster status shows as running.

The cluster summary shows a running AWS Free cluster in US_EAST_1.

  • Return to the database access controls.
  • Confirm that the application user has readWrite access only on inventory_lab.

The user entry shows the inventory-cluster restriction.

  • Return to the project IP Access List.
  • Confirm that your current Mac IP appears in the list.

Both cloud environments now show the access boundaries required for deployment.

✔️ Awesome, I've got everything!

Great. Your AWS account plus Atlas project are ready for the container deployment.

ⓧ I'd like to double check the full setup

Compare your cloud state with each final requirement below.

  • AWS CLI: Installed and authenticated with temporary AWS IAM Identity Center or console credentials. The default Region is us-east-1.
  • AWS account: Root MFA is enabled. No root access keys exist. Free Tier usage alerts are enabled. A zero-spend budget plus a USD 5 monthly cost budget are configured.
  • Atlas project race-safe-inventory: One running AWS Free cluster exists in US_EAST_1. The database user has readWrite access only on inventory_lab. The current Mac IP appears in the access list.

That is your cloud foundation secured. Next, you will package the existing service as an ARM64 container and watch its localhost assumption fail inside the container boundary.

Containerize and Expose the Localhost Boundary

The guarded Amazon Web Services account plus your MongoDB Atlas cluster are ready. However, the existing application still assumes its database lives on the same localhost as the Node.js process.

This step packages the unchanged service into a Docker container image for ARM64. You will run the image to reveal what localhost means from inside a container.

In this step, get ready to:
  • Create a production container recipe plus a focused build context.
  • Build the service for linux/arm64.
  • Trigger the planned database connection failure from inside the container.
Create the production container files

A Dockerfile defines the image layers plus the process that starts inside the container. A .dockerignore file keeps local dependencies plus development files out of the build context.

  • Press Cmd+Space on macOS or the Windows key on Windows to search for Docker Desktop.
  • Press Enter to open Docker Desktop.

Docker Desktop can take a minute to start its container engine. That brief wait is normal after a fresh launch.

  • Wait until Docker Desktop indicates that its engine is running.

Docker Desktop is ready. It can now build the local service into the same architecture that the cloud deployment uses.

Why Keep the Application Unchanged?

The current application still uses its existing localhost database fallback. Keeping that code intact lets the container expose the networking boundary through a real connection attempt.

  • Press Cmd+Space on macOS or the Windows key on Windows to search for Visual Studio Code.
  • Press Enter to open Visual Studio Code.
  • Select the completed local inventory project folder that contains package.json.
  • Confirm that app.js plus product.js appear in the VS Code Explorer sidebar.
  • Select the new-file control in the Explorer sidebar.

You will see a filename field inside the project folder.

  • Enter Dockerfile in the filename field.
  • Press Enter to create the file.

You will see an empty Dockerfile open in the editor.

  • Define the production image by pasting this code into Dockerfile:
FROM node:24.22.0-alpine3.24
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node app.js product.js ./
USER node
EXPOSE 3000
CMD ["node", "app.js"]

What Does This Container Recipe Do?

  • The pinned Node.js base image gives each build the same runtime.
  • The dependency layer uses package-lock.json to install production packages.
  • The application layer copies app.js plus product.js under ownership of the non-root node user.
  • The final instruction starts the existing application process on container launch.
  • Save Dockerfile.
  • Confirm that the file begins with FROM node:24.22.0-alpine3.24.
  • Confirm that the file ends with CMD ["node", "app.js"].

Does the Dockerfile Look Different?

Check that the filename is exactly Dockerfile with no extension. Confirm that every instruction appears on its own line.

Confirm that the base image tag remains node:24.22.0-alpine3.24.

Help me check my Dockerfile.

✔️ Awesome, I've got everything!

Your saved Dockerfile now defines the pinned production image plus its non-root process.

ⓧ I'd like to double check the full code

FROM node:24.22.0-alpine3.24
ENV NODE_ENV=production
WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --chown=node:node app.js product.js ./
USER node
EXPOSE 3000
CMD ["node", "app.js"]

What Should Match?

Compare every line with your saved Dockerfile. The base image tag plus file paths must match exactly.

  • Select the new-file control in the VS Code Explorer sidebar.

You will see another filename field inside the same project folder.

  • Enter .dockerignore in the filename field.
  • Press Enter to create the file.

You will see an empty .dockerignore open in the editor.

  • Limit the build context by pasting these entries into .dockerignore:
node_modules
npm-debug.log*
.git
.gitignore
compose.yaml
race.js
.env
.env.*

What Does This Ignore List Protect?

  • Local dependency folders stay outside the image build context.
  • Git history plus local Git settings stay outside the image build context.
  • The local Compose configuration plus the race runner stay outside the production image.
  • Files beginning with .env stay outside the build context.
  • Save .dockerignore.
  • Confirm that node_modules is the first entry.
  • Confirm that .env.* is the final entry.

Is the Ignore File Missing?

Check that the filename starts with a full stop. Confirm that VS Code did not append a text-file extension.

Help me check my ignore file.

✔️ Awesome, I've got everything!

Your production build context now excludes local dependencies plus development-only files.

ⓧ I'd like to double check the full code

node_modules
npm-debug.log*
.git
.gitignore
compose.yaml
race.js
.env
.env.*

What Should Match?

Compare the eight entries with your saved .dockerignore. Their spelling plus order must match this reference.

Test the container recipe

The target deployment expects an image built for linux/arm64. Loading the result into Docker Desktop also makes the image available for the local boundary test.

  • Return to a terminal whose current folder contains the saved Dockerfile.
  • Build the pinned ARM64 image by running this command:
docker buildx build --platform linux/arm64 --load -t race-safe-inventory-service:1.0.0 .

What Does This Build Command Do?

  • The platform option selects the Linux ARM64 target.
  • The load option places the finished image in Docker Desktop's local image store.
  • The tag names the image race-safe-inventory-service:1.0.0.

The first build can take a few minutes while Docker downloads the pinned base image. The terminal continues through each image layer before completing the build.

Does the Image Build Fail?

Confirm that Docker Desktop still reports a running engine. Check that the terminal is in the folder containing Dockerfile plus package-lock.json.

Compare the filename casing if the build cannot find the container recipe.

Help me diagnose the image build.

The slowest local build is complete. Your image is now stored locally under its immutable release tag.

  • Inspect the built image architecture by running this command:
docker image inspect race-safe-inventory-service:1.0.0 --format '{{.Architecture}}'

What Does This Inspection Check?

The command reads the architecture metadata from the tagged local image. Its formatted output isolates the architecture value.

You will see arm64. This confirms that the local image matches the architecture required for the later deployment.

Do You See a Different Architecture?

Confirm that the build command included the Linux ARM64 platform. Check that you inspected the race-safe-inventory-service:1.0.0 tag.

Help me inspect the image architecture.

Expose the localhost boundary

Inside a container, 127.0.0.1 points back to that container. The existing MongoDB fallback URI therefore targets port 27017 inside the application container.

Before you run the image, do you think its unchanged database connection can reach the MongoDB service on your Mac?

  • Start the unchanged application image without a database environment variable by running this command:
docker run --name race-safe-inventory -p 3000:3000 race-safe-inventory-service:1.0.0

This Failure Is Expected

The command starts the application in a container named race-safe-inventory. The process exits after its database connection targets 127.0.0.1:27017 inside that container.

This planned failure proves that the Mac plus the container have separate localhost boundaries.

  • Inspect the stopped container's startup output by running this command:
docker logs race-safe-inventory

What Do These Logs Prove?

The logs preserve output from the stopped application process. The connection details identify 127.0.0.1:27017 as the unreachable database destination.

You will see a connection error containing 127.0.0.1:27017. The image ran correctly far enough to expose the application's local database assumption.

  • Remove the stopped test container by running this command:
docker rm race-safe-inventory

What Does This Cleanup Remove?

The command removes the stopped container named race-safe-inventory. The reusable image tagged race-safe-inventory-service:1.0.0 remains in the local image store.

You have packaged the service for ARM64 plus captured the exact localhost failure. Next, you will replace that local assumption with runtime configuration plus observable service endpoints.

Configure Health and Reservation Routes

The planned localhost failure inside your container exposed the boundary between the image and your Mac. The service now needs a database address that the container can receive when it starts.

You will add runtime configuration for MongoDB Atlas. You will also expose health, inventory, and reservation contracts that work through HTTP.

In this step, get ready to:
  • Configure the application through runtime environment values.
  • Add health plus race-safe reservation routes.
  • Rebuild the ARM64 image and validate it against Atlas.
Replace the application entry point

The entry point connects Mongoose before starting Express. Its routes become the observable contract used by local checks and the later cloud deployment.

  • Return to Visual Studio Code from earlier.
  • Select app.js in the Explorer sidebar.
  • Replace the contents of app.js with the complete file in the double-check tab below.

✔️ Awesome, I've got everything!

Great. Your application entry point now contains the cloud configuration and service routes.

ⓧ I'd like to double check the full code

import express from 'express';
import mongoose from 'mongoose';
import { Product } from './product.js';

const MONGODB_URI = process.env.MONGODB_URI ?? 'mongodb://127.0.0.1:27017/inventory_lab';
const SKU = 'keyboard';
const PORT = Number(process.env.PORT ?? 3000);

async function main() {
  await mongoose.connect(MONGODB_URI);
  await Product.init();

  const existingProduct = await Product.findOne({ sku: SKU });
  if (!existingProduct) await Product.create({ sku: SKU, stock: 5 });

  const app = express();
  app.use(express.json());

  app.get('/health', (request, response) => {
    response.status(200).json({ status: 'ok' });
  });

  app.get('/inventory', async (request, response) => {
    try {
      const product = await Product.findOne({ sku: SKU });
      response.json({ method: request.method, product });
    } catch (error) {
      console.error(error);
      response.status(500).json({ error: 'database error' });
    }
  });

  app.post('/reservations', async (request, response) => {
    const quantity = Number(request.body.quantity);
    if (!Number.isInteger(quantity) || quantity <= 0) {
      return response.status(400).json({ error: 'quantity must be a positive integer' });
    }

    try {
      const updated = await Product.findOneAndUpdate(
        { sku: SKU, stock: { $gte: quantity } },
        { $inc: { stock: -quantity } },
        { returnDocument: 'after', runValidators: true }
      );

      if (!updated) {
        console.log(JSON.stringify({ event: 'reservation-conflict', quantity }));
        return response.status(409).json({ outcome: 'conflict' });
      }

      console.log(JSON.stringify({
        event: 'reservation-created',
        quantity,
        remainingStock: updated.stock
      }));
      return response.status(201).json({
        outcome: 'reserved',
        quantity,
        remainingStock: updated.stock
      });
    } catch (error) {
      console.error(error);
      return response.status(500).json({ error: 'database error' });
    }
  });

  const server = app.listen(PORT, () => {
    console.log(`Inventory service listening on port ${PORT}`);
  });

  const shutdown = () => {
    server.close(async () => {
      await mongoose.disconnect();
      process.exit(0);
    });
  };

  process.once('SIGTERM', shutdown);
  process.once('SIGINT', shutdown);
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

How Does the Service Contract Work?

  • The MONGODB_URI value comes from the runtime environment. The local address remains a fallback for the original lab.
  • The PORT value lets the hosting environment choose the listening port.
  • The /health route returns a small JSON response for health checks.
  • The /reservations route validates a positive integer before touching stock.
  • The conditional findOneAndUpdate() call checks $gte while applying $inc atomically.
  • The shutdown function closes the server before disconnecting from MongoDB.
  • Save app.js.
  • Confirm the editor shows no red syntax underlines in app.js.

Seeing Syntax Problems in app.js?

Confirm that every route closes before the next route begins. Check the parentheses around findOneAndUpdate() carefully.

Compare the final main().catch() block with the full file above.

Help me find the syntax problem in app.js.

Extend the race runner

The existing runner proves the local naive and atomic behaviors. A remote mode keeps the direct Atlas reset while sending both reservation attempts through the service boundary.

  • Select race.js in the Explorer sidebar.
  • Replace the contents of race.js with the complete file in the double-check tab below.

✔️ Awesome, I've got everything!

Great. Your runner now preserves both local modes and adds the remote service test.

ⓧ I'd like to double check the full code

import mongoose from 'mongoose';
import { Product } from './product.js';

const MONGODB_URI = process.env.MONGODB_URI ?? 'mongodb://127.0.0.1:27017/inventory_lab';
const SERVICE_URL = process.env.SERVICE_URL?.replace(/\/$/, '');
const SKU = 'keyboard';
const STARTING_STOCK = 5;
const QUANTITY = 4;

function createTwoPartyBarrier() {
  let arrivals = 0;
  let release;
  const bothReady = new Promise(resolve => { release = resolve; });

  return async function waitForBothReaders() {
    arrivals += 1;
    if (arrivals === 2) release();
    await bothReady;
  };
}

async function resetInventory() {
  return Product.findOneAndUpdate(
    { sku: SKU },
    { $set: { stock: STARTING_STOCK } },
    { upsert: true, returnDocument: 'after', runValidators: true }
  );
}

async function naiveReserve(label, waitForBothReaders) {
  const product = await Product.findOne({ sku: SKU });
  if (!product || product.stock < QUANTITY) return { label, outcome: 'insufficient stock' };
  await waitForBothReaders();
  product.stock -= QUANTITY;
  await product.save();
  return { label, outcome: 'reserved', observedStock: product.stock };
}

async function atomicReserve(label, waitForBothReaders) {
  const current = await Product.findOne({ sku: SKU });
  if (!current || current.stock < QUANTITY) return { label, outcome: 'insufficient stock' };
  await waitForBothReaders();
  const updated = await Product.findOneAndUpdate(
    { sku: SKU, stock: current.stock },
    { $inc: { stock: -QUANTITY } },
    { returnDocument: 'after' }
  );
  if (!updated) return { label, outcome: 'conflict' };
  return { label, outcome: 'reserved', observedStock: updated.stock };
}

async function remoteReserve(label) {
  const response = await fetch(`${SERVICE_URL}/reservations`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ quantity: QUANTITY })
  });
  return { label, httpStatus: response.status, ...await response.json() };
}

async function main() {
  const mode = process.argv[2] === 'atomic'
    ? 'atomic'
    : process.argv[2] === 'remote' ? 'remote' : 'naive';

  if (mode === 'remote' && !SERVICE_URL) throw new Error('SERVICE_URL is required for remote mode');

  try {
    await mongoose.connect(MONGODB_URI);
    await Product.init();
    await resetInventory();

    let results;
    if (mode === 'remote') {
      results = await Promise.all([remoteReserve('request-a'), remoteReserve('request-b')]);
    } else {
      const reserve = mode === 'atomic' ? atomicReserve : naiveReserve;
      const waitForBothReaders = createTwoPartyBarrier();
      results = await Promise.all([
        reserve('request-a', waitForBothReaders),
        reserve('request-b', waitForBothReaders)
      ]);
    }

    const finalProduct = await Product.findOne({ sku: SKU });
    console.log(JSON.stringify({
      mode,
      startingStock: STARTING_STOCK,
      quantityPerRequest: QUANTITY,
      results,
      finalStock: finalProduct.stock
    }, null, 2));
  } finally {
    await mongoose.disconnect();
  }
}

main().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

How Does Remote Mode Work?

  • The local naiveReserve() and atomicReserve() functions remain available.
  • The SERVICE_URL value identifies the running service without hard-coding its address.
  • The remoteReserve() function sends a reservation request with fetch().
  • The runner resets Atlas to five units before starting both requests.
  • The final output records both HTTP statuses plus the remaining stock.
  • Save race.js.
  • Confirm the editor shows no red syntax underlines in race.js.

The runner needs a named script so the remote test can be launched consistently. The dependency versions stay pinned while the script list gains race:remote.

  • Select package.json in the Explorer sidebar.
  • Replace its contents with the code below:
{
  "name": "race-safe-inventory-service",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node app.js",
    "race:naive": "node race.js naive",
    "race:atomic": "node race.js atomic",
    "race:remote": "node race.js remote"
  },
  "engines": { "node": "24.22.0" },
  "dependencies": {
    "express": "5.3.0",
    "mongoose": "9.11.2"
  }
}

What Does This Change Add?

The race:remote script selects the new runner mode. The existing start, naive, and atomic scripts remain unchanged.

The pinned dependency versions keep the container build reproducible.

  • Save package.json.
  • Refresh package-lock.json by running this command in the project terminal:
npm install

What Does This Command Update?

The command aligns package-lock.json with the scripts and dependencies declared in package.json.

Good. Your local source now contains the application contract plus the remote race runner.

Does npm Install Fail?

Confirm the terminal is inside the existing project folder that contains package.json.

Check the commas around the scripts object. Invalid JSON prevents the package file from loading.

Help me diagnose npm install.

Rebuild and validate the container

Runtime environment values keep credentials outside the image. Docker passes the Atlas URI into the process only when the container starts.

The Atlas URI contains a database password. Keeping it in the current shell prevents it from being written into app.js, the image, or the project history.

  • Return to the project terminal from earlier.
  • Replace <your_connection_string> with the full Atlas URI whose database path is /inventory_lab.
  • Export the completed URI only in the current shell by running this command:
export MONGODB_URI='<your_connection_string>'

Where Does the URI Live?

The current shell holds MONGODB_URI for commands launched from this terminal. Closing the terminal removes that exported value.

The command normally prints no output. Avoid displaying the value because it contains your database password.

  • Rebuild the ARM64 image with the updated source by running this command:
docker buildx build --platform linux/arm64 --load -t race-safe-inventory-service:1.0.0 .

What Does the Rebuild Produce?

The build packages the new app.js while keeping the target platform at linux/arm64. The completed image replaces the local race-safe-inventory-service:1.0.0 tag.

Does the Image Build Fail?

Confirm Docker Desktop is still running. Check that package-lock.json finished updating before the build started.

Help me diagnose the ARM64 image build.

Before you start the container, do you expect the earlier localhost connection failure or a listening service backed by Atlas?

  • Start the rebuilt image with the exported database URI by running this command:
docker run --name race-safe-inventory --env MONGODB_URI --env NODE_ENV=production -p 3000:3000 race-safe-inventory-service:1.0.0

What Does This Run Command Supply?

  • The MONGODB_URI value passes from the current shell into the container.
  • The NODE_ENV=production value sets the runtime environment explicitly.
  • Port 3000 on your Mac maps to port 3000 inside the container.

You will see Inventory service listening on port 3000. The container has crossed the boundary that failed in the previous section.

Before you check the routes, what response should prove the process is healthy? What inventory should confirm Atlas is connected?

  • Enter http://localhost:3000/health in your browser address bar.
  • Load the health route.
  • Enter http://localhost:3000/inventory in your browser address bar.
  • Load the inventory route.

The health response shows status: "ok". The inventory response shows the keyboard product with five units of stock.

  • Return to the terminal running the container.
  • Press Ctrl+C to stop the service.
  • Remove the stopped test container by running this command:
docker rm race-safe-inventory

What Did You Remove?

The command removes the stopped test container. The rebuilt race-safe-inventory-service:1.0.0 image remains available for publishing.

Can’t Remove the Test Container?

Confirm the foreground service stopped after you pressed Ctrl+C.

Help me remove the test container safely.

That is the boundary fixed. Your ARM64 container now reaches Atlas through runtime configuration and exposes observable service routes.

Your image is ready for a registry. Next, you will publish its immutable tag and give the deployment narrowly scoped access to its database secret.

Publish the Image and Create ECS Roles

Your ARM64 container already runs against Atlas from your Mac. Amazon ECR now gives that image an address AWS can pull.

Amazon ECS Express Mode also needs two IAM roles. The execution role handles task startup.

The infrastructure role manages the resources around the service. Only the execution role receives permission to retrieve the database secret.

In this step, get ready to:
  • Publish the ARM64 image under an immutable ECR tag.
  • Store the Atlas URI in AWS Secrets Manager.
  • Create separate execution and infrastructure roles for ECS Express Mode.
Publish the immutable image

An immutable repository prevents another image from replacing tag 1.0.0. The tag remains tied to the version you already tested.

Publishing creates cloud storage that can consume AWS credits. The cleanup section removes the repository in the same session.

  • Return to the authenticated terminal from earlier.
  • Create the private ECR repository by running this command:
aws ecr create-repository \
  --repository-name race-safe-inventory-service \
  --image-tag-mutability IMMUTABLE \
  --region us-east-1

What Does This Command Create?

  • The repository name matches the local image name.
  • The IMMUTABLE setting prevents a pushed tag from being overwritten.
  • The repository lives in us-east-1 with your other project resources.

The response identifies race-safe-inventory-service as a private repository. It also reports immutable image tags.

Repository Creation Failed?

Confirm that your CLI session is still authenticated. Check that the command targets us-east-1.

If the repository already exists from an earlier attempt, inspect its tag mutability before continuing.

Help me diagnose the ECR repository command.

ECR requires a short-lived registry login before Docker can push the image. Your existing AWS CLI session supplies the credentials for that login.

  • Capture your account details and authenticate Docker by running these commands:
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=us-east-1
aws ecr get-login-password --region "$REGION" \
  | docker login --username AWS --password-stdin \
  "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"

How Does Registry Login Work?

  • The first assignment stores your AWS account ID in ACCOUNT_ID.
  • The second assignment keeps the target region in REGION.
  • The login pipeline passes a temporary ECR password directly to Docker.

The terminal confirms that Docker is authenticated to your account's ECR registry.

Registry Login Failed?

Confirm that Docker Desktop is still running. Reauthenticate the AWS CLI if the temporary session expired.

Help me fix my ECR registry login.

  • Tag the validated image and push it by running these commands:
docker tag race-safe-inventory-service:1.0.0 \
  "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/race-safe-inventory-service:1.0.0"
docker push \
  "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/race-safe-inventory-service:1.0.0"

What Does the Push Preserve?

  • The tag command gives the local image its account-specific registry address.
  • The push command uploads the tested ARM64 image under tag 1.0.0.
  • ECR assigns a content digest that identifies the uploaded image bytes.

The push output completes each image layer. It finishes by reporting a digest for tag 1.0.0.

Image Push Failed?

Repeat the registry login if authentication expired. Confirm that the repository name in the remote tag matches race-safe-inventory-service.

Help me diagnose my Docker push.

  • Keep the complete image address in the current shell by running this assignment:
IMAGE_URI="$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/race-safe-inventory-service:1.0.0"

Why Keep the Image URI?

The IMAGE_URI value identifies the exact repository tag that ECS Express Mode deploys. Keeping it in the current shell avoids rebuilding that address later.

Store the Atlas URI

AWS Secrets Manager keeps the database credential outside the image. ECS retrieves the value when the task starts.

This secret consumes AWS credits while stored. The final cleanup schedules it for deletion without exposing its value.

You are about to handle the live MongoDB Atlas credential. Keep the URI out of screenshots while you complete the wizard.

  • Switch back to the signed-in AWS console from earlier.
  • Confirm that the console region is us-east-1.
  • Enter AWS Secrets Manager in the console search bar.
  • Select AWS Secrets Manager from the search results.
  • Choose Store a new secret.
  • Select Other type of secret.
  • Select the Plaintext tab.
  • Paste the full Atlas URI whose database path ends with /inventory_lab.

Why Store Plaintext Here?

Plaintext describes the secret's input format. Secrets Manager encrypts the stored value with the selected encryption key.

ECS needs the complete connection string as one environment variable. Keeping it as one secret preserves that runtime contract.

  • Choose aws/secretsmanager for Encryption key.
  • Choose Next.
  • Enter race-safe-inventory/mongodb-uri in Secret name.
  • Choose Next.
  • Keep automatic rotation off.
  • Choose Next.
  • Review the secret configuration.
  • Choose Store.

The secret details page now shows the new secret's metadata. The credential value remains protected.

  • Copy the secret ARN into your Secrets Manager secret ARN.
  • Return to the authenticated terminal from earlier.
  • Store the secret ARN in the current shell by running this command:
SECRET_ARN=$(aws secretsmanager describe-secret --secret-id race-safe-inventory/mongodb-uri --query ARN --output text)

What Does This Command Retrieve?

The command retrieves the secret's metadata ARN. It does not retrieve the protected Atlas URI.

The shell stores that ARN in SECRET_ARN for the execution role policy.

Secret ARN Lookup Failed?

Confirm that the secret name uses the full race-safe-inventory/mongodb-uri path. Check that the CLI session targets us-east-1.

Help me find my secret ARN.

Create the two ECS roles

A trust policy defines which AWS service can assume a role. A permissions policy defines what that service can do after assuming it.

The application receives no task role on the main path. ECS handles the image pull plus secret injection before the Node.js process starts.

  • Create the task execution role and infrastructure role by running these commands:
aws iam create-role --role-name ecsTaskExecutionRole \
  --assume-role-policy-document '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}'

aws iam create-role --role-name ecsInfrastructureRoleForExpressServices \
  --assume-role-policy-document '{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowAccessInfrastructureForECSExpressServices",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}'

How Are the Roles Trusted?

  • The ecsTaskExecutionRole trust policy allows ECS tasks to assume the role.
  • The ecsInfrastructureRoleForExpressServices trust policy allows ECS to assume the role.
  • Each trust relationship names one service principal.

The terminal returns metadata for both roles. Each response contains the corresponding role name plus its trust policy.

Role Creation Failed?

Confirm that your current AWS identity can create IAM roles. If a named role already exists, inspect its trust relationship before continuing.

Help me diagnose the ECS role creation commands.

  • Attach the documented ECS managed policies by running these commands:
aws iam attach-role-policy --role-name ecsTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam attach-role-policy --role-name ecsInfrastructureRoleForExpressServices \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRoleforExpressGatewayServices

Why Use Separate Managed Policies?

AmazonECSTaskExecutionRolePolicy gives ECS the standard startup permissions for image pulls plus logs.

AmazonECSInfrastructureRoleforExpressGatewayServices lets Express Mode manage its surrounding infrastructure.

These attachment commands return control without printing a response. The role details page provides the visible confirmation.

Managed Policy Attachment Failed?

Confirm that both role names match the names returned by the creation commands. Check the managed policy ARN for the role that failed.

Help me fix the managed policy attachments.

  • Switch back to the AWS console.
  • Enter IAM in the console search bar.
  • Select IAM from the search results.
  • Choose Roles in the navigation pane.
  • Select ecsTaskExecutionRole.
  • Choose the Permissions tab.
  • Choose Add permissions.
  • Choose Create inline policy.

Why Add an Inline Policy?

The standard execution policy does not grant access to your database secret. The inline policy adds one action against one secret ARN.

This resource boundary keeps the Atlas credential permission attached to the role that performs secret injection.

  • Choose the Visual policy editor.
  • Select AWS Secrets Manager in Select a service.
  • Select GetSecretValue as the only allowed action.
  • Restrict the policy to a specific secret resource.
  • Paste your Secrets Manager secret ARN as the only resource ARN.
  • Choose Next.
  • Enter ReadInventoryMongoUri as the policy name.
  • Choose Create policy.

The execution role now lists its AWS managed policy plus ReadInventoryMongoUri. The inline policy shows one allowed action against one secret.

Inline Policy Will Not Save?

Confirm that GetSecretValue is the only selected action. Check that the resource contains the full secret ARN.

Help me check my least-privilege secret policy.

  • Return to the Roles list.
  • Select ecsInfrastructureRoleForExpressServices.
  • Confirm that AmazonECSInfrastructureRoleforExpressGatewayServices appears under permissions.

Before you check the registry, what should prove that tag 1.0.0 refers to uploaded image content?

  • Return to the authenticated terminal.
  • Verify the published image by running this command:
aws ecr describe-images --repository-name race-safe-inventory-service --image-ids imageTag=1.0.0

What Does This Check Prove?

ECR looks up tag 1.0.0 inside the named repository. Its response includes the image tag plus the content digest.

The digest begins with sha256. That value gives the pushed image a content-based identity.

You will see tag 1.0.0 plus a sha256 digest in the response. That confirms the immutable ARM64 image is ready for ECS.

Image Metadata Missing?

Confirm that the push completed in the same AWS account. Check that the repository contains tag 1.0.0 in us-east-1.

Help me locate my ECR image metadata.

The image, secret, plus service roles are ready. Next, you will combine them in a public ECS Express Mode deployment.

Deploy, Observe, and Test Remotely

Your immutable ARM64 image is stored in Amazon ECR. Its database secret is ready in AWS Secrets Manager.

Now Amazon ECS Express Mode can connect those pieces to a public HTTPS service. You will test the stock invariant through that remote boundary.

In this step, get ready to:
  • Add temporary Atlas access for changing ECS public egress.
  • Deploy the Express Mode service and inspect its operations.
  • Run the concurrent reservation test against public HTTPS.
Open temporary Atlas access and deploy

The Free cluster in MongoDB Atlas cannot use private networking. Express Mode tasks also use changing public egress addresses.

Keep Public Access Temporary

The access rule permits connection attempts from anywhere. Its six-hour expiry limits the learning window.

TLS remains enabled. The SCRAM user retains database-scoped access to inventory_lab.

  • Return to the IP Access List in your MongoDB Atlas project from earlier.
  • Click Add an IP Address.
  • Enter 0.0.0.0/0.
  • Choose temporary access for 6 hours.
  • Confirm the new access-list entry.

You should see your Mac IP plus the temporary 0.0.0.0/0 entry. The expiry should show a six-hour access window.

This Deployment Consumes AWS Credits

Creating this service starts Fargate plus related managed infrastructure. An active AWS Free plan uses credits for that usage.

A Paid plan bills the underlying resources. You will remove them in the cleanup section.

  • Switch back to the terminal session that holds ACCOUNT_ID.
  • Confirm that the same session holds REGION.
  • Confirm that the same session holds IMAGE_URI.
  • Confirm that the same session holds SECRET_ARN.
  • Create the Express Mode service by running these commands:
EXECUTION_ROLE_ARN="arn:aws:iam::$ACCOUNT_ID:role/ecsTaskExecutionRole"
INFRASTRUCTURE_ROLE_ARN="arn:aws:iam::$ACCOUNT_ID:role/ecsInfrastructureRoleForExpressServices"
PRIMARY_CONTAINER=$(printf '{"image":"%s","containerPort":3000,"environment":[{"name":"NODE_ENV","value":"production"}],"secrets":[{"name":"MONGODB_URI","valueFrom":"%s"}]}' "$IMAGE_URI" "$SECRET_ARN")

aws ecs create-express-gateway-service \
  --service-name race-safe-inventory-service \
  --primary-container "$PRIMARY_CONTAINER" \
  --execution-role-arn "$EXECUTION_ROLE_ARN" \
  --infrastructure-role-arn "$INFRASTRUCTURE_ROLE_ARN" \
  --cpu-architecture ARM64 \
  --health-check-path "/health" \
  --scaling-target '{"minTaskCount":1,"maxTaskCount":2,"autoScalingMetric":"AVERAGE_CPU","autoScalingTargetValue":60}' \
  --monitor-resources DEPLOYMENT \
  --region "$REGION"

What Does This Deployment Configure?

  • The PRIMARY_CONTAINER definition selects your ECR image. It exposes container port 3000.
  • The execution role pulls the image. It injects MONGODB_URI from the single secret.
  • The infrastructure role manages ingress plus service resources. The application receives no task role.
  • The service runs the ARM64 image. It checks /health for a successful response.
  • Autoscaling keeps at least one task. It can add a second task when average CPU reaches the target value of 60.

Express Mode can take several minutes to provision its managed resources. A quiet wait during this stage is expected.

  • Keep the terminal session open until deployment monitoring finishes.
  • Return to the Amazon ECS console in us-east-1.
  • Select the race-safe-inventory-service service.
  • Wait until the service status shows ACTIVE.

The waiting paid off. Your ARM64 container is now running behind an AWS-managed public endpoint.

Service Still Provisioning?

Check the deployment events for a failed image pull or an unhealthy task. Confirm that IMAGE_URI still points to the immutable 1.0.0 image.

Confirm that ecsTaskExecutionRole can read only SECRET_ARN. Confirm that the Atlas temporary access entry has not expired.

Help me diagnose my Express Mode deployment.

Inspect the service and public endpoints

The Application URL is the public entry point for your container. Service output flows into Amazon CloudWatch for operational checks.

Before you load the public routes, what response do you expect from each contract?

  • Record the HTTPS Application URL here: your application url.
  • Enter your application url followed by /health in your browser address bar.
  • Load the health route.
  • Replace /health with /inventory.
  • Load the inventory route.

The health response contains status with the value ok. The inventory response shows the keyboard product with five units in stock.

  • Select Express service Overview to confirm one running task.
  • Select Observability to confirm healthy checks.
  • Select Logs to confirm the startup message.
  • Select Resources to confirm the managed infrastructure.
  • Select Deployments to confirm image tag 1.0.0.
  • Select Configuration to confirm the ARM64 architecture plus /health path.
Run the remote race test

The remote runner resets stock through your Mac connection to Atlas. It then sends two concurrent reservation requests to the public service.

  • Return to the Atlas IP Access List from earlier.
  • Confirm that your current Mac IP remains allowlisted.

The next command handles the database credential through a hidden prompt. The value stays in the current shell instead of a project file.

  • Return to the terminal in the local inventory project from earlier.
  • Copy the full Atlas URI from your secure storage.
  • Prepare to paste the URI when the terminal asks for Paste Atlas URI:.
  • Set both runtime variables in the current shell by running these commands:
read -s "MONGODB_URI?Paste Atlas URI: "
export MONGODB_URI
export SERVICE_URL="[[SERVICE_URL="your application url"]]"

Why Does the Runner Need Both Values?

MONGODB_URI lets the local runner reset the Atlas stock before the test. SERVICE_URL directs both reservation requests to the deployed service.

The silent prompt keeps the URI off the screen. Closing this terminal session removes the exported values from the shell environment.

Before you run the test, do you think both oversized reservations can still succeed?

  • Run the remote concurrency test with this command:
npm run race:remote

What Proves Race Safety?

One result has HTTP 201. The other result has HTTP 409.

The output reports finalStock as 1. The conditional atomic update keeps the stock non-negative across concurrent public requests.

  • Refresh the public /inventory route.
  • Return to Logs in the ECS service details.
  • Find the reservation-created event.
  • Find the reservation-conflict event.

You should see one unit remaining in the inventory response. CloudWatch should contain both reservation outcomes.

Remote Test Not Matching?

If the runner cannot reset stock, confirm that your current Mac IP remains in the Atlas access list. Confirm that MONGODB_URI belongs to this terminal session.

If the public requests fail, confirm that the service remains ACTIVE. Confirm that the temporary Atlas entry has not expired.

Help me diagnose my remote race test.

That is the full cloud proof complete. Your deployed service preserves the stock invariant through public HTTPS.

Secret mission

Replace the Database Password with an ECS Task Role

Replace the stored Atlas database password with temporary credentials from an ECS task role. Then prove that the public inventory service still survives the remote race test.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep the deployment available, remove the live service for later, or delete every cloud resource created in this project. The active AWS deployment can consume credits or incur usage charges, while the MongoDB Atlas Free cluster costs $0.

Cost warning

Running Amazon ECS Express Mode consumes AWS credits under an eligible Free plan. A Paid plan bills the underlying services by usage.

The old AWS Secrets Manager secret is already scheduled for deletion. AWS does not charge the secret during its recovery window.

Resources you used:

  • The ECS Express Mode service race-safe-inventory-service with its managed compute, ingress, scaling, networking, and CloudWatch observability resources.
  • The private Amazon ECR repository race-safe-inventory-service with immutable images 1.0.0 and 1.1.0-iam.
  • The IAM roles ecsTaskExecutionRole, ecsInfrastructureRoleForExpressServices, and ecsTaskRoleForAtlas.
  • The Atlas project containing the Free cluster, the IAM database user, your Mac IP entry, and the temporary 0.0.0.0/0 entry.
  • The secret race-safe-inventory/mongodb-uri that is already scheduled for deletion.

Keep everything running

Keep this option while you are actively demonstrating the public service. The deployment stays available at its HTTPS Application URL.

  • Monitor your remaining AWS credits from Billing and Cost Management.
  • Monitor the budgets you created for unexpected usage.
  • Keep the ECS Express Mode service race-safe-inventory-service active.
  • Keep the ECR repository race-safe-inventory-service.
  • Keep the Atlas project available.
  • Leave race-safe-inventory/mongodb-uri scheduled for deletion because the service now uses IAM authentication.

The temporary 0.0.0.0/0 entry expires after six hours. The service loses Atlas network access when that entry expires.

Keeping this deployment available requires another network access decision after expiry. Delete the service when you finish your demonstration.

Pause - I'll come back to this later

Removing the live service stops its runtime resources. Your ECR images remain available.

Your Atlas project also remains available for a later deployment.

  • Return to the service details for race-safe-inventory-service in Amazon ECS.
  • Copy the service ARN.
  • Replace <service-arn> in the command below with the copied ARN.
  • Delete the live service by running this command:
aws ecs delete-express-gateway-service --service-arn <service-arn> --monitor-resources RESOURCE --region us-east-1

What does this command do?

The command deletes the Express Mode service. Resource monitoring reports which managed components AWS removes.

  • Wait until resource monitoring finishes.
  • Review the removed resources in the command output.
  • Review any retained resources in the command output.
  • Return to the previous Application URL after deletion finishes.

The HTTPS health endpoint no longer responds. This confirms that the live service has stopped.

Service deletion not starting?

  • Confirm that the copied service ARN replaced <service-arn>.
  • Refresh your AWS CLI authentication if the session expired.

Ask for help with the deletion command: Help me delete my ECS Express Mode service safely..

  • Return to the Atlas IP Access List.
  • Remove the temporary 0.0.0.0/0 entry.
  • Keep your Mac IP entry for local access.
  • Keep the ECR repository race-safe-inventory-service.
  • Keep the three IAM roles for a later deployment.

Atlas automatically pauses an inactive Free cluster after 30 days. A Free cluster cannot be paused manually.

Delete - I don't want to use this again

Full cleanup is intentionally permanent. Your local project files remain untouched.

Delete the live service first so AWS can remove its managed infrastructure before you remove the supporting roles.

  • Return to the service details for race-safe-inventory-service in Amazon ECS.
  • Copy the service ARN.
  • Replace <service-arn> in the command below with the copied ARN.
  • Delete the service by running this command:
aws ecs delete-express-gateway-service --service-arn <service-arn> --monitor-resources RESOURCE --region us-east-1

What does this command remove?

The command deletes the Express Mode service. It also removes the managed infrastructure dedicated to that service.

The monitoring output distinguishes removed resources from retained resources. Review both groups before continuing.

  • Wait until resource monitoring finishes.
  • Review every removed resource in the command output.
  • Review every retained resource in the command output.
  • Return to the previous Application URL after deletion finishes.

The HTTPS health endpoint no longer responds. This proves that public traffic can no longer reach the service.

Service still available?

  • Confirm that deletion monitoring has finished before testing the URL.
  • Confirm that the command used the service ARN from us-east-1.

Ask for help checking the remaining resources: Why is my ECS Express Mode service still available?.

  • Return to the Atlas IP Access List.
  • Remove the temporary 0.0.0.0/0 entry immediately.

The broad temporary network entry is now gone. Atlas no longer accepts connection attempts from every public IP address.

  • Delete the ECR repository with both immutable images by running this command:
aws ecr delete-repository --repository-name race-safe-inventory-service --force --region us-east-1

What does this command remove?

The command deletes the private ECR repository. The force option also removes the images stored inside it.

  • Return to the ECR repository list after the command succeeds.
  • Confirm that race-safe-inventory-service no longer appears.

Both immutable image tags are gone with the repository. ECR storage usage for this project has ended.

Repository deletion failed?

  • Confirm that your AWS CLI session is authenticated.
  • Confirm that the repository exists in us-east-1.

Ask for help with the repository cleanup: Help me delete my ECR repository..

The old database secret no longer authenticates the service. Its deletion is already in progress.

  • Return to AWS Secrets Manager.
  • Confirm that race-safe-inventory/mongodb-uri remains scheduled for deletion.
  • Leave the secret scheduled for deletion.

The execution role already has no ReadInventoryMongoUri policy. No additional inline policy cleanup is required.

  • Return to the AWS IAM page for ecsTaskExecutionRole.
  • Detach AmazonECSTaskExecutionRolePolicy from the execution role.
  • Delete ecsTaskExecutionRole.
  • Return to the AWS IAM page for ecsInfrastructureRoleForExpressServices.
  • Detach AmazonECSInfrastructureRoleforExpressGatewayServices from the infrastructure role.
  • Delete ecsInfrastructureRoleForExpressServices.
  • Return to the AWS IAM page for ecsTaskRoleForAtlas.
  • Delete ecsTaskRoleForAtlas.

The three project roles no longer appear in AWS IAM. This completes the AWS permission cleanup.

Role cannot be deleted?

  • Confirm that the ECS Express Mode service deletion has finished.
  • Confirm that each required managed policy is detached from its role.

Ask for help finding the remaining dependency: Why can I not delete this IAM role?.

Deleting the Atlas project removes its Free cluster, database users, and remaining network entries. This also removes the IAM database user for the deleted task role.

  • Return to the Atlas project.
  • Open Project Settings.
  • Choose Delete Project.
  • Complete the Recommended Actions.
  • Review the project data listed by Atlas.
  • Acknowledge every Required actions item.
  • Choose Delete project forever.

The race-safe-inventory project no longer appears in your Atlas project list. Its cluster, users, and IP access entries are deleted.

Atlas blocks project deletion?

  • Complete every outstanding item under Recommended Actions.
  • Acknowledge every item under Required actions.

Ask for help with the deletion wizard: Help me finish deleting my Atlas project..

  • End the authenticated AWS CLI session by running this command:
aws logout

What does this command do?

The command ends the current AWS CLI login session. Your deleted cloud resources stay deleted.

Your AWS CLI session is now closed. The cloud cleanup is complete.

Nice Work!

Nice Work!

You did it! Your race-safe inventory service is live through Amazon ECS Express Mode at a public HTTPS URL.

You've learned how to:

  • Package the existing service as a pinned, non-root ARM64 container. Use the planned localhost boundary failure to diagnose why the database URI needs runtime configuration.
  • Publish immutable image tags to Amazon ECR. Separate the ECS execution role from the infrastructure role. Limit the execution role to retrieving the single MongoDB Atlas URI from AWS Secrets Manager.
  • Verify the public service through its health endpoint. Trace reservation events in Amazon CloudWatch logs. Prove the stock invariant with the remote concurrency test. Observe 201 for one request. Observe 409 for the other. Confirm finalStock remains 1.
  • Secret Mission: Replace the stored database password with temporary credentials from ecsTaskRoleForAtlas. Keep database access scoped to readWrite on inventory_lab.

Ready to quiz yourself?