diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,9 @@
-## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.2.2...main)
+## [_Unreleased_](https://github.com/freckle/stackctl/compare/v1.4.3.0...main)
+
+## [v1.4.3.0](https://github.com/freckle/stackctl/compare/v1.4.2.2...v1.4.3.0)
+
+- Add `awsWithAuth`
+- Add `forEachSpec_`
 
 ## [v1.4.2.2](https://github.com/freckle/stackctl/compare/v1.4.2.1...v1.4.2.2)
 
diff --git a/src/Stackctl/AWS/Core.hs b/src/Stackctl/AWS/Core.hs
--- a/src/Stackctl/AWS/Core.hs
+++ b/src/Stackctl/AWS/Core.hs
@@ -2,6 +2,7 @@
   ( AwsEnv
   , HasAwsEnv (..)
   , awsEnvDiscover
+  , awsWithAuth
   , awsSimple
   , awsSend
   , awsPaginate
@@ -32,7 +33,7 @@
 import qualified Amazonka as AWS
 import Amazonka.Auth.Keys (fromSession)
 import Amazonka.Data.Text (FromText (..), ToText (..))
-import Amazonka.Env (env_logger, env_region)
+import Amazonka.Env (env_auth, env_logger, env_region)
 import Amazonka.STS.AssumeRole
 import Conduit (ConduitM)
 import Control.Monad.Logger (defaultLoc, toLogStr)
@@ -73,6 +74,12 @@
 
 instance HasAwsEnv AwsEnv where
   awsEnvL = id
+
+awsWithAuth
+  :: (MonadIO m, MonadReader env m, HasAwsEnv env) => (AuthEnv -> m a) -> m a
+awsWithAuth f = do
+  auth <- view $ awsEnvL . unL . env_auth . to runIdentity
+  withAuth auth f
 
 awsSimple
   :: ( MonadResource m
diff --git a/src/Stackctl/Spec/Changes.hs b/src/Stackctl/Spec/Changes.hs
--- a/src/Stackctl/Spec/Changes.hs
+++ b/src/Stackctl/Spec/Changes.hs
@@ -76,9 +76,7 @@
         Nothing -> pushLoggerLn formatted
         Just p -> liftIO $ T.appendFile p $ formatted <> "\n"
 
-  specs <- discoverSpecs
-
-  for_ specs $ \spec -> do
+  forEachSpec_ $ \spec -> do
     withThreadContext ["stackName" .= stackSpecStackName spec] $ do
       emChangeSet <- createChangeSet spec scoParameters scoTags
 
diff --git a/src/Stackctl/Spec/Deploy.hs b/src/Stackctl/Spec/Deploy.hs
--- a/src/Stackctl/Spec/Deploy.hs
+++ b/src/Stackctl/Spec/Deploy.hs
@@ -88,9 +88,7 @@
     removed <- inferRemovedStacks
     traverse_ (deleteRemovedStack sdoDeployConfirmation) removed
 
-  specs <- discoverSpecs
-
-  for_ specs $ \spec -> do
+  forEachSpec_ $ \spec -> do
     withThreadContext ["stackName" .= stackSpecStackName spec] $ do
       checkIfStackRequiresDeletion sdoDeployConfirmation
         $ stackSpecStackName spec
diff --git a/src/Stackctl/Spec/Discover.hs b/src/Stackctl/Spec/Discover.hs
--- a/src/Stackctl/Spec/Discover.hs
+++ b/src/Stackctl/Spec/Discover.hs
@@ -1,5 +1,6 @@
 module Stackctl.Spec.Discover
-  ( discoverSpecs
+  ( forEachSpec_
+  , discoverSpecs
   , buildSpecPath
   ) where
 
@@ -16,6 +17,20 @@
 import Stackctl.StackSpecPath
 import System.FilePath (isPathSeparator)
 import System.FilePath.Glob
+
+forEachSpec_
+  :: ( MonadMask m
+     , MonadResource m
+     , MonadLogger m
+     , MonadReader env m
+     , HasAwsScope env
+     , HasConfig env
+     , HasDirectoryOption env
+     , HasFilterOption env
+     )
+  => (StackSpec -> m ())
+  -> m ()
+forEachSpec_ f = traverse_ f =<< discoverSpecs
 
 discoverSpecs
   :: ( MonadMask m
diff --git a/src/Stackctl/Spec/List.hs b/src/Stackctl/Spec/List.hs
--- a/src/Stackctl/Spec/List.hs
+++ b/src/Stackctl/Spec/List.hs
@@ -40,10 +40,9 @@
   => ListOptions
   -> m ()
 runList _ = do
-  specs <- discoverSpecs
   Colors {..} <- getColorsLogger
 
-  for_ specs $ \spec -> do
+  forEachSpec_ $ \spec -> do
     let
       path = stackSpecFilePath spec
       name = stackSpecStackName spec
diff --git a/stackctl.cabal b/stackctl.cabal
--- a/stackctl.cabal
+++ b/stackctl.cabal
@@ -1,6 +1,6 @@
 cabal-version:   1.18
 name:            stackctl
-version:         1.4.2.2
+version:         1.4.3.0
 license:         MIT
 license-file:    LICENSE
 copyright:       2022 Renaissance Learning Inc
