Repository: pwrdrvr/microapps-core (language: python)

Issue:
v0.3 - Cross Account Deployments
## Motivations
- Larger organizations will have single CloudFront distributions with different dev teams deploying apps into different AWS accounts
- MicroApps needs the ability to register child account Lambdas on a parent account CloudFront / MicroApps deploy

## To-Do
- [x] Split Lambda and DynamoDB access into two different DeployerSvc requests
- [x] Update `microapps-publish` to detect and use the new style when it is supported
- [x] Setup GHPublicChild account for cross account testing
- [x] Create Child account CDK construct
- [x] Create Child account CDK stack
- [x] Add parameters to Parent account construct / stack to allow child account DeployerSvc to invoke
- [x] CI - Add Child account CDK stack deployment
- [x] CI - Add Child account deploy of `child-demo` app
- [x] Test and confirm that `child-demo` app appears in Release app and that app invoke works
- [x] Address inevitable issues with edge-to-origin request signing - Most likely there will be a problem with the target account or region being passed to the signing functions
- [x] Fix colliding names for the `edge-role` child stack for the `basic` and `basic-prefix` stacks
- [x] Fix registration of child account permission to invoke parent account deployer

## Design

- DeployerSvc
  - Add a stub version of this that invokes the Deployer in the primary account
    - This Deployer stub should grant the API Gateway in the primary account permission to invoke the lambda in the secondary account
- `microapps-publish`
  - Add any parameters needed to publish to another account?
  - This can probably be handled with no parameters as the Deployer lambda can return the necessary config info
- Apps from multiple accounts can all be served from a single prefix

### Request Dispatch

![MicroApps - Architecture](https://user-images.githubusercontent.com/5617868/211132720-604510fa-de44-4ac6-a79b-c28c829d2490.png)

### Deployment

![Image](https://user-images.githubusercontent.com/5617868/210928443-9f090d3f-9145-4c88-9bcd-18fc178b4dac.png)



Discussion / hints:
The base work for this is done but proving that this sorts of deployments work remains to be completed

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 68e38944..1c54e5f3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -240,12 +240,14 @@ jobs:
       - name: Set Hotswap Flag on PRs
         run: |
           if [ -n "${PR_NUMBER}" ]; then
-            echo "HOTSWAP_FLAG=--hotswap" >> $GITHUB_ENV
+            echo "HOTSWAP_FLAG=" >> $GITHUB_ENV
           else
             echo "HOTSWAP_FLAG=" >> $GITHUB_ENV
           fi
 
       - name: Deploy CDK Stack
+        env:
+          AWS_ACCOUNT_ID_CHILD: ${{ secrets.AWS_ACCOUNT_ID_CHILD }}
         run: |
           npx cdk deploy \
             ${HOTSWAP_FLAG} \
@@ -260,8 +262,11 @@ jobs:
           echo "::set-output name=prefix::"${PREFIX}
           echo "::set-output name=edgeDomain::"$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-edge-domain-name\`].Value" --no-paginate --output text)
           echo "EDGE_DOMAIN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-edge-domain-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "EDGE_TO_ORIGIN_ROLE_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-role-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-role-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "DEPLOYER_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-deployer-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "DEPLOYER_LAMBDA_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-deployer-func-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "DEMO_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "NEXTJS_DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-nextjs-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "NEXTJS_DEMO_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-nextjs-demo-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
           echo "RELEASE_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghpublic-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-release-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
@@ -269,7 +274,7 @@ jobs:
 
       - name: Echo Exports
         run: |
-          env | grep EDGE_DOMAIN
+          env | grep EDGE_
           env | grep DEPLOYER_
           env | grep DEMO_APP_
           env | grep NEXTJS_DEMO_APP_
@@ -431,6 +436,65 @@ jobs:
           echo Testing App Method Invocation
           curl -H"accept: application/json" --fail https://${EDGE_DOMAIN}${PREFIX}/${RELEASE_APP_NAME}/${{ needs.build.outputs.releaseAppPackageVersion }}/api/refresh/${DEMO_APP_NAME}
 
+      #
+      # Child Account Deploy
+      #
+
+      # Change to Child Account Creds
+      - name: Configure AWS Credentials
+        uses: aws-actions/configure-aws-credentials@v1
+        with:
+          role-session-name: microapps-ghpublic-build
+          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID_CHILD }}:role/builder-writeRole
+          aws-region: ${{ env.AWS_REGION }}
+
+      - name: Deploy Child CDK Stack
+        if: ${{ matrix.deployName == 'microapps-core' }}
+        env:
+          PARENT_DEPLOYER_LAMBDA_ARN: ${{ env.DEPLOYER_LAMBDA_ARN }}
+        run: |
+          npx cdk deploy \
+            ${HOTSWAP_FLAG} \
+            --require-approval never ${{ matrix.deployName }}-child
+
+      - name: Get Child CDK Exports
+        if: ${{ matrix.deployName == 'microapps-core' }}
+        id: getChildCDKExports
+        run: |
+          echo "CHILD_DEPLOYER_LAMBDA_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghchild-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-deployer-func-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "CHILD_DEMO_APP_LAMBDA_NAME="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghchild-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-func-name\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+          echo "CHILD_DEMO_APP_LAMBDA_VERSION_ARN="$(aws cloudformation list-exports --query "Exports[?Name==\`${{ matrix.deployName }}-ghchild-${NODE_ENV}${{ needs.build.outputs.prSuffix }}-demo-app-vers-arn\`].Value" --no-paginate --output text) >> $GITHUB_ENV
+      
+      - name: Echo Exports
+        if: ${{ matrix.deployName == 'microapps-core' }}
+        run: |
+          env | grep CHILD_
+
+      - name: Publish Demo App to Child MicroApps
+        if: ${{ matrix.deployName == 'microapps-core' }}
+        run: |
+          npx microapps-publish publish \
+            -a ${DEMO_APP_NAME} \
+            -t lambda-url \
+            --startup-type direct \
+            -n 0.0.0-child.1 \
+            -d ${CHILD_DEPLOYER_LAMBDA_ARN} \
+            -l ${CHILD_DEMO_APP_LAMBDA_VERSION_ARN} \
+            -s packages/demo-app/static_files \
+            -i index.html --overwrite
+
+      - name: Test Demo App - Child
+        if: ${{ matrix.deployName == 'microapps-core' }}
+        run: |
+          echo Testing App Frame Loading
+          curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/
+          echo Testing App HTML Loading
+          curl -H"accept: text/html" --fail https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/0.0.0-child.1/index.html
+          echo Testing App Method Invocation
+          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/0.0.0-child.1/serverIncrement?currValue=1"
+          echo Testing App Method Invocation on _next/data Route
+          curl -H"accept: application/json" --fail "https://${EDGE_DOMAIN}${PREFIX}/${DEMO_APP_NAME}/_next/data/0.0.0-child.1/serverIncrement?currValue=1"
+
   build-jsii:
     if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && contains( github.event.pull_request.labels.*.name, 'BUILD-JSII'))
     runs-on: ubuntu-latest
diff --git a/packages/cdk/bin/cdk.ts b/packages/cdk/bin/cdk.ts
index b16cef10..06112641 100644
--- a/packages/cdk/bin/cdk.ts
+++ b/packages/cdk/bin/cdk.ts
@@ -2,6 +2,7 @@
 import 'source-map-support/register';
 import { App, Environment } from 'aws-cdk-lib';
 import { MicroAppsStack } from '../lib/MicroApps';
+import { MicroAppsChildStack } from '../lib/MicroAppsChild';
 import { MicroAppsBuilder } from '../lib/MicroAppsBuilder';
 import { SharedProps } from '../lib/SharedProps';
 
@@ -36,6 +37,24 @@ new MicroAppsStack(app, 'microapps-core', {
   // We need to know the origin region for signing requests
   // Accessing Aws.REGION will end up writing a Token into the config file
   originRegion: shared.region,
+  ...(process.env.AWS_ACCOUNT_ID_CHILD
+    ? {
+        childDeployenRoleArns: [
+          `arn:aws:iam::${process.env.AWS_ACCOUNT_ID_CHILD}:role/microapps-core-ghchild-deployer${shared.envSuffix}${shared.prSuffix}`,
+        ],
+        allowedFunctionUrlAccounts: [process.env.AWS_ACCOUNT_ID_CHILD],
+      }
+    : {}),
+});
+
+new MicroAppsChildStack(app, 'microapps-core-child', {
+  env,
+  stackName: `microapps-core-ghchild${shared.envSuffix}${shared.prSuffix}`,
+  autoDeleteEverything: true,
+  assetNameRoot: 'microapps-core-ghchild',
+  assetNameSuffix: `${shared.envSuffix}${shared.prSuffix}`,
+  parentDeployerLambdaARN: process.env.PARENT_DEPLOYER_LAMBDA_ARN || '',
+  edgeToOriginRoleARN: process.env.EDGE_TO_ORIGIN_ROLE_ARN || '',
 });
 
 new MicroAppsStack(app, 'microapps-basic', {
diff --git a/packages/cdk/lib/DemoApp.ts b/packages/cdk/lib/DemoApp.ts
index 0b2c3eb5..b4596731 100644
--- a/packages/cdk/lib/DemoApp.ts
+++ b/packages/cdk/lib/DemoApp.ts
@@ -1,5 +1,5 @@
 import { Construct } from 'constructs';
-import { Duration, RemovalPolicy } from 'aws-cdk-lib';
+import { Duration, RemovalPolicy, Tags } from 'aws-cdk-lib';
 import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';
 import * as logs from 'aws-cdk-lib/aws-logs';
@@ -45,7 +45,7 @@ export interface DemoAppProps {
  * Represents a `DemoApp`
  */
 export interface IDemoApp {
-  lambdaFunction: lambda.IFunction;
+  lambdaFunction: lambda.Function;
 }
 
 /**
@@ -57,7 +57,7 @@ export interface IDemoApp {
  */
 export class DemoApp extends Construct implements IDemoApp {
   private _lambdaFunction: lambda.Function;
-  public get lambdaFunction(): lambda.IFunction {
+  public get lambdaFunction(): lambda.Function {
     return this._lambdaFunction;
   }
 
@@ -100,5 +100,6 @@ export class DemoApp extends Construct implements IDemoApp {
     if (removalPolicy !== undefined) {
       this._lambdaFunction.applyRemovalPolicy(removalPolicy);
     }
+    Tags.of(this._lambdaFunction).add('microapp-managed', 'true');
   }
 }
diff --git a/packages/cdk/lib/MicroApps.ts b/packages/cdk/lib/MicroApps.ts
index 057abed3..ec7056a6 100644
--- a/packages/cdk/lib/MicroApps.ts
+++ b/packages/cdk/lib/MicroApps.ts
@@ -1,6 +1,7 @@
 import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
 import { Construct } from 'constructs';
 import * as acm from 'aws-cdk-lib/aws-certificatemanager';
+import * as iam from 'aws-cdk-lib/aws-iam';
 import * as lambda from 'aws-cdk-lib/aws-lambda';
 import * as r53 from 'aws-cdk-lib/aws-route53';
 import { MicroApps, MicroAppsProps, MicroAppsTable } from '@pwrdrvr/microapps-cdk';
@@ -134,6 +135,31 @@ export interface MicroAppsStackProps extends StackProps {
    * DynamoDB Table name - Needed for Edge routing
    */
   readonly tableName?: string;
+
+  /**
+   * Optional child account deployer role ARNs that
+   * can invoke this parent deployer Lambda
+   *
+   * @default []
+   */
+  readonly childDeployenRoleArns?: string[];
+
+  /**
+   * Account IDs allowed for cross-account Function URL invocations
+   *
+   * @example ['123456789012']
+   * @default []
+   */
+  readonly allowedFunctionUrlAccounts?: string[];
+
+  /**
+   * Optional parent account origin request lambdas
+   * granted permission to invoke the function URL
+   * via a resource policy on each alias
+   *
+   * @default []
+   */
+  readonly parentEdgeToOriginRoleArns?: string[];
 }
 
 export class MicroAppsStack extends Stack {
@@ -166,6 +192,9 @@ export class MicroAppsStack extends Stack {
       rootPathPrefix,
       originRegion,
       tableName,
+      childDeployenRoleArns = [],
+      allowedFunctionUrlAccounts = [],
+      parentEdgeToOriginRoleArns = [],
     } = props;
 
     let removalPolicy: RemovalPolicy | undefined = undefined;
@@ -238,11 +267,32 @@ export class MicroAppsStack extends Stack {
       originRegion,
       table: table.table,
       tableNameForEdgeToOrigin: tableName ? tableName : `${assetNameRoot}${assetNameSuffix}`,
+      allowedFunctionUrlAccounts,
       ...optionalAssetNameOpts,
       ...optionals3PolicyOpts,
       ...optionalCustomDomainOpts,
     });
 
+    // Give the current version an alias
+    const deployerAlias = new lambda.Alias(this, 'deployer-alias', {
+      aliasName: 'currentVersion',
+      version: microapps.svcs.deployerFunc.currentVersion,
+    });
+    // Allow cross-account invokes if specified
+    // TODO: Actually handle the list of account ID
+    if (childDeployenRoleArns.length > 0) {
+      const childRole = iam.Role.fromRoleArn(this, 'deployer-child-role', childDeployenRoleArns[0]);
+
+      microapps.svcs.deployerFunc.addPermission('deployer-child-permission', {
+        principal: childRole,
+        scope: this,
+      });
+      deployerAlias.addPermission('deployer-child-permission-alias', {
+        principal: new iam.ArnPrincipal(childDeployenRoleArns[0]),
+        scope: this,
+      });
+    }
+
     if (deployDemoApp) {
       const demoApp = new DemoApp(this, 'demo-app', {
         appName: 'demo-app',
@@ -254,10 +304,28 @@ export class MicroAppsStack extends Stack {
       const appVersion = (demoApp.lambdaFunction as lambda.Function).currentVersion;
       appVersion.applyRemovalPolicy(RemovalPolicy.RETAIN);
 
+      // TODO: Grant any parent Edge to Origin Lambdas permission to invoke this function
+      // via URL
+
+      // if (parentEdgeToOriginRoleArns && parentEdgeToOriginRoleArns.length > 0) {
+      //   parentEdgeToOriginRoleArns.forEach((parentRoleArn) => {
+      //     appVersion.addPermission('demo-app-permission', {
+      //       principal: new iam.ArnPrincipal(parentRoleArn),
+      //       scope: this,
+      //       action: 'lambda:InvokeFunctionUrl',
+      //     });
+      //   });
+      // }
+
       new CfnOutput(this, 'demo-app-func-name', {
         value: `${demoApp.lambdaFunction.functionName}`,
         exportName: `${this.stackName}-demo-app-func-name`,
       });
+
+      new CfnOutput(this, 'demo-app-vers-arn', {
+        value: `${appVersion.functionArn}`,
+        exportName: `${this.stackName}-demo-app-vers-arn`,
+      });
     }
 
     if (deployReleaseApp) {
@@ -305,11 +373,27 @@ export class MicroAppsStack extends Stack {
       });
     }
 
+    // Import the Edge to Origin Lambda function so we can get the role ARN
+    const edgeToOriginFunc = lambda.Function.fromFunctionArn(
+      this,
+      'edge-to-origin-func-import',
+      // The functionArn will have the version number at the end, so we need to
+      // remove that to get the ARN of the function itself
+      (microapps.edgeToOrigin?.edgeToOriginFunction?.functionArn || '')
+        .split(':')
+        .slice(0, 7)
+        .join(':'),
+    );
+
     // Exports
     new CfnOutput(this, 'edge-domain-name', {
       value: domainNameEdge ? domainNameEdge : microapps.cf.cloudFrontDistro.domainName,
       exportName: `${this.stackName}-edge-domain-name`,
     });
+    new CfnOutput(this, 'edge-to-origin-role-arn', {
+      value: `${microapps.edgeToOrigin?.edgeToOriginFunction?.role?.roleArn}`,
+      exportName: `${this.stackName}-edge-to-origin-arn`,
+    });
     new CfnOutput(this, 'dynamodb-table-name', {
       value: `${tableName ? tableName : microapps.svcs.table.tableName}`,
       exportName: `${this.stackName}-dynamodb-table-name`,
@@ -318,5 +402,9 @@ export class MicroAppsStack extends Stack {
       value: `${microapps.svcs.deployerFunc.functionName}`,
       exportName: `${this.stackName}-deployer-func-name`,
     });
+    new CfnOutput(this, 'deployer-func-arn', {
+      value: `${microapps.svcs.deployerFunc.functionArn}`,
+      exportName: `${this.stackName}-deployer-func-arn`,
+    });
   }
 }
diff --git a/packages/cdk/lib/MicroAppsChild.ts b/packages/cdk/lib/MicroAppsChild.ts
new file mode 100644
index 00000000..53872c41
--- /dev/null
+++ b/packages/cdk/lib/MicroAppsChild.ts
@@ -0,0 +1,128 @@
+import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
+import { Construct } from 'constructs';
+import * as lambda from 'aws-cdk-lib/aws-lambda';
+import { MicroAppsChildDeployer, MicroAppsChildDeployerProps } from '@pwrdrvr/microapps-cdk';
+import { DemoApp } from './DemoApp';
+import { Env } from './Types';
+import { SharedTags } from './SharedTags';
+
+/**
+ * Properties to initialize an instance of `MicroAppsChildStack`.
+ */
+export interface MicroAppsChildStackProps extends StackProps {
+  /**
+   * Parent deployer Lambda ARN
+   */
+  readonly parentDeployerLambdaARN: string;
+
+  /**
+   * ARN of the IAM Role for the Edge to Origin Lambda Function
+   */
+  readonly edgeToOriginRoleARN: string;
+
+  /**
+   * Automatically destroy all assets when stack is deleted
+   *
+   * @default false
+   */
+  readonly autoDeleteEverything?: boolean;
+
+  /**
+   * Optional asset name root
+   *
+   * @example microapps
+   * @default - resource names auto assigned
+   */
+  readonly assetNameRoot?: string;
+
+  /**
+   * Optional asset name suffix
+   *
+   * @example -dev-pr-12
+   * @default none
+   */
+  readonly assetNameSuffix?: string;
+
+  /**
+   * NODE_ENV to pass to apps
+   *
+   * @default dev
+   */
+  readonly nodeEnv?: Env;
+
+  /**
+   * Path prefix on the root of the CloudFront distribution
+   *
+   * @example dev/
+   */
+  readonly rootPathPrefix?: string;
+}
+
+export class MicroAppsChildStack extends Stack {
+  constructor(scope: Construct, id: string, props?: MicroAppsChildStackProps) {
+    super(scope, id, props);
+
+    if (props === undefined) {
+      throw new Error('props must be set');
+    }
+
+    SharedTags.addSharedTags(this);
+
+    const {
+      autoDeleteEverything = false,
+      assetNameRoot,
+      assetNameSuffix,
+      nodeEnv = 'dev',
+      parentDeployerLambdaARN,
+      edgeToOriginRoleARN,
+    } = props;
+
+    let removalPolicy: RemovalPolicy | undefined = undefined;
+    if (autoDeleteEverything) {
+      removalPolicy = RemovalPolicy.DESTROY;
+    }
+
+    const optionalAssetNameOpts: Partial<MicroAppsChildDeployerProps> = {
+      assetNameRoot,
+      assetNameSuffix,
+    };
+
+    const microapps = new MicroAppsChildDeployer(this, 'microapps-deployer', {
+      parentDeployerLambdaARN,
+      edgeToOriginRoleARN,
+      removalPolicy,
+      appEnv: nodeEnv,
+      ...optionalAssetNameOpts,
+    });
+
+    const demoApp = new DemoApp(this, 'demo-app', {
+      appName: 'demo-app',
+      assetNameRoot,
+      assetNameSuffix,
+      removalPolicy,
+    });
+
+    const appVersion = (demoApp.lambdaFunction as lambda.Function).currentVersion;
+    appVersion.applyRemovalPolicy(RemovalPolicy.RETAIN);
+
+    new CfnOutput(this, 'demo-app-func-name', {
+      value: `${demoApp.lambdaFunction.functionName}`,
+      exportName: `${this.stackName}-demo-app-func-name`,
+    });
+
+    new CfnOutput(this, 'demo-app-vers-arn', {
+      value: `${appVersion.functionArn}`,
+      exportName: `${this.stackName}-demo-app-vers-arn`,
+    });
+
+    new CfnOutput(this, 'deployer-func-name', {
+      value: `${microapps.deployerFunc.functionName}`,
+      exportName: `${this.stackName}-deployer-func-name`,
+    });
+
+    new CfnOutput(this, 'deployer-func-arn', {
+      value: `${microapps.deployerFunc.functionArn}`,
+      exportName: `${this.stackName}-deployer-func-arn`,
+    });
+  }
+}
diff --git a/packages/demo-app/src/index.ts b/packages/demo-app/src/index.ts
index 4c6a2900..58959598 100644
--- a/packages/demo-app/src/index.ts
+++ b/packages/demo-app/src/index.ts
@@ -4,6 +4,9 @@ import { readFileSync } from 'fs';
 
 const html = readFileSync(`./index.html`, 'utf8');
 
+const buildTrigger = '2023-01-24-01';
+console.info('Demo-app build trigger', { buildTrigger });
+
 // eslint-disable-next-line @typescript-eslint/require-await
 export async function handler(
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/packages/microapps-cdk/API.md b/packages/microapps-cdk/API.md
index 935f17dc..d3272504 100644
--- a/packages/microapps-cdk/API.md
+++ b/packages/microapps-cdk/API.md
@@ -228,6 +228,51 @@ The CloudFront distribution.
 ---
 
 
+### MicroAppsChildDeployer <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployer"></a>
+
+- *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsChildDeployer`](#@pwrdrvr/microapps-cdk.IMicroAppsChildDeployer)
+
+Create a new MicroApps Child Deployer construct.
+
+#### Initializer <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployer.Initializer"></a>
+
+```typescript
+import { MicroAppsChildDeployer } from '@pwrdrvr/microapps-cdk'
+
+new MicroAppsChildDeployer(scope: Construct, id: string, props?: MicroAppsChildDeployerProps)
+```
+
+##### `scope`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployer.scope"></a>
+
+- *Type:* [`constructs.Construct`](#constructs.Construct)
+
+---
+
+##### `id`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployer.id"></a>
+
+- *Type:* `string`
+
+---
+
+##### `props`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployer.props"></a>
+
+- *Type:* [`@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps`](#@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps)
+
+---
+
+
+
+#### Properties <a name="Properties"></a>
+
+##### `deployerFunc`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployer.deployerFunc"></a>
+
+- *Type:* [`aws-cdk-lib.aws_lambda.IFunction`](#aws-cdk-lib.aws_lambda.IFunction)
+
+Lambda function for the Deployer.
+
+---
+
+
 ### MicroAppsEdgeToOrigin <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin"></a>
 
 - *Implements:* [`@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin)
@@ -416,7 +461,7 @@ new MicroAppsSvcs(scope: Construct, id: string, props?: MicroAppsSvcsProps)
 
 ##### `deployerFunc`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcs.deployerFunc"></a>
 
-- *Type:* [`aws-cdk-lib.aws_lambda.IFunction`](#aws-cdk-lib.aws_lambda.IFunction)
+- *Type:* [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
 
 Lambda function for the Deployer.
 
@@ -432,7 +477,7 @@ DynamoDB table used by Router, Deployer, and Release console app.
 
 ##### `routerFunc`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsSvcs.routerFunc"></a>
 
-- *Type:* [`aws-cdk-lib.aws_lambda.IFunction`](#aws-cdk-lib.aws_lambda.IFunction)
+- *Type:* [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
 
 Lambda function for the Router.
 
@@ -909,6 +954,82 @@ Path prefix on the root of the CloudFront distribution.
 
 ---
 
+### MicroAppsChildDeployerProps <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps"></a>
+
+Properties to initialize an instance of `MicroAppsChildDeployer`.
+
+#### Initializer <a name="[object Object].Initializer"></a>
+
+```typescript
+import { MicroAppsChildDeployerProps } from '@pwrdrvr/microapps-cdk'
+
+const microAppsChildDeployerProps: MicroAppsChildDeployerProps = { ... }
+```
+
+##### `appEnv`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.appEnv"></a>
+
+- *Type:* `string`
+
+Application environment, passed as `NODE_ENV` to the Router and Deployer Lambda functions.
+
+---
+
+##### `edgeToOriginRoleARN`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.edgeToOriginRoleARN"></a>
+
+- *Type:* `string`
+
+ARN of the IAM Role for the Edge to Origin Lambda Function.
+
+---
+
+##### `parentDeployerLambdaARN`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.parentDeployerLambdaARN"></a>
+
+- *Type:* `string`
+
+ARN of the parent Deployer Lambda Function.
+
+---
+
+##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.assetNameRoot"></a>
+
+- *Type:* `string`
+- *Default:* resource names auto assigned
+
+Optional asset name root.
+
+---
+
+##### `assetNameSuffix`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.assetNameSuffix"></a>
+
+- *Type:* `string`
+- *Default:* none
+
+Optional asset name suffix.
+
+---
+
+##### `deployerTimeout`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.deployerTimeout"></a>
+
+- *Type:* [`aws-cdk-lib.Duration`](#aws-cdk-lib.Duration)
+- *Default:* 2 minutes
+
+Deployer timeout.
+
+For larger applications this needs to be set up to 2-5 minutes for the S3 copy
+
+---
+
+##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsChildDeployerProps.removalPolicy"></a>
+
+- *Type:* [`aws-cdk-lib.RemovalPolicy`](#aws-cdk-lib.RemovalPolicy)
+- *Default:* per resource default
+
+RemovalPolicy override for child resources.
+
+Note: if set to DESTROY the S3 buckes will have `autoDeleteObjects` set to `true`
+
+---
+
 ### MicroAppsEdgeToOriginProps <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps"></a>
 
 Properties to initialize an instance of `MicroAppsEdgeToOrigin`.
@@ -935,6 +1056,15 @@ Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Orig
 
 ---
 
+##### `allowedFunctionUrlAccounts`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.allowedFunctionUrlAccounts"></a>
+
+- *Type:* `string`[]
+- *Default:* []
+
+Account IDs allowed for cross-account Function URL invocations.
+
+---
+
 ##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.assetNameRoot"></a>
 
 - *Type:* `string`
@@ -960,6 +1090,9 @@ Optional asset name suffix.
 
 Origin region that API Gateway will be deployed to, used for the config.yml on the Edge function to sign requests for the correct region.
 
+Note that Lambda FunctionURLs get the region from the Lambda ARN
+and do not need this to be configured.
+
 ---
 
 ##### `removalPolicy`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsEdgeToOriginProps.removalPolicy"></a>
@@ -1068,6 +1201,15 @@ Note: if true, creates OriginRequest Lambda @ Edge function for API Gateway Orig
 
 ---
 
+##### `allowedFunctionUrlAccounts`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.allowedFunctionUrlAccounts"></a>
+
+- *Type:* `string`[]
+- *Default:* []
+
+Account IDs allowed for cross-account Function URL invocations.
+
+---
+
 ##### `assetNameRoot`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.MicroAppsProps.assetNameRoot"></a>
 
 - *Type:* `string`
@@ -1744,6 +1886,23 @@ The CloudFront distribution.
 
 ---
 
+### IMicroAppsChildDeployer <a name="@pwrdrvr/microapps-cdk.IMicroAppsChildDeployer"></a>
+
+- *Implemented By:* [`@pwrdrvr/microapps-cdk.MicroAppsChildDeployer`](#@pwrdrvr/microapps-cdk.MicroAppsChildDeployer), [`@pwrdrvr/microapps-cdk.IMicroAppsChildDeployer`](#@pwrdrvr/microapps-cdk.IMicroAppsChildDeployer)
+
+Represents a MicroApps Child Deployer.
+
+
+#### Properties <a name="Properties"></a>
+
+##### `deployerFunc`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsChildDeployer.deployerFunc"></a>
+
+- *Type:* [`aws-cdk-lib.aws_lambda.IFunction`](#aws-cdk-lib.aws_lambda.IFunction)
+
+Lambda function for the Deployer.
+
+---
+
 ### IMicroAppsEdgeToOrigin <a name="@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin"></a>
 
 - *Implemented By:* [`@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.MicroAppsEdgeToOrigin), [`@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin`](#@pwrdrvr/microapps-cdk.IMicroAppsEdgeToOrigin)
@@ -1831,7 +1990,7 @@ Represents a MicroApps Services.
 
 ##### `deployerFunc`<sup>Required</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsSvcs.deployerFunc"></a>
 
-- *Type:* [`aws-cdk-lib.aws_lambda.IFunction`](#aws-cdk-lib.aws_lambda.IFunction)
+- *Type:* [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
 
 Lambda function for the Deployer.
 
@@ -1847,7 +2006,7 @@ DynamoDB table used by Router, Deployer, and Release console app.
 
 ##### `routerFunc`<sup>Optional</sup> <a name="@pwrdrvr/microapps-cdk.IMicroAppsSvcs.routerFunc"></a>
 
-- *Type:* [`aws-cdk-lib.aws_lambda.IFunction`](#aws-cdk-lib.aws_lambda.IFunction)
+- *Type:* [`aws-cdk-lib.aws_lambda.Function`](#aws-cdk-lib.aws_lambda.Function)
 
 Lambda function for the Router.
 
diff --git a/packages/microapps-cdk/src/MicroApps.ts b/packages/microapps-cdk/src/MicroApps.ts
index 4f3d904d..584859a4 100644
--- a/packages/microapps-cdk/src/MicroApps.ts
+++ b/packages/microapps-cdk/src/MicroApps.ts
@@ -295,6 +295,13 @@ export interface MicroAppsProps {
    * Additional edge lambda functions
    */
   readonly edgeLambdas?: cf.EdgeLambda[];
+
+  /**
+   * Account IDs allowed for cross-account Function URL invocations
+   *
+   * @default []
+   */
+  readonly allowedFunctionUrlAccounts?: string[];
 }
 
 /**
@@ -394,6 +401,7 @@ export class MicroApps extends Construct implements IMicroApps {
       table,
       tableNameForEdgeToOrigin,
       originShieldRegion = originRegion,
+      allowedFunctionUrlAccounts = [],
     } = props;
 
     this._s3 = new MicroAppsS3(this, 's3', {
@@ -450,6 +458,7 @@ export class MicroApps extends Construct implements IMicroApps {
         signingMode,
         rootPathPrefix,
         tableRulesArn: tableNameForEdgeToOrigin || this._svcs.table.tableName,
+        allowedFunctionUrlAccounts,
       });
 
       edgeLambdas.push(...this._edgeToOrigin.edgeToOriginLambdas);
diff --git a/packages/microapps-cdk/src/MicroAppsChildDeployer.ts b/packages/microapps-cdk/src/MicroAppsChildDeployer.ts
index 1aa39199..cdea78b7 100644
--- a/packages/microapps-cdk/src/MicroAppsChildDeployer.ts
+++ b/packages/microapps-cdk/src/MicroAppsChildDeployer.ts
@@ -16,6 +16,11 @@ export interface MicroAppsChildDeployerProps {
    */
   readonly parentDeployerLambdaARN: string;
 
+  /**
+   * ARN of the IAM Role for the Edge to Origin Lambda Function
+   */
+  readonly edgeToOriginRoleARN: string;
+
   /**
    * RemovalPolicy override for child resources
    *
@@ -90,18 +95,42 @@ export class MicroAppsChildDeployer extends Construct implements IMicroAppsChild
       assetNameSuffix,
       removalPolicy,
       parentDeployerLambdaARN,
+      edgeToOriginRoleARN,
     } = props;
 
     //
     // Deployer Lambda Function
     //
 
+    const iamRoleDeployerName = assetNameRoot
+      ? `${assetNameRoot}-deployer${assetNameSuffix}`
+      : undefined;
+    const iamRoleDeployer = new iam.Role(this, 'deployer-role', {
+      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
+      roleName: iamRoleDeployerName,
+      managedPolicies: [
+        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
+      ],
+      inlinePolicies: {
+        deployPolicy: new iam.PolicyDocument({
+          statements: [
+            new iam.PolicyStatement({
+              effect: iam.Effect.ALLOW,
+              actions: ['lambda:InvokeFunction'],
+              resources: [`${parentDeployerLambdaARN}:currentVersion`],
+            }),
+          ],
+        }),
+      },
+    });
+
     // Create Deployer Lambda Function
     const deployerFuncName = assetNameRoot
       ? `${assetNameRoot}-deployer${assetNameSuffix}`
       : undefined;
     const deployerFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
       functionName: deployerFuncName,
+      role: iamRoleDeployer,
       memorySize: 1769,
       logRetention: logs.RetentionDays.ONE_MONTH,
       runtime: lambda.Runtime.NODEJS_16_X,
@@ -110,6 +139,7 @@ export class MicroAppsChildDeployer extends Construct implements IMicroAppsChild
         NODE_ENV: appEnv,
         AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
         PARENT_DEPLOYER_LAMBDA_ARN: parentDeployerLambdaARN,
+        EDGE_TO_ORIGIN_ROLE_ARN: edgeToOriginRoleARN,
       },
     };
     if (
diff --git a/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts b/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
index 8336899a..8d52abbc 100644
--- a/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
+++ b/packages/microapps-cdk/src/MicroAppsEdgeToOrigin.ts
@@ -3,7 +3,16 @@ import * as crypto from 'crypto';
 import { copyFileSync, existsSync, writeFileSync } from 'fs';
 import * as os from 'os';
 import * as path from 'path';
-import { Aws, Duration, RemovalPolicy, Stack, Tags } from 'aws-cdk-lib';
+import {
+  Aws,
+  CfnOutput,
+  Duration,
+  PhysicalName,
+  RemovalPolicy,
+  Stack,
+  StackProps,
+  Tags,
+} from 'aws-cdk-lib';
 import * as cf from 'aws-cdk-lib/aws-cloudfront';
 import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
 import * as iam from 'aws-cdk-lib/aws-iam';
@@ -27,6 +36,11 @@ export interface IMicroAppsEdgeToOrigin {
    * Configuration of the edge to origin lambda functions
    */
   readonly edgeToOriginLambdas: cf.EdgeLambda[];
+
+  /**
+   * The IAM Role for the edge to origin function
+   */
+  readonly edgeToOriginRole: iam.Role;
 }
 
 /**
@@ -105,6 +119,9 @@ export interface MicroAppsEdgeToOriginProps {
    * for the config.yml on the Edge function to sign requests for
    * the correct region
    *
+   * Note that Lambda FunctionURLs get the region from the Lambda ARN
+   * and do not need this to be configured.
+   *
    * @default undefined
    */
   readonly originRegion?: string;
@@ -124,6 +141,13 @@ export interface MicroAppsEdgeToOriginProps {
    * @default false
    */
   readonly setupApiGatewayPermissions?: boolean;
+
+  /**
+   * Account IDs allowed for cross-account Function URL invocations
+   *
+   * @default []
+   */
+  readonly allowedFunctionUrlAccounts?: string[];
 }
 
 export interface GenerateEdgeToOriginConfigOptions {
@@ -135,6 +159,119 @@ export interface GenerateEdgeToOriginConfigOptions {
   readonly rootPathPrefix?: string;
 }
 
+interface IMicroAppsEdgeToOriginRoleStackProps extends StackProps {
+  assetNameRoot?: string;
+  assetNameSuffix?: string;
+  setupApiGatewayPermissions?: boolean;
+
+  /**
+   * Account IDs allowed for cross-account Function URL invocations
+   *
+   * @default []
+   */
+  readonly allowedFunctionUrlAccounts?: string[];
+}
+
+class MicroAppsEdgeToOriginRoleStack extends Stack {
+  private _role: iam.Role;
+  public get role(): iam.Role {
+    return this._role;
+  }
+
+  constructor(scope: Construct, id: string, props: IMicroAppsEdgeToOriginRoleStackProps) {
+    super(scope, id, {
+      stackName: `${props.assetNameRoot || 'MicroAppsEdgeToOrigin'}-role${
+        props.assetNameSuffix || ''
+      }`,
+      env: props.env || {
+        region: Aws.REGION,
+      },
+    });
+
+    const {
+      assetNameRoot,
+      assetNameSuffix,
+      setupApiGatewayPermissions,
+      allowedFunctionUrlAccounts,
+    } = props;
+
+    // Create IAM Role for the Edge Function
+    this._role = new iam.Role(this, 'edge-to-origin-role', {
+      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
+      roleName: assetNameRoot
+        ? `${assetNameRoot}-edge-to-origin-role${assetNameSuffix}`
+        : PhysicalName.GENERATE_IF_NEEDED,
+      managedPolicies: [
+        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
+      ],
+      inlinePolicies: {
+        edgeToOriginPolicy: new iam.PolicyDocument({
+          statements: [
+            // This can't have a reference to the httpApi because it would mean
+            // the parent stack (this stack) has to be created before the us-east-1
+            // child stack for the Edge Lambda Function.
+            // That's why we use a tag-based policy to allow the Edge Function
+            // to invoke any API Gateway API that we apply a tag to
+            // We allow the edge function to sign for all regions since
+            // we may use custom closest region in the future.
+            ...(setupApiGatewayPermissions
+              ? [
+                  new iam.PolicyStatement({
+                    actions: ['execute-api:Invoke'],
+                    resources: [`arn:aws:execute-api:*:${Aws.ACCOUNT_ID}:*/*/*/*`],
+                    // Unfortunately, API Gateway access cannot be restricted using
+                    // tags on the target resource
+                    // https://docs.aws.amazon.com/IAM/latest/UserGuide/access_tags.html
+                    // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html#networking_svcs
+                    // conditions: {
+                    //   // TODO: Set this to a string unique to each stack
+                    //   StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
+                    // },
+                  }),
+                ]
+              : []),
+            //
+            // Grant permission to invoke tagged Function URLs
+            //
+            new iam.PolicyStatement({
+              actions: ['lambda:InvokeFunctionUrl'],
+              resources: [`arn:aws:lambda:*:${Aws.ACCOUNT_ID}:*`],
+              conditions: {
+                StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
+              },
+            }),
+            //
+            // Grant permission to invoke Function URLs in listed accounts
+            //
+            ...(allowedFunctionUrlAccounts && allowedFunctionUrlAccounts.length > 0
+              ? [
+                  new iam.PolicyStatement({
+                    actions: ['lambda:InvokeFunctionUrl'],
+                    resources: allowedFunctionUrlAccounts.map(
+                      (accountId) => `arn:aws:lambda:*:${accountId}:*`,
+                    ),
+                  }),
+                ]
+              : []),
+          ],
+        }),
+      },
+    });
+    this._role.assumeRolePolicy?.addStatements(
+      new iam.PolicyStatement({
+        principals: [new iam.ServicePrincipal('edgelambda.amazonaws.com')],
+        actions: ['sts:AssumeRole'],
+        effect: iam.Effect.ALLOW,
+      }),
+    );
+
+    new CfnOutput(this, 'edge-to-origin-role-arn', {
+      value: `${this.role.roleArn}`,
+      exportName: `${this.stackName}-role-arn`,
+    });
+  }
+}
+
 /**
  * Create a new MicroApps Edge to Origin Function w/ `config.yml`
  */
@@ -163,6 +300,11 @@ ${props.rootPathPrefix ? `rootPathPrefix: '${props.rootPathPrefix}'` : ''}`;
     return this._edgeToOriginLambdas;
   }
 
+  private _edgeToOriginRole: iam.Role;
+  public get edgeToOriginRole(): iam.Role {
+    return this._edgeToOriginRole;
+  }
+
   constructor(scope: Construct, id: string, props: MicroAppsEdgeToOriginProps) {
     super(scope, id);
 
@@ -172,6 +314,7 @@ ${props.rootPathPrefix ? `rootPathPrefix: '${props.rootPathPrefix}'` : ''}`;
 
     const {
       addXForwardedHostHeader = true,
+      allowedFunctionUrlAccounts = [],
       assetNameRoot,
       assetNameSuffix,
       originRegion,
@@ -197,50 +340,29 @@ ${props.rootPathPrefix ? `rootPathPrefix: '${props.rootPathPrefix}'` : ''}`;
         : {}),
     });
 
+    const roleStack = new MicroAppsEdgeToOriginRoleStack(this, 'role-stack', {
+      assetNameRoot,
+      assetNameSuffix,
+      allowedFunctionUrlAccounts,
+      setupApiGatewayPermissions,
+      env: {
+        region: Stack.of(this).region,
+        account: Stack.of(this).account,
+      },
+    });
+    Stack.of(this).addDependency(roleStack);
+    this._edgeToOriginRole = roleStack.role;
+
     //
     // Create the Edge to Origin Function
     //
     const edgeToOriginFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
       functionName: assetNameRoot ? `${assetNameRoot}-edge-to-origin${assetNameSuffix}` : undefined,
+      role: this._edgeToOriginRole,
       memorySize: 1769,
       logRetention: logs.RetentionDays.ONE_MONTH,
       runtime: lambda.Runtime.NODEJS_16_X,
       timeout: Duration.seconds(5),
-      initialPolicy: [
-        // This can't have a reference to the httpApi because it would mean
-        // the parent stack (this stack) has to be created before the us-east-1
-        // child stack for the Edge Lambda Function.
-        // That's why we use a tag-based policy to allow the Edge Function
-        // to invoke any API Gateway API that we apply a tag to
-        // We allow the edge function to sign for all regions since
-        // we may use custom closest region in the future.
-        ...(setupApiGatewayPermissions
-          ? [
-              new iam.PolicyStatement({
-                actions: ['execute-api:Invoke'],
-                resources: [`arn:aws:execute-api:*:${Aws.ACCOUNT_ID}:*/*/*/*`],
-                // Unfortunately, API Gateway access cannot be restricted using
-                // tags on the target resource
-                // https://docs.aws.amazon.com/IAM/latest/UserGuide/access_tags.html
-                // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html#networking_svcs
-                // conditions: {
-                //   // TODO: Set this to a string unique to each stack
-                //   StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
-                // },
-              }),
-            ]
-          : []),
-        //
-        // Grant permission to invoke tagged Function URLs
-        //
-        new iam.PolicyStatement({
-          actions: ['lambda:InvokeFunctionUrl'],
-          resources: [`arn:aws:lambda:*:${Aws.ACCOUNT_ID}:*`],
-          conditions: {
-            StringEquals: { 'aws:ResourceTag/microapp-managed': 'true' },
-          },
-        }),
-      ],
       ...(removalPolicy ? { removalPolicy } : {}),
     };
     const rootDistPath = path.join(__dirname, '..', '..', 'microapps-edge-to-origin', 'dist');
diff --git a/packages/microapps-cdk/src/MicroAppsSvcs.ts b/packages/microapps-cdk/src/MicroAppsSvcs.ts
index 6cc877a8..379063c4 100644
--- a/packages/microapps-cdk/src/MicroAppsSvcs.ts
+++ b/packages/microapps-cdk/src/MicroAppsSvcs.ts
@@ -192,12 +192,12 @@ export interface IMicroAppsSvcs {
   /**
    * Lambda function for the Deployer
    */
-  readonly deployerFunc: lambda.IFunction;
+  readonly deployerFunc: lambda.Function;
 
   /**
    * Lambda function for the Router
    */
-  readonly routerFunc?: lambda.IFunction;
+  readonly routerFunc?: lambda.Function;
 }
 
 /**
@@ -212,12 +212,12 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
   }
 
   private _deployerFunc: lambda.Function;
-  public get deployerFunc(): lambda.IFunction {
+  public get deployerFunc(): lambda.Function {
     return this._deployerFunc;
   }
 
   private _routerFunc?: lambda.Function;
-  public get routerFunc(): lambda.IFunction | undefined {
+  public get routerFunc(): lambda.Function | undefined {
     return this._routerFunc;
   }
 
@@ -286,11 +286,22 @@ export class MicroAppsSvcs extends Construct implements IMicroAppsSvcs {
     const iamRoleUploadName = assetNameRoot
       ? `${assetNameRoot}-deployer-upload${assetNameSuffix}`
       : undefined;
+    const iamRoleDeployerName = assetNameRoot
+      ? `${assetNameRoot}-deployer${assetNameSuffix}`
+      : undefined;
+    const iamRoleDeployer = new iam.Role(this, 'deployer-role', {
+      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
+      roleName: iamRoleDeployerName,
+      managedPolicies: [
+        iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
+      ],
+    });
     const deployerFuncName = assetNameRoot
       ? `${assetNameRoot}-deployer${assetNameSuffix}`
       : undefined;
     const deployerFuncProps: Omit<lambda.FunctionProps, 'handler' | 'code'> = {
       functionName: deployerFuncName,
+      role: iamRoleDeployer,
       memorySize: 1769,
       logRetention: logs.RetentionDays.ONE_MONTH,
       runtime: lambda.Runtime.NODEJS_16_X,
diff --git a/packages/microapps-cdk/src/index.ts b/packages/microapps-cdk/src/index.ts
index 2d501777..e0bb8da2 100644
--- a/packages/microapps-cdk/src/index.ts
+++ b/packages/microapps-cdk/src/index.ts
@@ -1,6 +1,7 @@
 export * from './MicroApps';
 export * from './MicroAppsAPIGwy';
 export * from './MicroAppsCF';
+export * from './MicroAppsChildDeployer';
 export * from './MicroAppsEdgeToOrigin';
 export * from './MicroAppsS3';
 export * from './MicroAppsSvcs';
diff --git a/packages/microapps-deployer/src/config/Config.ts b/packages/microapps-deployer/src/config/Config.ts
index 6944077a..8d60e7bc 100644
--- a/packages/microapps-deployer/src/config/Config.ts
+++ b/packages/microapps-deployer/src/config/Config.ts
@@ -18,8 +18,8 @@ export interface IConfig {
   readonly awsAccountID: string;
   readonly awsRegion: string;
 
-  readonly parentDeployerLambdaARN: string;
-  readonly edgeToOriginRoleARN: string;
+  readonly parentDeployerLambdaARN?: string;
+  readonly edgeToOriginRoleARN?: string;
 
   readonly uploadRoleName: string;
 
diff --git a/packages/microapps-deployer/src/controllers/version/DeployVersionLite.spec.ts b/packages/microapps-deployer/src/controllers/version/DeployVersionLite.spec.ts
index 2ee4dc93..c90835bb 100644
--- a/packages/microapps-deployer/src/controllers/version/DeployVersionLite.spec.ts
+++ b/packages/microapps-deployer/src/controllers/version/DeployVersionLite.spec.ts
@@ -127,6 +127,7 @@ describe('DeployVersionLite', () => {
         .rejects()
         .on(lambda.InvokeCommand, {
           FunctionName: config.parentDeployerLambdaARN,
+          Qualifier: 'currentVersion',
           Payload: Buffer.from(JSON.stringify(request)),
         })
         .resolves({ Payload: Buffer.from(JSON.stringify({ statusCode: 200 })) });
diff --git a/packages/microapps-deployer/src/controllers/version/LambdaAlias.ts b/packages/microapps-deployer/src/controllers/version/LambdaAlias.ts
index 4a4e6965..b7c19904 100644
--- a/packages/microapps-deployer/src/controllers/version/LambdaAlias.ts
+++ b/packages/microapps-deployer/src/controllers/version/LambdaAlias.ts
@@ -415,15 +415,25 @@ async function AddCrossAccountPermissionsToAlias({
   }
 
   // Add statements if they do not already exist
-  if (addStatements) {
+  if (addStatements && config.edgeToOriginRoleARN) {
+    // await lambdaClient.send(
+    //   new lambda.AddPermissionCommand({
+    //     Principal: 'apigateway.amazonaws.com',
+    //     StatementId: 'microapps-edge-to-origin',
+    //     Action: 'lambda:InvokeFunctionUrl',
+    //     FunctionName: lambdaBaseARN,
+    //     Qualifier: lambdaAlias,
+    //     SourceArn: config.edgeToOriginRoleARN,
+    //   }),
+    // );
+
     await lambdaClient.send(
       new lambda.AddPermissionCommand({
-        Principal: 'apigateway.amazonaws.com',
+        Principal: config.edgeToOriginRoleARN,
         StatementId: 'microapps-edge-to-origin',
         Action: 'lambda:InvokeFunctionUrl',
         FunctionName: lambdaBaseARN,
         Qualifier: lambdaAlias,
-        SourceArn: config.edgeToOriginRoleARN,
       }),
     );
   }
diff --git a/packages/microapps-deployer/src/index.spec.ts b/packages/microapps-deployer/src/index.spec.ts
index dd614e5f..050eebbc 100644
--- a/packages/microapps-deployer/src/index.spec.ts
+++ b/packages/microapps-deployer/src/index.spec.ts
@@ -9,7 +9,7 @@ Object.defineProperty(Config, 'instance', {
   enumerable: false,
   get: jest.fn((): IConfig => {
     return {
-      awsAccountID: 123456,
+      awsAccountID: '0123456',
       awsRegion: 'mock',
       db: {
         tableName: 'microapps',
@@ -24,6 +24,7 @@ Object.defineProperty(Config, 'instance', {
       uploadRoleName: 'microapps-upload-test-role',
       rootPathPrefix: 'qa',
       requireIAMAuthorization: false,
+      edgeToOriginRoleARN: 'arn:aws:iam::123456789012:role/edge-to-origin-role',
     };
   }),
 });
diff --git a/packages/microapps-deployer/src/index.ts b/packages/microapps-deployer/src/index.ts
index 70e7a29d..8291a8d7 100644
--- a/packages/microapps-deployer/src/index.ts
+++ b/packages/microapps-deployer/src/index.ts
@@ -33,6 +33,9 @@ let dynamoClient = new DynamoDBClient({
   maxAttempts: 8,
 });
 
+const buildTrigger = '2023-01-26-01';
+Log.Instance.info('Deployer build trigger', { buildTrigger });
+
 export function overrideDBManager(opts: {
   dbManager: DBManager;
   dynamoClient: DynamoDBClient;
@@ -78,17 +81,21 @@ export async function handler(
   try {
     // Handle proxied requests when in proxy mode
     if (config.parentDeployerLambdaARN) {
-      // TODO: Need `deployVersionLite` here that only cleans DB records and/or S3 files
-      if (['deployVersionPreflight', 'deployVersionLite', 'deleteVersion'].includes(event.type)) {
+      if (
+        ['createApp', 'deployVersionPreflight', 'deployVersionLite', 'deleteVersion'].includes(
+          event.type,
+        )
+      ) {
         const response = await lambdaClient.send(
           new InvokeCommand({
             FunctionName: config.parentDeployerLambdaARN,
+            Qualifier: 'currentVersion',
             Payload: Buffer.from(JSON.stringify(event)),
           }),
         );
 
         const responsePayload = response.Payload
-          ? (JSON.parse(response.Payload.toString()) as IDeployerResponse)
+          ? (JSON.parse(Buffer.from(response.Payload).toString()) as IDeployerResponse)
           : { statusCode: 500 };
         Log.Instance.info('response from parent deployer', {
           ...response,
diff --git a/packages/microapps-edge-to-origin/src/index.ts b/packages/microapps-edge-to-origin/src/index.ts
index 72c5977d..167f0752 100644
--- a/packages/microapps-edge-to-origin/src/index.ts
+++ b/packages/microapps-edge-to-origin/src/index.ts
@@ -20,6 +20,9 @@ const appFrame = loadAppFrame({ basePath: __dirname });
 
 log.info('loaded config', { config });
 
+const buildTrigger = '2023-01-25-01';
+log.info('Edge-to-origin build trigger', { buildTrigger });
+
 let dbManager: DBManager | undefined;
 let dynamoClient = new DynamoDBClient({
   maxAttempts: 8,
diff --git a/packages/microapps-publish/src/lib/DeployClient.ts b/packages/microapps-publish/src/lib/DeployClient.ts
index c7ab5a75..22e49784 100644
--- a/packages/microapps-publish/src/lib/DeployClient.ts
+++ b/packages/microapps-publish/src/lib/DeployClient.ts
@@ -133,11 +133,18 @@ export default class DeployClient {
       if (dResponse.statusCode === 404) {
         output(`App/Version does not exist: ${config.app.name}/${config.app.semVer}`);
         return { exists: false, response: dResponse };
+      } else if (dResponse.statusCode > 299) {
+        // @ts-expect-error remove awsCredentials from response
+        delete dResponse.awsCredentials;
+        output(`DeployVersionPreflight failed: ${JSON.stringify(dResponse)}`);
+        throw new Error('DeployVersionPreflight failed');
       } else {
         output(`App/Version exists: ${config.app.name}/${config.app.semVer}`);
         return { exists: true, response: dResponse };
       }
     } else {
+      // @ts-expect-error remove awsCredentials from response
+      delete dResponse.awsCredentials;
       throw new Error(`Lambda call to DeployVersionPreflight failed: ${JSON.stringify(response)}`);
     }
   }


Test patch:
diff --git a/packages/microapps-cdk/test/MicroApps.test.ts b/packages/microapps-cdk/test/MicroApps.test.ts
index 2b113e27..8d9528f6 100644
--- a/packages/microapps-cdk/test/MicroApps.test.ts
+++ b/packages/microapps-cdk/test/MicroApps.test.ts
@@ -16,6 +16,7 @@ describe('MicroApps', () => {
     });
     const construct = new MicroApps(stack, 'construct', {
       appEnv: 'dev',
+      tableNameForEdgeToOrigin: 'table',
     });
 
     expect(construct).toBeDefined();
@@ -75,6 +76,7 @@ describe('MicroApps', () => {
     const construct = new MicroApps(stack, 'construct', {
       appEnv: 'dev',
       domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      tableNameForEdgeToOrigin: 'table',
       certEdge,
       certOrigin: certEdge,
       r53Zone,
@@ -136,6 +138,7 @@ describe('MicroApps', () => {
     const construct = new MicroApps(stack, 'construct', {
       appEnv: 'dev',
       domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      tableNameForEdgeToOrigin: 'table',
       certEdge,
       certOrigin: certEdge,
       r53Zone,
@@ -205,6 +208,7 @@ describe('MicroApps', () => {
     const construct = new MicroApps(stack, 'construct', {
       appEnv: 'dev',
       domainNameEdge: 'mydeploy.test.pwrdrvr.com',
+      tableNameForEdgeToOrigin: 'table',
       certEdge,
       certOrigin: certEdge,
       r53Zone,