diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+## [_Unreleased_](https://github.com/freckle/freckle-app/compare/freckle-ecs-v0.0.0.0...main)
+
+## [v0.0.0.0](https://github.com/freckle/freckle-app/tree/freckle-ecs-v0.0.0.0/freckle-ecs)
+
+First release, sprouted from `freckle-app-1.21.0.0`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2021-2024 Renaissance Learning Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# freckle-ecs
+
+Small utility for Amazon Elastic Container Service
diff --git a/freckle-ecs.cabal b/freckle-ecs.cabal
new file mode 100644
--- /dev/null
+++ b/freckle-ecs.cabal
@@ -0,0 +1,50 @@
+cabal-version:      1.18
+name:               freckle-ecs
+version:            0.0.0.0
+license:            MIT
+license-file:       LICENSE
+maintainer:         Freckle Education
+homepage:           https://github.com/freckle/freckle-app#readme
+bug-reports:        https://github.com/freckle/freckle-app/issues
+synopsis:           Small utility for Amazon Elastic Container Service
+description:        Please see README.md
+category:           Prelude
+build-type:         Simple
+extra-source-files: package.yaml
+extra-doc-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type:     git
+    location: https://github.com/freckle/freckle-app
+
+library
+    exposed-modules:    Freckle.App.Ecs
+    hs-source-dirs:     library
+    other-modules:      Paths_freckle_ecs
+    default-language:   GHC2021
+    default-extensions:
+        DataKinds DeriveAnyClass DerivingVia DerivingStrategies GADTs
+        LambdaCase NoImplicitPrelude NoMonomorphismRestriction
+        OverloadedStrings TypeFamilies
+
+    ghc-options:
+        -fignore-optim-changes -fwrite-ide-info -Weverything
+        -Wno-all-missed-specialisations -Wno-missing-exported-signatures
+        -Wno-missing-import-lists -Wno-missing-kind-signatures
+        -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode
+        -Wno-monomorphism-restriction -Wno-prepositive-qualified-module
+        -Wno-safe -Wno-unsafe
+
+    build-depends:
+        aeson >=2.0.3.0,
+        base >=4.16.4.0 && <5,
+        extra >=1.7.13,
+        freckle-http >=0.0.0.0,
+        freckle-prelude >=0.0.1.1,
+        mtl >=2.2.2
+
+    if impl(ghc >=9.8)
+        ghc-options:
+            -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures
diff --git a/library/Freckle/App/Ecs.hs b/library/Freckle/App/Ecs.hs
new file mode 100644
--- /dev/null
+++ b/library/Freckle/App/Ecs.hs
@@ -0,0 +1,92 @@
+module Freckle.App.Ecs
+  ( EcsMetadata (..)
+  , EcsMetadataError (..)
+  , EcsContainerMetadata (..)
+  , EcsContainerTaskMetadata (..)
+  , getEcsMetadata
+  ) where
+
+import Freckle.App.Prelude
+
+import Control.Monad.Except (MonadError (..))
+import Data.Aeson
+import Data.List.Extra (dropPrefix)
+import Freckle.App.Http
+import System.Environment (lookupEnv)
+
+data EcsMetadata = EcsMetadata
+  { emContainerMetadata :: EcsContainerMetadata
+  , emContainerTaskMetadata :: EcsContainerTaskMetadata
+  }
+
+data EcsMetadataError
+  = EcsMetadataErrorNotEnabled
+  | EcsMetadataErrorInvalidURI String
+  | EcsMetadataErrorUnexpectedStatus Request Status
+  | EcsMetadataErrorInvalidJSON Request HttpDecodeError
+  deriving stock (Show)
+
+-- | Parsing for the @/@ response
+--
+-- <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html#task-metadata-endpoint-v4-examples>
+data EcsContainerMetadata = EcsContainerMetadata
+  { ecmDockerId :: Text
+  , ecmDockerName :: Text
+  , ecmImage :: Text
+  , ecmImageID :: Text
+  }
+  deriving stock (Generic)
+
+instance FromJSON EcsContainerMetadata where
+  parseJSON = genericParseJSON $ aesonDropPrefix "ecm"
+
+-- | Parsing of the @/task@ response
+--
+-- <https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html#task-metadata-endpoint-v4-response>
+data EcsContainerTaskMetadata = EcsContainerTaskMetadata
+  { ectmCluster :: Text
+  , ectmTaskARN :: Text
+  , ectmFamily :: Text
+  , ectmRevision :: Text
+  }
+  deriving stock (Generic)
+
+instance FromJSON EcsContainerTaskMetadata where
+  parseJSON = genericParseJSON $ aesonDropPrefix "ectm"
+
+aesonDropPrefix :: String -> Options
+aesonDropPrefix x = defaultOptions {fieldLabelModifier = dropPrefix x}
+
+getEcsMetadata :: (MonadIO m, MonadError EcsMetadataError m) => m EcsMetadata
+getEcsMetadata = do
+  mURI <-
+    liftIO $
+      (<|>)
+        <$> lookupEnv "ECS_CONTAINER_METADATA_URI_V4"
+        <*> lookupEnv
+          "ECS_CONTAINER_METADATA_URI"
+
+  uri <- maybe (throwError EcsMetadataErrorNotEnabled) pure mURI
+
+  EcsMetadata
+    <$> makeContainerMetadataRequest uri
+    <*> makeContainerMetadataRequest (uri <> "/task")
+
+makeContainerMetadataRequest
+  :: (MonadIO m, MonadError EcsMetadataError m, FromJSON a) => String -> m a
+makeContainerMetadataRequest uri = do
+  req <-
+    mapEither (EcsMetadataErrorInvalidURI . displayException) $
+      parseRequest uri
+  resp <- liftIO $ httpJson req
+
+  let status = getResponseStatus resp
+
+  unless (statusIsSuccessful status) $
+    throwError $
+      EcsMetadataErrorUnexpectedStatus req status
+
+  mapEither (EcsMetadataErrorInvalidJSON req) $ getResponseBody resp
+
+mapEither :: MonadError e m => (x -> e) -> Either x a -> m a
+mapEither f = either (throwError . f) pure
diff --git a/package.yaml b/package.yaml
new file mode 100644
--- /dev/null
+++ b/package.yaml
@@ -0,0 +1,61 @@
+name: freckle-ecs
+version: 0.0.0.0
+maintainer: Freckle Education
+category: Prelude
+github: freckle/freckle-app
+synopsis: Small utility for Amazon Elastic Container Service
+description: Please see README.md
+
+extra-doc-files:
+  - README.md
+  - CHANGELOG.md
+
+extra-source-files:
+  - package.yaml
+
+language: GHC2021
+
+ghc-options:
+  - -fignore-optim-changes
+  - -fwrite-ide-info
+  - -Weverything
+  - -Wno-all-missed-specialisations
+  - -Wno-missing-exported-signatures # re-enables missing-signatures
+  - -Wno-missing-import-lists
+  - -Wno-missing-kind-signatures
+  - -Wno-missing-local-signatures
+  - -Wno-missing-safe-haskell-mode
+  - -Wno-monomorphism-restriction
+  - -Wno-prepositive-qualified-module
+  - -Wno-safe
+  - -Wno-unsafe
+
+when:
+  - condition: "impl(ghc >= 9.8)"
+    ghc-options:
+      - -Wno-missing-role-annotations
+      - -Wno-missing-poly-kind-signatures
+
+dependencies:
+  - base < 5
+
+default-extensions:
+  - DataKinds
+  - DeriveAnyClass
+  - DerivingVia
+  - DerivingStrategies
+  - GADTs
+  - LambdaCase
+  - NoImplicitPrelude
+  - NoMonomorphismRestriction
+  - OverloadedStrings
+  - TypeFamilies
+
+library:
+  source-dirs: library
+  dependencies:
+    - aeson
+    - extra
+    - freckle-http
+    - freckle-prelude
+    - mtl
