Repository: aws-samples/cdk-bootstrapless-synthesizer (language: python)

Issue:
Support push docker image into multi region



Discussion / hints:

Fix patch:
diff --git a/API.md b/API.md
index 50f661b..2ab1a32 100644
--- a/API.md
+++ b/API.md
@@ -38,9 +38,9 @@ new BootstraplessStackSynthesizer(props?: BootstraplessStackSynthesizerProps)
   * **fileAssetRegionSet** (<code>Array<string></code>)  The regions set of file assets to be published only when `fileAssetBucketName` contains `${AWS::Region}`. __*Default*__: process.env.BSS_FILE_ASSET_REGION_SET // comma delimited list
   * **imageAssetAccountId** (<code>string</code>)  Override the ECR repository account id of the Docker Image assets. __*Default*__: process.env.BSS_IMAGE_ASSET_ACCOUNT_ID
   * **imageAssetPublishingRoleArn** (<code>string</code>)  The role to use to publish image assets to the ECR repository in this environment. __*Default*__: process.env.BSS_IMAGE_ASSET_PUBLISHING_ROLE_ARN
-  * **imageAssetRegion** (<code>string</code>)  Override the ECR repository region of the Docker Image assets. __*Default*__: process.env.BSS_IMAGE_ASSET_REGION
+  * **imageAssetRegionSet** (<code>Array<string></code>)  Override the ECR repository region of the Docker Image assets. __*Default*__: process.env.BSS_IMAGE_ASSET_REGION_SET // comma delimited list
   * **imageAssetRepositoryName** (<code>string</code>)  Name of the ECR repository to hold Docker Image assets. __*Default*__: process.env.BSS_IMAGE_ASSET_REPOSITORY_NAME
-  * **imageAssetTag** (<code>string</code>)  Override the tag of the Docker Image assets. __*Default*__: process.env.BSS_IMAGE_ASSET_TAG
+  * **imageAssetTagPrefix** (<code>string</code>)  Override the tag of the Docker Image assets. __*Default*__: process.env.BSS_IMAGE_ASSET_TAG_PREFIX
   * **templateBucketName** (<code>string</code>)  Override the name of the S3 bucket to hold Cloudformation template. __*Default*__: process.env.BSS_TEMPLATE_BUCKET_NAME
 
 
@@ -153,9 +153,9 @@ Name | Type | Description
 **fileAssetRegionSet**? | <code>Array<string></code> | The regions set of file assets to be published only when `fileAssetBucketName` contains `${AWS::Region}`.<br/>__*Default*__: process.env.BSS_FILE_ASSET_REGION_SET // comma delimited list
 **imageAssetAccountId**? | <code>string</code> | Override the ECR repository account id of the Docker Image assets.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_ACCOUNT_ID
 **imageAssetPublishingRoleArn**? | <code>string</code> | The role to use to publish image assets to the ECR repository in this environment.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_PUBLISHING_ROLE_ARN
-**imageAssetRegion**? | <code>string</code> | Override the ECR repository region of the Docker Image assets.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_REGION
+**imageAssetRegionSet**? | <code>Array<string></code> | Override the ECR repository region of the Docker Image assets.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_REGION_SET // comma delimited list
 **imageAssetRepositoryName**? | <code>string</code> | Name of the ECR repository to hold Docker Image assets.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_REPOSITORY_NAME
-**imageAssetTag**? | <code>string</code> | Override the tag of the Docker Image assets.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_TAG
+**imageAssetTagPrefix**? | <code>string</code> | Override the tag of the Docker Image assets.<br/>__*Default*__: process.env.BSS_IMAGE_ASSET_TAG_PREFIX
 **templateBucketName**? | <code>string</code> | Override the name of the S3 bucket to hold Cloudformation template.<br/>__*Default*__: process.env.BSS_TEMPLATE_BUCKET_NAME
 
 
diff --git a/README.md b/README.md
index dd4c91b..75354af 100644
--- a/README.md
+++ b/README.md
@@ -16,32 +16,35 @@ import { BootstraplessStackSynthesizer } from 'cdk-bootstrapless-synthesizer';
 const app = new cdk.App();
 
 // You can set arguments directly 
-new MyWidgetServiceStack(app, 'MyWidgetServiceStack', {
+new MyStack(app, 'my-stack-dev', {
   synthesizer: new BootstraplessStackSynthesizer({
     templateBucketName: 'cfn-template-bucket',
-    imageAssetRepositoryName: 'ecr-repo-name',
 
     fileAssetBucketName: 'file-asset-bucket-${AWS::Region}',
-    fileAssetRegionSet: ['us-east-1'],
+    fileAssetRegionSet: ['us-west-1', 'us-west-2'],
     fileAssetPrefix: 'file-asset-prefix/latest/',
 
-    imageAssetTag: 'docker-image-tag',
-    imageAssetRegion: 'us-east-1',
+    imageAssetRepositoryName: 'your-ecr-repo-name',
     imageAssetAccountId: '1234567890',
-  })
+    imageAssetTagPrefix: 'latest-',
+    imageAssetRegionSet: ['us-west-1', 'us-west-2'],
+  }),
 });
 
 // Or by environment variables
 // export BSS_TEMPLATE_BUCKET_NAME="cfn-template-bucket"
-// export BSS_IMAGE_ASSET_REPOSITORY_NAME="ecr-repo-name"
+
 // export BSS_FILE_ASSET_BUCKET_NAME="file-asset-bucket-\${AWS::Region}"
-// export BSS_FILE_ASSET_REGION_SET="us-east-1,us-west-1"
+// export BSS_FILE_ASSET_REGION_SET="us-west-1',us-west-2"
 // export BSS_FILE_ASSET_PREFIX="file-asset-prefix/latest/"
-// export BSS_IMAGE_ASSET_TAG="docker-image-tag"
-// export BSS_IMAGE_ASSET_REGION="us-east-1"
+
+// export BSS_IMAGE_ASSET_REPOSITORY_NAME="your-ecr-repo-name"
 // export BSS_IMAGE_ASSET_ACCOUNT_ID="1234567890"
-new MyWidgetServiceStack(app, 'MyWidgetServiceStack', {
-  synthesizer: new BootstraplessStackSynthesizer()
+// export BSS_IMAGE_ASSET_TAG_PREFIX="latest-"
+// export BSS_IMAGE_ASSET_REGION="us-west-1',us-west-2"
+
+new MyStack(app, 'my-stack-dev', {
+  synthesizer: new BootstraplessStackSynthesizer(),
 });
 ```
 
@@ -49,7 +52,7 @@ Synth AWS CloudFormation templates, assets and upload them
 
 ```shell
 $ cdk synth
-$ npx cdk-assets publish -p cdk.out/MyWidgetServiceStack.assets.json -v
+$ npx cdk-assets publish -p cdk.out/my-stack-dev.assets.json -v
 ```
 
 In your template
diff --git a/sample/cdk.out/my-stack-dev.assets.json b/sample/cdk.out/my-stack-dev.assets.json
index f911bbf..e6adfa3 100644
--- a/sample/cdk.out/my-stack-dev.assets.json
+++ b/sample/cdk.out/my-stack-dev.assets.json
@@ -1,21 +1,21 @@
 {
   "version": "14.0.0",
   "files": {
-    "ed0039ce1464f5fe3b3e767aa85cc570837f7ec31beb592d971c75db4f3050fe": {
+    "713bb8386428293da6e120d9844f696685a07336b6c246ff8702c396d916dbcf": {
       "source": {
-        "path": "asset.ed0039ce1464f5fe3b3e767aa85cc570837f7ec31beb592d971c75db4f3050fe",
+        "path": "asset.713bb8386428293da6e120d9844f696685a07336b6c246ff8702c396d916dbcf",
         "packaging": "zip"
       },
       "destinations": {
-        "us-east-1": {
-          "bucketName": "file-asset-bucket-us-east-1",
-          "objectKey": "file-asset-prefix/latest/ed0039ce1464f5fe3b3e767aa85cc570837f7ec31beb592d971c75db4f3050fe.zip",
-          "region": "us-east-1"
-        },
         "us-west-1": {
           "bucketName": "file-asset-bucket-us-west-1",
-          "objectKey": "file-asset-prefix/latest/ed0039ce1464f5fe3b3e767aa85cc570837f7ec31beb592d971c75db4f3050fe.zip",
+          "objectKey": "file-asset-prefix/latest/713bb8386428293da6e120d9844f696685a07336b6c246ff8702c396d916dbcf.zip",
           "region": "us-west-1"
+        },
+        "us-west-2": {
+          "bucketName": "file-asset-bucket-us-west-2",
+          "objectKey": "file-asset-prefix/latest/713bb8386428293da6e120d9844f696685a07336b6c246ff8702c396d916dbcf.zip",
+          "region": "us-west-2"
         }
       }
     },
@@ -25,15 +25,15 @@
         "packaging": "zip"
       },
       "destinations": {
-        "us-east-1": {
-          "bucketName": "file-asset-bucket-us-east-1",
-          "objectKey": "file-asset-prefix/latest/c8ffddbe3e327bdd704b2c819d8bd3e96f670f41609a1a78b38cf8eceb9a1a55.zip",
-          "region": "us-east-1"
-        },
         "us-west-1": {
           "bucketName": "file-asset-bucket-us-west-1",
           "objectKey": "file-asset-prefix/latest/c8ffddbe3e327bdd704b2c819d8bd3e96f670f41609a1a78b38cf8eceb9a1a55.zip",
           "region": "us-west-1"
+        },
+        "us-west-2": {
+          "bucketName": "file-asset-bucket-us-west-2",
+          "objectKey": "file-asset-prefix/latest/c8ffddbe3e327bdd704b2c819d8bd3e96f670f41609a1a78b38cf8eceb9a1a55.zip",
+          "region": "us-west-2"
         }
       }
     },
@@ -51,14 +51,20 @@
     }
   },
   "dockerImages": {
-    "081d61aabae441170677433ce52c96bcc8d23f6e4d70643bd2ea6b2e6dd570f5": {
+    "d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3": {
       "source": {
-        "directory": "asset.081d61aabae441170677433ce52c96bcc8d23f6e4d70643bd2ea6b2e6dd570f5"
+        "directory": "asset.d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3"
       },
       "destinations": {
-        "current_account-current_region": {
+        "us-west-1": {
+          "repositoryName": "your-ecr-repo-name",
+          "imageTag": "latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3",
+          "region": "us-west-1"
+        },
+        "us-west-2": {
           "repositoryName": "your-ecr-repo-name",
-          "imageTag": "latest"
+          "imageTag": "latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3",
+          "region": "us-west-2"
         }
       }
     }
diff --git a/sample/cdk.out/my-stack-dev.template.json b/sample/cdk.out/my-stack-dev.template.json
index b2cb63c..f5c04f3 100644
--- a/sample/cdk.out/my-stack-dev.template.json
+++ b/sample/cdk.out/my-stack-dev.template.json
@@ -2,7 +2,7 @@
   "Outputs": {
     "output": {
       "Value": {
-        "Fn::Sub": "1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/your-ecr-repo-name:latest"
+        "Fn::Sub": "1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/your-ecr-repo-name:latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3"
       }
     }
   },
@@ -14,7 +14,7 @@
           "S3Bucket": {
             "Fn::Sub": "file-asset-bucket-${AWS::Region}"
           },
-          "S3Key": "file-asset-prefix/latest/ed0039ce1464f5fe3b3e767aa85cc570837f7ec31beb592d971c75db4f3050fe.zip"
+          "S3Key": "file-asset-prefix/latest/713bb8386428293da6e120d9844f696685a07336b6c246ff8702c396d916dbcf.zip"
         },
         "CompatibleRuntimes": [
           "nodejs12.x"
@@ -23,7 +23,7 @@
       },
       "Metadata": {
         "aws:cdk:path": "my-stack-dev/MyLayer/Resource",
-        "aws:asset:path": "asset.ed0039ce1464f5fe3b3e767aa85cc570837f7ec31beb592d971c75db4f3050fe",
+        "aws:asset:path": "asset.713bb8386428293da6e120d9844f696685a07336b6c246ff8702c396d916dbcf",
         "aws:asset:property": "Content"
       }
     },
@@ -96,7 +96,7 @@
     "CDKMetadata": {
       "Type": "AWS::CDK::Metadata",
       "Properties": {
-        "Analytics": "v2:deflate64:H4sIAAAAAAAA/1WQQWvDMAyFf8vujrq0g123dgwKhUIKvWuOFtw0dpHkjmL832en62Gn96QH3xNqoV0u4fnpDX+ksf24SDYwQToo2tG8i5AWOzg/mM2330e9RDWb4EU5Wq27jiREtlR9CXqnLvhsKi+R5QYrQ+Aj2JF4O+FAM9WUDDq6BHEa+LZGIXPG6atHSDu8ER+JpZAq9t/8Gb3Vv+Dhs5HVo+hOlxWsY2nUGexwgtSF83xk1Zyz8aEnOMni2r5A+1p+cBLnGo5e3UTQ3fUX9l5UICABAAA="
+        "Analytics": "v2:deflate64:H4sIAAAAAAAA/1WQQWvDMAyFf8vujrqshV63dgwGhUIKvWuOFtw0VrHkjWL832un62Gn96QH3xNqoX1ZwfPTK/5KY/txkSwHgnRQtKN5EyEtdnB+MNtvv496iWq27EVDtFp3HQnHYKn6EvROHftsKi+RDQ1WhsA725HC54QDzVRTMujowuKUw3WDQuaM01ePkHZ4pXCkIIVUsf/mj+it/gUPn40sH0V3uixhE0ujzmCHE6SOz/ORVXPOxnNPcJLFT7uCdl1+cBLnmhC9uomgu+sNUszKQSABAAA="
       },
       "Metadata": {
         "aws:cdk:path": "my-stack-dev/CDKMetadata/Default"
diff --git a/sample/src/main.ts b/sample/src/main.ts
index c628263..f36a6e1 100644
--- a/sample/src/main.ts
+++ b/sample/src/main.ts
@@ -54,12 +54,13 @@ new MyStack(app, 'my-stack-dev', {
     templateBucketName: 'cfn-template-bucket',
 
     fileAssetBucketName: 'file-asset-bucket-${AWS::Region}',
-    fileAssetRegionSet: ['us-east-1', 'us-west-1'],
+    fileAssetRegionSet: ['us-west-1', 'us-west-2'],
     fileAssetPrefix: 'file-asset-prefix/latest/',
 
     imageAssetRepositoryName: 'your-ecr-repo-name',
     imageAssetAccountId: '1234567890',
-    imageAssetTag: 'latest',
+    imageAssetTagPrefix: 'latest-',
+    imageAssetRegionSet: ['us-west-1', 'us-west-2'],
   }),
 });
 
diff --git a/src/index.ts b/src/index.ts
index 3d4a82d..e077fb2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -93,16 +93,19 @@ export interface BootstraplessStackSynthesizerProps {
   /**
    * Override the tag of the Docker Image assets
    *
-   * @default - process.env.BSS_IMAGE_ASSET_TAG
+   * @default - process.env.BSS_IMAGE_ASSET_TAG_PREFIX
    */
-  readonly imageAssetTag?: string;
+  readonly imageAssetTagPrefix?: string;
 
   /**
    * Override the ECR repository region of the Docker Image assets
    *
-   * @default - process.env.BSS_IMAGE_ASSET_REGION
+   * For examples:
+   * `['us-east-1', 'us-west-1']`
+   *
+   * @default - process.env.BSS_IMAGE_ASSET_REGION_SET // comma delimited list
    */
-  readonly imageAssetRegion?: string;
+  readonly imageAssetRegionSet?: string[];
 
   /**
    * Override the ECR repository account id of the Docker Image assets
@@ -125,8 +128,8 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
   private fileAssetPrefix?: string;
   private fileAssetRegionSet?: string[];
   private templateBucketName?: string;
-  private imageAssetTag?: string;
-  private imageAssetRegion?: string;
+  private imageAssetTagPrefix?: string;
+  private imageAssetRegionSet?: string[];
   private imageAssetAccountId?: string;
 
 
@@ -146,8 +149,8 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
       BSS_FILE_ASSET_REGION_SET,
 
       BSS_TEMPLATE_BUCKET_NAME,
-      BSS_IMAGE_ASSET_TAG,
-      BSS_IMAGE_ASSET_REGION,
+      BSS_IMAGE_ASSET_TAG_PREFIX,
+      BSS_IMAGE_ASSET_REGION_SET,
       BSS_IMAGE_ASSET_ACCOUNT_ID,
     } = process.env;
     this.bucketName = props.fileAssetBucketName ?? BSS_FILE_ASSET_BUCKET_NAME;
@@ -155,13 +158,11 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
     this.fileAssetPublishingRoleArn = props.fileAssetPublishingRoleArn ?? BSS_FILE_ASSET_PUBLISHING_ROLE_ARN;
     this.imageAssetPublishingRoleArn = props.imageAssetPublishingRoleArn ?? BSS_IMAGE_ASSET_PUBLISHING_ROLE_ARN;
     this.fileAssetPrefix = props.fileAssetPrefix ?? BSS_FILE_ASSET_PREFIX;
-    this.fileAssetRegionSet = props.fileAssetRegionSet ?? (BSS_FILE_ASSET_REGION_SET ? BSS_FILE_ASSET_REGION_SET.split(',') : undefined);
+    this.fileAssetRegionSet = props.fileAssetRegionSet ?? commaSplit(BSS_FILE_ASSET_REGION_SET);
     this.templateBucketName = props.templateBucketName ?? BSS_TEMPLATE_BUCKET_NAME;
-    this.imageAssetTag = props.imageAssetTag ?? BSS_IMAGE_ASSET_TAG;
-    this.imageAssetRegion = props.imageAssetRegion ?? BSS_IMAGE_ASSET_REGION;
+    this.imageAssetTagPrefix = (props.imageAssetTagPrefix ?? BSS_IMAGE_ASSET_TAG_PREFIX) ?? '';
+    this.imageAssetRegionSet = props.imageAssetRegionSet ?? commaSplit(BSS_IMAGE_ASSET_REGION_SET);
     this.imageAssetAccountId = props.imageAssetAccountId ?? BSS_IMAGE_ASSET_ACCOUNT_ID;
-
-    this.imageAssetRegion = this.imageAssetRegion?.trim();
   }
 
   public bind(stack: Stack): void {
@@ -203,6 +204,7 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
   private _addFileAsset(asset: FileAssetSource, overrideBucketname?: string): FileAssetLocation {
     assertNotNull(this.stack, ERR_MSG_CALL_BIND_FIRST);
     assertNotNull(this.bucketName, 'The bucketName is null');
+    validateFileAssetSource(asset);
 
     const bucketName = overrideBucketname ?? this.bucketName;
     const objectKey = this.fileAssetPrefix + asset.sourceHash + (asset.packaging === FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : '');
@@ -253,8 +255,29 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
   public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation {
     assertNotNull(this.stack, ERR_MSG_CALL_BIND_FIRST);
     assertNotNull(this.repositoryName, 'The repositoryName is null');
+    validateDockerImageAssetSource(asset);
 
-    const imageTag = this.imageAssetTag ?? asset.sourceHash;
+    const imageTag = this.imageAssetTagPrefix + asset.sourceHash;
+    const destinations: { [id: string]: cxschema.DockerImageDestination } = {};
+
+    if (this.imageAssetRegionSet?.length) {
+      for (const region of this.imageAssetRegionSet.map(r => r.trim())) {
+        if (!region) { continue; }
+        destinations[region] = {
+          repositoryName: this.repositoryName,
+          imageTag,
+          region,
+          assumeRoleArn: this.fileAssetPublishingRoleArn,
+        };
+      }
+    } else {
+      destinations[this.manifestEnvName] = {
+        repositoryName: this.repositoryName,
+        imageTag,
+        region: resolvedOr(this.stack.region, undefined),
+        assumeRoleArn: this.imageAssetPublishingRoleArn,
+      };
+    }
 
     // Add to manifest
     this.dockerImages[asset.sourceHash] = {
@@ -264,24 +287,15 @@ export class BootstraplessStackSynthesizer extends StackSynthesizer {
         dockerBuildTarget: asset.dockerBuildTarget,
         dockerFile: asset.dockerFile,
       },
-      destinations: {
-        [this.manifestEnvName]: {
-          repositoryName: this.repositoryName,
-          imageTag,
-          region: this.imageAssetRegion ?? resolvedOr(this.stack.region, undefined),
-          assumeRoleArn: this.imageAssetPublishingRoleArn,
-        },
-      },
+      destinations,
     };
 
-    let { account, region, urlSuffix } = stackLocationOrInstrinsics(this.stack);
-    region = this.imageAssetRegion ?? region;
+    let { account, urlSuffix } = stackLocationOrInstrinsics(this.stack);
     account = this.imageAssetAccountId ?? account;
 
-    // Return CFN expression
     return {
       repositoryName: cfnify(this.repositoryName),
-      imageUri: cfnify(`${account}.dkr.ecr.${region}.${urlSuffix}/${this.repositoryName}:${imageTag}`),
+      imageUri: cfnify(`${account}.dkr.ecr.${REGION_PLACEHOLDER}.${urlSuffix}/${this.repositoryName}:${imageTag}`),
     };
   }
 
@@ -440,3 +454,36 @@ function assertNotNull<A>(x: A | undefined, msg: string = 'Null value error'): a
     throw new Error(msg);
   }
 }
+
+function commaSplit(v?: string): string[] | undefined {
+  if (v) {
+    return v.split(',');
+  }
+  return undefined;
+}
+
+function validateFileAssetSource(asset: FileAssetSource) {
+  if (!!asset.executable === !!asset.fileName) {
+    throw new Error(`Exactly one of 'fileName' or 'executable' is required, got: ${JSON.stringify(asset)}`);
+  }
+
+  if (!!asset.packaging !== !!asset.fileName) {
+    throw new Error(`'packaging' is expected in combination with 'fileName', got: ${JSON.stringify(asset)}`);
+  }
+}
+
+function validateDockerImageAssetSource(asset: DockerImageAssetSource) {
+  if (!!asset.executable === !!asset.directoryName) {
+    throw new Error(`Exactly one of 'directoryName' or 'executable' is required, got: ${JSON.stringify(asset)}`);
+  }
+
+  check('dockerBuildArgs');
+  check('dockerBuildTarget');
+  check('dockerFile');
+
+  function check<K extends keyof DockerImageAssetSource>(key: K) {
+    if (asset[key] && !asset.directoryName) {
+      throw new Error(`'${key}' is only allowed in combination with 'directoryName', got: ${JSON.stringify(asset)}`);
+    }
+  }
+}
\ No newline at end of file


Test patch:
diff --git a/test/BootstraplessStackSynthesizer.test.ts b/test/BootstraplessStackSynthesizer.test.ts
index 2ed3c46..4ef9617 100644
--- a/test/BootstraplessStackSynthesizer.test.ts
+++ b/test/BootstraplessStackSynthesizer.test.ts
@@ -12,8 +12,8 @@ beforeEach(() => {
   delete process.env.BSS_FILE_ASSET_PREFIX;
   delete process.env.BSS_FILE_ASSET_REGION_SET;
   delete process.env.BSS_TEMPLATE_BUCKET_NAME;
-  delete process.env.BSS_IMAGE_ASSET_TAG;
-  delete process.env.BSS_IMAGE_ASSET_REGION;
+  delete process.env.BSS_IMAGE_ASSET_TAG_PREFIX;
+  delete process.env.BSS_IMAGE_ASSET_REGION_SET;
   delete process.env.BSS_IMAGE_ASSET_ACCOUNT_ID;
 });
 
@@ -238,9 +238,9 @@ test.each([
 
 
 test.each([
-  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTag: 'latest' }),
-  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG: 'latest' }),
-])('#%# addDockerImageAsset when imageAssetsTag is specified', (mksynthzerFn) => {
+  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagPrefix: 'latest-' }),
+  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_PREFIX: 'latest-' }),
+])('#%# addDockerImageAsset when imageAssetTagPrefix is specified', (mksynthzerFn) => {
   const stack = new Stack();
   const synthesizer = mksynthzerFn();
   synthesizer.bind(stack);
@@ -256,7 +256,7 @@ test.each([
   expect(json.dockerImages.abcdef.destinations).toEqual({
     'current_account-current_region': {
       repositoryName: 'the-repo',
-      imageTag: 'latest',
+      imageTag: 'latest-abcdef',
     },
   });
   expect(json.files).toEqual({});
@@ -264,9 +264,9 @@ test.each([
 
 
 test.each([
-  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetRegion: 'us-west-1' }),
-  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_REGION: 'us-west-1' }),
-])('#%# addDockerImageAsset when imageAssetsRegion is specified', (mksynthzerFn) => {
+  () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetRegionSet: ['us-west-1'] }),
+  () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_REGION_SET: 'us-west-1' }),
+])('#%# addDockerImageAsset when imageAssetsRegionSet is specified', (mksynthzerFn) => {
   const stack = new Stack();
   const synthesizer = mksynthzerFn();
   synthesizer.bind(stack);
@@ -277,13 +277,13 @@ test.each([
   const json = JSON.parse(synthesizer.dumps());
 
   expect(stack.resolve(location.imageUri)).toEqual({
-    'Fn::Sub': '${AWS::AccountId}.dkr.ecr.us-west-1.${AWS::URLSuffix}/the-repo:abcdef',
+    'Fn::Sub': '${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/the-repo:abcdef',
   });
   expect(json.dockerImages.abcdef.source).toEqual({
     directory: __dirname,
   });
   expect(json.dockerImages.abcdef.destinations).toEqual({
-    'current_account-current_region': {
+    'us-west-1': {
       repositoryName: 'the-repo',
       imageTag: 'abcdef',
       region: 'us-west-1',