Repository: GoogleContainerTools/skaffold
Pull request #7745 (2022-08-04 19:01:05)

feat: implement creating `ManifestList` from single platform images
<!-- Thank you for your contribution! -->

<!-- Include if applicable: -->
Fixes: #7632 <!-- tracking issues that this PR will close -->
**Related**: #7082

**Description**
<!-- Describe your changes here. The more detail, the easier the review! -->
In this PR, Skaffold will iteratively create single platform images for builders that don't support native multi-arch builds, and then generate a ManifestList programmatically that consists of these individual platform manifests.

**User facing changes**
Users can now provide multiple arguments to the `--platform` flag.

**Testing instructions**:

1. In the `examples/getting-started` project, run:
    ```
    skaffold build --default-repo=your/container/registry --platform=linux/amd64,linux/arm64
    ```

2. You should get the output:
    ```
    ...
    Building [skaffold-example]...
    Target platforms: [linux/amd64,linux/arm64]
    ...
    [+] Building 0.3s (13/13) FINISHED
    ...
    => => writing image sha256:10af3142e460566f5791c48758f0040cef6932cbcb0766082dcbb0d8db7653e7
    => => naming to gcr.io/k8s-skaffold/skaffold-example:latest_linux_amd64
    ...
    latest_linux_amd64: digest: sha256:15bd4f2380e99b3563f8add1aba9691e414d4cc5701363d9c74960a20fb276c4 size: 739
    ...
    [+] Building 52.8s (13/13) FINISHED
    ...
    => => writing image sha256:68866691e2d6f079b116e097ae4e67a53eaf89e825b52d6f31f2e9cc566974de
    => => naming to gcr.io/k8s-skaffold/skaffold-example:latest_linux_arm64
    ...
    4e0c2525c370: Pushed
    latest_linux_arm64: digest: sha256:868d0aec1cc7d2ed1fa1e840f38ff1aa50c3cc3d3232ea17a065618eaec4e82b size: 739
    Build [skaffold-example] succeeded
    ```

3. Run the following `docker` command to validate the built image:

    ```
    docker manifest inspect your/container/registry/skaffold-example:latest | grep -A 3 "platform"
    
      "platform": {
          "architecture": "amd64",
          "os": "linux"
      }
    --
      "platform": {
          "architecture": "arm64",
          "os": "linux"
      }
    ```

**Pending**
<!-- Mention any related follow up work to this PR. -->

- [ ] Add unit tests for the `CreateManifestList` function.
- [ ] Add integration test using `examples/getting-started`. 

<!--
Please be sure your PR includes unit tests - we don't merge code that brings down test coverage! 
Integration tests are sometimes an appropriate substitute. 
-->


codecov[bot]: # [Codecov](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) Report
> Merging [#7745](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (2b91018) into [main](https://codecov.io/gh/GoogleContainerTools/skaffold/commit/290280e8c6c19231e9ed5b5c027f2c01d22f75be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) (290280e) will **decrease** coverage by `4.01%`.
> The diff coverage is `53.59%`.

```diff
@@            Coverage Diff             @@
##             main    #7745      +/-   ##
==========================================
- Coverage   70.48%   66.46%   -4.02%     
==========================================
  Files         515      589      +74     
  Lines       23150    28315    +5165     
==========================================
+ Hits        16317    18820    +2503     
- Misses       5776     8119    +2343     
- Partials     1057     1376     +319     
```


| [Impacted Files](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | Coverage Δ | |
|---|---|---|
| [cmd/skaffold/app/cmd/credits/export.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvY3JlZGl0cy9leHBvcnQuZ28=) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/deploy.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZGVwbG95Lmdv) | `40.90% <0.00%> (-12.94%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/test.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdGVzdC5nbw==) | `44.44% <0.00%> (ø)` | |
| [cmd/skaffold/skaffold.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL3NrYWZmb2xkLmdv) | `0.00% <0.00%> (ø)` | |
| [cmd/skaffold/app/cmd/inspect\_tests.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvaW5zcGVjdF90ZXN0cy5nbw==) | `62.50% <14.28%> (-1.14%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/render.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcmVuZGVyLmdv) | `35.48% <18.18%> (-5.90%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/lsp.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvbHNwLmdv) | `28.12% <28.12%> (ø)` | |
| [cmd/skaffold/app/cmd/run.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvcnVuLmdv) | `64.28% <33.33%> (-9.63%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/fix.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvZml4Lmdv) | `56.41% <37.50%> (-20.07%)` | :arrow_down: |
| [cmd/skaffold/app/cmd/verify.go](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools#diff-Y21kL3NrYWZmb2xkL2FwcC9jbWQvdmVyaWZ5Lmdv) | `41.17% <41.17%> (ø)` | |
| ... and [347 more](https://codecov.io/gh/GoogleContainerTools/skaffold/pull/7745/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools) | |

:mega: Codecov can now indicate which changes are the most critical in Pull Requests. [Learn more](https://about.codecov.io/product/feature/runtime-insights/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GoogleContainerTools)


Patch:
diff --git a/cmd/skaffold/app/cmd/flags.go b/cmd/skaffold/app/cmd/flags.go
index f11984d82..ba27daea6 100644
--- a/cmd/skaffold/app/cmd/flags.go
+++ b/cmd/skaffold/app/cmd/flags.go
@@ -645,10 +645,15 @@ var flagRegistry = []Flag{
 		DefinedOn:     []string{"verify"},
 	},
 	{
-		Name:          "enable-platform-node-affinity",
-		Usage:         "If true, skaffold will add platform (os/arch) node affinity to rendered manifests based on the image platforms",
-		Value:         &opts.EnablePlatformNodeAffinity,
-		DefValue:      false,
+		Name:     "enable-platform-node-affinity",
+		Usage:    "If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms",
+		Value:    &opts.EnablePlatformNodeAffinity,
+		DefValue: false,
+		DefValuePerCommand: map[string]interface{}{
+			"dev":   true,
+			"debug": true,
+			"run":   true,
+		},
 		FlagAddMethod: "BoolVar",
 		DefinedOn:     []string{"dev", "render", "run", "debug", "deploy"},
 	},
diff --git a/docs-v2/content/en/docs/_index.md b/docs-v2/content/en/docs/_index.md
index c83e27bf0..4249cc1cf 100644
--- a/docs-v2/content/en/docs/_index.md
+++ b/docs-v2/content/en/docs/_index.md
@@ -24,6 +24,7 @@ Kubernetes cluster.
 * Skaffold projects work everywhere
   * **share with other developers** - Skaffold is the easiest way to **share your project** with the world: `git clone` and `skaffold run`
   * **context aware** - use Skaffold profiles, local user config, environment variables, and flags to easily incorporate differences across environments
+  * **platform aware** - use cross-platform and multi-platform **build** support, with automatic platform detection, to easily handle operating system and architecture differences between the development machine and Kubernetes cluster nodes.
   * **CI/CD building blocks** - use `skaffold build`, `skaffold test` and `skaffold deploy` as part of your CI/CD pipeline, or simply `skaffold run` end-to-end
   * **GitOps integration** - use `skaffold render` to build your images and render templated Kubernetes manifests for use in GitOps workflows
 * skaffold.yaml - a single pluggable, declarative configuration for your project
diff --git a/docs-v2/content/en/docs/pipeline-stages/builders/_index.md b/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
index 76c7f6bf4..d8af95356 100644
--- a/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
+++ b/docs-v2/content/en/docs/pipeline-stages/builders/_index.md
@@ -162,9 +162,9 @@ Skaffold currently supports [Docker]({{<relref "/docs/pipeline-stages/builders/d
 [Jib]({{<relref "/docs/pipeline-stages/builders/jib#remotely-with-google-cloud-build">}})
 on Google Cloud Build.
 
-## Cross-platform build support
+## Cross-platform and Multi-platform build support
 
-Skaffold selectively supports building for a platform that is different than the host machine platform. The target platform for an artifact can be specified in one of the following ways:
+Skaffold selectively supports building for an architecture that is different than the development machine architecture (`cross-platform` build) or building for multiple architectures (`multiple-platform` build). The target platforms for an artifact can be specified in one of the following ways:
 
 - The pipeline's `platforms` property in the `skaffold.yaml` file.
 {{% readfile file="samples/builders/platforms/pipeline-constraints.yaml" %}}
@@ -175,7 +175,7 @@ Skaffold selectively supports building for a platform that is different than the
 - The CLI flag `--platform` which overrides the values set in both the previous ways.
 
 ```cmd
-skaffold build --platform=linux/arm64
+skaffold build --platform=linux/arm64,linux/amd64
 ```
 
 Additionally, for `skaffold dev`, `skaffold debug` and `skaffold run` commands, where the build output gets deployed immediately, skaffold checks the platform for the kubernetes cluster nodes and attempts to build artifacts for that target platform.
@@ -184,7 +184,7 @@ The final list of target platforms need to ultimately be supported by the target
 
 |    | Local Build | In Cluster Build | Remote on Google Cloud Build |
 |----|:-----------:|:----------------:|:----------------------------:|
-| **Dockerfile** | Cross-platform supported | Cross-platform supported but platform should match cluster node running the pod. | Cross-platform supported |
+| **Dockerfile** | Cross-platform and multi-platform supported | Cross-platform supported but platform should match cluster node running the pod. | Cross-platform and multi-platform supported |
 | **Jib Maven and Gradle** | Cross-platform and multi-platform supported | - | Cross-platform and multi-platform supported |
 | **Cloud Native Buildpacks** | Only supports `linux/amd64` | - | Only supports `linux/amd64` |
 | **Bazel** | Cross-platform supported but requires explicit platform specific rules. Not yet implemented | - | - |
@@ -192,5 +192,5 @@ The final list of target platforms need to ultimately be supported by the target
 | **Custom Script** | Cross-platform and multi-platform supported but requires user to implement it in the build script | Cross-platform and multi-platform supported but requires user to implement it in the build script | - |
 
 {{< alert title="Note" >}}
-Skaffold supports multi-platform image builds using the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}).
+Skaffold supports multi-platform image builds natively for the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}). For other builders that support building cross-architecture images, Skaffold will iteratively build a single platform image for each target architecture and stitch them together into a multi-platform image, and push it to the registry.
 {{< /alert >}}
diff --git a/docs-v2/content/en/docs/workflows/handling-platforms.md b/docs-v2/content/en/docs/workflows/handling-platforms.md
new file mode 100644
index 000000000..072ca54f4
--- /dev/null
+++ b/docs-v2/content/en/docs/workflows/handling-platforms.md
@@ -0,0 +1,231 @@
+---
+title: "Managing ARM workloads [NEW]"
+linkTitle: "Managing ARM workloads [NEW]"
+featureId: build.platforms
+weight: 50
+---
+
+Skaffold has a lot of intelligence built-in to simplify working with ARM workloads. Whether developing on an Apple Silicon Macbook that uses an ARM based chip, or deploying to a GKE Kubernetes cluster having ARM nodes, Skaffold can take away the complexities that arise when the architecture of your development machine and Kubernetes cluster don't match.
+
+## Why is image architecture important?
+
+Container images are built targeting specific [Instruction Set Architectures](https://en.wikipedia.org/wiki/Instruction_set_architecture) like `amd64`, `arm64`, etc. **You must use container images that are compatible with the architecture of the node where you intend to run the workloads.** For example, to deploy to a GKE cluster running ARM nodes, the image needs to be built for `linux/arm64` platform.
+
+All image builders build for different default architecture and not all support cross-architecture builds. For instance [Docker]({{<relref "/docs/pipeline-stages/builders/docker">}}) will build the image for the same architecture as the host machine, whereas [Buildpacks]({{<relref "/docs/pipeline-stages/builders/buildpacks">}}) will always build it for `amd64`.
+
+Additionally, the following combination of development machine and cluster node architectures can make it difficult to build and deploy images correctly:
+
+* Dev machine architecture is `amd64` while the target cluster runs `arm64` nodes.
+* Dev machine architecture is `arm64` (say Apple Silicon Macbooks) while the target cluster runs `amd64` nodes.
+* The target cluster runs both `arm64` and `amd64` nodes (mixed node pools).
+
+🎉 *Skaffold provides an opionated way to handle all these cases effectively.* 🎉
+
+## Skaffold can set the image architecture automatically
+
+When running Skaffold in an interactive mode like `skaffold dev`, `skaffold debug` or `skaffold run` where the intention is to build an image from the application code, and immediately deploy it to a Kubernetes cluster, Skaffold will check the active Kubernetes cluster node architecture and provide that as an argument to the respective image builder. If the cluster has multiple architecture nodes, then Skaffold will also create appropriate Kubernetes [`affinity`](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) rules so that the Kubernetes Pods with these images are assigned to matching architecture nodes.
+
+{{< alert title="Note" >}}
+Skaffold will create platform node `affinity` rules only for clusters having multiple architecture nodes. You can also force this using the flag `--enable-platform-node-affinity=true` to always create these affinity rules in the Kubernetes manifests for built images.
+{{< /alert >}}
+
+Let's test this in a [sample Golang](https://github.com/GoogleContainerTools/skaffold/tree/main/examples/cross-platform-builds) project:
+
+* The `skaffold.yaml` file defines a single [Docker build artifact](https://github.com/GoogleContainerTools/skaffold/blob/main/examples/cross-platform-builds/Dockerfile) and deploys it in a [`Kubernetes Pod`](https://github.com/GoogleContainerTools/skaffold/blob/main/examples/cross-platform-builds/k8s-pod.yaml).
+
+* First set the active Kubernetes context to a cluster having only `linux/amd64` nodes, and run:
+
+  ```cmd
+  skaffold dev --default-repo=your/container/registy
+  ```
+
+* Skaffold will detect the cluster node platform `linux/amd64` and build the image for this platform:
+
+  ```cmd
+  skaffold dev --default-repo=gcr.io/k8s-skaffold
+  Listing files to watch...
+  - skaffold-example
+  Generating tags...
+  - skaffold-example -> gcr.io/k8s-skaffold/skaffold-example:latest
+  Starting build...
+  Building [skaffold-example]...
+  Target platforms: [linux/amd64]
+  ...
+  Build [skaffold-example] succeeded
+  Starting deploy...
+  - pod/getting-started created
+  Waiting for deployments to stabilize...
+  - pods is ready.
+  Deployments stabilized in 7.42 seconds
+  Press Ctrl+C to exit
+  Watching for changes...
+  [getting-started] Hello world! Running on linux/amd64
+  ```
+
+* Now set the active Kubernetes context to a cluster containing only `linux/arm64` nodes. See [here](https://cloud.google.com/kubernetes-engine/docs/how-to/prepare-arm-workloads-for-deployment) to know how you can create an ARM GKE cluster.
+
+* Re-running the `dev` command will now build a `linux/arm64` image.
+
+  ```cmd
+  skaffold dev --default-repo=gcr.io/k8s-skaffold
+  ...
+  ...
+  [getting-started] Hello world! Running on linux/arm64
+  ```
+
+* Now set the active Kubernetes context to a cluster containing both `linux/arm64` and `linux/amd64` nodes. You can create a GKE cluster with 2 node pools, one having `linux/amd64` nodes, and the other having `linux/arm64` nodes.
+
+* Re-run the `dev` command but with an explicit platform target this time.
+
+  ```cmd
+  skaffold dev --default-repo=your/container/registy --platform=linux/amd64
+  ```
+
+* Skaffold will build a `linux/amd64` image and insert a `nodeAffinity` definition to the `Pod` so that it get scheduled on the matching architecture node.
+
+  ```cmd
+  skaffold dev --default-repo=gcr.io/k8s-skaffold --platform=linux/amd64
+  ...
+  ...
+  [getting-started] Hello world! Running on linux/amd64
+  ```
+
+* Validate that the `nodeAffinity` was applied by running the command (skip `| jq` if you don't have `jq` installed):
+
+  ```cmd
+  kubectl get pod getting-started  -o=jsonpath='{.spec.affinity}' | jq
+  {
+    "nodeAffinity": {
+      "requiredDuringSchedulingIgnoredDuringExecution": {
+        "nodeSelectorTerms": [
+          {
+            "matchExpressions": [
+              {
+                "key": "kubernetes.io/os",
+                "operator": "In",
+                "values": [
+                  "linux"
+                ]
+              },
+              {
+                "key": "kubernetes.io/arch",
+                "operator": "In",
+                "values": [
+                  "amd64"
+                ]
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }
+  ```
+
+This example will run the same whether you're using an `arm64` machine (say an Apple Silicon Macbook) or an `amd64` machine.
+
+Skaffold also supports cross-architecture builds on [Google Cloud Build](https://cloud.google.com/build). You can rerun this example, with the additional flag `--profile cloudbuild` to all the `dev` commands to build on `Google Cloud Build` instead of the local Docker daemon.
+
+## What about multi-arch images?
+
+A [multi-arch image](https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/) is an image that can support multiple architectures. It looks like a single image with a single tag, but is actually a list of images targeting multiple architectures organized by an [image index](https://github.com/opencontainers/image-spec/blob/main/image-index.md). When you deploy a multi-arch image to a cluster, the container runtime automatically chooses the right image that is compatible with the architecture of the node to which it is being deployed. This simplifies targeting multiple clusters of different architecture nodes, and/or mixed-architecture nodes.
+
+Skaffold supports building multi-platform images natively for the [jib builder]({{<relref "/docs/pipeline-stages/builders/jib" >}}), the [ko builder]({{<relref "/docs/pipeline-stages/builders/ko">}}) and the [custom builder]({{<relref "/docs/pipeline-stages/builders/custom" >}}). For other builders that support building cross-architecture images, Skaffold will iteratively build a single platform image for each target architecture and stitch them together into a multi-platform image, and push it to the registry.
+
+Let's test this in the same [sample Golang](https://github.com/GoogleContainerTools/skaffold/tree/main/examples/cross-platform-builds) project as before:
+
+* Run this command to build for the target architectures `linux/amd64` and `linux/arm64`:
+
+  ```cmd
+  skaffold dev -t latest --default-repo=your/container/registy --platform=linux/amd64,linux/arm64
+  ...
+  Building [skaffold-example]...
+  Target platforms: [linux/amd64,linux/arm64]
+  ...
+  [+] Building 0.3s (13/13) FINISHED
+  ...
+  => => writing image sha256:10af3142e460566f5791c48758f0040cef6932cbcb0766082dcbb0d8db7653e7
+  => => naming to gcr.io/k8s-skaffold/skaffold-example:latest_linux_amd64
+  ...
+  latest_linux_amd64: digest: sha256:15bd4f2380e99b3563f8add1aba9691e414d4cc5701363d9c74960a20fb276c4 size: 739
+  ...
+  [+] Building 52.8s (13/13) FINISHED
+  ...
+  => => writing image sha256:68866691e2d6f079b116e097ae4e67a53eaf89e825b52d6f31f2e9cc566974de
+  => => naming to gcr.io/k8s-skaffold/skaffold-example:latest_linux_arm64
+  ...
+  4e0c2525c370: Pushed
+  latest_linux_arm64: digest: sha256:868d0aec1cc7d2ed1fa1e840f38ff1aa50c3cc3d3232ea17a065618eaec4e82b size: 739
+  Build [skaffold-example] succeeded
+  ```
+
+* Validate that the image just built was multi-arch, by running the following `docker` command:
+
+  ```cmd
+  docker manifest inspect your/container/registry/skaffold-example:latest | grep -A 3 "platform"
+  ```
+
+  Outputs:
+
+  ```cmd
+    "platform": {
+        "architecture": "amd64",
+        "os": "linux"
+    }
+  --
+    "platform": {
+        "architecture": "arm64",
+        "os": "linux"
+    }
+  ```
+
+* Now if we render the Kubernetes Pod manifest for this multi-arch image, then it'll have platform affinity definition targeting both `linux/amd64` and `linux/arm64` architectures.
+
+  ```cmd
+  skaffold render --default-repo=your/container/registry --enable-platform-node-affinity
+  ```
+  
+  Outputs:
+
+  ```cmd
+  apiVersion: v1
+  kind: Pod
+  metadata:
+    name: getting-started
+    namespace: default
+  spec:
+    affinity:
+      nodeAffinity:
+        requiredDuringSchedulingIgnoredDuringExecution:
+          nodeSelectorTerms:
+          - matchExpressions:
+            - key: kubernetes.io/os
+              operator: In
+              values:
+              - linux
+            - key: kubernetes.io/arch
+              operator: In
+              values:
+              - amd64
+          - matchExpressions:
+            - key: kubernetes.io/os
+              operator: In
+              values:
+              - linux
+            - key: kubernetes.io/arch
+              operator: In
+              values:
+              - arm64
+    containers:
+    - image: gcr.io/k8s-skaffold/skaffold-example:latest@sha256:9ecf4e52f7ff64b35deacf9d6eedc03f35d69e0b4bf3679b97ba492f4389f784
+      name: getting-started
+  ```
+
+{{< alert title="Note" >}}
+
+* Multi-arch images need to be pushed to a container registry, as the local Docker deamon doesn't yet support storing multi-arch images.
+
+* For interactive modes like `skaffold dev` and `skaffold debug` requiring fast and repeated `build-render-deploy` iterations, Skaffold will choose only one build architecture and build a single-platform image, even if you specify multiple target platforms.
+
+* If you need to build a multi-arch image with an interactive mode then use `skaffold run`. This will build the multi-arch image and deploy it to the active Kubernetes cluster.
+{{< /alert >}}
diff --git a/docs-v2/content/en/schemas/v3alpha1.json b/docs-v2/content/en/schemas/v3alpha1.json
index 060a4403e..a94787ac5 100755
--- a/docs-v2/content/en/schemas/v3alpha1.json
+++ b/docs-v2/content/en/schemas/v3alpha1.json
@@ -1894,6 +1894,11 @@
           "x-intellij-html-description": "image that runs a Cloud Native Buildpacks build. See <a href=\"https://cloud.google.com/cloud-build/docs/cloud-builders\">Cloud Builders</a>.",
           "default": "gcr.io/k8s-skaffold/pack"
         },
+        "platformEmulatorInstallStep": {
+          "$ref": "#/definitions/PlatformEmulatorInstallStep",
+          "description": "specifies a pre-build step to install the required tooling for QEMU emulation on the GoogleCloudBuild containers. This enables performing cross-platform builds on GoogleCloudBuild. If unspecified, Skaffold uses the `docker/binfmt` image by default.",
+          "x-intellij-html-description": "specifies a pre-build step to install the required tooling for QEMU emulation on the GoogleCloudBuild containers. This enables performing cross-platform builds on GoogleCloudBuild. If unspecified, Skaffold uses the <code>docker/binfmt</code> image by default."
+        },
         "projectId": {
           "type": "string",
           "description": "ID of your Cloud Platform Project. If it is not provided, Skaffold will guess it from the image name. For example, given the artifact image name `gcr.io/myproject/image`, Skaffold will use the `myproject` GCP project.",
@@ -1930,7 +1935,8 @@
         "koImage",
         "concurrency",
         "workerPool",
-        "region"
+        "region",
+        "platformEmulatorInstallStep"
       ],
       "additionalProperties": false,
       "type": "object",
@@ -3139,6 +3145,41 @@
       "description": "describes a lifecycle hook definition to execute on a named container.",
       "x-intellij-html-description": "describes a lifecycle hook definition to execute on a named container."
     },
+    "PlatformEmulatorInstallStep": {
+      "required": [
+        "image"
+      ],
+      "properties": {
+        "args": {
+          "items": {
+            "type": "string"
+          },
+          "type": "array",
+          "description": "specifies arguments passed to the emulator installer image.",
+          "x-intellij-html-description": "specifies arguments passed to the emulator installer image.",
+          "default": "[]"
+        },
+        "entrypoint": {
+          "type": "string",
+          "description": "specifies the ENTRYPOINT argument to the emulator installer image.",
+          "x-intellij-html-description": "specifies the ENTRYPOINT argument to the emulator installer image."
+        },
+        "image": {
+          "type": "string",
+          "description": "specifies the image that will install the required tooling for QEMU emulation on the GoogleCloudBuild containers.",
+          "x-intellij-html-description": "specifies the image that will install the required tooling for QEMU emulation on the GoogleCloudBuild containers."
+        }
+      },
+      "preferredOrder": [
+        "image",
+        "args",
+        "entrypoint"
+      ],
+      "additionalProperties": false,
+      "type": "object",
+      "description": "specifies a pre-build step to install the required tooling for QEMU emulation on the GoogleCloudBuild containers. This enables performing cross-platform builds on GoogleCloudBuild.",
+      "x-intellij-html-description": "specifies a pre-build step to install the required tooling for QEMU emulation on the GoogleCloudBuild containers. This enables performing cross-platform builds on GoogleCloudBuild."
+    },
     "PortForwardResource": {
       "properties": {
         "address": {
diff --git a/docs-v2/data/maturity.json b/docs-v2/data/maturity.json
index ed2c91506..061a10c69 100644
--- a/docs-v2/data/maturity.json
+++ b/docs-v2/data/maturity.json
@@ -41,6 +41,18 @@
     "maturity": "GA",
     "description": "Define build artifact dependencies"
   },
+  "build.platforms": {
+    "dev": "x",
+    "build": "x",
+    "run": "x",
+    "debug": "x",
+    "render": "x",
+    "deploy": "x",
+    "area": "Build",
+    "feature": "Cross-arch and Multi-arch builds",
+    "maturity": "alpha",
+    "description": "Build cross-architecture and Multi-architecture images"
+  },
   "build.ko": {
     "dev": "x",
     "build": "x",
diff --git a/docs/content/en/docs/references/cli/_index.md b/docs/content/en/docs/references/cli/_index.md
index 361cadacb..142526fcb 100644
--- a/docs/content/en/docs/references/cli/_index.md
+++ b/docs/content/en/docs/references/cli/_index.md
@@ -414,7 +414,7 @@ Options:
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
-      --enable-platform-node-affinity=false: If true, skaffold will add platform (os/arch) node affinity to rendered manifests based on the image platforms
+      --enable-platform-node-affinity=true: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
       --hydration-dir='.kpt-pipeline': The directory to where the (kpt) hydration takes place. Default to a hidden directory .kpt-pipeline.
@@ -596,7 +596,7 @@ Options:
   -c, --config='': File for global configurations (defaults to $HOME/.skaffold/config)
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
-      --enable-platform-node-affinity=false: If true, skaffold will add platform (os/arch) node affinity to rendered manifests based on the image platforms
+      --enable-platform-node-affinity=false: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
       --hydration-dir='.kpt-pipeline': The directory to where the (kpt) hydration takes place. Default to a hidden directory .kpt-pipeline.
@@ -700,7 +700,7 @@ Options:
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
       --digest-source='': Set to 'remote' to skip builds and resolve the digest of images by tag from the remote registry. Set to 'local' to build images locally and use digests from built images. Set to 'tag' to use tags directly from the build. Set to 'none' to use tags directly from the Kubernetes manifests. If unspecified, defaults to 'remote' for remote clusters, and 'tag' for local clusters like kind or minikube.
-      --enable-platform-node-affinity=false: If true, skaffold will add platform (os/arch) node affinity to rendered manifests based on the image platforms
+      --enable-platform-node-affinity=true: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
       --hydration-dir='.kpt-pipeline': The directory to where the (kpt) hydration takes place. Default to a hidden directory .kpt-pipeline.
@@ -971,7 +971,7 @@ Options:
       --cache-artifacts=true: Set to false to disable default caching of artifacts
   -d, --default-repo='': Default repository value (overrides global config)
       --digest-source='': Set to 'remote' to skip builds and resolve the digest of images by tag from the remote registry. Set to 'local' to build images locally and use digests from built images. Set to 'tag' to use tags directly from the build. Set to 'none' to use tags directly from the Kubernetes manifests. If unspecified, defaults to 'remote' for remote clusters, and 'tag' for local clusters like kind or minikube.
-      --enable-platform-node-affinity=false: If true, skaffold will add platform (os/arch) node affinity to rendered manifests based on the image platforms
+      --enable-platform-node-affinity=false: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --hydration-dir='.kpt-pipeline': The directory to where the (kpt) hydration takes place. Default to a hidden directory .kpt-pipeline.
   -i, --images=: A list of pre-built images to deploy, either tagged images or NAME=TAG pairs
@@ -1049,7 +1049,7 @@ Options:
   -d, --default-repo='': Default repository value (overrides global config)
       --detect-minikube=true: Use heuristics to detect a minikube cluster
       --digest-source='': Set to 'remote' to skip builds and resolve the digest of images by tag from the remote registry. Set to 'local' to build images locally and use digests from built images. Set to 'tag' to use tags directly from the build. Set to 'none' to use tags directly from the Kubernetes manifests. If unspecified, defaults to 'remote' for remote clusters, and 'tag' for local clusters like kind or minikube.
-      --enable-platform-node-affinity=false: If true, skaffold will add platform (os/arch) node affinity to rendered manifests based on the image platforms
+      --enable-platform-node-affinity=true: If true, when deploying to a mixed node cluster, skaffold will add platform (os/arch) node affinity definition to rendered manifests based on the image platforms
   -f, --filename='skaffold.yaml': Path or URL to the Skaffold config file
       --force=false: Recreate Kubernetes resources if necessary for deployment, warning: might cause downtime!
       --hydration-dir='.kpt-pipeline': The directory to where the (kpt) hydration takes place. Default to a hidden directory .kpt-pipeline.
diff --git a/examples/buildpacks-python/requirements.txt b/examples/buildpacks-python/requirements.txt
index cf6ed5429..c9edc1a7f 100644
--- a/examples/buildpacks-python/requirements.txt
+++ b/examples/buildpacks-python/requirements.txt
@@ -1,1 +1,1 @@
-Flask==2.2.1
+Flask==2.2.2
diff --git a/examples/cross-platform-builds/Dockerfile b/examples/cross-platform-builds/Dockerfile
index 326833f37..67f8c3cbb 100644
--- a/examples/cross-platform-builds/Dockerfile
+++ b/examples/cross-platform-builds/Dockerfile
@@ -1,12 +1,10 @@
-FROM --platform=$BUILDPLATFORM golang:1.18 as builder
+FROM golang:1.18 as builder
 WORKDIR /code
 COPY main.go .
 COPY go.mod .
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
-ARG TARGETOS
-ARG TARGETARCH
 ARG SKAFFOLD_GO_GCFLAGS
-RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
 
 FROM alpine:3
 # Define GOTRACEBACK to mark this container as using the Go language runtime
diff --git a/examples/cross-platform-builds/skaffold.yaml b/examples/cross-platform-builds/skaffold.yaml
index bfad2ba76..1dee227e8 100644
--- a/examples/cross-platform-builds/skaffold.yaml
+++ b/examples/cross-platform-builds/skaffold.yaml
@@ -1,7 +1,6 @@
 apiVersion: skaffold/v2beta29
 kind: Config
 build:
-  platforms: ["linux/amd64"]
   artifacts:
   - image: skaffold-example
 deploy:
@@ -9,7 +8,6 @@ deploy:
     manifests:
       - k8s-*
 profiles:
-  - name: googleCloudBuild
+  - name: cloudbuild
     build:
-      googleCloudBuild:
-        projectId: k8s-skaffold
+      googleCloudBuild: {}
diff --git a/examples/docker-deploy/bert/Dockerfile b/examples/docker-deploy/bert/Dockerfile
index c01cebfca..8bea1d135 100644
--- a/examples/docker-deploy/bert/Dockerfile
+++ b/examples/docker-deploy/bert/Dockerfile
@@ -1,5 +1,6 @@
 FROM golang:1.18 as builder
 WORKDIR /code
+COPY test .
 COPY main.go .
 COPY go.mod .
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
diff --git a/integration/examples/buildpacks-python/requirements.txt b/integration/examples/buildpacks-python/requirements.txt
index cf6ed5429..c9edc1a7f 100644
--- a/integration/examples/buildpacks-python/requirements.txt
+++ b/integration/examples/buildpacks-python/requirements.txt
@@ -1,1 +1,1 @@
-Flask==2.2.1
+Flask==2.2.2
diff --git a/integration/examples/cross-platform-builds/Dockerfile b/integration/examples/cross-platform-builds/Dockerfile
index 326833f37..67f8c3cbb 100644
--- a/integration/examples/cross-platform-builds/Dockerfile
+++ b/integration/examples/cross-platform-builds/Dockerfile
@@ -1,12 +1,10 @@
-FROM --platform=$BUILDPLATFORM golang:1.18 as builder
+FROM golang:1.18 as builder
 WORKDIR /code
 COPY main.go .
 COPY go.mod .
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
-ARG TARGETOS
-ARG TARGETARCH
 ARG SKAFFOLD_GO_GCFLAGS
-RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -trimpath -o /app main.go
 
 FROM alpine:3
 # Define GOTRACEBACK to mark this container as using the Go language runtime
diff --git a/integration/examples/cross-platform-builds/skaffold.yaml b/integration/examples/cross-platform-builds/skaffold.yaml
index 2b8de1eec..3a3303969 100644
--- a/integration/examples/cross-platform-builds/skaffold.yaml
+++ b/integration/examples/cross-platform-builds/skaffold.yaml
@@ -8,10 +8,12 @@ build:
       dockerfile: Dockerfile
   tagPolicy:
     gitCommit: {}
-  platforms:
-  - linux/amd64
   local:
     concurrency: 1
 manifests:
   rawYaml:
   - k8s-*
+profiles:
+  - name: cloudbuild
+    build:
+      googleCloudBuild: {}
\ No newline at end of file
diff --git a/integration/testdata/getting-started/testdir/testfile b/integration/testdata/getting-started/testdir/testfile
deleted file mode 100644
index e69de29bb..000000000
diff --git a/pkg/skaffold/build/builder_mux.go b/pkg/skaffold/build/builder_mux.go
index d2bdfac2c..345bf9ebc 100644
--- a/pkg/skaffold/build/builder_mux.go
+++ b/pkg/skaffold/build/builder_mux.go
@@ -99,11 +99,18 @@ func (b *BuilderMux) Build(ctx context.Context, out io.Writer, tags tag.ImageTag
 			return "", err
 		}
 		r := hooks.BuildRunner(artifact.LifecycleHooks, hooksOpts)
-		var built string
 		if err = r.RunPreHooks(ctx, out); err != nil {
 			return "", err
 		}
-		if built, err = artifactBuilder(ctx, out, artifact, tag, platforms); err != nil {
+		var built string
+
+		if platforms.IsMultiPlatform() && !SupportsMultiPlatformBuild(*artifact) {
+			built, err = CreateMultiPlatformImage(ctx, out, artifact, tag, platforms, artifactBuilder)
+		} else {
+			built, err = artifactBuilder(ctx, out, artifact, tag, platforms)
+		}
+
+		if err != nil {
 			return "", err
 		}
 
@@ -117,8 +124,10 @@ func (b *BuilderMux) Build(ctx context.Context, out io.Writer, tags tag.ImageTag
 		if err = r.RunPostHooks(ctx, out); err != nil {
 			return "", err
 		}
+
 		return built, nil
 	}
+
 	ar, err := InOrder(ctx, out, tags, resolver, artifacts, builderF, b.concurrency, b.store)
 	if err != nil {
 		return nil, err
diff --git a/pkg/skaffold/build/gcb/docker.go b/pkg/skaffold/build/gcb/docker.go
index b70fc9f2c..9f0f5631d 100644
--- a/pkg/skaffold/build/gcb/docker.go
+++ b/pkg/skaffold/build/gcb/docker.go
@@ -21,6 +21,7 @@ import (
 	"fmt"
 	"os"
 
+	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 	cloudbuild "google.golang.org/api/cloudbuild/v1"
 
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
@@ -29,6 +30,10 @@ import (
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringslice"
 )
 
+var defaultPlatformEmulatorInstallStep = latest.PlatformEmulatorInstallStep{
+	Image: "docker/binfmt:a7996909642ee92942dcd6cff44b9b95f08dad64",
+}
+
 // dockerBuildSpec lists the build steps required to build a docker image.
 func (b *Builder) dockerBuildSpec(a *latest.Artifact, tag string, platforms platform.Matcher) (cloudbuild.Build, error) {
 	a = adjustCacheFrom(a, tag)
@@ -37,7 +42,9 @@ func (b *Builder) dockerBuildSpec(a *latest.Artifact, tag string, platforms plat
 	if err != nil {
 		return cloudbuild.Build{}, err
 	}
-	steps := b.cacheFromSteps(a.DockerArtifact, platforms)
+	var steps []*cloudbuild.BuildStep
+	steps = append(steps, platformEmulatorInstallStep(b.GoogleCloudBuild, platforms)...)
+	steps = append(steps, b.cacheFromSteps(a.DockerArtifact, platforms)...)
 	buildStep := &cloudbuild.BuildStep{
 		Name: b.DockerImage,
 		Args: args,
@@ -54,11 +61,27 @@ func (b *Builder) dockerBuildSpec(a *latest.Artifact, tag string, platforms plat
 	}, nil
 }
 
+func platformEmulatorInstallStep(cfg *latest.GoogleCloudBuild, p platform.Matcher) []*cloudbuild.BuildStep {
+	if !p.IsMultiPlatform() && (p.IsEmpty() || p.Contains(v1.Platform{Architecture: "amd64", OS: "linux"})) {
+		// if the build is not multi-platform, or it only targets linux/amd64, then skip platform emulator install step.
+		return nil
+	}
+	step := defaultPlatformEmulatorInstallStep
+	if cfg.PlatformEmulatorInstallStep != nil {
+		step = *cfg.PlatformEmulatorInstallStep
+	}
+	return []*cloudbuild.BuildStep{{
+		Name:       step.Image,
+		Args:       step.Args,
+		Entrypoint: step.Entrypoint,
+	}}
+}
+
 // cacheFromSteps pulls images used by `--cache-from`.
 func (b *Builder) cacheFromSteps(artifact *latest.DockerArtifact, platforms platform.Matcher) []*cloudbuild.BuildStep {
 	var steps []*cloudbuild.BuildStep
 	argFmt := "docker pull %s || true"
-	if platforms.IsNotEmpty() {
+	if !platforms.IsMultiPlatform() && platforms.IsNotEmpty() {
 		argFmt = "docker pull --platform " + platforms.String() + " %s || true"
 	}
 	for _, cacheFrom := range artifact.CacheFrom {
diff --git a/pkg/skaffold/build/platform.go b/pkg/skaffold/build/platform.go
new file mode 100644
index 000000000..ddfe2499e
--- /dev/null
+++ b/pkg/skaffold/build/platform.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2020 The Skaffold Authors
+
+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.
+*/
+
+package build
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"strings"
+
+	specs "github.com/opencontainers/image-spec/specs-go/v1"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
+)
+
+func SupportsMultiPlatformBuild(a latest.Artifact) bool {
+	switch {
+	case a.DockerArtifact != nil || a.BazelArtifact != nil || a.BuildpackArtifact != nil:
+		return false
+	case a.JibArtifact != nil || a.CustomArtifact != nil || a.KoArtifact != nil:
+		return true
+	default:
+		return false
+	}
+}
+
+func CreateMultiPlatformImage(ctx context.Context, out io.Writer, a *latest.Artifact, tag string, matcher platform.Matcher, ab ArtifactBuilder) (string, error) {
+	images, err := buildImageForPlatforms(ctx, out, a, tag, matcher, ab)
+	if err != nil {
+		return "", err
+	}
+
+	return docker.CreateManifestList(ctx, images, tag)
+}
+
+func buildImageForPlatforms(ctx context.Context, out io.Writer, a *latest.Artifact, tag string, matcher platform.Matcher, ab ArtifactBuilder) ([]docker.SinglePlatformImage, error) {
+	var images []docker.SinglePlatformImage
+
+	for _, p := range matcher.Platforms {
+		m := platform.Matcher{
+			All:       false,
+			Platforms: []specs.Platform{p},
+		}
+		tagWithPlatform := fmt.Sprintf("%s_%s", tag, strings.ReplaceAll(platform.Format(p), "/", "_"))
+		imageID, err := ab(ctx, out, a, tagWithPlatform, m)
+
+		if err != nil {
+			return nil, err
+		}
+
+		pl := util.ConvertToV1Platform(p)
+		images = append(images, docker.SinglePlatformImage{
+			Platform: &pl,
+			Image:    imageID,
+		})
+	}
+
+	return images, nil
+}
diff --git a/pkg/skaffold/config/types.go b/pkg/skaffold/config/types.go
index aa7a0e667..7a30d3983 100644
--- a/pkg/skaffold/config/types.go
+++ b/pkg/skaffold/config/types.go
@@ -172,8 +172,9 @@ func (m Muted) mute(phase string) bool {
 }
 
 type Cluster struct {
-	Local       bool
-	PushImages  bool
-	LoadImages  bool
-	DefaultRepo StringOrUndefined
+	Local           bool
+	PushImages      bool
+	LoadImages      bool
+	IsMixedPlatform bool
+	DefaultRepo     StringOrUndefined
 }
diff --git a/pkg/skaffold/config/util.go b/pkg/skaffold/config/util.go
index d325a6d5b..71d2bbda2 100644
--- a/pkg/skaffold/config/util.go
+++ b/pkg/skaffold/config/util.go
@@ -232,6 +232,7 @@ func GetCluster(ctx context.Context, opts GetClusterOpts) (Cluster, error) {
 
 	kubeContext := cfg.Kubecontext
 	isKindCluster, isK3dCluster := IsKindCluster(kubeContext), IsK3dCluster(kubeContext)
+	isMixedPlatform := IsMixedPlatformCluster(ctx, kubeContext)
 
 	var local bool
 	switch {
@@ -254,7 +255,6 @@ func GetCluster(ctx context.Context, opts GetClusterOpts) (Cluster, error) {
 	default:
 		local = false
 	}
-
 	var defaultRepo = opts.DefaultRepo
 
 	if local && defaultRepo.Value() == nil {
@@ -265,10 +265,11 @@ func GetCluster(ctx context.Context, opts GetClusterOpts) (Cluster, error) {
 		case registry != nil:
 			log.Entry(context.TODO()).Infof("using default-repo=%s from cluster configmap", *registry)
 			return Cluster{
-				Local:       local,
-				LoadImages:  false,
-				PushImages:  true,
-				DefaultRepo: NewStringOrUndefined(registry),
+				Local:           local,
+				LoadImages:      false,
+				PushImages:      true,
+				DefaultRepo:     NewStringOrUndefined(registry),
+				IsMixedPlatform: isMixedPlatform,
 			}, nil
 		}
 	}
@@ -283,13 +284,30 @@ func GetCluster(ctx context.Context, opts GetClusterOpts) (Cluster, error) {
 	pushImages := !local || (isKindCluster && kindDisableLoad) || (isK3dCluster && k3dDisableLoad)
 
 	return Cluster{
-		Local:       local,
-		LoadImages:  loadImages,
-		PushImages:  pushImages,
-		DefaultRepo: defaultRepo,
+		Local:           local,
+		LoadImages:      loadImages,
+		PushImages:      pushImages,
+		DefaultRepo:     defaultRepo,
+		IsMixedPlatform: isMixedPlatform,
 	}, nil
 }
 
+func IsMixedPlatformCluster(ctx context.Context, kubeContext string) bool {
+	client, err := kubeclient.Client(kubeContext)
+	if err != nil {
+		return false
+	}
+	nodes, err := client.CoreV1().Nodes().List(ctx, api_v1.ListOptions{})
+	if err != nil || nodes == nil {
+		return false
+	}
+	set := make(map[string]interface{})
+	for _, n := range nodes.Items {
+		set[fmt.Sprintf("%s/%s", n.Status.NodeInfo.OperatingSystem, n.Status.NodeInfo.Architecture)] = struct{}{}
+	}
+	return len(set) > 1
+}
+
 // IsKindCluster checks that the given `kubeContext` is talking to `kind`.
 func IsKindCluster(kubeContext string) bool {
 	switch {
diff --git a/pkg/skaffold/docker/dependencies.go b/pkg/skaffold/docker/dependencies.go
index 5cef07446..9ce5013d6 100644
--- a/pkg/skaffold/docker/dependencies.go
+++ b/pkg/skaffold/docker/dependencies.go
@@ -251,7 +251,7 @@ func readDockerignore(workspace string, absDockerfilePath string) ([]string, err
 }
 
 // WalkWorkspace walks the given host directories and records all files found.
-// Note: if you change this function, you might also want to modify `walkWorkspaceWithDestinations`.
+// Note: if you change this function, you might also want to modify walkWorkspaceWithDestinations.
 func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool, error) {
 	dockerIgnored, err := NewDockerIgnorePredicate(workspace, excludes)
 	if err != nil {
@@ -264,9 +264,9 @@ func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool,
 		absFrom := filepath.Join(workspace, dep)
 
 		keepFile := func(path string, info walk.Dirent) (bool, error) {
-			// Always keep root folders.
-			if info.IsDir() && path == absFrom {
-				return true, nil
+			// Ignore non empty dirs
+			if info.IsDir() && !util.IsEmptyDir(path) {
+				return false, nil
 			}
 
 			ignored, err := dockerIgnored(path, info)
@@ -277,7 +277,7 @@ func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool,
 			return !ignored, nil
 		}
 
-		if err := walk.From(absFrom).Unsorted().When(keepFile).WhenIsFile().Do(func(path string, info walk.Dirent) error {
+		if err := walk.From(absFrom).Unsorted().When(keepFile).Do(func(path string, info walk.Dirent) error {
 			relPath, err := filepath.Rel(workspace, path)
 			if err != nil {
 				return err
@@ -289,6 +289,5 @@ func WalkWorkspace(workspace string, excludes, deps []string) (map[string]bool,
 			return nil, fmt.Errorf("walking %q: %w", absFrom, err)
 		}
 	}
-
 	return files, nil
 }
diff --git a/pkg/skaffold/docker/manifest_list.go b/pkg/skaffold/docker/manifest_list.go
new file mode 100644
index 000000000..0b140bb64
--- /dev/null
+++ b/pkg/skaffold/docker/manifest_list.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2019 The Skaffold Authors
+
+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.
+*/
+
+package docker
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/google/go-containerregistry/pkg/name"
+	v1 "github.com/google/go-containerregistry/pkg/v1"
+	"github.com/google/go-containerregistry/pkg/v1/empty"
+	"github.com/google/go-containerregistry/pkg/v1/mutate"
+	"github.com/google/go-containerregistry/pkg/v1/remote"
+	"github.com/google/go-containerregistry/pkg/v1/types"
+
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
+)
+
+// Describes the result of an image build.
+type SinglePlatformImage struct {
+	// Platform (OS + architecture) associated with the image built.
+	Platform *v1.Platform
+
+	// Name of the image built.
+	Image string
+}
+
+// CreateManifestList returns a manifest list that contains the given images.
+func CreateManifestList(ctx context.Context, images []SinglePlatformImage, targetTag string) (string, error) {
+	adds := make([]mutate.IndexAddendum, len(images))
+
+	for i, image := range images {
+		ref, err := name.ParseReference(image.Image, name.WeakValidation)
+		if err != nil {
+			return "", err
+		}
+
+		img, err := remote.Image(ref)
+		if err != nil {
+			return "", err
+		}
+
+		adds[i] = mutate.IndexAddendum{
+			Add: img,
+			Descriptor: v1.Descriptor{
+				Platform: image.Platform,
+			},
+		}
+	}
+	idx := mutate.AppendManifests(mutate.IndexMediaType(empty.Index, types.DockerManifestList), adds...)
+	targetRef, err := name.ParseReference(targetTag, name.WeakValidation)
+	if err != nil {
+		return "", err
+	}
+
+	err = remote.WriteIndex(targetRef, idx, remote.WithAuthFromKeychain(primaryKeychain))
+	if err != nil {
+		return "", err
+	}
+
+	h, err := idx.Digest()
+	if err != nil {
+		return "", err
+	}
+
+	dig := h.String()
+	log.Entry(ctx).Printf("Created ManifestList for image %s. Digest: %s\n", targetRef, dig)
+	parsed, err := ParseReference(targetTag)
+	if err != nil {
+		return "", err
+	}
+
+	// TODO: manifestlists are only supported in a container registry so we
+	// return the fully qualified image name with tag and digest. When the local
+	// docker daemon can support multi-platform images, we'll have to return the
+	// image with imageID.
+	return fmt.Sprintf("%s:%s@%s", parsed.BaseName, parsed.Tag, dig), nil
+}
diff --git a/pkg/skaffold/docker/syncmap.go b/pkg/skaffold/docker/syncmap.go
index 0d4f0601a..3cb9032e3 100644
--- a/pkg/skaffold/docker/syncmap.go
+++ b/pkg/skaffold/docker/syncmap.go
@@ -23,6 +23,7 @@ import (
 	"path"
 	"path/filepath"
 
+	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
 	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/walk"
 )
 
@@ -76,9 +77,9 @@ func walkWorkspaceWithDestinations(workspace string, excludes []string, fts []Fr
 		switch mode := fi.Mode(); {
 		case mode.IsDir():
 			keepFile := func(path string, info walk.Dirent) (bool, error) {
-				// Always keep root folders.
-				if info.IsDir() && path == absFrom {
-					return true, nil
+				// Ignore non empty dirs
+				if info.IsDir() && !util.IsEmptyDir(path) {
+					return false, nil
 				}
 
 				ignored, err := dockerIgnored(path, info)
diff --git a/pkg/skaffold/platform/platform.go b/pkg/skaffold/platform/platform.go
index 0b91dbae7..99a228121 100644
--- a/pkg/skaffold/platform/platform.go
+++ b/pkg/skaffold/platform/platform.go
@@ -90,6 +90,15 @@ func (m Matcher) Intersect(other Matcher) Matcher {
 	return res
 }
 
+// Contains returns if the Matcher contains the other platform
+func (m Matcher) Contains(other specs.Platform) bool {
+	if m.All {
+		return true
+	}
+	matcher := platforms.Any(m.Platforms...)
+	return matcher.Match(other)
+}
+
 func Parse(ps []string) (Matcher, error) {
 	var sl []specs.Platform
 	for _, p := range ps {
diff --git a/pkg/skaffold/runner/runcontext/v2/context.go b/pkg/skaffold/runner/runcontext/v2/context.go
index 94bae7814..d55d4043f 100644
--- a/pkg/skaffold/runner/runcontext/v2/context.go
+++ b/pkg/skaffold/runner/runcontext/v2/context.go
@@ -280,7 +280,7 @@ func (rc *RunContext) JSONParseConfig() latest.JSONParseConfig {
 	return rc.DefaultPipeline().Deploy.Logs.JSONParse
 }
 func (rc *RunContext) EnablePlatformNodeAffinityInRenderedManifests() bool {
-	return rc.Opts.EnablePlatformNodeAffinity
+	return rc.Opts.EnablePlatformNodeAffinity && rc.Cluster.IsMixedPlatform
 }
 
 // GetRenderConfig returns the top tier RenderConfig.
diff --git a/pkg/skaffold/util/util.go b/pkg/skaffold/util/util.go
index baeb49d8f..98dd26799 100644
--- a/pkg/skaffold/util/util.go
+++ b/pkg/skaffold/util/util.go
@@ -23,6 +23,7 @@ import (
 	"crypto/rand"
 	"encoding/json"
 	"fmt"
+	"io"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -287,6 +288,19 @@ func IsDir(path string) bool {
 	return (err == nil || !os.IsNotExist(err)) && info.IsDir()
 }
 
+// IsEmptyDir returns true for empty directories otherwise false
+func IsEmptyDir(path string) bool {
+	d, err := os.Open(path)
+	if err != nil {
+		return false
+	}
+	defer d.Close()
+	if _, err := d.ReadDir(1); err == io.EOF {
+		return true
+	}
+	return false
+}
+
 // IsHiddenDir returns if a directory is hidden.
 func IsHiddenDir(filename string) bool {
 	// Return false for current dir


Test patch:
diff --git a/integration/run_test.go b/integration/run_test.go
index b623c53ff..7a09b32c4 100644
--- a/integration/run_test.go
+++ b/integration/run_test.go
@@ -18,6 +18,7 @@ package integration
 
 import (
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
@@ -28,6 +29,10 @@ import (
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
+const (
+	emptydir = "testdata/empty-dir"
+)
+
 // Note: `custom-buildx` is not included as it depends on having a
 // `skaffold-builder` builder configured and a registry to push to.
 // TODO: remove nolint once we've reenabled integration tests
@@ -42,14 +47,14 @@ var tests = []struct {
 	targetLog   string
 }{
 	{
-		description: "copying directory",
-		dir:         "examples/getting-started",
-		pods:        []string{"getting-started"},
+		description: "copying-empty-directory",
+		dir:         emptydir,
+		pods:        []string{"empty-dir"},
 		targetLog:   "Hello world!",
 	},
 	{
 		description: "getting-started",
-		dir:         "testdata/getting-started",
+		dir:         "examples/getting-started",
 		pods:        []string{"getting-started"},
 		targetLog:   "Hello world!",
 	},
@@ -172,8 +177,14 @@ func TestRun(t *testing.T) {
 	for _, test := range tests {
 		t.Run(test.description, func(t *testing.T) {
 			ns, client := SetupNamespace(t)
-
 			args := append(test.args, "--cache-artifacts=false")
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
 			skaffold.Run(args...).InDir(test.dir).InNs(ns.Name).WithEnv(test.env).RunOrFail(t)
 
 			client.WaitForPodsReady(test.pods...)
@@ -192,6 +203,13 @@ func TestRunTail(t *testing.T) {
 			if test.targetLog == "" {
 				t.SkipNow()
 			}
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
 			ns, _ := SetupNamespace(t)
 
 			args := append(test.args, "--tail")
@@ -212,6 +230,13 @@ func TestRunTailDefaultNamespace(t *testing.T) {
 			if test.targetLog == "" {
 				t.SkipNow()
 			}
+			if test.dir == emptydir {
+				err := os.MkdirAll(filepath.Join(test.dir, "emptydir"), 0755)
+				t.Log("Creating empty directory")
+				if err != nil {
+					t.Errorf("Error creating empty dir: %s", err)
+				}
+			}
 
 			args := append(test.args, "--tail")
 			out := skaffold.Run(args...).InDir(test.dir).WithEnv(test.env).RunLive(t)
diff --git a/integration/testdata/empty-dir/Dockerfile b/integration/testdata/empty-dir/Dockerfile
new file mode 100644
index 000000000..cf73acf30
--- /dev/null
+++ b/integration/testdata/empty-dir/Dockerfile
@@ -0,0 +1,13 @@
+FROM golang:1.15 as builder
+COPY main.go .
+COPY emptydir .
+# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
+ARG SKAFFOLD_GO_GCFLAGS
+RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o /app main.go
+
+FROM alpine:3
+# Define GOTRACEBACK to mark this container as using the Go language runtime
+# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
+ENV GOTRACEBACK=single
+CMD ["./app"]
+COPY --from=builder /app .
diff --git a/integration/testdata/empty-dir/README.md b/integration/testdata/empty-dir/README.md
new file mode 100644
index 000000000..efcec7466
--- /dev/null
+++ b/integration/testdata/empty-dir/README.md
@@ -0,0 +1,9 @@
+### Example: Getting started with a simple go app
+
+[![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://ssh.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/GoogleContainerTools/skaffold&cloudshell_open_in_editor=README.md&cloudshell_workspace=examples/getting-started)
+
+This is a simple example based on:
+
+* **building** a single Go file app and with a multistage `Dockerfile` using local docker to build
+* **tagging** using the default tagPolicy (`gitCommit`)
+* **deploying** a single container pod using `kubectl`
\ No newline at end of file
diff --git a/integration/testdata/empty-dir/k8s-pod.yaml b/integration/testdata/empty-dir/k8s-pod.yaml
new file mode 100644
index 000000000..e86cc049f
--- /dev/null
+++ b/integration/testdata/empty-dir/k8s-pod.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Pod
+metadata:
+  name: empty-dir
+spec:
+  containers:
+  - name: empty-dir
+    image: skaffold-example
diff --git a/integration/testdata/empty-dir/main.go b/integration/testdata/empty-dir/main.go
new file mode 100644
index 000000000..593721cfe
--- /dev/null
+++ b/integration/testdata/empty-dir/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+	"fmt"
+	"time"
+)
+
+func main() {
+	for {
+		fmt.Println("Hello world!")
+
+		time.Sleep(time.Second * 1)
+	}
+}
diff --git a/integration/testdata/empty-dir/skaffold.yaml b/integration/testdata/empty-dir/skaffold.yaml
new file mode 100644
index 000000000..7a42ccfa3
--- /dev/null
+++ b/integration/testdata/empty-dir/skaffold.yaml
@@ -0,0 +1,8 @@
+apiVersion: skaffold/v3alpha1
+kind: Config
+build:
+  artifacts:
+  - image: skaffold-example
+manifests:
+  rawYaml:
+    - k8s-*
diff --git a/integration/testdata/getting-started/Dockerfile b/integration/testdata/getting-started/Dockerfile
index 7936c8f6e..fb06046e6 100644
--- a/integration/testdata/getting-started/Dockerfile
+++ b/integration/testdata/getting-started/Dockerfile
@@ -1,6 +1,5 @@
 FROM golang:1.15 as builder
 COPY main.go .
-COPY testdir .
 
 # `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
 ARG SKAFFOLD_GO_GCFLAGS
diff --git a/pkg/skaffold/build/gcb/docker_test.go b/pkg/skaffold/build/gcb/docker_test.go
index ad5994b13..b29cc30e9 100644
--- a/pkg/skaffold/build/gcb/docker_test.go
+++ b/pkg/skaffold/build/gcb/docker_test.go
@@ -157,6 +157,8 @@ func TestDockerBuildSpec(t *testing.T) {
 					},
 				},
 				Steps: []*cloudbuild.BuildStep{{
+					Name: defaultPlatformEmulatorInstallStep.Image,
+				}, {
 					Name: "docker/docker",
 					Args: []string{"build", "--tag", "nginx", "-f", "Dockerfile", "--platform", "freebsd/arm", "--build-arg", "arg1=value1", "--build-arg", "arg2", "."},
 					Env:  []string{"DOCKER_BUILDKIT=1"},
@@ -269,6 +271,8 @@ func TestPullCacheFrom(t *testing.T) {
 			tag:       "nginx2",
 			platforms: platform.Matcher{Platforms: []v1.Platform{{Architecture: "arm", OS: "freebsd"}}},
 			expected: []*cloudbuild.BuildStep{{
+				Name: defaultPlatformEmulatorInstallStep.Image,
+			}, {
 				Name:       "docker/docker",
 				Entrypoint: "sh",
 				Args:       []string{"-c", "docker pull --platform freebsd/arm from/image1 || true"},
diff --git a/pkg/skaffold/config/util_test.go b/pkg/skaffold/config/util_test.go
index a55589746..016094ab6 100644
--- a/pkg/skaffold/config/util_test.go
+++ b/pkg/skaffold/config/util_test.go
@@ -25,6 +25,7 @@ import (
 	"testing"
 	"time"
 
+	"github.com/google/uuid"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -446,6 +447,56 @@ func TestIsKindCluster(t *testing.T) {
 	}
 }
 
+func TestIsMixedPlatformCluster(t *testing.T) {
+	type platform struct {
+		os   string
+		arch string
+	}
+	tests := []struct {
+		description string
+		nodes       []platform
+		expected    bool
+	}{
+		{
+			description: "no nodes",
+			expected:    false,
+		},
+		{
+			description: "single platform nodes",
+			nodes: []platform{
+				{os: "linux", arch: "amd64"},
+				{os: "linux", arch: "amd64"},
+			},
+			expected: false,
+		},
+		{
+			description: "mixed platform nodes",
+			nodes: []platform{
+				{os: "linux", arch: "amd64"},
+				{os: "linux", arch: "amd64"},
+				{os: "linux", arch: "arm64"},
+			},
+			expected: true,
+		},
+	}
+	for _, test := range tests {
+		testutil.Run(t, test.description, func(t *testutil.T) {
+			nodes := &v1.NodeList{}
+			for _, n := range test.nodes {
+				nodes.Items = append(nodes.Items, v1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: uuid.New().String(),
+					},
+					Status: v1.NodeStatus{NodeInfo: v1.NodeSystemInfo{MachineID: uuid.New().String(), Architecture: n.arch, OperatingSystem: n.os}},
+				})
+			}
+			cl := fake.NewSimpleClientset(nodes)
+			t.Override(&kubeclient.Client, func(kubeContext string) (kubernetes.Interface, error) { return cl, nil })
+			t.CheckDeepEqual(test.expected, IsMixedPlatformCluster(context.Background(), ""))
+		})
+	}
+}
+
 func TestKindClusterName(t *testing.T) {
 	tests := []struct {
 		kubeCluster  string
diff --git a/pkg/skaffold/docker/dependencies_test.go b/pkg/skaffold/docker/dependencies_test.go
index 0df3eb893..bdd91f79e 100644
--- a/pkg/skaffold/docker/dependencies_test.go
+++ b/pkg/skaffold/docker/dependencies_test.go
@@ -30,6 +30,11 @@ import (
 	"github.com/GoogleContainerTools/skaffold/testutil"
 )
 
+const copyEmptydirectory = `
+FROM ubuntu:14.04
+COPY emptydir .
+`
+
 const copyServerGo = `
 FROM ubuntu:14.04
 COPY server.go .
@@ -283,6 +288,12 @@ func TestGetDependencies(t *testing.T) {
 		expected  []string
 		shouldErr bool
 	}{
+		{
+			description: "copy empty directory",
+			dockerfile:  copyEmptydirectory,
+			workspace:   ".",
+			expected:    []string{"Dockerfile", "emptydir"},
+		},
 		{
 			description: "buildkit dockerfile",
 			dockerfile:  buildKitDockerfile,
@@ -380,7 +391,7 @@ func TestGetDependencies(t *testing.T) {
 		{
 			description: "dockerignore test",
 			dockerfile:  copyDirectory,
-			ignore:      "bar\ndocker/*",
+			ignore:      "emptydir\nbar\ndocker/*",
 			workspace:   ".",
 			expected:    []string{".dot", "Dockerfile", "file", "server.go", "test.conf", "worker.go"},
 		},
@@ -402,40 +413,40 @@ func TestGetDependencies(t *testing.T) {
 			description: "ignore none",
 			dockerfile:  copyAll,
 			workspace:   ".",
-			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "ignore dotfiles",
 			dockerfile:  copyAll,
 			workspace:   ".",
 			ignore:      ".*",
-			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "ignore dotfiles (root syntax)",
 			dockerfile:  copyAll,
 			workspace:   ".",
 			ignore:      "/.*",
-			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{"Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "dockerignore with context in parent directory",
 			dockerfile:  copyDirectory,
 			workspace:   "docker/..",
-			ignore:      "bar\ndocker\n*.go",
+			ignore:      "emptydir\nbar\ndocker\n*.go",
 			expected:    []string{".dot", "Dockerfile", "file", "test.conf"},
 		},
 		{
 			description: "onbuild test",
 			dockerfile:  onbuild,
 			workspace:   ".",
-			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "file", "server.go", "test.conf", "worker.go"},
+			expected:    []string{".dot", "Dockerfile", "bar", filepath.Join("docker", "bar"), filepath.Join("docker", "nginx.conf"), "emptydir", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
 			description: "onbuild with dockerignore",
 			dockerfile:  onbuild,
 			workspace:   ".",
-			ignore:      "bar\ndocker/*",
+			ignore:      "emptydir\nbar\ndocker/*",
 			expected:    []string{".dot", "Dockerfile", "file", "server.go", "test.conf", "worker.go"},
 		},
 		{
@@ -579,7 +590,7 @@ func TestGetDependencies(t *testing.T) {
 			description:    "find specific dockerignore",
 			dockerfile:     copyDirectory,
 			workspace:      ".",
-			ignore:         "bar\ndocker/*",
+			ignore:         "emptydir\nbar\ndocker/*",
 			ignoreFilename: "Dockerfile.dockerignore",
 			expected:       []string{".dot", "Dockerfile", "Dockerfile.dockerignore", "file", "server.go", "test.conf", "worker.go"},
 		},
@@ -605,6 +616,7 @@ func TestGetDependencies(t *testing.T) {
 
 			tmpDir := t.NewTempDir().
 				Touch("docker/nginx.conf", "docker/bar", "server.go", "test.conf", "worker.go", "bar", "file", ".dot")
+			tmpDir.Mkdir("emptydir")
 			if test.dockerfile != "" {
 				tmpDir.Write(test.workspace+"/Dockerfile", test.dockerfile)
 			}
@@ -622,7 +634,6 @@ func TestGetDependencies(t *testing.T) {
 				mode: config.RunModes.Dev,
 			}
 			deps, err := GetDependencies(context.Background(), NewBuildConfig(workspace, "test", "Dockerfile", test.buildArgs), m)
-
 			t.CheckError(test.shouldErr, err)
 			t.CheckDeepEqual(test.expected, deps)
 		})
diff --git a/pkg/skaffold/schema/latest/config.go b/pkg/skaffold/schema/latest/config.go
index 73e0139c9..7d3524533 100644
--- a/pkg/skaffold/schema/latest/config.go
+++ b/pkg/skaffold/schema/latest/config.go
@@ -402,6 +402,20 @@ type GoogleCloudBuild struct {
 	// the build will be run in global(non-regional).
 	// See [Cloud Build locations](https://cloud.google.com/build/docs/locations).
 	Region string `yaml:"region,omitempty"`
+
+	// PlatformEmulatorInstallStep specifies a pre-build step to install the required tooling for QEMU emulation on the GoogleCloudBuild containers. This enables performing cross-platform builds on GoogleCloudBuild.
+	// If unspecified, Skaffold uses the `docker/binfmt` image by default.
+	PlatformEmulatorInstallStep *PlatformEmulatorInstallStep `yaml:"platformEmulatorInstallStep,omitempty"`
+}
+
+// PlatformEmulatorInstallStep specifies a pre-build step to install the required tooling for QEMU emulation on the GoogleCloudBuild containers. This enables performing cross-platform builds on GoogleCloudBuild.
+type PlatformEmulatorInstallStep struct {
+	// Image specifies the image that will install the required tooling for QEMU emulation on the GoogleCloudBuild containers.
+	Image string `yaml:"image" yamltags:"required"`
+	// Args specifies arguments passed to the emulator installer image.
+	Args []string `yaml:"args,omitempty"`
+	// Entrypoint specifies the ENTRYPOINT argument to the emulator installer image.
+	Entrypoint string `yaml:"entrypoint,omitempty"`
 }
 
 // KanikoCache configures Kaniko caching. If a cache is specified, Kaniko will
diff --git a/pkg/skaffold/util/util_test.go b/pkg/skaffold/util/util_test.go
index 112175c22..4ecd1f173 100644
--- a/pkg/skaffold/util/util_test.go
+++ b/pkg/skaffold/util/util_test.go
@@ -449,3 +449,12 @@ func TestIsURL(t *testing.T) {
 func stringPointer(s string) *string {
 	return &s
 }
+
+func TestIsEmptyDir(t *testing.T) {
+	tmpDir := testutil.NewTempDir(t).Touch("file")
+	emptyTmpDir := testutil.NewTempDir(t)
+
+	testutil.CheckDeepEqual(t, true, IsEmptyDir(emptyTmpDir.Root()))
+	testutil.CheckDeepEqual(t, false, IsEmptyDir(tmpDir.Root()))
+	testutil.CheckDeepEqual(t, false, IsEmptyDir(filepath.Join(tmpDir.Root(), "file")))
+}