Repository: awslabs/seed-farmer (language: python)

Issue:
Feature/logging errors
*Issue #, if available:*
#42 
#29



*Description of changes:*
Adding support for custom error classes from codeseeder, optimized logging and log outputs, code refactoring for extensibility

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2695b830..782d2e55 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,9 +7,36 @@ This project adheres to [Semantic Versioning](http://semver.org/) and [Keep a Ch
 
 
 
+## Unreleased
+
+### New
+- support logging output and url gereration from codebuild output / codeseeder
+- added use of CodeSeederRuntimeError
+- added ModuleDeploymentResponse object 
+- added export of moduledata (metadata) in UNIX format from CLI (--export-local-env)
+
+### Changes
+- moved Parameter support to its own script
+
+### Fixes
+- eventual consistency of IAM policy to module-spcific role (issue 43)
+
 
 ---
-## Unreleased
+
+## v0.1.2 (2022-07-12)
+
+### New
+- added example modules for buckets and networking
+
+### Changes
+
+### Fixes
+- use HTTPS instead off SSH for git cloning 
+
+
+---
+## v0.1.1 (2022-07-07)
 
 ### New
  - feature - added CLI ability to list deployspec of deployed module
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 7f3cf165..d068bfd0 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -13,18 +13,16 @@
 import os
 import sys
 
-os.environ['USE_CODESEEDER']='False'
+os.environ["USE_CODESEEDER"] = "False"
 sys.path.insert(0, os.path.abspath("../../seedfarmer"))
 
 import seedfarmer
 
-
-
 # -- Project information -----------------------------------------------------
 
-project = 'Seed-Farmer'
-copyright = '2022, AWS Professional Seervices'
-author = 'AWS Professional Seervices'
+project = "Seed-Farmer"
+copyright = "2022, AWS Professional Seervices"
+author = "AWS Professional Seervices"
 
 # The full version, including alpha/beta/rc tags
 release = seedfarmer.__version__
@@ -38,7 +36,7 @@
 extensions = ["sphinx.ext.autosectionlabel", "sphinx.ext.napoleon", "myst_parser", "autoapi.extension"]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # AutoAPI configuration
 autoapi_type = "python"
@@ -50,7 +48,7 @@
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store','conf','app']
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "conf", "app"]
 
 
 # -- Options for HTML output -------------------------------------------------
@@ -63,4 +61,4 @@
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
\ No newline at end of file
+html_static_path = ["_static"]
diff --git a/examples/exampleproject/manifests/examples/deployment.yaml b/examples/exampleproject/manifests/examples/deployment.yaml
new file mode 100644
index 00000000..009caebe
--- /dev/null
+++ b/examples/exampleproject/manifests/examples/deployment.yaml
@@ -0,0 +1,4 @@
+name: examples
+groups:
+  - name: optionals
+    path: manifests/examples/optional-modules.yaml
diff --git a/examples/exampleproject/manifests/examples/optional-modules.yaml b/examples/exampleproject/manifests/examples/optional-modules.yaml
new file mode 100644
index 00000000..a51b8d06
--- /dev/null
+++ b/examples/exampleproject/manifests/examples/optional-modules.yaml
@@ -0,0 +1,11 @@
+name: networking
+path: modules/optionals/networking/
+parameters:
+  - name: internet-accessible
+    value: true
+---
+name: buckets
+path: modules/optionals/buckets
+parameters:
+  - name: encryption-type
+    value: SSE
\ No newline at end of file
diff --git a/examples/exampleproject/modules/optionals/buckets/README.md b/examples/exampleproject/modules/optionals/buckets/README.md
new file mode 100644
index 00000000..6e5aea5f
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/README.md
@@ -0,0 +1,51 @@
+# Buckets
+
+## Description
+
+This module creates buckets and policies . 
+
+This module:
+- creates buckets
+  - Logs Data Bucket
+  - Artifact Data Bucket
+- creates access policies for the buckets
+  - READ-ONLY
+  - FULL ACCESS 
+
+
+## Inputs/Outputs
+
+### Input Paramenters
+None
+
+#### Required
+
+None
+
+#### Optional
+- `encryption-type`: the type of encryption on data stored in the buckets
+  - `SSE` or `KMS` 
+  - Assumed to be `SSE`
+- `retention-type`: type of data retention policy when deleteing the buckets
+  - `DESTROY` or `RETAIN`
+  - Assumed to be `DESTROY`
+
+
+### Module Metadata Outputs
+- `ArtifactsBucketName`: name of the bucket housing artifacts used for processing
+- `LogsBucketName`: name of the bucket housing logs
+- `ReadOnlyPolicyArn`: ARN of the policy generated giving read-only access to content
+- `FullAccessPolicyArn`: ARN of the policy generated giving full access to content
+
+
+
+#### Output Example
+
+```json
+{
+  "ArtifactsBucketName": "examples-dep-artifacts-bucket-us-east-1-12345678901",
+  "LogsBucketName": "examples-dep-logs-bucket-us-east-1-123456789012",
+  "FullAccessPolicyArn": "arn:aws:iam::123456789012:policy/examples-dep-optionals-datalake-buckets-us-east-1-123456789012-full-access",
+  "ReadOnlyPolicyArn": "arn:aws:iam::123456789012:policy/examples-dep-optionals-datalake-buckets-us-east-1-123456789012-readonly-access"
+}
+```
diff --git a/examples/exampleproject/modules/optionals/buckets/app.py b/examples/exampleproject/modules/optionals/buckets/app.py
new file mode 100644
index 00000000..0214720f
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/app.py
@@ -0,0 +1,59 @@
+import os
+
+import aws_cdk
+from aws_cdk import App, CfnOutput
+from stack import BucketsStack
+
+project_name = os.getenv("AWS_CODESEEDER_NAME")
+
+
+def _proj(name: str) -> str:
+    return f"{project_name.upper()}_{name}"
+
+
+def _param(name: str) -> str:
+    return f"{project_name.upper()}_PARAMETER_{name}"
+
+
+deployment_name = os.getenv(_proj("DEPLOYMENT_NAME"), "")
+module_name = os.getenv(_proj("MODULE_NAME"), "")
+hash = os.getenv(_proj("HASH"), "")
+
+
+buckets_encryption_type = os.getenv(_param("ENCRYPTION_TYPE"), "SSE")
+buckets_retention = os.getenv(_param("RETENTION_TYPE"), "DESTROY")
+
+
+app = App()
+
+
+stack = BucketsStack(
+    scope=app,
+    id=f"{project_name}-{deployment_name}-{module_name}",
+    project_name=project_name,
+    deployment_name=deployment_name,
+    module_name=module_name,
+    buckets_encryption_type=buckets_encryption_type,
+    buckets_retention=buckets_retention,
+    hash=hash,
+    env=aws_cdk.Environment(
+        account=os.environ["CDK_DEFAULT_ACCOUNT"],
+        region=os.environ["CDK_DEFAULT_REGION"],
+    ),
+)
+
+CfnOutput(
+    scope=stack,
+    id="metadata",
+    value=stack.to_json_string(
+        {
+            "ArtifactsBucketName": stack.artifacts_bucket.bucket_name,
+            "LogsBucketName": stack.logs_bucket.bucket_name,
+            "ReadOnlyPolicyArn": stack.readonly_policy.managed_policy_arn,
+            "FullAccessPolicyArn": stack.fullaccess_policy.managed_policy_arn,
+        }
+    ),
+)
+
+
+app.synth(force=True)
diff --git a/examples/exampleproject/modules/optionals/buckets/deployspec.yaml b/examples/exampleproject/modules/optionals/buckets/deployspec.yaml
new file mode 100644
index 00000000..00409214
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/deployspec.yaml
@@ -0,0 +1,28 @@
+deploy:
+  phases:
+    install:
+      commands:
+      # Install whatever additional build libraries 
+      - npm install -g aws-cdk@2.20.0
+      - pip install -r requirements.txt
+    build:
+      commands:
+      # execute the CDK
+      - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json
+      # derive the metadata environment parameter name that seedfarmer is looking for from the CODESEEDER project name
+      - export P_METADATA=$(echo $AWS_CODESEEDER_NAME | tr '[:lower:]' '[:upper:]')_MODULE_METADATA
+      # assign that paramter name the output from CDK by calling a custom python script
+      - export ${P_METADATA}=$(python export_metadata.py)
+destroy:
+  phases:
+    install:
+      commands:
+      # Install whatever additional build libraries 
+      - npm install -g aws-cdk@2.20.0
+      - pip install -r requirements.txt
+    build:
+      commands:
+      # execute the CDK
+      - cdk destroy --force --app "python app.py"
+
+
diff --git a/examples/exampleproject/modules/optionals/buckets/export_metadata.py b/examples/exampleproject/modules/optionals/buckets/export_metadata.py
new file mode 100644
index 00000000..2bc1c450
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/export_metadata.py
@@ -0,0 +1,8 @@
+import json
+import os
+
+p = os.getenv("AWS_CODESEEDER_NAME")
+d = os.getenv(f"{p.upper()}_DEPLOYMENT_NAME")
+m = os.getenv(f"{p.upper()}_MODULE_NAME")
+file = open("cdk-exports.json")
+print(json.load(file)[f"{p}-{d}-{m}"]["metadata"])
diff --git a/examples/exampleproject/modules/optionals/buckets/modulestack.template b/examples/exampleproject/modules/optionals/buckets/modulestack.template
new file mode 100644
index 00000000..0d079807
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/modulestack.template
@@ -0,0 +1,15 @@
+AWSTemplateFormatVersion: 2010-09-09
+Description: This template deploys a Module specific IAM permissions
+
+Parameters:
+  DeploymentName:
+    Type: String
+    Description: The name of the deployment
+  ModuleName:
+    Type: String
+    Description: The name of the Module
+  RoleName:
+    Type: String
+    Description: The name of the IAM Role
+
+Resources:
diff --git a/examples/exampleproject/modules/optionals/buckets/pyproject.toml b/examples/exampleproject/modules/optionals/buckets/pyproject.toml
new file mode 100644
index 00000000..024b629d
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/pyproject.toml
@@ -0,0 +1,29 @@
+[tool.black]
+line-length = 120
+target-version = ["py36", "py37", "py38"]
+exclude = '''
+/(
+    \.eggs
+  | \.git
+  | \.hg
+  | \.mypy_cache
+  | \.tox
+  | \.venv
+  | \.env
+  | _build
+  | buck-out
+  | build
+  | dist
+  | codeseeder.out
+)/
+'''
+
+[tool.isort]
+multi_line_output = 3
+include_trailing_comma = true
+force_grid_wrap = 0
+use_parentheses = true
+ensure_newline_before_comments = true
+line_length = 120
+py_version = 36
+skip_gitignore = false
\ No newline at end of file
diff --git a/examples/exampleproject/modules/optionals/buckets/requirements.txt b/examples/exampleproject/modules/optionals/buckets/requirements.txt
new file mode 100644
index 00000000..d67aecb5
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/requirements.txt
@@ -0,0 +1,4 @@
+aws-cdk-lib==2.20.0
+cdk-nag==2.12.29
+constructs==10.0.91
+boto3
diff --git a/examples/exampleproject/modules/optionals/buckets/setup.cfg b/examples/exampleproject/modules/optionals/buckets/setup.cfg
new file mode 100644
index 00000000..4a2fe15f
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/setup.cfg
@@ -0,0 +1,28 @@
+[metadata]
+license_files =
+  LICENSE
+  NOTICE
+  VERSION
+
+[flake8]
+max-line-length = 120
+extend-ignore = E203, W503
+exclude =
+    .git,
+    __pycache__,
+    docs/source/conf.py,
+    old,
+    build,
+    dist,
+    .venv,
+    codeseeder.out,
+    bundle
+
+[mypy]
+python_version = 3.7
+strict = True
+ignore_missing_imports = True
+allow_untyped_decorators = True
+exclude =
+  codeseeder.out/|example/
+warn_unused_ignores = False
diff --git a/examples/exampleproject/modules/optionals/buckets/stack.py b/examples/exampleproject/modules/optionals/buckets/stack.py
new file mode 100755
index 00000000..6007b1d4
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/buckets/stack.py
@@ -0,0 +1,179 @@
+#  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License").
+#    You may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+
+import logging
+from typing import Any, cast
+import hashlib
+
+import aws_cdk
+import aws_cdk.aws_iam as aws_iam
+import aws_cdk.aws_s3 as aws_s3
+import cdk_nag
+from aws_cdk import Aspects, Stack, Tags
+from cdk_nag import NagSuppressions
+from constructs import Construct, IConstruct
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
+
+class BucketsStack(Stack):  # type: ignore
+    def __init__(
+        self,
+        scope: Construct,
+        id: str,
+        project_name: str,
+        deployment_name: str,
+        module_name: str,
+        hash: str,
+        buckets_encryption_type: str,
+        buckets_retention: str,
+        **kwargs: Any,
+    ) -> None:
+
+        # CDK Env Vars
+        account: str = aws_cdk.Aws.ACCOUNT_ID
+        region: str = aws_cdk.Aws.REGION
+
+        super().__init__(scope, id, **kwargs)
+        Tags.of(scope=cast(IConstruct, self)).add(key="Deployment", value=f"{project_name}-{deployment_name}")
+
+        artifact_bucket_name =f"{project_name}-{deployment_name}-artifacts-bucket-{hash}"
+        unique_ab=(hashlib.sha1(module_name.encode("UTF-8")).hexdigest())[:(60-len(artifact_bucket_name))]
+        
+        artifacts_bucket = aws_s3.Bucket(
+            self,
+            id="artifacts-bucket",
+            bucket_name=f"{artifact_bucket_name}-{unique_ab}",
+            removal_policy=aws_cdk.RemovalPolicy.RETAIN
+            if buckets_retention.upper() == "RETAIN"
+            else aws_cdk.RemovalPolicy.DESTROY,
+            auto_delete_objects=None if buckets_retention.upper() == "RETAIN" else True,
+            encryption=aws_s3.BucketEncryption.KMS_MANAGED
+            if buckets_encryption_type.upper() == "KMS"
+            else aws_s3.BucketEncryption.S3_MANAGED,
+            versioned=True,
+            block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL,
+            enforce_ssl=True,
+        )
+
+        log_bucket_name =f"{project_name}-{deployment_name}-logs-bucket-{hash}"
+        unique_log=(hashlib.sha1(module_name.encode("UTF-8")).hexdigest())[:(60-len(log_bucket_name))]        
+
+        logs_bucket = aws_s3.Bucket(
+            self,
+            id="logs-bucket",
+            bucket_name=f"{log_bucket_name}-{unique_log}",
+            removal_policy=aws_cdk.RemovalPolicy.RETAIN
+            if buckets_retention.upper() == "RETAIN"
+            else aws_cdk.RemovalPolicy.DESTROY,
+            auto_delete_objects=None if buckets_retention.upper() == "RETAIN" else True,
+            encryption=aws_s3.BucketEncryption.KMS_MANAGED
+            if buckets_encryption_type.upper() == "KMS"
+            else aws_s3.BucketEncryption.S3_MANAGED,
+            block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL,
+            enforce_ssl=True,
+        )
+
+        # ReadOnly IAM Policy
+        readonly_policy = aws_iam.ManagedPolicy(
+            self,
+            id="readonly_policy",
+            managed_policy_name=f"{project_name}-{deployment_name}-{module_name}-{region}-{account}-readonly-access",
+            statements=[
+                aws_iam.PolicyStatement(
+                    effect=aws_iam.Effect.ALLOW,
+                    actions=[
+                        "kms:Decrypt",
+                        "kms:Encrypt",
+                        "kms:ReEncrypt*",
+                        "kms:DescribeKey",
+                        "kms:GenerateDataKey",
+                    ],
+                    resources=[f"arn:aws:kms::{account}:*"],
+                ),
+                aws_iam.PolicyStatement(
+                    effect=aws_iam.Effect.ALLOW,
+                    actions=[
+                        "s3:GetObject",
+                        "s3:GetObjectAcl",
+                        "s3:ListBucket",
+                    ],
+                    resources=[
+                        f"{artifacts_bucket.bucket_arn}/*",
+                        f"{artifacts_bucket.bucket_arn}",
+                    ],
+                ),
+            ],
+        )
+
+        # FullAccess IAM Policy
+        fullaccess_policy = aws_iam.ManagedPolicy(
+            self,
+            id="fullaccess_policy",
+            managed_policy_name=f"{project_name}-{deployment_name}-{module_name}-{region}-{account}-full-access",
+            statements=[
+                aws_iam.PolicyStatement(
+                    effect=aws_iam.Effect.ALLOW,
+                    actions=[
+                        "kms:Decrypt",
+                        "kms:Encrypt",
+                        "kms:ReEncrypt*",
+                        "kms:DescribeKey",
+                        "kms:GenerateDataKey",
+                    ],
+                    resources=[f"arn:aws:kms::{account}:*"],
+                ),
+                aws_iam.PolicyStatement(
+                    effect=aws_iam.Effect.ALLOW,
+                    actions=[
+                        "s3:GetObject",
+                        "s3:GetObjectAcl",
+                        "s3:ListBucket",
+                    ],
+                    resources=[
+                        f"{artifacts_bucket.bucket_arn}/*",
+                        f"{artifacts_bucket.bucket_arn}",
+                    ],
+                ),
+                aws_iam.PolicyStatement(
+                    actions=["s3:PutObject", "s3:PutObjectAcl"],
+                    resources=[
+                        f"{artifacts_bucket.bucket_arn}/*",
+                        f"{artifacts_bucket.bucket_arn}",
+                    ],
+                ),
+            ],
+        )
+
+        self.artifacts_bucket = artifacts_bucket
+        self.logs_bucket = logs_bucket
+        self.readonly_policy = readonly_policy
+        self.fullaccess_policy = fullaccess_policy
+
+        Aspects.of(self).add(cdk_nag.AwsSolutionsChecks())
+
+        suppressions = [
+            {
+                "id": "AwsSolutions-S1",
+                "reason": "Logging has been disabled for demo purposes",
+                "applies_to": "*",
+            },
+            {
+                "id": "AwsSolutions-IAM5",
+                "reason": "Resource access restriced to resources",
+                "applies_to": "*",
+            },
+        ]
+
+        NagSuppressions.add_stack_suppressions(self, suppressions)
diff --git a/examples/exampleproject/modules/optionals/networking/README.md b/examples/exampleproject/modules/optionals/networking/README.md
new file mode 100644
index 00000000..c95ea202
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/README.md
@@ -0,0 +1,49 @@
+# Networking Module
+
+## Description
+
+This module creates netowrking resources.  It is not required as 
+modules can leverage existing VPC networks.
+
+This module:
+- creates networking resources
+  - VPC and Subnets
+  - VPC Endpoints
+  - module-specific role with least privileges policy
+- exports module metadata
+
+
+
+## Inputs/Outputs
+
+### Input Paramenters
+None
+
+#### Required
+
+None
+
+#### Optional
+
+- `internet-accessible`: a boolean flag indicating whether or not the subnets have internet access.
+  - `True` or `False` 
+  - Assumed to be True
+
+### Module Metadata Outputs
+
+- `VpcId`: The VPC ID created
+- `PublicSubnetIds`: An array of the public subnets 
+- `PrivateSubnetIds`: An array of the private subnets 
+- `IsolatedSubnetIds`: An array of the isolated subnets  (only if `internet-accessible` is `False`)
+
+
+#### Output Example
+
+```json
+{
+  "IsolatedSubnetIds": [],
+  "PrivateSubnetIds": ["subnet-1234567890abc", "subnet-1234567890def"],
+  "PublicSubnetIds": ["subnet-1234567890ghi", "subnet-1234567890jkl"],
+  "VpcId": "vpc-1234567890mno"
+}
+```
diff --git a/examples/exampleproject/modules/optionals/networking/app.py b/examples/exampleproject/modules/optionals/networking/app.py
new file mode 100644
index 00000000..c472f01a
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/app.py
@@ -0,0 +1,83 @@
+import os
+
+import aws_cdk
+from aws_cdk import App, CfnOutput
+from stack import NetworkingStack
+
+project_name = os.getenv("AWS_CODESEEDER_NAME")
+
+
+def _proj(name: str) -> str:
+    return f"{project_name.upper()}_{name}"
+
+
+def _param(name: str) -> str:
+    return f"{project_name.upper()}_PARAMETER_{name}"
+
+
+# get the env parameters with proper prefixes
+deployment_name = os.getenv(_proj("DEPLOYMENT_NAME"), "")
+module_name = os.getenv(_proj("MODULE_NAME"), "")
+hash = os.getenv(_proj("HASH"), "")
+internet_accessible = os.getenv(_param("INTERNET_ACCESSIBLE"), True)
+
+app = App()
+
+stack = NetworkingStack(
+    scope=app,
+    id=f"{project_name}-{deployment_name}-{module_name}",
+    project_name=project_name,
+    deployment_name=deployment_name,
+    module_name=module_name,
+    internet_accessible=internet_accessible,
+    env=aws_cdk.Environment(
+        account=os.environ["CDK_DEFAULT_ACCOUNT"],
+        region=os.environ["CDK_DEFAULT_REGION"],
+    ),
+)
+
+CfnOutput(
+    scope=stack,
+    id="vpcId",
+    value=stack.vpc.vpc_id,
+)
+
+CfnOutput(
+    scope=stack,
+    id="publicSubnetIds",
+    value=",".join(stack.public_subnets.subnet_ids),
+)
+
+CfnOutput(
+    scope=stack,
+    id="privateSubnetIds",
+    value=",".join(stack.private_subnets.subnet_ids),
+)
+
+if not stack.internet_accessible:
+    CfnOutput(
+        scope=stack,
+        id="isolatedSubnetIds",
+        value=",".join(stack.isolated_subnets.subnet_ids),
+    )
+
+CfnOutput(
+    scope=stack,
+    id="nodeSubnetIds",
+    value=",".join(stack.nodes_subnets.subnet_ids),
+)
+
+CfnOutput(
+    scope=stack,
+    id="metadata",
+    value=stack.to_json_string(
+        {
+            "VpcId": stack.vpc.vpc_id,
+            "PublicSubnetIds": stack.public_subnets.subnet_ids,
+            "PrivateSubnetIds": stack.private_subnets.subnet_ids,
+            "IsolatedSubnetIds": stack.isolated_subnets.subnet_ids if not stack.internet_accessible else [],
+        }
+    ),
+)
+
+app.synth(force=True)
diff --git a/examples/exampleproject/modules/optionals/networking/deployspec.yaml b/examples/exampleproject/modules/optionals/networking/deployspec.yaml
new file mode 100644
index 00000000..00409214
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/deployspec.yaml
@@ -0,0 +1,28 @@
+deploy:
+  phases:
+    install:
+      commands:
+      # Install whatever additional build libraries 
+      - npm install -g aws-cdk@2.20.0
+      - pip install -r requirements.txt
+    build:
+      commands:
+      # execute the CDK
+      - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json
+      # derive the metadata environment parameter name that seedfarmer is looking for from the CODESEEDER project name
+      - export P_METADATA=$(echo $AWS_CODESEEDER_NAME | tr '[:lower:]' '[:upper:]')_MODULE_METADATA
+      # assign that paramter name the output from CDK by calling a custom python script
+      - export ${P_METADATA}=$(python export_metadata.py)
+destroy:
+  phases:
+    install:
+      commands:
+      # Install whatever additional build libraries 
+      - npm install -g aws-cdk@2.20.0
+      - pip install -r requirements.txt
+    build:
+      commands:
+      # execute the CDK
+      - cdk destroy --force --app "python app.py"
+
+
diff --git a/examples/exampleproject/modules/optionals/networking/export_metadata.py b/examples/exampleproject/modules/optionals/networking/export_metadata.py
new file mode 100644
index 00000000..2bc1c450
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/export_metadata.py
@@ -0,0 +1,8 @@
+import json
+import os
+
+p = os.getenv("AWS_CODESEEDER_NAME")
+d = os.getenv(f"{p.upper()}_DEPLOYMENT_NAME")
+m = os.getenv(f"{p.upper()}_MODULE_NAME")
+file = open("cdk-exports.json")
+print(json.load(file)[f"{p}-{d}-{m}"]["metadata"])
diff --git a/examples/exampleproject/modules/optionals/networking/modulestack.template b/examples/exampleproject/modules/optionals/networking/modulestack.template
new file mode 100644
index 00000000..8285c019
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/modulestack.template
@@ -0,0 +1,15 @@
+AWSTemplateFormatVersion: 2010-09-09
+Description: This template deploys a Module specific IAM permissions
+
+Parameters:
+  DeploymentName:
+    Type: String
+    Description: The name of the deployment
+  ModuleName:
+    Type: String
+    Description: The name of the Module
+  RoleName:
+    Type: String
+    Description: The name of the IAM Role
+Resources:
+ 
\ No newline at end of file
diff --git a/examples/exampleproject/modules/optionals/networking/pyproject.toml b/examples/exampleproject/modules/optionals/networking/pyproject.toml
new file mode 100644
index 00000000..ca47d706
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/pyproject.toml
@@ -0,0 +1,30 @@
+
+[tool.black]
+line-length = 120
+target-version = ["py36", "py37", "py38"]
+exclude = '''
+/(
+    \.eggs
+  | \.git
+  | \.hg
+  | \.mypy_cache
+  | \.tox
+  | \.venv
+  | \.env
+  | _build
+  | buck-out
+  | build
+  | dist
+  | codeseeder.out
+)/
+'''
+
+[tool.isort]
+multi_line_output = 3
+include_trailing_comma = true
+force_grid_wrap = 0
+use_parentheses = true
+ensure_newline_before_comments = true
+line_length = 120
+py_version = 36
+skip_gitignore = false
\ No newline at end of file
diff --git a/examples/exampleproject/modules/optionals/networking/requirements.txt b/examples/exampleproject/modules/optionals/networking/requirements.txt
new file mode 100644
index 00000000..98519e0f
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/requirements.txt
@@ -0,0 +1,8 @@
+aws-cdk-lib==2.20.0
+cdk-nag==2.12.29
+constructs==10.0.91
+mypy~=0.790
+black~=20.8b1
+flake8~=3.8.4
+cfn-lint~=0.53.0
+isort~=5.7.0
diff --git a/examples/exampleproject/modules/optionals/networking/setup.cfg b/examples/exampleproject/modules/optionals/networking/setup.cfg
new file mode 100644
index 00000000..4a2fe15f
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/setup.cfg
@@ -0,0 +1,28 @@
+[metadata]
+license_files =
+  LICENSE
+  NOTICE
+  VERSION
+
+[flake8]
+max-line-length = 120
+extend-ignore = E203, W503
+exclude =
+    .git,
+    __pycache__,
+    docs/source/conf.py,
+    old,
+    build,
+    dist,
+    .venv,
+    codeseeder.out,
+    bundle
+
+[mypy]
+python_version = 3.7
+strict = True
+ignore_missing_imports = True
+allow_untyped_decorators = True
+exclude =
+  codeseeder.out/|example/
+warn_unused_ignores = False
diff --git a/examples/exampleproject/modules/optionals/networking/stack.py b/examples/exampleproject/modules/optionals/networking/stack.py
new file mode 100755
index 00000000..592ee563
--- /dev/null
+++ b/examples/exampleproject/modules/optionals/networking/stack.py
@@ -0,0 +1,225 @@
+#  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License").
+#    You may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+
+import logging
+from typing import Any, List, cast
+
+import aws_cdk.aws_ec2 as ec2
+import cdk_nag
+from aws_cdk import Aspects, Stack, Tags
+from constructs import Construct, IConstruct
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
+
+class NetworkingStack(Stack):  # type: ignore
+    def __init__(
+        self,
+        scope: Construct,
+        id: str,
+        project_name: str,
+        deployment_name: str,
+        module_name: str,
+        internet_accessible: bool,
+        **kwargs: Any,
+    ) -> None:
+
+        super().__init__(scope, id, **kwargs)
+        dep_mod = f"{project_name}-{deployment_name}-{module_name}"
+        dep_mod = dep_mod[:27]
+
+        Tags.of(scope=cast(IConstruct, self)).add(key="Deployment", value=f"{dep_mod}")
+        self.vpc: ec2.Vpc = self._create_vpc(internet_accessible=internet_accessible)
+
+        self.internet_accessible = internet_accessible
+
+        self.public_subnets = (
+            self.vpc.select_subnets(subnet_type=ec2.SubnetType.PUBLIC)
+            if self.vpc.public_subnets
+            else self.vpc.select_subnets(subnet_group_name="")
+        )
+        self.private_subnets = (
+            self.vpc.select_subnets(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT)  # type: ignore
+            if self.vpc.private_subnets
+            else self.vpc.select_subnets(subnet_group_name="")
+        )
+        if not internet_accessible:
+            self.isolated_subnets = (
+                self.vpc.select_subnets(subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT)  # type: ignore
+                if self.vpc.isolated_subnets
+                else self.vpc.select_subnets(subnet_group_name="")
+            )
+            self.nodes_subnets = self.isolated_subnets
+        else:
+            self.nodes_subnets = self.private_subnets
+
+        self._vpc_security_group = ec2.SecurityGroup(
+            self, "vpc-sg", vpc=cast(ec2.IVpc, self.vpc), allow_all_outbound=False
+        )
+        # Adding ingress rule to VPC CIDR
+        self._vpc_security_group.add_ingress_rule(
+            peer=ec2.Peer.ipv4(self.vpc.vpc_cidr_block), connection=ec2.Port.all_tcp()
+        )
+
+        # Creating Gateway Endpoints
+        vpc_gateway_endpoints = {
+            "s3": ec2.GatewayVpcEndpointAwsService.S3,
+            "dynamodb": ec2.GatewayVpcEndpointAwsService.DYNAMODB,
+        }
+
+        for name, gateway_vpc_endpoint_service in vpc_gateway_endpoints.items():
+            self.vpc.add_gateway_endpoint(
+                id=name,
+                service=gateway_vpc_endpoint_service,
+                subnets=[
+                    ec2.SubnetSelection(subnets=self.nodes_subnets.subnets),
+                ],
+            )
+
+        if not internet_accessible:
+            self._create_vpc_endpoints()
+
+    def _create_vpc(self, internet_accessible: bool) -> ec2.Vpc:
+        if internet_accessible:
+            subnet_configuration = [
+                ec2.SubnetConfiguration(name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24),
+                ec2.SubnetConfiguration(
+                    name="Private", subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT, cidr_mask=21  # type: ignore
+                ),
+            ]
+        else:
+            subnet_configuration = [
+                ec2.SubnetConfiguration(name="Public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24),
+                ec2.SubnetConfiguration(
+                    name="Private", subnet_type=ec2.SubnetType.PRIVATE_WITH_NAT, cidr_mask=21  # type: ignore
+                ),
+                ec2.SubnetConfiguration(
+                    name="Isolated", subnet_type=ec2.SubnetType.PRIVATE_ISOLATED, cidr_mask=21  # type: ignore
+                ),
+            ]
+
+        vpc = ec2.Vpc(
+            scope=self,
+            id="vpc",
+            default_instance_tenancy=ec2.DefaultInstanceTenancy.DEFAULT,
+            cidr="10.0.0.0/16",
+            enable_dns_hostnames=True,
+            enable_dns_support=True,
+            max_azs=2,
+            nat_gateways=1,
+            subnet_configuration=subnet_configuration,
+        )
+
+        # Enabling subnets for deploying Load Balancers via EKS
+        NetworkingStack._tag_subnets(vpc.private_subnets, "kubernetes.io/role/internal-elb")
+        NetworkingStack._tag_subnets(vpc.public_subnets, "kubernetes.io/role/elb")
+
+        return vpc
+
+    @staticmethod
+    def _tag_subnets(subnets: List[ec2.ISubnet], tag: str) -> None:
+        for subnet in subnets:
+            Tags.of(subnet).add(tag, "1")
+
+    def _create_vpc_endpoints(self) -> None:
+
+        vpc_interface_endpoints = {
+            "cloudwatch_endpoint": ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH,
+            "cloudwatch_logs_endpoint": ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
+            "cloudwatch_events": ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_EVENTS,
+            "ecr_docker_endpoint": ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER,
+            "ecr_endpoint": ec2.InterfaceVpcEndpointAwsService.ECR,
+            "ec2_endpoint": ec2.InterfaceVpcEndpointAwsService.EC2,
+            "ecs": ec2.InterfaceVpcEndpointAwsService.ECS,
+            "ecs_agent": ec2.InterfaceVpcEndpointAwsService.ECS_AGENT,
+            "ecs_telemetry": ec2.InterfaceVpcEndpointAwsService.ECS_TELEMETRY,
+            "git_endpoint": ec2.InterfaceVpcEndpointAwsService.CODECOMMIT_GIT,
+            "ssm_endpoint": ec2.InterfaceVpcEndpointAwsService.SSM,
+            "ssm_messages_endpoint": ec2.InterfaceVpcEndpointAwsService.SSM_MESSAGES,
+            "secrets_endpoint": ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
+            "kms_endpoint": ec2.InterfaceVpcEndpointAwsService.KMS,
+            "sagemaker_endpoint": ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_API,
+            "sagemaker_runtime": ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_RUNTIME,
+            "notebook_endpoint": ec2.InterfaceVpcEndpointAwsService.SAGEMAKER_NOTEBOOK,
+            "athena_endpoint": ec2.InterfaceVpcEndpointAwsService("athena"),
+            "glue_endpoint": ec2.InterfaceVpcEndpointAwsService("glue"),
+            "sqs": ec2.InterfaceVpcEndpointAwsService.SQS,
+            "step_function_endpoint": ec2.InterfaceVpcEndpointAwsService("states"),
+            "sns_endpoint": ec2.InterfaceVpcEndpointAwsService.SNS,
+            "kinesis_firehose_endpoint": ec2.InterfaceVpcEndpointAwsService("kinesis-firehose"),
+            "api_gateway": ec2.InterfaceVpcEndpointAwsService.APIGATEWAY,
+            "sts_endpoint": ec2.InterfaceVpcEndpointAwsService.STS,
+            "efs": ec2.InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM,
+            "elb": ec2.InterfaceVpcEndpointAwsService.ELASTIC_LOAD_BALANCING,
+            "autoscaling": ec2.InterfaceVpcEndpointAwsService("autoscaling"),
+            "cloudformation_endpoint": ec2.InterfaceVpcEndpointAwsService("cloudformation"),
+            "codebuild_endpoint": ec2.InterfaceVpcEndpointAwsService("codebuild"),
+            "emr-containers": ec2.InterfaceVpcEndpointAwsService("emr-containers"),
+            "databrew": ec2.InterfaceVpcEndpointAwsService("databrew"),
+        }
+
+        for name, interface_service in vpc_interface_endpoints.items():
+            self.vpc.add_interface_endpoint(
+                id=name,
+                service=interface_service,
+                subnets=ec2.SubnetSelection(subnets=self.nodes_subnets.subnets),
+                private_dns_enabled=True,
+                security_groups=[cast(ec2.ISecurityGroup, self._vpc_security_group)],
+            )
+        # Adding CodeArtifact VPC endpoints
+        self.vpc.add_interface_endpoint(
+            id="code_artifact_repo_endpoint",
+            service=cast(
+                ec2.IInterfaceVpcEndpointService,
+                ec2.InterfaceVpcEndpointAwsService("codeartifact.repositories"),
+            ),
+            subnets=ec2.SubnetSelection(subnets=self.nodes_subnets.subnets),
+            private_dns_enabled=False,
+            security_groups=[cast(ec2.ISecurityGroup, self._vpc_security_group)],
+        )
+        self.vpc.add_interface_endpoint(
+            id="code_artifact_api_endpoint",
+            service=cast(
+                ec2.IInterfaceVpcEndpointService,
+                ec2.InterfaceVpcEndpointAwsService("codeartifact.api"),
+            ),
+            subnets=ec2.SubnetSelection(subnets=self.nodes_subnets.subnets),
+            private_dns_enabled=False,
+            security_groups=[cast(ec2.ISecurityGroup, self._vpc_security_group)],
+        )
+
+        # Adding Lambda and Redshift endpoints with CDK low level APIs
+        endpoint_url_template = "com.amazonaws.{}.{}"
+        ec2.CfnVPCEndpoint(
+            self,
+            "redshift_endpoint",
+            vpc_endpoint_type="Interface",
+            service_name=endpoint_url_template.format(self.region, "redshift"),
+            vpc_id=self.vpc.vpc_id,
+            security_group_ids=[self._vpc_security_group.security_group_id],
+            subnet_ids=self.nodes_subnets.subnet_ids,
+            private_dns_enabled=True,
+        )
+        ec2.CfnVPCEndpoint(
+            self,
+            "lambda_endpoint",
+            vpc_endpoint_type="Interface",
+            service_name=endpoint_url_template.format(self.region, "lambda"),
+            vpc_id=self.vpc.vpc_id,
+            security_group_ids=[self._vpc_security_group.security_group_id],
+            subnet_ids=self.nodes_subnets.subnet_ids,
+            private_dns_enabled=True,
+        )
+
+        Aspects.of(self).add(cdk_nag.AwsSolutionsChecks())
diff --git a/examples/exampleproject/resources/projectpolicy.yaml b/examples/exampleproject/resources/projectpolicy.yaml
new file mode 100644
index 00000000..ded8d94c
--- /dev/null
+++ b/examples/exampleproject/resources/projectpolicy.yaml
@@ -0,0 +1,79 @@
+AWSTemplateFormatVersion: '2010-09-09'
+Description: "This template deploys Project deployment policy"
+Parameters:
+  ProjectName:
+    Type: String
+    Description: The name of the project
+  DeploymentName:
+    Type: String
+    Description: The name of the deployment
+Resources:
+  ProjectPolicy:
+    Type: AWS::IAM::ManagedPolicy
+    Properties:
+      Description: Managed Policy granting access to build a project
+      Path: /
+      PolicyDocument:
+        Statement:
+          - Effect: Allow
+            Action:
+              - "cloudformation:Describe*"
+              - "cloudformation:GetTemplate"
+              - "ec2:Describe*"
+              - "ec2:Get*"
+              - "ecr:Describe*"
+              - "ecr:Get*"
+              - "ecr:List*"
+              - "elasticloadbalancing:Describe*"
+              - "iam:Get*"
+              - "iam:List*"
+              - "logs:CreateLogGroup"
+              - "logs:Describe*"
+              - "logs:DescribeLogGroups"
+              - "s3:List*"
+              - 'kms:CreateKey'
+              - 'kms:Describe*'
+              - 'kms:List*'
+              - 'kms:TagResource'
+            Resource: '*'
+          - Effect: Allow
+            Action:
+              - ssm:Get*
+              - ssm:Describe*
+              - ssm:PutParameter
+              - ssm:DeleteParameter
+              - ssm:DeleteParameters
+            Resource:
+              -  !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${ProjectName}*"
+          - Effect: Allow
+            Action:
+              - logs:CreateLogStream
+              - logs:PutLogEvents
+              - logs:GetLogEvents
+              - logs:GetLogRecord
+              - logs:GetLogGroupFields
+              - logs:GetQueryResults
+            Resource:
+              - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/codeseeder-${ProjectName}*"
+          - Effect: Allow
+            Action:
+              - ssm:GetParameter
+            Resource:
+              -  !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/cdk-bootstrap*"
+          - Effect: Allow
+            Action:
+              - iam:UpdateAssumeRolePolicy
+            Resource:
+              -  !Sub "arn:aws:iam::${AWS::AccountId}:role/${ProjectName}*"
+          - Effect: Allow
+            Action:
+              - "iam:PassRole"
+              - "sts:AssumeRole"
+            Resource:
+              - !Sub "arn:aws:iam::${AWS::AccountId}:role/cdk*"
+        Version: '2012-10-17'
+
+Outputs:
+  ProjectPolicyARN:
+    Value: !Ref 'ProjectPolicy'
+
diff --git a/examples/exampleproject/resources/sample-permissionboundary.yaml b/examples/exampleproject/resources/sample-permissionboundary.yaml
new file mode 100644
index 00000000..fabb0b39
--- /dev/null
+++ b/examples/exampleproject/resources/sample-permissionboundary.yaml
@@ -0,0 +1,57 @@
+AWSTemplateFormatVersion: 2010-09-09
+Description: This template deploys a sample Permission Boundary Managed policy
+Parameters:
+  ManagedPolicyName:
+    Type: String
+    Description: The name of the ManagedPolicy for Permission Boundary
+Resources:
+  ProjectPolicy:
+    Properties:
+      ManagedPolicyName: !Ref ManagedPolicyName
+      Description: Permission Boundary Managed policy
+      Path: /
+      PolicyDocument:
+        Statement:
+          - Action: '*'
+            Effect: Allow
+            Resource: '*'
+            Sid: ServiceBoundaries
+          - Action:
+              - 'iam:CreateRole'
+              - 'iam:DeleteRolePolicy'
+              - 'iam:AttachRolePolicy'
+              - 'iam:DetachRolePolicy'
+              - 'iam:PutRolePermissionsBoundary'
+              - 'iam:PutRolePolicy'
+            Condition:
+              StringEquals:
+                'iam:PermissionsBoundary': !Sub 'arn:aws:iam::${AWS::AccountId}:policy/${ManagedPolicyName}'
+            Effect: Allow
+            Resource: '*'
+            Sid: CreateOrChangeOnlyWithBoundary
+          - Action:
+              - 'iam:CreatePolicyVersion'
+              - 'iam:DeletePolicy'
+              - 'iam:DeletePolicyVersion'
+              - 'iam:SetDefaultPolicyVersion'
+            Effect: Deny
+            Resource:
+              - !Sub 'arn:aws:iam::${AWS::AccountId}:policy/${ManagedPolicyName}'
+            Sid: DenyPermBoundaryIAMPolicyAlteration
+          - Action:
+              - 'iam:DeleteUserPermissionsBoundary'
+              - 'iam:DeleteRolePermissionsBoundary'
+            Condition:
+              StringEquals:
+                'iam:PermissionsBoundary': !Sub 'arn:aws:iam::${AWS::AccountId}:policy/${ManagedPolicyName}'
+            Effect: Deny
+            Resource:
+              - !Sub 'arn:aws:iam::${AWS::AccountId}:user/*'
+              - !Sub 'arn:aws:iam::${AWS::AccountId}:role/*'
+            Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole
+          - Action: 'iam:PassRole'
+            Effect: Deny
+            Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/*'
+            Sid: DenyIAMPassRoleEscalation
+        Version: 2012-10-17
+    Type: 'AWS::IAM::ManagedPolicy'
diff --git a/examples/exampleproject/seedfarmer.yaml b/examples/exampleproject/seedfarmer.yaml
new file mode 100644
index 00000000..79c8d3e8
--- /dev/null
+++ b/examples/exampleproject/seedfarmer.yaml
@@ -0,0 +1,2 @@
+project: exampleproj
+description: This is an example project
diff --git a/seedfarmer/__main__.py b/seedfarmer/__main__.py
index d1f1afc7..9dec9671 100644
--- a/seedfarmer/__main__.py
+++ b/seedfarmer/__main__.py
@@ -63,7 +63,7 @@ def apply(spec: str, debug: bool, dry_run: bool, show_manifest: bool) -> None:
     f"Deploy a(n) {PROJECT.upper()} environemnt based on a deployspec file (yaml)." ""
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.info(f"Apply request with manifest {spec} ")
+    _logger.info("Apply request with manifest %s", spec)
     if dry_run:
         print_bolded(" ***   This is a dry-run...NO ACTIONS WILL BE TAKEN  *** ", "white")
 
@@ -103,7 +103,7 @@ def destroy(
     f"Destroy an {PROJECT.upper} deployment ."
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.info(f"Destroy for {deployment} ")
+    _logger.info("Destroy for %s", deployment)
     if dry_run:
         print_bolded(" ***   This is a dry-run...NO ACTIONS WILL BE TAKEN  *** ", "white")
     commands.destroy(deployment_name=deployment, dryrun=dry_run, show_manifest=show_manifest)
@@ -203,8 +203,7 @@ def store_module_metadata(
 ) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-
-    _logger.debug(f"Writing metadata for module {module} in deployment {deployment}")
+    _logger.debug("Writing metadata for module %s in deployment %s", module, deployment)
     d = yaml.load(sys.stdin.read(), Loader=utils.CfnSafeYamlLoader)
     if d:
         mi.write_metadata(deployment=deployment, group=group, module=module, data=d)
@@ -256,7 +255,7 @@ def store_module_md5(
 ) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.debug(f"Writing md5 of {type} for module {module} in deployment {deployment}")
+    _logger.debug("Writing md5 of  %s for module %s in deployment %s", type, module, deployment)
     d = sys.stdin.readline().strip()
     if d:
         if type.casefold() == "bundle":
@@ -310,7 +309,7 @@ def list_deployspec(
 ) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.debug(f"We are getting deployspec for {module} in {group} of {deployment}")
+    _logger.debug("We are getting deployspec for  %s in %s of deployment %s", module, group, deployment)
 
     val = mi.get_deployspec(deployment=deployment, group=group, module=module)
     print_json(val)
@@ -338,6 +337,12 @@ def list_deployspec(
     help="The Module Name",
     required=True,
 )
+@click.option(
+    "--export-local-env/--no-export-local-env",
+    default=False,
+    help="Print the moduledata as env parameters for local development support INSTEAD of json (default is FALSE)",
+    show_default=True,
+)
 @click.option(
     "--debug/--no-debug",
     default=False,
@@ -348,14 +353,21 @@ def list_module_metadata(
     deployment: str,
     group: str,
     module: str,
+    export_local_env: str,
     debug: bool,
 ) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.debug(f"We are getting module data for {module} in {group} of {deployment}")
-
-    val = json.dumps(mi.get_module_metadata(deployment, group, module))
-    sys.stdout.write(val)
+    _logger.debug("We are getting module data for  %s in %s of %s", module, group, deployment)
+    metadata_json = mi.get_module_metadata(deployment, group, module)
+    if not export_local_env:
+        sys.stdout.write(json.dumps(metadata_json))
+    else:
+        envs = commands.generate_export_env_params(metadata_json)
+        if envs:
+            for exp in envs:
+                sys.stdout.write(exp)
+                sys.stdout.write("\n")
 
 
 @list.command(name="modules", help="List the modules in a group")
@@ -378,7 +390,7 @@ def list_modules(
 ) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.debug(f"We are getting modules for  {deployment}")
+    _logger.debug("We are getting modules for %s", deployment)
 
     cache = du.generate_deployment_cache(deployment_name=deployment)
     dep_manifest = du.generate_deployed_manifest(
@@ -447,7 +459,7 @@ def remove_module_data(
 ) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.debug(f"We are removing module data for {module} of group {group} in {deployment}")
+    _logger.debug(" We are removing module data for %s of group %s in %s", module, group, deployment)
 
     mi.remove_module_info(deployment, group, module)
 
@@ -467,7 +479,7 @@ def init() -> None:
 @click.option(
     "--template-url",
     "-t",
-    default="git@github.com:awslabs/seed-farmer.git",
+    default="https://github.com/awslabs/seed-farmer.git",
     help=(
         "The template URL. If not specified, the default template repo is " "`https://github.com/awslabs/seed-farmer`"
     ),
@@ -490,7 +502,7 @@ def init_project(template_url: str) -> None:
 @click.option(
     "--template-url",
     "-t",
-    default="git@github.com:awslabs/seed-farmer.git",
+    default="https://github.com/awslabs/seed-farmer.git",
     help=(
         "The template URL. If not specified, the default template repo is " "`https://github.com/awslabs/seed-farmer`"
     ),
@@ -500,7 +512,8 @@ def init_project(template_url: str) -> None:
 def init_module(group_name: str, module_name: str, template_url: str, debug: bool) -> None:
     if debug:
         enable_debug(format=DEBUG_LOGGING_FORMAT)
-    _logger.debug(f"Initializing module {module_name}")
+    _logger.debug("Initializing module %s", module_name)
+
     minit.create_module_dir(
         group_name=group_name,
         module_name=module_name,
diff --git a/seedfarmer/__metadata__.py b/seedfarmer/__metadata__.py
index e3d7c5d4..c99ec672 100644
--- a/seedfarmer/__metadata__.py
+++ b/seedfarmer/__metadata__.py
@@ -13,5 +13,5 @@
 #    limitations under the License.
 
 __title__: str = "seed-farmer"
-__description__: str = "The AWS Professional CLI tool SeedFarmer for GitOps support with AWS_CodeSeeder"
+__description__: str = "The AWS Professional Services CLI tool SeedFarmer for GitOps support with AWS_CodeSeeder"
 __license__: str = "Apache License 2.0"
diff --git a/seedfarmer/commands/__init__.py b/seedfarmer/commands/__init__.py
index f0e30a71..d185de0b 100644
--- a/seedfarmer/commands/__init__.py
+++ b/seedfarmer/commands/__init__.py
@@ -14,6 +14,7 @@
 
 from seedfarmer.commands._deployment_commands import apply, destroy
 from seedfarmer.commands._module_commands import deploy_module, destroy_module
+from seedfarmer.commands._parameter_commands import generate_export_env_params
 from seedfarmer.commands._stack_commands import (
     deploy_managed_policy_stack,
     deploy_module_stack,
@@ -30,4 +31,5 @@
     "deploy_module_stack",
     "destroy_module_stack",
     "deploy_seedkit",
+    "generate_export_env_params",
 ]
diff --git a/seedfarmer/commands/_deployment_commands.py b/seedfarmer/commands/_deployment_commands.py
index 065a6b99..d94b9d01 100644
--- a/seedfarmer/commands/_deployment_commands.py
+++ b/seedfarmer/commands/_deployment_commands.py
@@ -17,13 +17,14 @@
 import json
 import logging
 import os
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional
 
 import checksumdir
 import yaml
 
 import seedfarmer.mgmt.deploy_utils as du
 from seedfarmer import OPS_ROOT, commands
+from seedfarmer.commands._parameter_commands import load_parameter_values
 from seedfarmer.mgmt.module_info import (
     _get_deployspec_path,
     _get_modulestack_path,
@@ -34,62 +35,27 @@
     remove_group_info,
     write_deployment_manifest,
 )
-from seedfarmer.models.manifests import DeploymentManifest, DeploySpec, ModuleManifest, ModuleParameter, ModulesManifest
-from seedfarmer.output_utils import _print_modules, print_bolded, print_manifest_inventory, print_manifest_json
+from seedfarmer.models.deploy_responses import ModuleDeploymentResponse
+from seedfarmer.models.manifests import DeploymentManifest, DeploySpec, ModuleManifest, ModulesManifest
+from seedfarmer.output_utils import (
+    _print_modules,
+    print_bolded,
+    print_errored_modules,
+    print_manifest_inventory,
+    print_manifest_json,
+)
 
 _logger: logging.Logger = logging.getLogger(__name__)
 
 
-def _load_parameter_values(deployment_name: str, parameters: List[ModuleParameter]) -> List[ModuleParameter]:
-    parameter_values = []
-    parameter_values_cache = {}
-    for parameter in parameters:
-        _logger.debug("parameter: %s", parameter.dict())
-        if parameter.value:
-            _logger.debug("static parameter value: %s", parameter.value)
-            parameter_values.append(parameter)
-        # Load parameter from Module Metadata
-        elif parameter.value_from and parameter.value_from.module_metadata:
-            group = parameter.value_from.module_metadata.group
-            name = parameter.value_from.module_metadata.name
-            _logger.debug("Loading metadata for dependency group, module: %s, %s" % (group, name))
-
-            # Ensure we only retrieve the SSM Parameter value once per module
-            if (deployment_name, group, name) not in parameter_values_cache:
-                parameter_values_cache[(deployment_name, group, name)] = get_module_metadata(
-                    deployment_name,
-                    group,
-                    name,
-                )
-            parameter_value = parameter_values_cache[(deployment_name, group, name)]
-            _logger.debug("loaded parameter value: %s", parameter_value)
-
-            parameter_value = (
-                parameter_value.get(parameter.value_from.module_metadata.key, None)
-                if parameter_value is not None and parameter.value_from.module_metadata.key is not None
-                else parameter_value
-            )
-            _logger.debug("parsed parameter value: %s", parameter_value)
-
-            if parameter_value is not None:
-                parameter_values.append(
-                    ModuleParameter(
-                        name=parameter.name,
-                        value=parameter_value,
-                    )
-                )
-    return parameter_values
-
-
 def _execute_deploy(
     d_name: str,
     g_name: str,
     m: ModuleManifest,
     d_secret: Optional[str] = None,
     permission_boundary_arn: Optional[str] = None,
-) -> Tuple[str, Optional[Dict[str, str]]]:
-    # Resolve parameters with valueFroms to values
-    parameters = _load_parameter_values(deployment_name=d_name, parameters=m.parameters)
+) -> ModuleDeploymentResponse:
+    parameters = load_parameter_values(deployment_name=d_name, parameters=m.parameters)
 
     # Deploys the IAM role per module
     commands.deploy_module_stack(
@@ -127,7 +93,7 @@ def _execute_destroy(
     g_name: str,
     m: ModuleManifest,
     d_secret: Optional[str] = None,
-) -> Optional[Tuple[str, Optional[Dict[str, str]]]]:
+) -> Optional[ModuleDeploymentResponse]:
     if m.deploy_spec is None:
         raise ValueError(f"Invalid value for ModuleManifest.deploy_spec in group {g_name} and module : {m.name}")
 
@@ -137,7 +103,7 @@ def _execute_destroy(
         module_path=m.path,
         module_deploy_spec=m.deploy_spec,
         module_manifest_name=m.name,
-        parameters=_load_parameter_values(deployment_name=d_name, parameters=m.parameters),
+        parameters=load_parameter_values(deployment_name=d_name, parameters=m.parameters),
         module_metadata=None,
     )
     commands.destroy_module_stack(
@@ -167,13 +133,16 @@ def _deploy_deployment_is_not_dry_run(
             deployment_manifest_wip,
             True,
         )
-        _logger.debug(f"DeploymentManifest for deploy after filter = {json.dumps(deployment_manifest_wip.dict())}")
+        if _logger.isEnabledFor(logging.DEBUG):
+            _logger.debug(
+                "DeploymentManifest for deploy after filter =  %s", json.dumps(deployment_manifest_wip.dict())
+            )
         for _group in deployment_manifest_wip.groups:
             if len(_group.modules) > 0:
                 threads = _group.concurrency if _group.concurrency else len(_group.modules)
                 with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as workers:
 
-                    def _exec_deploy(args: Dict[str, Any]) -> Optional[Tuple[str, Optional[Dict[str, str]]]]:
+                    def _exec_deploy(args: Dict[str, Any]) -> ModuleDeploymentResponse:
                         return _execute_deploy(
                             args["d_name"], args["g"], args["m"], args["d_secret"], args["permission_boundary_arn"]
                         )
@@ -189,10 +158,17 @@ def _exec_deploy(args: Dict[str, Any]) -> Optional[Tuple[str, Optional[Dict[str,
                         for _module in _group.modules
                         if _module and _module.deploy_spec
                     ]
-                    _ = list(workers.map(_exec_deploy, params))
+                    deploy_response = list(workers.map(_exec_deploy, params))
+                    _logger.debug(deploy_response)
+                    for dep_resp_object in deploy_response:
+                        if dep_resp_object.status in ["ERROR", "error", "Error"]:
+                            _logger.error("At least one module failed to deploy...exiting deployment")
+                            print_errored_modules("These modules had errors deploying", deploy_response)  # type: ignore
+                            exit(0)
+
         print_manifest_inventory(f"Modules Deployed: {deployment_manifest_wip.name}", deployment_manifest_wip, False)
     else:
-        _logger.info(f"All modules in {deployment_manifest_wip.name} up to date")
+        _logger.info(" All modules in %s up to date", deployment_manifest_wip.name)
     # Write the deployment manifest once completed to preserve group order
     du.write_deployed_deployment_manifest(deployment_manifest=deployment_manifest)
 
@@ -253,7 +229,7 @@ def destroy_deployment(
                 threads = _group.concurrency if _group.concurrency else len(_group.modules)
                 with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as workers:
 
-                    def _exec_destroy(args: Dict[str, Any]) -> Optional[Tuple[str, Optional[Dict[str, str]]]]:
+                    def _exec_destroy(args: Dict[str, Any]) -> Optional[ModuleDeploymentResponse]:
                         return _execute_destroy(
                             args["d"],
                             args["g"],
@@ -271,7 +247,13 @@ def _exec_destroy(args: Dict[str, Any]) -> Optional[Tuple[str, Optional[Dict[str
                         for _module in _group.modules
                         if _module and _module.deploy_spec
                     ]
-                    _ = list(workers.map(_exec_destroy, params))
+                    destroy_response = list(workers.map(_exec_destroy, params))
+                    _logger.debug(destroy_response)
+                    for dep_resp_object in destroy_response:
+                        if dep_resp_object and dep_resp_object.status in ["ERROR", "error", "Error"]:
+                            _logger.error("At least one module failed to destroy...exiting deployment")
+                            print_errored_modules("The following modules had errors destroying ", destroy_response)
+                            exit(0)
 
         print_manifest_inventory(f"Modules Destroyed: {deployment_name}", destroy_manifest, False, "red")
         if remove_deploy_manifest:
@@ -316,7 +298,8 @@ def deploy_deployment(
     permission_boundary_arn = (
         deployment_manifest_wip.permission_boundary_arn if deployment_manifest_wip.permission_boundary_arn else None
     )
-    _logger.debug(f"Setting up deployment for {deployment_name}")
+    _logger.debug("Setting up deployment for %s", deployment_name)
+
     print_manifest_inventory(
         f"Modules added to manifest: {deployment_manifest_wip.name}", deployment_manifest_wip, True
     )
@@ -331,10 +314,9 @@ def deploy_deployment(
         group_name = group.name
         du.write_group_manifest(deployment_name=deployment_name, group_manifest=working_group)
         modules_to_deploy = []
-        _logger.info(f"Verifying all modules in {group.name} for deploy")
+        _logger.info(" Verifying all modules in %s for deploy ", group.name)
         for module in group.modules:
-            _logger.debug(f"Working on -- {module}")
-
+            _logger.debug("Working on --  %s", module)
             if not module.path:
                 raise Exception("Unable to parse module manifest, `path` not specified")
 
@@ -479,7 +461,7 @@ def destroy(deployment_name: str, dryrun: bool = False, show_manifest: bool = Fa
         By default False
 
     """
-    _logger.debug(f"Preparing to destroy {deployment_name}")
+    _logger.debug("Preparing to destroy %s", deployment_name)
     deployment_params_cache = du.generate_deployment_cache(deployment_name=deployment_name)
     destroy_manifest = du.generate_deployed_manifest(
         deployment_name=deployment_name, deployment_params_cache=deployment_params_cache, skip_deploy_spec=False
@@ -487,4 +469,4 @@ def destroy(deployment_name: str, dryrun: bool = False, show_manifest: bool = Fa
     if destroy_manifest:
         destroy_deployment(destroy_manifest, remove_deploy_manifest=True, dryrun=dryrun, show_manifest=show_manifest)
     else:
-        _logger.info(f"Deployment {deployment_name} was not found, ignoring...")
+        _logger.info("Deployment %s was not found, ignoring... ", deployment_name)
diff --git a/seedfarmer/commands/_module_commands.py b/seedfarmer/commands/_module_commands.py
index 01a138bf..e15eecf7 100644
--- a/seedfarmer/commands/_module_commands.py
+++ b/seedfarmer/commands/_module_commands.py
@@ -21,8 +21,10 @@
 
 import botocore.exceptions
 from aws_codeseeder import codeseeder
+from aws_codeseeder.error.CodeSeederError import CodeSeederRuntimeError
 
 from seedfarmer import CONFIG_FILE, OPS_ROOT, PROJECT
+from seedfarmer.models.deploy_responses import CodeSeederMetadata, ModuleDeploymentResponse, StatusType
 from seedfarmer.models.manifests import DeploySpec, ModuleParameter
 from seedfarmer.utils import generate_hash
 
@@ -72,7 +74,7 @@ def deploy_module(
     module_bundle_md5: Optional[str] = None,
     docker_credentials_secret: Optional[str] = None,
     permission_boundary_arn: Optional[str] = None,
-) -> Tuple[str, Optional[Dict[str, str]]]:
+) -> ModuleDeploymentResponse:
     env_vars = _env_vars(
         deployment_name=deployment_name,
         group_name=group_name,
@@ -103,18 +105,40 @@ def deploy_module(
         raise ValueError("Missing `deploy` in module's deployspec.yaml")
 
     _phases = module_deploy_spec.deploy.phases
-    return _execute_module_commands(
-        deployment_name=deployment_name,
-        group_name=group_name,
-        module_manifest_name=module_manifest_name,
-        extra_dirs={"module": module_path},
-        extra_install_commands=["cd module/"] + _phases.install.commands,
-        extra_pre_build_commands=["cd module/"] + _phases.pre_build.commands,
-        extra_build_commands=["cd module/"] + _phases.build.commands,
-        extra_post_build_commands=["cd module/"] + _phases.post_build.commands + md5_put + metadata_put,
-        extra_env_vars=env_vars,
-        codebuild_compute_type=module_deploy_spec.build_type,
-    )
+    try:
+        resp_dict_str, dict_metadata = _execute_module_commands(
+            deployment_name=deployment_name,
+            group_name=group_name,
+            module_manifest_name=module_manifest_name,
+            extra_dirs={"module": module_path},
+            extra_install_commands=["cd module/"] + _phases.install.commands,
+            extra_pre_build_commands=["cd module/"] + _phases.pre_build.commands,
+            extra_build_commands=["cd module/"] + _phases.build.commands,
+            extra_post_build_commands=["cd module/"] + _phases.post_build.commands + md5_put + metadata_put,
+            extra_env_vars=env_vars,
+            codebuild_compute_type=module_deploy_spec.build_type,
+        )
+        _logger.debug("CodeSeeder Metadata response is %s", dict_metadata)
+
+        resp = ModuleDeploymentResponse(
+            deployment=deployment_name,
+            group=group_name,
+            module=module_manifest_name,
+            status=StatusType.SUCCESS.value,
+            codeseeder_metadata=CodeSeederMetadata(**json.loads(resp_dict_str)) if resp_dict_str else None,
+            codeseeder_output=dict_metadata,
+        )
+    except CodeSeederRuntimeError as csre:
+        _logger.error(f"Error Response from CodeSeeder: {csre} - {csre.error_info}")
+        l_case_error = {k.lower(): csre.error_info[k] for k in csre.error_info.keys()}
+        resp = ModuleDeploymentResponse(
+            deployment=deployment_name,
+            group=group_name,
+            module=module_manifest_name,
+            status=StatusType.ERROR.value,
+            codeseeder_metadata=CodeSeederMetadata(**l_case_error),
+        )
+    return resp
 
 
 def destroy_module(
@@ -125,7 +149,7 @@ def destroy_module(
     module_manifest_name: str,
     parameters: Optional[List[ModuleParameter]] = None,
     module_metadata: Optional[str] = None,
-) -> Tuple[str, Optional[Dict[str, str]]]:
+) -> ModuleDeploymentResponse:
     env_vars = _env_vars(
         deployment_name=deployment_name,
         group_name=group_name,
@@ -148,18 +172,38 @@ def destroy_module(
         )
 
     _phases = module_deploy_spec.destroy.phases
-    return _execute_module_commands(
-        deployment_name=deployment_name,
-        group_name=group_name,
-        module_manifest_name=module_manifest_name,
-        extra_dirs={"module": module_path},
-        extra_install_commands=["cd module/"] + _phases.install.commands,
-        extra_pre_build_commands=["cd module/"] + _phases.pre_build.commands + export_info,
-        extra_build_commands=["cd module/"] + _phases.build.commands,
-        extra_post_build_commands=["cd module/"] + _phases.post_build.commands + remove_ssm,
-        extra_env_vars=env_vars,
-        codebuild_compute_type=module_deploy_spec.build_type,
-    )
+
+    try:
+        resp_dict_str, _ = _execute_module_commands(
+            deployment_name=deployment_name,
+            group_name=group_name,
+            module_manifest_name=module_manifest_name,
+            extra_dirs={"module": module_path},
+            extra_install_commands=["cd module/"] + _phases.install.commands,
+            extra_pre_build_commands=["cd module/"] + _phases.pre_build.commands + export_info,
+            extra_build_commands=["cd module/"] + _phases.build.commands,
+            extra_post_build_commands=["cd module/"] + _phases.post_build.commands + remove_ssm,
+            extra_env_vars=env_vars,
+            codebuild_compute_type=module_deploy_spec.build_type,
+        )
+        resp = ModuleDeploymentResponse(
+            deployment=deployment_name,
+            group=group_name,
+            module=module_manifest_name,
+            status=StatusType.SUCCESS.value,
+            codeseeder_metadata=CodeSeederMetadata(**json.loads(resp_dict_str)) if resp_dict_str else None,
+        )
+    except CodeSeederRuntimeError as csre:
+        _logger.error(f"Error Response from CodeSeeder: {csre} - {csre.error_info}")
+        l_case_error = {k.lower(): csre.error_info[k] for k in csre.error_info.keys()}
+        resp = ModuleDeploymentResponse(
+            deployment=deployment_name,
+            group=group_name,
+            module=module_manifest_name,
+            status=StatusType.ERROR.value,
+            codeseeder_metadata=CodeSeederMetadata(**l_case_error),
+        )
+    return resp
 
 
 def _execute_module_commands(
@@ -200,8 +244,13 @@ def _execute_module_commands(
         extra_env_vars: Optional[Dict[str, Any]] = None,
         codebuild_compute_type: Optional[str] = None,
     ) -> str:
-        _logger.info("Deployment: %s", deployment_name)
-        return "Remote function execution complete"
+        deploy_info = {
+            "aws_region": os.environ.get("AWS_DEFAULT_REGION"),
+            "aws_account_id": os.environ.get("AWS_ACCOUNT_ID"),
+            "codebuild_build_id": os.environ.get("CODEBUILD_BUILD_ID"),
+            "codebuild_log_path": os.environ.get("CODEBUILD_LOG_PATH"),
+        }
+        return json.dumps(deploy_info)
 
     count = 0
     while True:
diff --git a/seedfarmer/commands/_parameter_commands.py b/seedfarmer/commands/_parameter_commands.py
new file mode 100644
index 00000000..643dd7ac
--- /dev/null
+++ b/seedfarmer/commands/_parameter_commands.py
@@ -0,0 +1,88 @@
+#  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License").
+#    You may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+
+
+import logging
+from typing import Any, Dict, List, Optional, Tuple
+
+from seedfarmer import PROJECT
+from seedfarmer.mgmt.module_info import get_module_metadata
+from seedfarmer.models.manifests import ModuleParameter
+from seedfarmer.utils import upper_snake_case
+
+_logger: logging.Logger = logging.getLogger(__name__)
+
+
+def generate_export_env_params(metadata: Optional[Dict[str, Any]]) -> Optional[List[str]]:
+    envs: List[str] = [
+        f"export {PROJECT.upper()}_PARAMETER_{upper_snake_case(k)}={metadata[k]}"
+        for k in metadata.keys()  # type: ignore
+        if metadata is not None and metadata.keys() is not None
+    ]
+    return envs
+
+
+def load_parameter_values(deployment_name: str, parameters: List[ModuleParameter]) -> List[ModuleParameter]:
+    parameter_values = []
+    parameter_values_cache: Dict[Tuple[str, str, str], Any] = {}
+    for parameter in parameters:
+        if _logger.isEnabledFor(logging.DEBUG):
+            _logger.debug("parameter: %s", parameter.dict())
+        if parameter.value:
+            _logger.debug("static parameter value: %s", parameter.value)
+            parameter_values.append(parameter)
+        # Load parameter from Module Metadata
+        elif parameter.value_from and parameter.value_from.module_metadata:
+            module_metatdata = _module_metatdata(deployment_name, parameter, parameter_values_cache)
+            parameter_values.append(module_metatdata) if module_metatdata else None
+    return parameter_values
+
+
+def _get_param_value_cache(
+    d_name: str, g_name: str, m_name: str, parameter_values_cache: Dict[Tuple[str, str, str], Any]
+) -> Dict[Any, Any]:
+    if (d_name, g_name, m_name) not in parameter_values_cache:
+        parameter_values_cache[(d_name, g_name, m_name)] = get_module_metadata(
+            d_name,
+            g_name,
+            m_name,
+        )
+    return parameter_values_cache[(d_name, g_name, m_name)]
+
+
+def _module_metatdata(
+    deployment_name: str, parameter: ModuleParameter, parameter_values_cache: Dict[Tuple[str, str, str], Any]
+) -> Optional[ModuleParameter]:
+    if parameter.value_from and parameter.value_from.module_metadata:
+        group = parameter.value_from.module_metadata.group
+        module_name = parameter.value_from.module_metadata.name
+        _logger.debug("Loading metadata for dependency group, module: %s, %s" % (group, module_name))
+
+        # Ensure we only retrieve the SSM Parameter value once per module
+        parameter_value = _get_param_value_cache(deployment_name, group, module_name, parameter_values_cache)
+        _logger.debug("loaded parameter value: %s", parameter_value)
+
+        parameter_value = (
+            parameter_value.get(parameter.value_from.module_metadata.key, None)
+            if parameter_value is not None and parameter.value_from.module_metadata.key is not None
+            else parameter_value
+        )
+        _logger.debug("parsed parameter value: %s", parameter_value)
+
+        if parameter_value is not None:
+            return ModuleParameter(
+                name=parameter.name,
+                value=parameter_value,
+            )
+    return None
diff --git a/seedfarmer/commands/_stack_commands.py b/seedfarmer/commands/_stack_commands.py
index 60699771..ae91d9e7 100644
--- a/seedfarmer/commands/_stack_commands.py
+++ b/seedfarmer/commands/_stack_commands.py
@@ -62,8 +62,7 @@ def deploy_managed_policy_stack(deployment_name: str, deployment_manifest: Deplo
         _logger.debug(
             f"Validated the existence of Project Managed Policy Template at:{project_managed_policy_template}"
         )
-
-        _logger.info(f"Deploying {PROJECT_MANAGED_POLICY_CFN_NAME}")
+        _logger.info("Deploying %s", PROJECT_MANAGED_POLICY_CFN_NAME)
         services.cfn.deploy_template(
             stack_name=PROJECT_MANAGED_POLICY_CFN_NAME,
             filename=project_managed_policy_template,
@@ -97,7 +96,7 @@ def destroy_module_stack(
 
     policies_arn = []
     if seedkit_stack_exists:
-        _logger.debug(f"Seedkit stack exists - {seedkit_stack_name}")
+        _logger.debug("Seedkit stack exists - %s", seedkit_stack_name)
         seedkit_managed_policy_arn = stack_outputs.get("SeedkitResourcesPolicyArn")
         policies_arn.append(seedkit_managed_policy_arn)
 
@@ -112,7 +111,7 @@ def destroy_module_stack(
     _logger.debug(
         f"seedkit_managed_policy {seedkit_managed_policy_arn}  project_managed_policy {project_managed_policy_arn}"
     )
-    _logger.debug(f"module_role_name {module_role_name}")
+    _logger.debug("module_role_name %s", module_role_name)
 
     for policy_arn in policies_arn:
         iam.detach_policy_from_role(role_name=module_role_name, policy_arn=policy_arn)
@@ -197,7 +196,6 @@ def deploy_module_stack(
                     stack_parameters[k] = ",".join(value)
                 else:
                     json.dumps(value)
-
         _logger.debug("stack_parameters: %s", stack_parameters)
 
         # Create/Update Module IAM Policy
@@ -213,7 +211,7 @@ def deploy_module_stack(
     _logger.debug("Extracting the Codeseeder Managed policy")
     seedkit_stack_exists, seedkit_stack_name, stack_outputs = commands.seedkit_deployed(seedkit_name=PROJECT)
     if seedkit_stack_exists:
-        _logger.debug(f"Seedkit stack exists - {seedkit_stack_name}")
+        _logger.debug("Seedkit stack exists - %s", seedkit_stack_name)
         seedkit_managed_policy_arn = stack_outputs.get("SeedkitResourcesPolicyArn")
 
     # Extract Project Managed policy name
@@ -221,7 +219,7 @@ def deploy_module_stack(
         stack_name=PROJECT_MANAGED_POLICY_CFN_NAME
     )
 
-    _logger.debug(f"project_managed_policy_output is : {stack_outputs}")
+    _logger.debug("project_managed_policy_output is : %s", stack_outputs)
     if project_managed_policy_stack_exists:
         project_managed_policy_arn = stack_outputs.get("ProjectPolicyARN")
 
@@ -229,8 +227,10 @@ def deploy_module_stack(
         raise ValueError("Project Managed Stack is missing the export `ProjectPolicyARN`")
 
     policies = [seedkit_managed_policy_arn, project_managed_policy_arn]
-    iam.attach_policy_to_role(module_role_name, policies)
-    time.sleep(3)  # if first deployment, these roles take time to propogate
+    policies_attached = iam.attach_policy_to_role(module_role_name, policies)
+    if policies.sort() == policies_attached.sort():
+        _logger.info("Delaying module %s deployment to allow IAM Roles and Policies to take effect", group_module_name)
+        time.sleep(10)  # on first deployment roles and policy attachments need time to take effect
 
     # Attaching Docker Credentials Secret Optionally
     policy_body = json.dumps(
diff --git a/seedfarmer/mgmt/deploy_utils.py b/seedfarmer/mgmt/deploy_utils.py
index 2027c76e..2b4cf7d3 100644
--- a/seedfarmer/mgmt/deploy_utils.py
+++ b/seedfarmer/mgmt/deploy_utils.py
@@ -212,7 +212,7 @@ def _populate_groups_to_remove(
     set_requested_groups = set([g.name for g in apply_groups])
     set_deployed_groups = set(mi.get_all_groups(dep_name))
     destroy_groups = list(sorted(set_deployed_groups - set_requested_groups))
-    _logger.debug(f"Groups already deployed that will be DESTROYED : {destroy_groups}")
+    _logger.debug("Groups already deployed that will be DESTROYED : %s", destroy_groups)
     destroy_group_list = []
     for destroy_group in destroy_groups:
         group_manifests = mi.get_group_manifest(dep_name, destroy_group, deployment_params_cache)
@@ -241,7 +241,8 @@ def _populate_modules_to_remove(
     set_requests_modules = set([m.name for m in apply_modules])
     set_deployed_modules = set(mi.get_deployed_modules(dep_name, group, deployment_params_cache))
     destroy_modules = list(sorted(set_deployed_modules - set_requests_modules))
-    _logger.debug(f"Modules of Group {group} already deployed that will be DESTROYED: {destroy_modules}  ")
+    _logger.debug("Modules of Group %s already deployed that will be DESTROYED: %s", group, destroy_modules)
+
     destroy_module_list = []
     for destroy_module in destroy_modules:
         mod_manifest = mi.get_module_manifest(dep_name, group, destroy_module, deployment_params_cache)
diff --git a/seedfarmer/mgmt/module_info.py b/seedfarmer/mgmt/module_info.py
index 0646caf1..9cb7e47b 100644
--- a/seedfarmer/mgmt/module_info.py
+++ b/seedfarmer/mgmt/module_info.py
@@ -498,7 +498,8 @@ def write_deployment_manifest(deployment: str, data: Dict[str, Any]) -> None:
     data : Dict[str, Any]
         A dict of the deployment manifest
     """
-    _logger.debug(f"Writing to {_deployment_manifest_key(deployment)} value {data}")
+    if _logger.isEnabledFor(logging.DEBUG):
+        _logger.debug("Writing to %s values %s", _deployment_manifest_key(deployment), data)
     store.put_parameter(name=_deployment_manifest_key(deployment), obj=data)
 
 
@@ -515,7 +516,8 @@ def write_deployed_deployment_manifest(deployment: str, data: Dict[str, Any]) ->
         A dict of the deployment manifest
     """
     key = _deployed_deployment_manifest_key(deployment)
-    _logger.debug(f"Writing to {key} value {data}")
+    _logger.debug("Writing to %s value %s", key, data)
+
     store.put_parameter(name=key, obj=data)
 
 
diff --git a/seedfarmer/mgmt/module_init.py b/seedfarmer/mgmt/module_init.py
index eb00fca6..55bc4ee3 100644
--- a/seedfarmer/mgmt/module_init.py
+++ b/seedfarmer/mgmt/module_init.py
@@ -46,15 +46,15 @@ def create_module_dir(module_name: str, group_name: Optional[str], template_url:
         output_dir = os.path.join(module_root, group_name)
 
         if not os.path.exists(output_dir):
-            _logger.info(f"Creating group dir: {output_dir}")
+            _logger.info("Creating group dir: %s", output_dir)
             os.makedirs(output_dir)
 
     if os.path.exists(module_path):
         raise Exception(f"The module {module_name} already exists under {output_dir}.")
 
-    checkout_branch = "init-module" if template_url == "git@github.com:awslabs/seed-farmer.git" else None
+    checkout_branch = "init-module" if template_url == "https://github.com/awslabs/seed-farmer.git" else None
 
-    _logger.info(f"New module will be created in the following dir: {output_dir}")
+    _logger.info("New module will be created in the following dir: %s", output_dir)
     cookiecutter(
         template=template_url,
         checkout=checkout_branch,
@@ -77,8 +77,8 @@ def create_project(template_url: Optional[str]) -> None:
         A URL, for example a Github repo, that is or contains templating for the initialization
     """
 
-    checkout_branch = "init-project" if template_url == "git@github.com:awslabs/seed-farmer.git" else None
-    _logger.info(f"New project will be created in the following dir: {os.path.join(OPS_ROOT,PROJECT)}")
+    checkout_branch = "init-project" if template_url == "https://github.com/awslabs/seed-farmer.git" else None
+    _logger.info(" New project will be created in the following dir: %s", os.path.join(OPS_ROOT, PROJECT))
     cookiecutter(
         template=template_url,
         checkout=checkout_branch,
diff --git a/seedfarmer/models/deploy_responses.py b/seedfarmer/models/deploy_responses.py
new file mode 100644
index 00000000..bcad2b2e
--- /dev/null
+++ b/seedfarmer/models/deploy_responses.py
@@ -0,0 +1,66 @@
+#  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License").
+#    You may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+
+from enum import Enum
+from typing import Any, Dict, Optional
+
+from pydantic import PrivateAttr
+
+from seedfarmer.models._base import CamelModel
+from seedfarmer.utils import generate_codebuild_url
+
+
+class StatusType(Enum):
+    SUCCESS = "SUCCESS"
+    ERROR = "ERROR"
+
+
+class CodeSeederMetadata(CamelModel):
+
+    _build_url: str = PrivateAttr()
+
+    aws_account_id: Optional[str]
+    aws_region: Optional[str]
+    codebuild_build_id: Optional[str]
+    codebuild_log_path: Optional[str]
+
+    def __init__(self, **data: Any) -> None:
+        super().__init__(**data)
+        self._build_url = generate_codebuild_url(
+            self.aws_account_id,  # type: ignore
+            self.aws_region,  # type: ignore
+            self.codebuild_build_id,  # type: ignore
+        )
+
+    @property
+    def build_url(self) -> str:
+        return self._build_url
+
+
+class ModuleDeploymentResponse(CamelModel):
+
+    deployment: str
+    group: Optional[str]
+    module: str
+    status: str
+    codeseeder_metadata: Optional[CodeSeederMetadata]
+    codeseeder_output: Optional[Dict[Any, Any]]
+
+    def __init__(self, **data: Any) -> None:
+        super().__init__(**data)
+        if data.get("status"):
+            status_in = str(data["status"]).upper()
+            self.status = status_in if status_in in StatusType.__members__.keys() else StatusType.SUCCESS.value
+        else:
+            self.build_type = StatusType.SUCCESS.value
diff --git a/seedfarmer/output_utils.py b/seedfarmer/output_utils.py
index c1b6174d..ea995fbc 100644
--- a/seedfarmer/output_utils.py
+++ b/seedfarmer/output_utils.py
@@ -14,9 +14,11 @@
 
 from typing import Any, Dict, List, Optional
 
+import pyshorteners
 from rich.console import Console
 from rich.table import Table
 
+from seedfarmer.models.deploy_responses import ModuleDeploymentResponse
 from seedfarmer.models.manifests import DeploymentManifest
 
 console = Console(record=True)
@@ -55,7 +57,7 @@ def print_manifest_inventory(
     description : str
         Give the table a title
     dep : DeploymentManifest
-        A poluated Deplopyment Manifest object
+        A populated Deplopyment Manifest object
     show_path : bool, optional
        Show the relative path of the module code, by default False
     color : str, optional
@@ -129,3 +131,41 @@ def print_bolded(message: str, color: str = "yellow") -> None:
        The color you want the message printed in.... default is "yellow"
     """ """"""
     console.print(f"[bold {color}]{message}")
+
+
+def print_errored_modules(
+    description: str, modules_data: List[Optional[ModuleDeploymentResponse]], color: str = "red"
+) -> None:
+    """
+    Print the modules that have errored on deployment of a group
+
+    Parameters
+    ----------
+    description : str
+        The custom text to head the output table
+    modules_data : List[Optional[ModuleDeploymentResponse]]
+        The object containing the metadata related to the failure
+    color : str, optional
+        The color of the text to head the description of the table, by default "red"
+    """
+
+    table = Table(title=f"[bold {color}]{description}", title_justify="left")
+
+    table.add_column("Deployemnt", justify="left", style="cyan", no_wrap=True)
+    table.add_column("Group", justify="left", style="magenta")
+    table.add_column("Module", justify="left", style="green")
+    table.add_column("CodeBuild Id", justify="left", style="white")
+    table.add_column("CodeBuild URL", justify="left", style="white")
+    for r_obj in modules_data:
+        if r_obj and r_obj.status in ["ERROR", "error", "Error"]:
+            if r_obj.codeseeder_metadata:
+                url = r_obj.codeseeder_metadata.build_url
+                shortened_url = pyshorteners.Shortener().tinyurl.short(url) if url not in ["NA", "N/A"] else url
+                table.add_row(
+                    r_obj.deployment if r_obj and r_obj.deployment else None,
+                    r_obj.group if r_obj and r_obj.group else None,
+                    r_obj.module if r_obj and r_obj.module else None,
+                    r_obj.codeseeder_metadata.codebuild_build_id,
+                    shortened_url,
+                )
+    console.print(table)
diff --git a/seedfarmer/services/_iam.py b/seedfarmer/services/_iam.py
index 37607843..79fdab6e 100644
--- a/seedfarmer/services/_iam.py
+++ b/seedfarmer/services/_iam.py
@@ -22,7 +22,7 @@
 
 
 def get_role(role_name: str) -> Optional[Dict[str, Any]]:
-    _logger.debug(f"Getting Role: {role_name}")
+    _logger.debug("Getting Role: %s", role_name)
 
     iam_client = boto3_client("iam")
     try:
@@ -32,7 +32,7 @@ def get_role(role_name: str) -> Optional[Dict[str, Any]]:
 
 
 def create_check_iam_role(trust_policy: Dict[str, Any], role_name: str, permission_boundary_arn: Optional[str]) -> None:
-    _logger.debug(f"Creating IAM Role with name: {role_name}")
+    _logger.debug("Creating IAM Role with name: %s ", role_name)
     iam_client = boto3_client("iam")
     try:
         iam_client.get_role(RoleName=role_name)
@@ -48,45 +48,46 @@ def create_check_iam_role(trust_policy: Dict[str, Any], role_name: str, permissi
         iam_client.create_role(**args)
 
 
-def attach_policy_to_role(role_name: str, policies: List[str]) -> None:
+def attach_policy_to_role(role_name: str, policies: List[str]) -> List[str]:
     iam_client = boto3_client("iam")
     try:
         response = iam_client.list_attached_role_policies(RoleName=role_name)
         attached_policies = [i["PolicyArn"] for i in response["AttachedPolicies"] if response["AttachedPolicies"]]
-        _logger.debug(f"List of Attached Policie(s) are: {attached_policies}")
+        _logger.debug("List of Attached Policie(s) are: %s ", attached_policies)
+        to_be_attached = list(set(policies) - set(attached_policies))
+        # if attached_polices is empty, to_bo_attached == policies
+
         if attached_policies:
-            to_be_attached = list(set(policies) - set(attached_policies))
-            _logger.debug(f"To be attached policie(s) are: {to_be_attached}")
-            for policy in to_be_attached:
-                iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy)
-                _logger.debug(f"Attached the policy: {policy}")
+            _logger.debug("To be attached policie(s) are %s ", to_be_attached)
         else:
-            _logger.debug(f"First time deployment, attaching the policies: {policies}")
-            for policy in policies:
-                iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy)
-                _logger.debug(f"Attached the policy: {policy}")
+            _logger.debug("First time deployment, attaching all policies: %s ", to_be_attached)
+
+        for policy in to_be_attached:
+            iam_client.attach_role_policy(RoleName=role_name, PolicyArn=policy)
+            _logger.debug("Attached the policy: %s ", policy)
+        return to_be_attached
     except Exception as e:
         raise e
 
 
 def attach_inline_policy(role_name: str, policy_body: str, policy_name: str) -> None:
     iam_client = boto3_client("iam")
-    _logger.debug(f"Attaching the Inline policy {policy_name} to the IAM Role: {role_name}")
+    _logger.debug("Attaching the Inline policy %s to the IAM Role: %s", policy_name, role_name)
     try:
         iam_client.put_role_policy(RoleName=role_name, PolicyName=policy_name, PolicyDocument=policy_body)
     except Exception as e:
-        _logger.debug(f"Failed to attach the Inline policy {policy_name} to the IAM Role: {role_name}")
+        _logger.debug("ailed to attach the Inline policy %s to the IAM Role: %s", policy_name, role_name)
         raise e
 
 
 def detach_policy_from_role(role_name: str, policy_arn: str) -> None:
-    _logger.debug(f"Detatching policy: {policy_arn} from the IAM Role: {role_name}")
+    _logger.debug("Detatching policy: %s  from the IAM Role: %s ", policy_arn, role_name)
     iam_client = boto3_client("iam")
     try:
         iam_client.detach_role_policy(RoleName=role_name, PolicyArn=policy_arn)
     except Exception as e:
-        _logger.info(f"Could not delete policy : {policy_arn}")
-        _logger.info(f"Caught exception: {e}")
+        _logger.info("Could not delete policy %s", policy_arn)
+        _logger.info("Caught exception: %s ", e)
 
 
 def delete_role(role_name: str) -> None:
@@ -94,8 +95,8 @@ def delete_role(role_name: str) -> None:
     try:
         iam_client.delete_role(RoleName=role_name)
     except iam_client.exceptions.NoSuchEntityException as e:
-        _logger.info(f"Could not delete role : {role_name}")
-        _logger.info(f"Caught exception: {e}")
+        _logger.info("Could not delete role %s", role_name)
+        _logger.info("Caught exception: %s ", e)
 
 
 def detach_inline_policy_from_role(role_name: str, policy_name: str) -> None:
diff --git a/seedfarmer/utils.py b/seedfarmer/utils.py
index c69a3dcc..2c2ae60c 100644
--- a/seedfarmer/utils.py
+++ b/seedfarmer/utils.py
@@ -64,9 +64,59 @@ def upper_snake_case(value: str) -> str:
 
 
 def generate_hash() -> str:
+    """
+    Generate a hexdigest hash of the project and the deployment - for use generating unique names
+
+    Returns
+    -------
+    str
+        The resulting hash as a string
+    """
     account = get_account_id()
     region = get_region()
     concatenated_string = f"{account}-{region}"
     hash_value = (hashlib.sha1(concatenated_string.encode("UTF-8")).hexdigest())[:8]
-    _logger.debug(f"HASH generated is {hash_value}")
+    _logger.debug("HASH generated is %s", hash_value)
     return hash_value
+
+
+def generate_codebuild_url(account_id: str, region: str, codebuild_id: str) -> str:
+    """
+    Generate a standard URL for codebuild build information
+
+    Parameters
+    ----------
+    account_id : str
+        The AWS account id where CodeBuild ran
+    region : str
+        The AWS region where CodeBuild ran
+    codebuild_id : str
+        The CodeBuild Build ID
+
+    Returns
+    -------
+    str
+        The standard URL with protocol and query parameters
+        ex: https://us-east-1.console.aws.amazon.com/codesuite/codebuild/123456789012/projects/
+            codebuild-id/build/codebuild-id:3413241234/?region-us-east-1
+    """
+    try:
+        b_id_enc = codebuild_id.replace(":", "%3A")
+        cb_p = codebuild_id.split(":")[0]
+        return "".join(
+            (
+                "https://",
+                f"{region}",
+                ".console.aws.amazon.com/codesuite/codebuild/",
+                f"{account_id}",
+                "/projects/",
+                f"{cb_p}",
+                "/build/",
+                f"{b_id_enc}",
+                "/?region=",
+                f"{region}",
+            )
+        )
+    except Exception as e:
+        _logger.error(f"Error...{account_id} - {region} - {codebuild_id} - {e} ")
+        return "N/A"
diff --git a/setup.py b/setup.py
index cb9b40e9..5875036a 100644
--- a/setup.py
+++ b/setup.py
@@ -27,6 +27,9 @@
 with open("VERSION", "r") as version_file:
     version = version_file.read().strip()
 
+with open("README.md", "r") as file:
+    long_description = file.read()
+
 setup(
     name=about["__title__"],
     version=version,
@@ -35,13 +38,14 @@
     url="https://github.com/awslabs/seed-farmer",
     project_urls={"Org Site": "https://aws.amazon.com/professional-services/"},
     description=about["__description__"],
+    long_description=long_description,
     long_description_content_type="text/markdown",
     license=about["__license__"],
     packages=find_packages(include=["seed-farmer", "seedfarmer", "seedfarmer.*", "seed-farmer.*"]),
     keywords=["aws", "cdk"],
     python_requires=">=3.7",
     install_requires=[
-        "aws-codeseeder~=0.3.2",
+        "aws-codeseeder~=0.3.3",
         "cookiecutter~=2.1.0",
         "pyhumps~=3.5.0",
         "pydantic~=1.9.0",
@@ -50,6 +54,7 @@
         "checksumdir~=1.2.0",
         "rich~=12.4.0",
         "requests==2.28.1",
+        "pyshorteners==1.0.1",
     ],
     entry_points={"console_scripts": ["seedfarmer = seedfarmer.__main__:main"]},
     classifiers=[


Test patch:
diff --git a/test/pytest.ini b/test/pytest.ini
index d9597f65..58380634 100644
--- a/test/pytest.ini
+++ b/test/pytest.ini
@@ -6,7 +6,7 @@ markers =
     apply: marks `seedfarmer apply` tests
     apply_working_module: marks `seedfarmer apply` test that creates a functioning module
     destroy: marks `seedfarmer destroy` tests
-    init: marks `addltcl init` tests
+    init: marks `seedfarmer init` tests
     list: marks all `seedfarmer list` tests
     list_deployments: marks all `seedfarmer list deployments` tests
     list_moduledata: marks all `seedfarmer list moduledata` tests
diff --git a/test/unit-test/mock_data/modules/module-test-broken-deploy/modulestack.yaml b/test/unit-test/mock_data/modules/module-test-broken-deploy/modulestack.yaml
index 26447906..4f41f61c 100644
--- a/test/unit-test/mock_data/modules/module-test-broken-deploy/modulestack.yaml
+++ b/test/unit-test/mock_data/modules/module-test-broken-deploy/modulestack.yaml
@@ -22,5 +22,5 @@ Resources:
             Action: '*'
             Resource: '*'
         Version: 2012-10-17
-      PolicyName: "addl-modulespecific-policy"
+      PolicyName: "myapp-modulespecific-policy"
       Roles: [!Ref RoleName]
\ No newline at end of file
diff --git a/test/unit-test/mock_data/modules/module-test-broken-destroy/app.py b/test/unit-test/mock_data/modules/module-test-broken-destroy/app.py
index 56395ee8..d157acbb 100644
--- a/test/unit-test/mock_data/modules/module-test-broken-destroy/app.py
+++ b/test/unit-test/mock_data/modules/module-test-broken-destroy/app.py
@@ -3,15 +3,15 @@
 from aws_cdk import App, CfnOutput
 from stack import Cognito
 
-deployment_name = os.getenv("ADDL_DEPLOYMENT_NAME")
-module_name = os.getenv("ADDL_MODULE_NAME")
-domain_name_prefix = os.getenv("ADDL_PARAMETER_DOMAIN_NAME_PREFIX", "")
+deployment_name = os.getenv("MYAPP_DEPLOYMENT_NAME")
+module_name = os.getenv("MYAPP_MODULE_NAME")
+domain_name_prefix = os.getenv("MYAPP_PARAMETER_DOMAIN_NAME_PREFIX", "")
 
 app = App()
 
 stack = Cognito(
     scope=app,
-    id=f"addl-{deployment_name}-{module_name}",
+    id=f"myapp-{deployment_name}-{module_name}",
     domain_name_prefix=domain_name_prefix,
 )
 
diff --git a/test/unit-test/mock_data/modules/module-test-broken-destroy/modulestack.yaml b/test/unit-test/mock_data/modules/module-test-broken-destroy/modulestack.yaml
index 26447906..4f41f61c 100644
--- a/test/unit-test/mock_data/modules/module-test-broken-destroy/modulestack.yaml
+++ b/test/unit-test/mock_data/modules/module-test-broken-destroy/modulestack.yaml
@@ -22,5 +22,5 @@ Resources:
             Action: '*'
             Resource: '*'
         Version: 2012-10-17
-      PolicyName: "addl-modulespecific-policy"
+      PolicyName: "myapp-modulespecific-policy"
       Roles: [!Ref RoleName]
\ No newline at end of file
diff --git a/test/unit-test/mock_data/modules/module-test/app.py b/test/unit-test/mock_data/modules/module-test/app.py
index 56395ee8..d157acbb 100644
--- a/test/unit-test/mock_data/modules/module-test/app.py
+++ b/test/unit-test/mock_data/modules/module-test/app.py
@@ -3,15 +3,15 @@
 from aws_cdk import App, CfnOutput
 from stack import Cognito
 
-deployment_name = os.getenv("ADDL_DEPLOYMENT_NAME")
-module_name = os.getenv("ADDL_MODULE_NAME")
-domain_name_prefix = os.getenv("ADDL_PARAMETER_DOMAIN_NAME_PREFIX", "")
+deployment_name = os.getenv("MYAPP_DEPLOYMENT_NAME")
+module_name = os.getenv("MYAPP_MODULE_NAME")
+domain_name_prefix = os.getenv("MYAPP_PARAMETER_DOMAIN_NAME_PREFIX", "")
 
 app = App()
 
 stack = Cognito(
     scope=app,
-    id=f"addl-{deployment_name}-{module_name}",
+    id=f"myapp-{deployment_name}-{module_name}",
     domain_name_prefix=domain_name_prefix,
 )
 
diff --git a/test/unit-test/mock_data/modules/module-test/deployspec.yaml b/test/unit-test/mock_data/modules/module-test/deployspec.yaml
index fec919f2..9d24efff 100644
--- a/test/unit-test/mock_data/modules/module-test/deployspec.yaml
+++ b/test/unit-test/mock_data/modules/module-test/deployspec.yaml
@@ -7,7 +7,7 @@ deploy:
     build:
       commands:
       - cdk deploy --require-approval never --progress events --app "python app.py" --outputs-file ./cdk-exports.json
-      - export ADDL_MODULE_METADATA=$(python -c "import json; file=open('cdk-exports.json'); print(json.load(file)['addl-${ADDL_DEPLOYMENT_NAME}-${ADDL_MODULE_NAME}']['metadata'])")
+      - export MYAPP_MODULE_METADATA=$(python -c "import json; file=open('cdk-exports.json'); print(json.load(file)['myapp-${MYAPP_DEPLOYMENT_NAME}-${MYAPP_MODULE_NAME}']['metadata'])")
 destroy:
   phases:
     install:
diff --git a/test/unit-test/mock_data/modules/module-test/modulestack.yaml b/test/unit-test/mock_data/modules/module-test/modulestack.yaml
index 26447906..4f41f61c 100644
--- a/test/unit-test/mock_data/modules/module-test/modulestack.yaml
+++ b/test/unit-test/mock_data/modules/module-test/modulestack.yaml
@@ -22,5 +22,5 @@ Resources:
             Action: '*'
             Resource: '*'
         Version: 2012-10-17
-      PolicyName: "addl-modulespecific-policy"
+      PolicyName: "myapp-modulespecific-policy"
       Roles: [!Ref RoleName]
\ No newline at end of file
diff --git a/test/unit-test/mock_data/seedfarmer.yaml b/test/unit-test/mock_data/seedfarmer.yaml
new file mode 100644
index 00000000..d2404458
--- /dev/null
+++ b/test/unit-test/mock_data/seedfarmer.yaml
@@ -0,0 +1,2 @@
+project: myapp
+description: unit testing
\ No newline at end of file
diff --git a/test/unit-test/test_cli_arg.py b/test/unit-test/test_cli_arg.py
index 62a3ed1b..0a3c39c0 100644
--- a/test/unit-test/test_cli_arg.py
+++ b/test/unit-test/test_cli_arg.py
@@ -21,8 +21,7 @@
 
 import seedfarmer.commands._stack_commands as _sc
 import seedfarmer.mgmt.module_init as minit
-from seedfarmer.__init__ import PROJECT
-from seedfarmer.__main__ import apply, destroy, init
+from seedfarmer.__main__ import PROJECT, apply, destroy, init
 from seedfarmer.__main__ import list as _list
 from seedfarmer.__main__ import remove, store
 
@@ -37,60 +36,58 @@
 
 
 @pytest.mark.init
-def test_init_help():
-    _test_command(
-        sub_command=init,
-        options=["--help"],
-        exit_code=0,
-        expected_output="Initialize a new module in the proper structure",
-    )
-
-
-@pytest.mark.init
-def test_init_create_module(tmp_path):
-    minit.MOD_ROOT = tmp_path
+def test_init_create_module():
     module_name = "test-module"
-    expected_module_path = os.path.join(tmp_path, module_name)
+    expected_module_path = os.path.join(minit.OPS_ROOT, "modules")
 
     # Creates a new module
-    _test_command(sub_command=init, options=["-m", module_name], exit_code=0, return_result=False)
+    _test_command(sub_command=init, options=["module", "-m", module_name], exit_code=0, return_result=False)
 
     # Creates a module that already exist
-    result = _test_command(sub_command=init, options=["-m", module_name], exit_code=1, return_result=True)
-    assert result.exception.args[0] == f"The module {module_name} already exists under {tmp_path}."
+    result = _test_command(sub_command=init, options=["module", "-m", module_name], exit_code=1, return_result=True)
+    print(result.exception.args[0])
+    assert result.exception.args[0] == f"The module {module_name} already exists under {expected_module_path}."
 
     # Checks if file exists from the project template
-    assert os.path.exists(os.path.join(expected_module_path, "deployspec.yaml"))
+    assert os.path.exists(os.path.join(expected_module_path, module_name, "deployspec.yaml"))
 
 
 @pytest.mark.init
-def test_init_create_group_module(tmp_path):
-    minit.MOD_ROOT = tmp_path
+def test_init_create_group_module():
     module_name = "test-module"
     group_name = "group"
-    expected_module_path = os.path.join(tmp_path, group_name, module_name)
+    expected_module_path = os.path.join(minit.OPS_ROOT, "modules", group_name, module_name)
 
     # Creates a group and a module within the group
-    _test_command(sub_command=init, options=["-g", group_name, "-m", module_name], exit_code=0)
+    _test_command(sub_command=init, options=["module", "-g", group_name, "-m", module_name], exit_code=0)
     assert os.path.exists(expected_module_path)
 
     # Creates a group and a module that already exists within the group
     result = _test_command(
-        sub_command=init, options=["-g", group_name, "-m", module_name], exit_code=1, return_result=True
+        sub_command=init, options=["module", "-g", group_name, "-m", module_name], exit_code=1, return_result=True
+    )
+    assert (
+        result.exception.args[0]
+        == f"The module {module_name} already exists under {minit.OPS_ROOT}/modules/{group_name}."
     )
-    assert result.exception.args[0] == f"The module {module_name} already exists under {tmp_path}/{group_name}."
 
     # Checks if a file from the project template was created within the new module
     assert os.path.exists(os.path.join(expected_module_path, "deployspec.yaml"))
 
 
-# @pytest.mark.init
-# def test_init_create_custom_template(tmp_path):
-#     pass
+@pytest.mark.init
+def test_init_create_project(tmp_path):
+    expected_project_path = os.path.join(minit.OPS_ROOT, PROJECT)
+
+    # Creates a new project
+    _test_command(sub_command=init, options=["project"], exit_code=0, return_result=False)
+
+    # Creates a project that already exist
+    result = _test_command(sub_command=init, options=["project"], exit_code=1, return_result=True)
+    assert result.exception.args[0] == f'Error: "{os.path.join(minit.OPS_ROOT, PROJECT)}" directory already exists'
 
-# @pytest.mark.init
-# def test_init_custom_template(tmp_path):
-#     pass
+    # Checks if file exists from the project template
+    assert os.path.exists(os.path.join(expected_project_path, "seedfarmer.yaml"))
 
 
 # -------------------------------------------
@@ -104,7 +101,7 @@ def test_apply_help():
         sub_command=apply,
         options=["--help"],
         exit_code=0,
-        expected_output=f"Apply a deployment spec relative path for {PROJECT.upper()}",
+        expected_output=f"Apply a deployment manifest relative path for {PROJECT.upper()}",
     )