Repository: skypilot-org/skypilot
Pull request: [UX] Support `--infra` option and deprecate `cloud/region/zone`
<!-- Describe the changes in this PR -->
This PR updates the UX for the output, and add support for infra

Also, fixes #5600

## Main changes
1. Support `--infra` and deprecate `--cloud`, `--region`, and `--zone`
2. Show `INFRA` in outputs instead of cloud/region/zone
3. Reorder/polishing table outputs

```
sky launch --infra aws/us-east-1                                                                                 
Considered resources (1 node):
------------------------------------------------------------------------------
 INFRA             INSTANCE      vCPUs   Mem(GB)   GPUS   COST ($)   CHOSEN   
------------------------------------------------------------------------------
 AWS (us-east-1)   m6i.2xlarge   8       32        -      0.38          ✔     
------------------------------------------------------------------------------
Launching a new cluster 'sky-472f-zhwu'. Proceed? [Y/n]:
```

```
sky launch --infra gcp                                                                                           ✭ ✱
Considered resources (1 node):
------------------------------------------------------------------------------------
 INFRA                 INSTANCE        vCPUs   Mem(GB)   GPUS   COST ($)   CHOSEN   
------------------------------------------------------------------------------------
 GCP (us-central1-a)   n2-standard-8   8       32        -      0.39          ✔     
------------------------------------------------------------------------------------
Launching a new cluster 'sky-c16f-zhwu'. Proceed? [Y/n]:
```

```
sky launch --cloud k8s                                                                                           ✭ ✱
The --cloud, --region, and --zone options are deprecated. Use --infra instead.
Considered resources (1 node):
-------------------------------------------------------------------------------------------
 INFRA                                   INSTANCE   vCPUs   Mem(GB)   GPUS   COST ($)   CHOSEN   
-------------------------------------------------------------------------------------------
 Kubernetes (gke_sky-dev...ypilotalpha)  -          2       2         -      0.00          ✔     
-------------------------------------------------------------------------------------------
Launching a new cluster 'sky-5d56-zhwu'. Proceed? [Y/n]:
```

## Optimizer table
Original
```
sky launch --gpus H100:8
Considered resources (1 node):
-------------------------------------------------------------------------------------------------------------------------------------
 CLOUD        INSTANCE            vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE            COST ($)   CHOSEN   
-------------------------------------------------------------------------------------------------------------------------------------
 Kubernetes   2CPU--2GB--8H100    2       8         H100:8         gke_sky-dev-xxx        0.00       ✔     
 RunPod       8x_H100_SECURE      128     640       H100:8         CA                     19.12               
 Lambda       gpu_8x_h100_sxm5    208     1800      H100:8         us-east-1              23.92               
 GCP          a3-highgpu-8g       208     1872      H100:8         us-central1-a          46.02               
 AWS          p5.48xlarge         192     2048      H100:8         us-east-1              98.32               
-------------------------------------------------------------------------------------------------------------------------------------
Launching a new cluster 'sky-b84f-zhwu'. Proceed? [Y/n]:
```
New
```
sky launch --gpus H100:8                                                                                                                   
Considered resources (1 node):
---------------------------------------------------------------------------------------------------------
 INFRA                                    INSTANCE           vCPUs   Mem(GB)   GPUS     COST ($)   CHOSEN   
---------------------------------------------------------------------------------------------------------
 Kubernetes (gke_sky-dev...ypilotalpha)   -                  2       8         H100:8   0.00          ✔     
 RunPod (CA)                              8x_H100_SECURE     128     640       H100:8   19.12               
 Lambda (us-east-1)                       gpu_8x_h100_sxm5   208     1800      H100:8   23.92               
 GCP (us-central1-a)                      a3-highgpu-8g      208     1872      H100:8   46.02               
 AWS (us-east-1)                          p5.48xlarge        192     2048      H100:8   98.32               
---------------------------------------------------------------------------------------------------------
Launching a new cluster 'sky-3d09-zhwu'. Proceed? [Y/n]:
```


## Status table
Original:
```
sky status -u                                                                                                                                              
Clusters
NAME                          USER  LAUNCHED     RESOURCES                                                                  STATUS   AUTOSTOP  COMMAND                       
test-normal                   zhwu  17 hrs ago   1x GCP(n2-standard-2)                                                      UP       -         sky launch -c test-normal...  
dashboard-workspace           zhwu  6 days ago   1x GCP(n2-standard-4, ports=['46580'])                                     INIT     -         sky launch -c dashboard-w...  
benchmark                     zhwu  2 weeks ago  1x Nebius(gpu-h200-sxm_8gpu-128vcpu-1600gb, mem=750+, {'H200': 8}, dis...  STOPPED  -         sky exec -c benchmark qwe...  
sky-jobs-controller-9ce1ce58  zhwu  14 hrs ago   1x GCP(n2-highmem-4, disk_size=50)                                         STOPPED  10m       sky jobs queue -r   
```

New:
```
sky status -u                                                                                                         
Clusters
NAME                          USER  INFRA                            RESOURCES                                      STATUS   AUTOSTOP  LAUNCHED     
sky-f4e8-zhwu                 zhwu  Kubernetes (gke_sky-dev-xxx...)  1x(gpus=H100:1, cpus=2, mem=8, ...)            UP       -         2 hrs ago    
test-normal                   zhwu  GCP (us-central1-a)              1x(cpus=2, mem=8, type=n2-standard-2, ...)     UP       -         21 hrs ago   
dashboard-workspace           zhwu  GCP (us-central1-a)              1x(cpus=4, mem=16, type=n2-standard-4, ...)    INIT     -         7 days ago   
benchmark                     zhwu  Nebius (eu-west1)                1x(gpus=H200:8, type=gpu-h200-sxm_8g..., ...)  STOPPED  -         2 weeks ago  
sky-jobs-controller-9ce1ce58  zhwu  GCP (us-central1-a)              1x(cpus=4, mem=32, type=n2-highmem-4, ...)     STOPPED  10m       17 hrs ago   
```

## `sky check`
Original
```
To enable a cloud, follow the hints above and rerun: sky check
If any problems remain, refer to detailed docs at: https://docs.skypilot.co/en/latest/getting-started/installation.html

🎉 Enabled clouds 🎉
  AWS [compute, storage]
  GCP [compute, storage]
  Kubernetes [compute]
    Active context: gke_sky-dev-xxx
  Lambda [compute]
  RunPod [compute]

Using SkyPilot API server: http://127.0.0.1:46580
```

New
```
To enable a cloud, follow the hints above and rerun: sky check
If any problems remain, refer to detailed docs at: https://docs.skypilot.co/en/latest/getting-started/installation.html

🎉 Enabled infra 🎉
  AWS [compute, storage]
  GCP [compute, storage]
  Kubernetes [compute]
    Active context: gke_sky-dev-xxx
  Lambda [compute]
  RunPod [compute]

Using SkyPilot API server: http://127.0.0.1:46580
```

## `sky show-gpus`

```
sky show-gpus --infra k8s/my-h100-cluster                                                                                                       
Kubernetes GPUs
Context: my-h100-cluster
GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION  
H100  1, 2, 4, 8                5 of 8 free  
H200  1, 2, 4, 8                8 of 8 free  
Kubernetes per-node GPU availability
CONTEXT          NODE                                GPU   UTILIZATION  
my-h100-cluster  gke-xxx-default-pool-ff931856-6uvd  -     0 of 0 free  
my-h100-cluster  gke-xxx-largecpu-05dae726-1usy      H100  5 of 8 free  
my-h100-cluster  gke-xxx-largecpu-05dae726-4rxa      H200  8 of 8 free
```

## Dashboard

Original
<img width="1707" alt="image" src="https://github.com/user-attachments/assets/d7f82129-ef9e-482c-9f04-7055e288b64e" />


New
<img width="1691" alt="image" src="https://github.com/user-attachments/assets/76716e8a-fbcf-4b98-a0be-de80f58b83a3" />


### Cluster
Original
![image](https://github.com/user-attachments/assets/7f0eeaef-fd01-48de-ae8b-10cc0b61d1cf)


New
<img width="1706" alt="image" src="https://github.com/user-attachments/assets/c325874c-4f5f-492d-9efa-26ce59cc1900" />
<img width="1709" alt="image" src="https://github.com/user-attachments/assets/6f7133b1-3eba-4e1f-bf3f-30952c224c20" />




## TODOs
- [x] Update `sky check`
- [x] Update `sky show-gpus`
- [x] Update smoke tests
- [x] Check ordered and any_of logic
- [x] Update docs with the new table
- [x] Test with an old jobs controller
- [x] Test with old YAML that has `cloud`, `region`, `zone` only


## Future TODOs

- [ ] Update the doc to infra centric instead of cloud/region centric (cc'ing @concretevitamin)

<!-- Describe the tests ran -->
<!-- Unit tests (tests/test_*.py) are part of GitHub CI; below are tests that launch on the cloud. -->

Tested (run the relevant ones):

- [x] Code formatting: install pre-commit (auto-check on commit) or `bash format.sh`
- [ ] Any manual or new tests for this PR (please specify below)
  - [x] Old controller, `jobs queue`, `jobs logs`, `jobs cancel` without `sky jobs launch` first
- [ ] All smoke tests: `/smoke-test` (CI) or `pytest tests/test_smoke.py` (local)
  - [x] `/smoke-test --aws` [4b79915](https://github.com/skypilot-org/skypilot/pull/5602/commits/4b799157940b27b9f77b6308f2d6069c3403a0e7)
  - [x] `/smoke-test --gcp` [4b79915](https://github.com/skypilot-org/skypilot/pull/5602/commits/4b799157940b27b9f77b6308f2d6069c3403a0e7) (except for `test_tpu`)
  - [x] `/smoke-test --kubernetes` [583360f](https://github.com/skypilot-org/skypilot/pull/5602/commits/583360f9670b667bd32795bf7d705776d5ae4901)
  - [x] `/smoke-test --azure` [be75cbe](https://github.com/skypilot-org/skypilot/pull/5602/commits/be75cbe33f8b6cf9c21e58ed8e8d757b0e829aa4)
- [ ] Relevant individual tests: `/smoke-test -k test_name` (CI) or `pytest tests/test_smoke.py::test_name` (local)
- [x] Backward compatibility: `/quicktest-core` (CI) or `pytest tests/smoke_tests/test_backward_compat.py` (local)
  - [x] `/quicktest-core` passed on [e39e339](https://github.com/skypilot-org/skypilot/pull/5602/commits/e39e3396de60a4fe6b1aaf003b61c67fec2b69f7)

<!-- CI commands (/-prefixed) can only be triggered by repo members -->


Problem statement:
[UX] Failover reason table has too narrow width
```
resources:
  accelerators: { H100:8, A100-80GB:8 }
  cpus: 32+
  disk_size: 512  # Ensure model checkpoints can fit.
  disk_tier: best
  ports: 8081  # Expose to internet traffic.
```
Launching the above on RunPod:

```
⨯ Failed to provision resources. View logs: sky api logs -l sky-2025-05-16-08-45-14-060233/provision.log
sky.exceptions.ResourcesUnavailableError: Failed to provision all possible launchable resources. Relax the task's resource requirements: 1x RunPod(cpus=32+, {'H100': 8}, disk_tier=best, disk_size=512, ports=['8081'])
To keep retrying until the cluster is up, use the `--retry-until-up` flag.
Reasons for provision failures (for details, please check the log above):
Resource                 Reason
RunPod(8x_A100-          Failed to acquire resources in all zones in CA for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in CZ for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in IS for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in NL for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in NO for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in RO for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in SE for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_A100-          Failed to acquire resources in all zones in US for
80GB_SECURE,             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
{'A100-80GB': 8},        disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_tier=best,          {'A100-80GB': 8}, disk_tier=best, disk_size=512,
disk_size=512,           ports=['8081'])}.
ports=['8081'])
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in CA for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in CZ for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in IS for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in NL for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in NO for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in RO for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in SE for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
RunPod(8x_H100_SECURE,   Failed to acquire resources in all zones in US for
{'H100': 8},             {RunPod(cpus=32+, {'H100': 8}, disk_tier=best,
disk_tier=best,          disk_size=512, ports=['8081']), RunPod(cpus=32+,
disk_size=512,           {'A100-80GB': 8}, disk_tier=best, disk_size=512,
ports=['8081'])          ports=['8081'])}.
```
The table should have more human-readable widths.

Hints from issue discussion:

Commit message: Add escape

Patch to review:
diff --git a/docs/source/cloud-setup/cloud-permissions/aws.rst b/docs/source/cloud-setup/cloud-permissions/aws.rst
index 91e4eea2d5d..8a25b67756d 100644
--- a/docs/source/cloud-setup/cloud-permissions/aws.rst
+++ b/docs/source/cloud-setup/cloud-permissions/aws.rst
@@ -90,10 +90,10 @@ Example of mixing the default profile and another profile:
 .. code-block:: console
 
     $ # A cluster launched under the default AWS identity.
-    $ sky launch --cloud aws -c default
+    $ sky launch --infra aws -c default
 
     $ # A cluster launched under a different profile.
-    $ AWS_PROFILE=AdministratorAccess-12345 sky launch --cloud aws -c other-profile-cluster
+    $ AWS_PROFILE=AdministratorAccess-12345 sky launch --infra aws -c other-profile-cluster
 
 If you are using a :ref:`remote API server <sky-api-server>`, the AWS credentials are configured on the remote server. Overriding ``AWS_PROFILE`` on the client side won't work.
 
diff --git a/docs/source/cloud-setup/cloud-permissions/gcp.rst b/docs/source/cloud-setup/cloud-permissions/gcp.rst
index 40e42ee6d78..f0a2bbb34be 100644
--- a/docs/source/cloud-setup/cloud-permissions/gcp.rst
+++ b/docs/source/cloud-setup/cloud-permissions/gcp.rst
@@ -69,7 +69,7 @@ The easiest way to grant permissions to a user access your GCP project without t
   roles/iam.securityAdmin
 
 .. note::
-    If the ``roles/iam.securityAdmin`` role is undesirable, you can do the following. First, include the role and have any user (e.g., the admin) run ``sky launch --cloud gcp`` successfully once. This is to create the necessary service account. Then, replace the role ``roles/iam.securityAdmin`` with ``roles/iam.roleViewer`` in the list above.
+    If the ``roles/iam.securityAdmin`` role is undesirable, you can do the following. First, include the role and have any user (e.g., the admin) run ``sky launch --infra gcp`` successfully once. This is to create the necessary service account. Then, replace the role ``roles/iam.securityAdmin`` with ``roles/iam.roleViewer`` in the list above.
 
 
 Optionally, to use TPUs, add the following role:
diff --git a/docs/source/cloud-setup/quota.rst b/docs/source/cloud-setup/quota.rst
index f30862b75fd..ce2e76ed327 100644
--- a/docs/source/cloud-setup/quota.rst
+++ b/docs/source/cloud-setup/quota.rst
@@ -17,7 +17,7 @@ AWS
 
 1. Go to the `EC2 Quotas console <https://console.aws.amazon.com/servicequotas/home/services/ec2/quotas>`_.
 2. **Select a region** on the top right.
-3. Choose an EC2 instance type from the list (e.g, ``Running On-Demand P instances`` or ``All P Spot Instance Requests``). Use ``sky show-gpus --cloud aws --all`` or check `here <https://aws.amazon.com/ec2/instance-types/>`__ for more instance types.
+3. Choose an EC2 instance type from the list (e.g, ``Running On-Demand P instances`` or ``All P Spot Instance Requests``). Use ``sky show-gpus --infra aws --all`` or check `here <https://aws.amazon.com/ec2/instance-types/>`__ for more instance types.
 4. Click the quota name, and then choose **Request quota increase**.
 5. For **Change quota value**, enter the new value.
 6. Choose **Request**.
@@ -57,7 +57,7 @@ OCI
 1. Go to the `OCI Limits, Quotas and Usage console <https://cloud.oracle.com/limits>`_ to check your current resources status.
 2. Click the **request a service limit increase** link on the page if you want to increase quotas.
 3. Choose a **Service Category** from the list (e.g, ``Compute``). 
-4. Choose a **Resource** from the list (e.g, ``GPUs for GPU.A10 based VM and BM Instances``). Use ``sky show-gpus --cloud oci --all`` or check `here <https://docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm>`__ for more instance types.
+4. Choose a **Resource** from the list (e.g, ``GPUs for GPU.A10 based VM and BM Instances``). Use ``sky show-gpus --infra oci --all`` or check `here <https://docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm>`__ for more instance types.
 5. Enter the **Limit** field for your new limit and **Reason for request** for justification.
 6. Click **Create Support Request** to submit.
 7. You may check `OCI Service Limits <https://docs.oracle.com/en-us/iaas/Content/General/Concepts/servicelimits.htm#computelimits>`_ for more information.
diff --git a/docs/source/compute/gpus.rst b/docs/source/compute/gpus.rst
index 4c30021b7ba..3fbcd583cbc 100644
--- a/docs/source/compute/gpus.rst
+++ b/docs/source/compute/gpus.rst
@@ -26,7 +26,7 @@ You can query the accelerators available in your Kubernetes clusters with:
 
 .. code-block:: console
 
-   $ sky show-gpus --cloud k8s
+   $ sky show-gpus --infra k8s
 
 
 .. code-block:: text
diff --git a/docs/source/examples/auto-failover.rst b/docs/source/examples/auto-failover.rst
index 596e9d2c415..b01988f01a7 100644
--- a/docs/source/examples/auto-failover.rst
+++ b/docs/source/examples/auto-failover.rst
@@ -91,11 +91,11 @@ GCP, where it succeeded after one region:
 
   Considered resources (1 node):
   ----------------------------------------------------------------------------------------------------
-   CLOUD   INSTANCE              vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE     COST ($)   CHOSEN
+   INFRA                    INSTANCE              vCPUs   Mem(GB)   GPUS     COST ($)   CHOSEN
   ----------------------------------------------------------------------------------------------------
-   Azure   Standard_ND96asr_v4   96      900       A100:8         eastus          27.20         ✔
-   GCP     a2-highgpu-8g         96      680       A100:8         us-central1-a   29.39
-   AWS     p4d.24xlarge          96      1152      A100:8         us-east-1       32.77
+   Azure (eastus)           Standard_ND96asr_v4   96      900       A100:8   27.20         ✔
+   GCP (us-central1-a)      a2-highgpu-8g         96      680       A100:8   29.39
+   AWS (us-east-1)          p4d.24xlarge          96      1152      A100:8   32.77
   ----------------------------------------------------------------------------------------------------
   Launching a new cluster 'a100-8'. Proceed? [Y/n]:
 
@@ -135,11 +135,11 @@ A10, L4, and A10g GPUs, using :code:`sky launch task.yaml`.
   $ sky launch task.yaml
   ...
   -----------------------------------------------------------------------------------------------------
-   CLOUD   INSTANCE                 vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE   COST ($)   CHOSEN
+   INFRA                  INSTANCE                 vCPUs   Mem(GB)   GPUS     COST ($)   CHOSEN
   -----------------------------------------------------------------------------------------------------
-   Azure   Standard_NV6ads_A10_v5   6       55        A10:1          eastus        0.45          ✔
-   GCP     g2-standard-4            4       16        L4:1           us-east4-a    0.70
-   AWS     g5.xlarge                4       16        A10G:1         us-east-1     1.01
+   Azure (eastus)         Standard_NV6ads_A10_v5   6       55        A10:1    0.45          ✔
+   GCP (us-east4-a)       g2-standard-4            4       16        L4:1     0.70
+   AWS (us-east-1)        g5.xlarge                4       16        A10G:1   1.01
   -----------------------------------------------------------------------------------------------------
 
 
@@ -165,11 +165,10 @@ If a task would like to specify multiple candidate resources (not only GPUs), th
 
   resources:
     ordered: # Candidate resources in a preference order
-      - cloud: gcp
+      - infra: gcp
         accelerators: A100-80GB
       - instance_type: g5.xlarge
-      - cloud: azure
-        region: eastus
+      - infra: azure/eastus
         accelerators: A100
 
 
@@ -178,11 +177,10 @@ If a task would like to specify multiple candidate resources (not only GPUs), th
 
     resources:
       any_of: # Candidate resources that can be chosen in any order
-        - cloud: gcp
+        - infra: gcp
           accelerators: A100-80GB
         - instance_type: g5.xlarge
-        - cloud: azure
-          region: eastus
+        - infra: azure/eastus
           accelerators: A100
 
 .. tip::
@@ -198,18 +196,18 @@ If a task would like to specify multiple candidate resources (not only GPUs), th
     accelerators: {A10g:8, A10:8, L4:8, A100:8}
     any_of:
       # AWS:
-      - region: us-east-1
-      - region: us-east-2
-      - region: us-west-1
-      - region: us-west-2
+      - infra: aws/us-east-1
+      - infra: aws/us-east-2
+      - infra: aws/us-west-1
+      - infra: aws/us-west-2
       # GCP
-      - region: us-central1
-      - region: us-east1
-      - region: us-east4
-      - region: us-west1
-      - region: us-west2
-      - region: us-west3
-      - region: us-west4
+      - infra: gcp/us-central1
+      - infra: gcp/us-east1
+      - infra: gcp/us-east4
+      - infra: gcp/us-west1
+      - infra: gcp/us-west2
+      - infra: gcp/us-west3
+      - infra: gcp/us-west4
 
 .. hint::
 
@@ -224,12 +222,12 @@ This will generate the following output:
 
   Considered resources (1 node):
   ---------------------------------------------------------------------------------------------
-   CLOUD   INSTANCE         vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE   COST ($)   CHOSEN
+   INFRA                  INSTANCE         vCPUs   Mem(GB)   GPUS     COST ($)   CHOSEN
   ---------------------------------------------------------------------------------------------
-   GCP     g2-standard-96   96      384       L4:8           us-east4-a    7.98          ✔
-   AWS     g5.48xlarge      192     768       A10G:8         us-east-1     16.29
-   GCP     a2-highgpu-8g    96      680       A100:8         us-east1-b    29.39
-   AWS     p4d.24xlarge     96      1152      A100:8         us-east-1     32.77
+   GCP (us-east4-a)       g2-standard-96   96      384       L4:8     7.98          ✔
+   AWS (us-east-1)        g5.48xlarge      192     768       A10G:8   16.29
+   GCP (us-east1-b)       a2-highgpu-8g    96      680       A100:8   29.39
+   AWS (us-east-1)        p4d.24xlarge     96      1152      A100:8   32.77
   ---------------------------------------------------------------------------------------------
 
   Launching a new cluster 'mycluster'. Proceed? [Y/n]:
diff --git a/docs/source/examples/managed-jobs.rst b/docs/source/examples/managed-jobs.rst
index 5d033fbd3f7..2365a88203d 100644
--- a/docs/source/examples/managed-jobs.rst
+++ b/docs/source/examples/managed-jobs.rst
@@ -19,9 +19,9 @@ To start a managed job, use :code:`sky jobs launch`:
   Managed job 'myjob' will be launched on (estimated):
   Considered resources (1 node):
   ------------------------------------------------------------------------------------------
-   CLOUD   INSTANCE      vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE   COST ($)   CHOSEN
+   INFRA              INSTANCE      vCPUs   Mem(GB)   GPUS   COST ($)   CHOSEN
   ------------------------------------------------------------------------------------------
-   AWS     m6i.2xlarge   8       32        -              us-east-1     0.38          ✔
+   AWS (us-east-1)    m6i.2xlarge   8       32        -      0.38          ✔
   ------------------------------------------------------------------------------------------
   Launching a managed job 'myjob'. Proceed? [Y/n]: Y
   ... <job is submitted and launched>
@@ -446,7 +446,7 @@ To submit the pipeline, the same command :code:`sky jobs launch` is used. The pi
   Fetching managed job statuses...
   Managed jobs
   In progress jobs: 1 RECOVERING
-  ID  TASK  NAME      RESOURCES                    SUBMITTED    TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
+  ID  TASK  NAME      REQUESTED                    SUBMITTED    TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
   8         pipeline  -                            50 mins ago  47m 45s        -             1            RECOVERING
    ↳  0     train     1x [V100:8][Spot|On-demand]  50 mins ago  47m 45s        -             1            RECOVERING
    ↳  1     eval      1x [T4:1]                    -            -              -             0            PENDING
@@ -560,8 +560,7 @@ To achieve the above, you can specify custom configs in :code:`~/.sky/config.yam
       resources:
         # All configs below are optional.
         # Specify the location of the jobs controller.
-        cloud: gcp
-        region: us-central1
+        infra: gcp/us-central1
         # Bump cpus to allow more managed jobs to be launched concurrently. (Default: 4+)
         cpus: 8+
         # Bump memory to allow more managed jobs to be running at once.
@@ -584,10 +583,10 @@ To see your current jobs controller, use :code:`sky status`.
   $ sky status --refresh
 
   Clusters
-  NAME                          LAUNCHED     RESOURCES                          STATUS   AUTOSTOP  COMMAND
-  my-cluster-1                  1 week ago   1x AWS(m6i.4xlarge)                STOPPED  -         sky launch --cpus 16 --cloud...
-  my-other-cluster              1 week ago   1x GCP(n2-standard-16)             STOPPED  -         sky launch --cloud gcp --...
-  sky-jobs-controller-919df126  1 day ago    1x AWS(r6i.xlarge, disk_size=50)   STOPPED  10m       sky jobs launch --cpus 2 ...
+  NAME                          INFRA             RESOURCES                                  STATUS   AUTOSTOP  LAUNCHED     
+  my-cluster-1                  AWS (us-east-1)   1x(cpus=16, type=m6i.4xlarge)              STOPPED  -         1 week ago   
+  my-other-cluster              GCP (us-central1) 1x(cpus=16, type=n2-standard-16)           STOPPED  -         1 week ago   
+  sky-jobs-controller-919df126  AWS (us-east-1)   1x(cpus=2, type=r6i.xlarge, disk_size=50)  STOPPED  10m       1 day ago    
 
   Managed jobs
   No in-progress managed jobs.
@@ -642,7 +641,7 @@ For maximum parallelism, the following configuration is recommended:
     controller:
       resources:
         # In our testing, aws > gcp > azure
-        cloud: aws
+        infra: aws
         cpus: 128
         # Azure does not have 128+ CPU instances, so use 96 instead
         # cpus: 96
diff --git a/docs/source/getting-started/installation.rst b/docs/source/getting-started/installation.rst
index c14c1718a48..58a34e8f787 100644
--- a/docs/source/getting-started/installation.rst
+++ b/docs/source/getting-started/installation.rst
@@ -21,7 +21,7 @@ Install SkyPilot using pip:
           conda create -y -n sky python=3.10
           conda activate sky
 
-          # Choose your cloud:
+          # Choose your infra:
 
           pip install "skypilot[kubernetes]"
           pip install "skypilot[aws]"
@@ -50,7 +50,7 @@ Install SkyPilot using pip:
           conda create -y -n sky python=3.10
           conda activate sky
 
-          # Choose your cloud:
+          # Choose your infra:
 
           pip install "skypilot-nightly[kubernetes]"
           pip install "skypilot-nightly[aws]"
@@ -83,7 +83,7 @@ Install SkyPilot using pip:
           git clone https://github.com/skypilot-org/skypilot.git
           cd skypilot
 
-          # Choose your cloud:
+          # Choose your infra:
 
           pip install -e ".[kubernetes]"
           pip install -e ".[aws]"
diff --git a/docs/source/getting-started/quickstart.rst b/docs/source/getting-started/quickstart.rst
index 2a7fecf7709..e8e54b88c80 100644
--- a/docs/source/getting-started/quickstart.rst
+++ b/docs/source/getting-started/quickstart.rst
@@ -32,7 +32,7 @@ Copy the following YAML into a ``hello_sky.yaml`` file:
 
   resources:
     # Optional; if left out, automatically pick the cheapest cloud.
-    cloud: aws
+    infra: aws
     # 8x NVIDIA A100 GPU
     accelerators: A100:8
 
@@ -126,9 +126,9 @@ This may show multiple clusters, if you have created several:
 
 .. code-block::
 
-  NAME       LAUNCHED     RESOURCES                          COMMAND                            STATUS
-  mygcp      1 day ago    1x GCP(n1-highmem-8)               sky launch -c mygcp --cloud gcp    STOPPED
-  mycluster  4 mins ago   1x AWS(p4d.24xlarge, {'A100': 8})  sky exec mycluster hello_sky.yaml  UP
+  NAME       INFRA                RESOURCES                                      STATUS   AUTOSTOP  LAUNCHED     
+  mygcp      GCP (us-central1-a)  1x(cpus=4, mem=16, type=n2-standard-4, ...)    STOPPED  -         1 day ago   
+  mycluster  AWS (us-east-1)      1x(gpus=A100:8, type=p4d.24xlarge, ...)        UP       -         4 mins ago  
 
 See here for a list of all possible :ref:`cluster states <sky-status>`.
 
diff --git a/docs/source/reference/api-server/api-server.rst b/docs/source/reference/api-server/api-server.rst
index 42fe4b43b31..b8a4fbe7021 100644
--- a/docs/source/reference/api-server/api-server.rst
+++ b/docs/source/reference/api-server/api-server.rst
@@ -128,16 +128,16 @@ To see other users' clusters and the job/serve controllers, use the ``-u`` flag.
 
     $ sky status -u
     Clusters
-    NAME                          USER        LAUNCHED      RESOURCES                         STATUS   AUTOSTOP  COMMAND
-    my-cluster-2                  my-user     2 hrs ago     1x GCP(n2-standard-8)             STOPPED  -         sky launch task-2.yaml
-    other-cluster                 other-user  1 week ago    1x AWS(m6i.16xlarge)              UP       -         sky launch --cloud aws...
-    my-cluster-1                  my-user     2 months ago  1x AWS(m6i.4xlarge)               STOPPED  -         sky launch task-1.yaml
-    sky-jobs-controller-7c3d4ff7  root        2 days ago    1x AWS(r6i.xlarge, disk_size=50)  STOPPED  10m       sky jobs launch --env PART...
+    NAME                          USER        LAUNCHED      INFRA                 RESOURCES                                     STATUS   AUTOSTOP
+    my-cluster-2                  my-user     2 hrs ago     GCP (us-central1-a)   1x(cpus=8, mem=32, type=n2-standard-8, ...)   STOPPED  -       
+    other-cluster                 other-user  1 week ago    AWS (us-east-1)       1x(cpus=64, mem=256, type=m6i.16xlarge, ...)  UP       -       
+    my-cluster-1                  my-user     2 months ago  AWS (us-east-1)       1x(cpus=16, mem=64, type=m6i.4xlarge, ...)    STOPPED  -       
+    sky-jobs-controller-7c3d4ff7  root        2 days ago    AWS (us-east-1)       1x(cpus=4, mem=32, type=r6i.xlarge, ...)      STOPPED  10m     
 
     $ sky jobs queue -u
     Fetching managed job statuses...
     Managed jobs
-    ID  TASK  NAME       USER        RESOURCES  SUBMITTED   TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
+    ID  TASK  NAME       USER        REQUESTED  SUBMITTED   TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
     3   -     job-2      my-user     1x[CPU:2]  2 days ago  2m 10s         1m 14s        0            CANCELLED
     2   -     other-job  other-user  1x[CPU:2]  2 days ago  11m 54s        10m 52s       0            CANCELLED
     1   -     job-1      my-use      1x[CPU:2]  5 days ago  1m 7s          3s            0            SUCCEEDED
diff --git a/docs/source/reference/async.rst b/docs/source/reference/async.rst
index 32316bfbfe1..1ce2e6015ae 100644
--- a/docs/source/reference/async.rst
+++ b/docs/source/reference/async.rst
@@ -38,10 +38,10 @@ For example, when a user runs ``sky launch -c my-cluster``, the following output
     $ sky launch -c my-cluster --cpus 2
     Considered resources (1 node):
     ---------------------------------------------------------------------------------------------
-    CLOUD        INSTANCE    vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE   COST ($)   CHOSEN
+    INFRA                   INSTANCE      vCPUs   Mem(GB)   GPUS      COST ($)   CHOSEN
     ---------------------------------------------------------------------------------------------
-    Kubernetes   2CPU--2GB   2       2         -              in-cluster    0.00          ✔
-    AWS          m6i.large   2       8         -              us-east-1     0.10
+    Kubernetes (my-cluster) 2CPU--2GB     2       2         -         0.00       ✔
+    AWS (us-east-1)         m6i.large     2       8         -         0.098     
     ---------------------------------------------------------------------------------------------
     Launching a new cluster 'my-cluster'. Proceed? [Y/n]:
     ⚙︎ Launching on Kubernetes.
diff --git a/docs/source/reference/auto-stop.rst b/docs/source/reference/auto-stop.rst
index f034c520d3b..9f5778aa82a 100644
--- a/docs/source/reference/auto-stop.rst
+++ b/docs/source/reference/auto-stop.rst
@@ -58,15 +58,15 @@ To view the status of the cluster, use ``sky status [--refresh]``:
 .. code-block:: bash
 
    $ sky status
-   NAME         LAUNCHED    RESOURCES            STATUS   AUTOSTOP       COMMAND
-   mycluster    1 min ago   2x AWS(m4.2xlarge)   UP       10 min         sky launch -d -c ...
-   mycluster2   1 min ago   2x AWS(m4.2xlarge)   UP       10 min(down)   sky launch -d -c ...
+   NAME         INFRA           RESOURCES                     STATUS   AUTOSTOP       LAUNCHED
+   mycluster    AWS (us-east-1) 2x(cpus=8, type=m4.2xlarge)   UP       10 min         1 min ago
+   mycluster2   AWS (us-east-1) 2x(cpus=8, type=m4.2xlarge)   UP       10 min(down)   1 min ago
 
    # Refresh the statuses by querying the cloud providers
    $ sky status --refresh
    I 06-27 13:36:11 backend_utils.py:2273] Autodowned cluster: mycluster2
-   NAME         LAUNCHED    RESOURCES            STATUS   AUTOSTOP  COMMAND
-   mycluster    11 min ago  2x AWS(m4.2xlarge)   STOPPED  10 min    sky launch -d -c ...
+   NAME         INFRA           RESOURCES                     STATUS   AUTOSTOP  LAUNCHED
+   mycluster    AWS (us-east-1) 2x(cpus=8, type=m4.2xlarge)   STOPPED  10 min    11 min ago
 
 Note that :code:`sky status` shows the cached statuses, which can be outdated for clusters with autostop/autodown scheduled.
 To query the latest statuses of those clusters, use :code:`sky status --refresh`.
diff --git a/docs/source/reference/config.rst b/docs/source/reference/config.rst
index 58bb7b66094..18460060836 100644
--- a/docs/source/reference/config.rst
+++ b/docs/source/reference/config.rst
@@ -37,8 +37,7 @@ Below is the configuration syntax and some example values. See detailed explanat
     :ref:`bucket <config-yaml-jobs-bucket>`: s3://my-bucket/
     controller:
       :ref:`resources <config-yaml-jobs-controller-resources>`:  # same spec as 'resources' in a task YAML
-        cloud: gcp
-        region: us-central1
+        infra: gcp/us-central1
         cpus: 4+  # number of vCPUs, max concurrent spot jobs = 2 * cpus
         disk_size: 100
       :ref:`autostop <config-yaml-jobs-controller-autostop>`:
@@ -214,8 +213,7 @@ Example:
     controller:
       resources:  # same spec as 'resources' in a task YAML
         # optionally set specific cloud/region
-        cloud: gcp
-        region: us-central1
+        infra: gcp/us-central1
         # default resources:
         cpus: 4+
         memory: 8x
diff --git a/docs/source/reference/kubernetes/kubernetes-deployment.rst b/docs/source/reference/kubernetes/kubernetes-deployment.rst
index 3324999007f..d42826e5b63 100644
--- a/docs/source/reference/kubernetes/kubernetes-deployment.rst
+++ b/docs/source/reference/kubernetes/kubernetes-deployment.rst
@@ -143,11 +143,11 @@ Deploying on Google Cloud GKE
 
      $ sky check
 
-5. [If using GPUs] Check available GPUs in the kubernetes cluster with :code:`sky show-gpus --cloud k8s`
+5. [If using GPUs] Check available GPUs in the kubernetes cluster with :code:`sky show-gpus --infra k8s`
 
    .. code-block:: console
 
-       $ sky show-gpus --cloud k8s
+       $ sky show-gpus --infra k8s
        GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION
        L4    1, 2, 4                   6 of 8 free
        A100  1, 2                      2 of 4 free
@@ -198,11 +198,11 @@ Deploying on Amazon EKS
 
      $ sky check
 
-5. [If using GPUs] Check available GPUs in the kubernetes cluster with :code:`sky show-gpus --cloud k8s`
+5. [If using GPUs] Check available GPUs in the kubernetes cluster with :code:`sky show-gpus --infra k8s`
 
    .. code-block:: console
 
-       $ sky show-gpus --cloud k8s
+       $ sky show-gpus --infra k8s
        GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION
        A100  1, 2                      2 of 2 free
 
diff --git a/docs/source/reference/kubernetes/kubernetes-getting-started.rst b/docs/source/reference/kubernetes/kubernetes-getting-started.rst
index b6ef3fba103..d6da95398f1 100644
--- a/docs/source/reference/kubernetes/kubernetes-getting-started.rst
+++ b/docs/source/reference/kubernetes/kubernetes-getting-started.rst
@@ -111,15 +111,15 @@ Once your cluster administrator has :ref:`setup a Kubernetes cluster <kubernetes
 
         Considered resources (1 node):
         ---------------------------------------------------------------------------------------------------
-         CLOUD        INSTANCE          vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE   COST ($)   CHOSEN
+         INFRA                        INSTANCE          vCPUs   Mem(GB)   GPUS     COST ($)   CHOSEN
         ---------------------------------------------------------------------------------------------------
-         Kubernetes   2CPU--2GB         2       2         -              kubernetes    0.00          ✔
-         AWS          m6i.large         2       8         -              us-east-1     0.10
-         Azure        Standard_D2s_v5   2       8         -              eastus        0.10
-         GCP          n2-standard-2     2       8         -              us-central1   0.10
-         IBM          bx2-8x32          8       32        -              us-east       0.38
-         Lambda       gpu_1x_a10        30      200       A10:1          us-east-1     0.60
-        ---------------------------------------------------------------------------------------------------.
+         Kubernetes (kind-skypilot)   -                 2       2         -        0.00          ✔
+         AWS (us-east-1)              m6i.large         2       8         -        0.10
+         Azure (eastus)               Standard_D2s_v5   2       8         -        0.10
+         GCP (us-central1-a)          n2-standard-2     2       8         -        0.10
+         IBM (us-east)                bx2-8x32          8       32        -        0.38
+         Lambda (us-east-1)           gpu_1x_a10        30      200       A10:1    0.60
+        ----------------------------------------------------------------------------------------------------
 
 
 .. note::
@@ -152,28 +152,28 @@ Unlike :code:`sky status` which lists only the SkyPilot resources launched by th
     $ sky status --k8s
     Kubernetes cluster state (context: mycluster)
     SkyPilot clusters
-    USER     NAME                           LAUNCHED    RESOURCES                                  STATUS
-    alice    infer-svc-1                    23 hrs ago  1x Kubernetes(cpus=1, mem=1, {'L4': 1})    UP
-    alice    sky-jobs-controller-80b50983   2 days ago  1x Kubernetes(cpus=4, mem=4)               UP
-    alice    sky-serve-controller-80b50983  23 hrs ago  1x Kubernetes(cpus=4, mem=4)               UP
-    bob      dev                            1 day ago   1x Kubernetes(cpus=2, mem=8, {'H100': 1})  UP
-    bob      multinode-dev                  1 day ago   2x Kubernetes(cpus=2, mem=2)               UP
-    bob      sky-jobs-controller-2ea485ea   2 days ago  1x Kubernetes(cpus=4, mem=4)               UP
+    USER     NAME                           LAUNCHED    INFRA      RESOURCES                                      STATUS
+    alice    infer-svc-1                    23 hrs ago  Kubernetes 1x(cpus=1, mem=1, L4:1)                       UP
+    alice    sky-jobs-controller-80b50983   2 days ago  Kubernetes 1x(cpus=4, mem=4)                             UP
+    alice    sky-serve-controller-80b50983  23 hrs ago  Kubernetes 1x(cpus=4, mem=4)                             UP
+    bob      dev                            1 day ago   Kubernetes 1x(cpus=2, mem=8, H100:1)                     UP
+    bob      multinode-dev                  1 day ago   Kubernetes 2x(cpus=2, mem=2)                             UP
+    bob      sky-jobs-controller-2ea485ea   2 days ago  Kubernetes 1x(cpus=4, mem=4)                             UP
 
     Managed jobs
     In progress tasks: 1 STARTING
-    USER     ID  TASK  NAME      RESOURCES   SUBMITTED   TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
+    USER     ID  TASK  NAME      REQUESTED   SUBMITTED   TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
     alice    1   -     eval      1x[CPU:1+]  2 days ago  49s            8s            0            SUCCEEDED
     bob      4   -     pretrain  1x[H100:4]  1 day ago   1h 1m 11s      1h 14s        0            SUCCEEDED
     bob      3   -     bigjob    1x[CPU:16]  1 day ago   1d 21h 11m 4s  -             0            STARTING
     bob      2   -     failjob   1x[CPU:1+]  1 day ago   54s            9s            0            FAILED
     bob      1   -     shortjob  1x[CPU:1+]  2 days ago  1h 1m 19s      1h 16s        0            SUCCEEDED
 
-You can also inspect the real-time GPU usage on the cluster with :code:`sky show-gpus --cloud k8s`.
+You can also inspect the real-time GPU usage on the cluster with :code:`sky show-gpus --infra k8s`.
 
 .. code-block:: console
 
-    $ sky show-gpus --cloud k8s
+    $ sky show-gpus --infra k8s
     Kubernetes GPUs
     GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION
     L4    1, 2, 4                   12 of 12 free
diff --git a/docs/source/reference/kubernetes/kubernetes-priorities.rst b/docs/source/reference/kubernetes/kubernetes-priorities.rst
index ed8cec474ea..f7911ed9a1b 100644
--- a/docs/source/reference/kubernetes/kubernetes-priorities.rst
+++ b/docs/source/reference/kubernetes/kubernetes-priorities.rst
@@ -70,7 +70,7 @@ We use two simple counter jobs in this example:
 
   # high-priority-job.yaml
   resources:
-    cloud: kubernetes
+    infra: kubernetes
     cpus: 4
 
   run: |
@@ -91,7 +91,7 @@ We use two simple counter jobs in this example:
 
   # low-priority-job.yaml
   resources:
-    cloud: kubernetes
+    infra: kubernetes
     cpus: 4
 
   run: |
diff --git a/docs/source/reference/kubernetes/kubernetes-setup.rst b/docs/source/reference/kubernetes/kubernetes-setup.rst
index 2fa3e80f119..e6b9ef101a3 100644
--- a/docs/source/reference/kubernetes/kubernetes-setup.rst
+++ b/docs/source/reference/kubernetes/kubernetes-setup.rst
@@ -217,7 +217,7 @@ You can also check the GPUs available on your nodes by running:
 
 .. code-block:: console
 
-    $ sky show-gpus --cloud k8s
+    $ sky show-gpus --infra k8s
     Kubernetes GPUs
     GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION
     L4    1, 2, 4                   12 of 12 free
diff --git a/docs/source/reference/kubernetes/kubernetes-troubleshooting.rst b/docs/source/reference/kubernetes/kubernetes-troubleshooting.rst
index 3abb8e42076..41ac13be372 100644
--- a/docs/source/reference/kubernetes/kubernetes-troubleshooting.rst
+++ b/docs/source/reference/kubernetes/kubernetes-troubleshooting.rst
@@ -87,7 +87,7 @@ Next, try running a simple hello world task to verify that SkyPilot can launch t
 
 .. code-block:: bash
 
-    $ sky launch -y -c mycluster --cloud k8s -- "echo hello world"
+    $ sky launch -y -c mycluster --infra k8s -- "echo hello world"
     # Task should run and print "hello world" to the console
 
     # Once you have verified that the task runs, you can delete it
@@ -174,7 +174,7 @@ Run :code:`sky check` to verify that SkyPilot can see your GPUs.
     # Should show `Kubernetes: Enabled` and should not print any warnings about GPU support.
 
     # List the available GPUs in your cluster
-    $ sky show-gpus --cloud k8s
+    $ sky show-gpus --infra k8s
 
 Step B4 - Try launching a dummy GPU task
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -184,7 +184,7 @@ Next, try running a simple GPU task to verify that SkyPilot can launch GPU tasks
 .. code-block:: bash
 
     # Replace the GPU type from the sky show-gpus output in the task launch command
-    $ sky launch -y -c mygpucluster --cloud k8s --gpu <gpu-type>:1 -- "nvidia-smi"
+    $ sky launch -y -c mygpucluster --infra k8s --gpu <gpu-type>:1 -- "nvidia-smi"
 
     # Task should run and print the nvidia-smi output to the console
 
@@ -298,7 +298,7 @@ Next, try running a simple task with a service to verify that SkyPilot can launc
 
 .. code-block:: bash
 
-    $ sky launch -y -c myserver --cloud k8s --ports 8080 -- "python -m http.server 8080"
+    $ sky launch -y -c myserver --infra k8s --ports 8080 -- "python -m http.server 8080"
 
     # Obtain the endpoint of the service
     $ sky status --endpoint 8080 myserver
diff --git a/docs/source/reference/kubernetes/multi-kubernetes.rst b/docs/source/reference/kubernetes/multi-kubernetes.rst
index 3d70cebb253..acbf2cde6f6 100644
--- a/docs/source/reference/kubernetes/multi-kubernetes.rst
+++ b/docs/source/reference/kubernetes/multi-kubernetes.rst
@@ -96,11 +96,11 @@ To check the enabled Kubernetes clusters, you can run ``sky check k8s``.
         ├── my-h100-cluster
         └── my-tpu-cluster
 
-To check GPUs available in a Kubernetes cluster, you can run ``sky show-gpus --cloud k8s``.
+To check GPUs available in a Kubernetes cluster, you can run ``sky show-gpus --infra k8s``.
 
 .. code-block:: console
 
-    $ sky show-gpus --cloud k8s
+    $ sky show-gpus --infra k8s
     Kubernetes GPUs
     GPU   UTILIZATION
     H100  16 of 16 free  
@@ -128,31 +128,33 @@ through the Kubernetes clusters in the same order as they are specified in the f
 
 .. code-block:: console
 
-    $ sky launch --gpus H100 --cloud k8s echo 'Hello World'
+    $ sky launch --gpus H100 --infra k8s echo 'Hello World'
 
     Considered resources (1 node):
-    ------------------------------------------------------------------------------------------------------------
-    CLOUD        INSTANCE           vCPUs   Mem(GB)   ACCELERATORS   REGION/ZONE           COST ($)   CHOSEN
-    ------------------------------------------------------------------------------------------------------------
-    Kubernetes   2CPU--8GB--1H100   2       8         H100:1         my-h100-cluster-gke   0.00          ✔
-    Kubernetes   2CPU--8GB--1H100   2       8         H100:1         my-h100-cluster-eks   0.00
-    ------------------------------------------------------------------------------------------------------------
+    ---------------------------------------------------------------------------------------------------------
+     INFRA                           INSTANCE          vCPUs   Mem(GB)   GPUS     COST ($)   CHOSEN
+    ---------------------------------------------------------------------------------------------------------
+     Kubernetes (my-eks-cluster)     2CPU--2GB         2       2         -        0.00       ✔
+     Kubernetes (gke-skypilot)       4CPU--8GB         4       8         -        0.00      
+     AWS (us-east-1)                 m6i.large         2       8         -        0.10     
+     GCP (us-central1-a)             n2-standard-2     2       8         -        0.10     
+    ---------------------------------------------------------------------------------------------------------
 
 
 Launching in a specific Kubernetes cluster
 ------------------------------------------
 
-SkyPilot uses the ``region`` field to denote a Kubernetes context. You can point to a Kubernetes cluster
-by specifying the ``--region`` with the context name for that cluster.
+SkyPilot uses the ``infra`` field to denote a Kubernetes context. You can point to a Kubernetes cluster
+by specifying the ``--infra`` with the context name for that cluster.
 
 .. code-block:: console
 
 
     $ # Launch in a specific Kubernetes cluster.
-    $ sky launch --cloud k8s --region my-tpu-cluster echo 'Hello World'
+    $ sky launch --infra k8s/my-tpu-cluster echo 'Hello World'
 
     $ # Check the GPUs available in a Kubernetes cluster
-    $ sky show-gpus --cloud k8s --region my-h100-cluster                                                  ✭ ✱
+    $ sky show-gpus --infra k8s/my-h100-cluster
     Kubernetes GPUs
     Context: my-h100-cluster
     GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION
@@ -163,7 +165,7 @@ by specifying the ``--region`` with the context name for that cluster.
     my-h100-cluster  gke-skypilotalpha-largecpu-05dae726-1usy      H100      8 of 8 free  
     my-h100-cluster  gke-skypilotalpha-largecpu-05dae726-4rxa      H100      8 of 8 free  
 
-When launching a SkyPilot cluster or task, you can also specify the context name with ``--region`` to launch the cluster or task in.
+When launching a SkyPilot cluster or task, you can also specify the context name with ``--infra`` to launch the cluster or task in.
 
 
 Dynamically updating clusters to use
diff --git a/docs/source/reference/yaml-spec.rst b/docs/source/reference/yaml-spec.rst
index 9c49cec3915..b800227aca8 100644
--- a/docs/source/reference/yaml-spec.rst
+++ b/docs/source/reference/yaml-spec.rst
@@ -26,10 +26,9 @@ Below is the configuration syntax and some example values.  See details under ea
   :ref:`num_nodes <yaml-spec-num-nodes>`: 4
 
   :ref:`resources <yaml-spec-resources>`:
-    # Location.
-    :ref:`cloud <yaml-spec-resources-cloud>`: aws
-    :ref:`region <yaml-spec-resources-region>`: us-east-1
-    :ref:`zone <yaml-spec-resources-zone>`: us-east-1a
+    # Infra to use. For example: ``aws``, ``aws/us-east-1``, ``kubernetes``,
+    # or, ``kubernetes/my-h100-cluster-context``.
+    :ref:`infra <yaml-spec-resources-infra>`: aws
 
     # Hardware.
     :ref:`accelerators <yaml-spec-resources-accelerators>`: H100:8
@@ -49,17 +48,14 @@ Below is the configuration syntax and some example values.  See details under ea
       my-label: my-value
 
     :ref:`any_of <yaml-spec-resources-any-of>`:
-      - cloud: aws
-        region: us-west-2
+      - infra: aws/us-west-2
         accelerators: H100
-      - cloud: gcp
+      - infra: gcp/us-central1
         accelerators: H100
 
     :ref:`ordered <yaml-spec-resources-ordered>`:
-      - cloud: aws
-        region: us-east-1
-      - cloud: aws
-        region: us-west-2
+      - infra: aws/us-east-1
+      - infra: aws/us-west-2
 
     :ref:`job_recovery <yaml-spec-resources-job-recovery>`: none
 
@@ -151,58 +147,50 @@ Per-node resource requirements (optional).
 .. code-block:: yaml
 
   resources:
-    cloud: aws
+    infra: aws
     instance_type: p3.8xlarge
 
 
-.. _yaml-spec-resources-cloud:
+.. _yaml-spec-resources-infra:
 
-``resources.cloud``
+``resources.infra``
 ~~~~~~~~~~~~~~~~~~~
 
-The cloud to use (optional).
 
-.. code-block:: yaml
-
-  resources:
-    cloud: aws
+Infrastructure to use (optional). Format: ``<cloud>``, ``<cloud>/<region>``, ``<cloud>/<region>/<zone>``, ``kubernetes/<context-name>``.
 
-OR
+Examples: ``aws``, ``aws/us-east-1``, ``aws/us-east-1/us-east-1a``, ``aws/*/us-east-1a``, ``kubernetes/my-cluster-context``.
 
 .. code-block:: yaml
 
   resources:
-    cloud: gcp
+    infra: aws
 
 
-.. _yaml-spec-resources-region:
-
-``resources.region``
-~~~~~~~~~~~~~~~~~~~~
+.. code-block:: yaml
 
-The region to use (optional).
+  resources:
+    infra: kubernetes
 
-Auto-failover will be disabled if this is specified.
+You can also specify a specific region, zone or kubernetes context.
 
 .. code-block:: yaml
 
   resources:
-    region: us-east-1
+    infra: aws/us-east-1
 
 
-.. _yaml-spec-resources-zone:
-
-``resources.zone``
-~~~~~~~~~~~~~~~~~~
+.. code-block:: yaml
 
-The zone to use (optional).
+  resources:
+    infra: aws/us-east-1/us-east-1a
 
-Auto-failover will be disabled if this is specified.
 
 .. code-block:: yaml
 
   resources:
-    zone: us-east-1a
+    infra: kubernetes/my-h100-cluster-context
+
 
 
 .. _yaml-spec-resources-accelerators:
@@ -658,12 +646,10 @@ Example:
 .. code-block:: yaml
 
   resources:
+    accelerators: H100
     any_of:
-      - cloud: aws
-        region: us-west-2
-        accelerators: H100
-      - cloud: gcp
-        accelerators: H100
+      - infra: aws/us-west-2
+      - infra: gcp/us-central1
 
 .. _yaml-spec-resources-ordered:
 
@@ -683,10 +669,8 @@ Example:
 
   resources:
     ordered:
-      - cloud: aws
-        region: us-east-1
-      - cloud: aws
-        region: us-west-2
+      - infra: aws/us-east-1
+      - infra: aws/us-west-2
 
 .. _yaml-spec-resources-job-recovery:
 
diff --git a/docs/source/reservations/existing-machines.rst b/docs/source/reservations/existing-machines.rst
index 1a6c14db730..e2a44b45310 100644
--- a/docs/source/reservations/existing-machines.rst
+++ b/docs/source/reservations/existing-machines.rst
@@ -106,7 +106,7 @@ Deploying SkyPilot
       ✔ Remote k3s is running.
       ✔ Nvidia GPU Operator installed successfully.
       Cluster deployment done. You can now run tasks on this cluster.
-      E.g., run a task with: sky launch --cloud kubernetes -- echo hello world.
+      E.g., run a task with: sky launch --infra kubernetes -- echo hello world.
       🎉 Remote cluster deployed successfully.
 
 
@@ -120,7 +120,7 @@ Deploying SkyPilot
 
    .. code-block:: console
 
-      $ sky show-gpus --cloud k8s
+      $ sky show-gpus --infra k8s
       Kubernetes GPUs
       GPU   REQUESTABLE_QTY_PER_NODE  UTILIZATION
       L4    1, 2, 4                   12 of 12
@@ -135,7 +135,7 @@ Deploying SkyPilot
       my-cluster-4               H100      8 of 8
       my-cluster-5               H100      8 of 8
 
-      $ sky launch --cloud k8s --gpus H100:1 -- nvidia-smi
+      $ sky launch --infra k8s --gpus H100:1 -- nvidia-smi
 
    .. tip::
 
@@ -194,27 +194,27 @@ You can then configure SkyPilot to use :ref:`multiple Kubernetes clusters <multi
 
    # ~/.sky/config.yaml
     allowed_contexts:
-      - cluster1
-      - cluster2
+      - cluster1-ctx
+      - cluster2-ctx
 
 
 .. code-block:: bash
 
     # Run on cluster1
-    sky launch --cloud k8s --region cluster1 -- echo "Running on cluster 1"
+    sky launch --infra k8s/cluster1-ctx -- echo "Running on cluster 1"
 
     # Run on cluster2
-    sky launch --cloud k8s --region cluster2 -- echo "Running on cluster 2"
+    sky launch --infra k8s/cluster2-ctx -- echo "Running on cluster 2"
 
     # Let SkyPilot automatically select the cluster with available resources
-    sky launch --cloud k8s -- echo "Running on SkyPilot selected cluster"
+    sky launch --infra k8s -- echo "Running on SkyPilot selected cluster"
 
 You can view the available clusters and GPUs using:
 
 .. code-block:: bash
 
     # List GPUs on cluster1
-    sky show-gpus --cloud k8s --region cluster1
+    sky show-gpus --infra k8s/cluster1-ctx
 
     # List GPUs on cluster2
-    sky show-gpus --cloud k8s --region cluster2
+    sky show-gpus --infra k8s/cluster2-ctx
diff --git a/docs/source/reservations/reservations.rst b/docs/source/reservations/reservations.rst
index a846f80e2f8..9dbf37a022d 100644
--- a/docs/source/reservations/reservations.rst
+++ b/docs/source/reservations/reservations.rst
@@ -77,7 +77,7 @@ For example, if you are launching a cluster with the following SkyPilot YAML:
 .. code-block:: yaml
 
     resources:
-      cloud: aws
+      infra: aws
       accelerators: A100:8
 
     num_nodes: 2
@@ -95,7 +95,7 @@ SkyPilot will utilize the capacity reservation/block as follows:
 
 .. hint::
 
-    If you have a capacity block with a starting time in the future, you can run ``sky jobs launch --region us-east-1 --gpus H100:8 task.yaml`` to let SkyPilot automatically wait until the starting time is reached. Namely, you don't have to wake up at 4:30am PDT to launch your job on a newly available capacity block.
+    If you have a capacity block with a starting time in the future, you can run ``sky jobs launch --infra aws/us-east-1 --gpus H100:8 task.yaml`` to let SkyPilot automatically wait until the starting time is reached. Namely, you don't have to wake up at 4:30am PDT to launch your job on a newly available capacity block.
 
 
 GCP reservations
@@ -163,7 +163,7 @@ In case you want to specify the DWS configuration for each job/cluster, you can
           provision_timeout: 900
 
     resources:
-      cloud: gcp
+      infra: gcp
       accelerators: A100:8
 
     num_nodes: 4
@@ -188,7 +188,7 @@ To launch a SkyPilot cluster or job on GKE with DWS, you can specify the DWS con
         provision_timeout: 900
 
     resources:
-      cloud: kubernetes
+      infra: kubernetes
       accelerators: A100:8
       labels:
         kueue.x-k8s.io/queue-name: dws-local-queue
diff --git a/docs/source/running-jobs/many-jobs.rst b/docs/source/running-jobs/many-jobs.rst
index 60e122b5f91..15e66ccd32c 100644
--- a/docs/source/running-jobs/many-jobs.rst
+++ b/docs/source/running-jobs/many-jobs.rst
@@ -301,10 +301,10 @@ Job statuses can be checked via ``sky jobs queue``:
   Fetching managed jobs...
   Managed jobs
   In progress tasks: 10 RUNNING
-  ID  TASK  NAME        RESOURCES  SUBMITTED    TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
-  10  -     train-job10 1x[V100:4] 5 mins ago   5m 5s          1m 12s        0            RUNNING
-  9   -     train-job9  1x[V100:4] 6 mins ago   6m 11s         2m 23s        0            RUNNING
-  8   -     train-job8  1x[V100:4] 7 mins ago   7m 15s         3m 31s        0            RUNNING
+  ID  TASK  NAME        REQUESTED   SUBMITTED    TOT. DURATION  JOB DURATION  #RECOVERIES  STATUS
+  10  -     train-job10 1x[V100:4]  5 mins ago   5m 5s          1m 12s        0            RUNNING
+  9   -     train-job9  1x[V100:4]  6 mins ago   6m 11s         2m 23s        0            RUNNING
+  8   -     train-job8  1x[V100:4]  7 mins ago   7m 15s         3m 31s        0            RUNNING
   ...
 
 
diff --git a/docs/source/serving/sky-serve.rst b/docs/source/serving/sky-serve.rst
index 15bf4232b52..046031f6420 100644
--- a/docs/source/serving/sky-serve.rst
+++ b/docs/source/serving/sky-serve.rst
@@ -527,8 +527,7 @@ To achieve the above, you can specify custom configs in :code:`~/.sky/config.yam
       resources:
         # All configs below are optional.
         # Specify the location of the SkyServe controller.
-        cloud: gcp
-        region: us-central1
+        infra: gcp/us-central1
         # Specify the maximum number of services that can be run concurrently.
         cpus: 2+  # number of vCPUs, max concurrent services = min(4 * cpus, memory in GiB)
         # Specify the disk_size in GB of the SkyServe controller.
diff --git a/docs/source/serving/spot-policy.rst b/docs/source/serving/spot-policy.rst
index 02af9a79f26..955ad5ecf47 100644
--- a/docs/source/serving/spot-policy.rst
+++ b/docs/source/serving/spot-policy.rst
@@ -88,11 +88,11 @@ When the service is up, we can check the status of the service and the replicas
     http-server  1        1m 17s  NO_REPLICA  0/4       54.227.229.217:30001
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED    RESOURCES             STATUS         REGION
-    http-server   1   1        -                          1 min ago   1x GCP([Spot]vCPU=2)  PROVISIONING  us-east1
-    http-server   2   1        -                          1 min ago   1x GCP([Spot]vCPU=2)  PROVISIONING  us-central1
-    http-server   3   1        -                          1 mins ago  1x GCP(vCPU=2)        PROVISIONING  us-east1
-    http-server   4   1        -                          1 min ago   1x GCP(vCPU=2)        PROVISIONING  us-central1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED    INFRA                RESOURCES                                         STATUS         
+    http-server   1   1        -                          1 min ago   GCP (us-east1)       1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  PROVISIONING  
+    http-server   2   1        -                          1 min ago   GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  PROVISIONING  
+    http-server   3   1        -                          1 mins ago  GCP (us-east1)       1x(cpus=2, mem=8, type=n2-standard-2, ...)        PROVISIONING  
+    http-server   4   1        -                          1 min ago   GCP (us-central1)    1x(cpus=2, mem=8, type=n2-standard-2, ...)        PROVISIONING  
 
 When the required number of spot replicas are not available, SkyServe will provision on-demand replicas to meet the target number of replicas. For example, when the target number is 2 and no spot replicas are ready, SkyServe will provision 2 on-demand replicas to meet the target number of replicas.
 
@@ -105,11 +105,11 @@ When the required number of spot replicas are not available, SkyServe will provi
     http-server  1        1m 17s  READY   2/4       54.227.229.217:30001
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED    RESOURCES             STATUS         REGION
-    http-server   1   1        http://34.23.22.160:8081   3 min ago   1x GCP([Spot]vCPU=2)  READY          us-east1
-    http-server   2   1        http://34.68.226.193:8081  3 min ago   1x GCP([Spot]vCPU=2)  READY          us-central1
-    http-server   3   1        -                          3 mins ago  1x GCP(vCPU=2)        SHUTTING_DOWN  us-east1
-    http-server   4   1        -                          3 min ago   1x GCP(vCPU=2)        SHUTTING_DOWN  us-central1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED    INFRA                RESOURCES                                         STATUS         
+    http-server   1   1        http://34.23.22.160:8081   3 min ago   GCP (us-east1)       1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY          
+    http-server   2   1        http://34.68.226.193:8081  3 min ago   GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY          
+    http-server   3   1        -                          3 mins ago  GCP (us-east1)       1x(cpus=2, mem=8, type=n2-standard-2, ...)        SHUTTING_DOWN  
+    http-server   4   1        -                          3 min ago   GCP (us-central1)    1x(cpus=2, mem=8, type=n2-standard-2, ...)        SHUTTING_DOWN  
 
 When the spot replicas are ready, SkyServe will automatically scale down on-demand replicas to maximize cost savings.
 
@@ -122,9 +122,9 @@ When the spot replicas are ready, SkyServe will automatically scale down on-dema
     http-server  1        3m 59s  READY   2/2       54.227.229.217:30001
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED    RESOURCES             STATUS  REGION
-    http-server   1   1        http://34.23.22.160:8081   4 mins ago  1x GCP([Spot]vCPU=2)  READY   us-east1
-    http-server   2   1        http://34.68.226.193:8081  4 mins ago  1x GCP([Spot]vCPU=2)  READY   us-central1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED    INFRA                RESOURCES                                         STATUS         
+    http-server   1   1        http://34.23.22.160:8081   4 mins ago  GCP (us-east1)       1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY          
+    http-server   2   1        http://34.68.226.193:8081  4 mins ago  GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY          
 
 In the event of spot instance interruptions (e.g. replica 1), SkyServe will automatically fallback to on-demand replicas (e.g. launch one on-demand replica) to meet the required capacity of replicas. SkyServe will continue trying to provision one spot replica in the event where spot availability is back. Note that SkyServe will try different regions and clouds to maximize the chance of successfully provisioning spot instances.
 
@@ -137,10 +137,10 @@ In the event of spot instance interruptions (e.g. replica 1), SkyServe will auto
     http-server  1        7m 2s   READY   1/3       54.227.229.217:30001
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED     RESOURCES             STATUS        REGION
-    http-server   2   1        http://34.68.226.193:8081  7 mins ago   1x GCP([Spot]vCPU=2)  READY         us-central1
-    http-server   5   1        -                          13 secs ago  1x GCP([Spot]vCPU=2)  PROVISIONING  us-central1
-    http-server   6   1        -                          13 secs ago  1x GCP(vCPU=2)        PROVISIONING  us-central1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED     INFRA                RESOURCES                                         STATUS         
+    http-server   2   1        http://34.68.226.193:8081  7 mins ago   GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY         
+    http-server   5   1        -                          13 secs ago  GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  PROVISIONING  
+    http-server   6   1        -                          13 secs ago  GCP (us-central1)    1x(cpus=2, mem=8, type=n2-standard-2, ...)        PROVISIONING  
 
 Eventually, when the spot availability is back, SkyServe will automatically scale down on-demand replicas.
 
@@ -153,6 +153,6 @@ Eventually, when the spot availability is back, SkyServe will automatically scal
     http-server  1        10m 5s  READY   2/3       54.227.229.217:30001
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED     RESOURCES             STATUS         REGION
-    http-server   2   1        http://34.68.226.193:8081  10 mins ago  1x GCP([Spot]vCPU=2)  READY          us-central1
-    http-server   5   1        http://34.121.49.94:8081   1 min ago    1x GCP([Spot]vCPU=2)  READY          us-central1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                   LAUNCHED     INFRA                RESOURCES                                         STATUS         
+    http-server   2   1        http://34.68.226.193:8081  10 mins ago  GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY          
+    http-server   5   1        http://34.121.49.94:8081   1 min ago    GCP (us-central1)    1x[spot](cpus=2, mem=8, type=n2-standard-2, ...)  READY          
diff --git a/docs/source/serving/update.rst b/docs/source/serving/update.rst
index ca4f5ddb0ba..1490f143963 100644
--- a/docs/source/serving/update.rst
+++ b/docs/source/serving/update.rst
@@ -57,9 +57,9 @@ We can use :code:`sky serve status http-server` to check the status of the servi
     http-server  1        1m 41s  READY   2/2       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED    RESOURCES       STATUS  REGION
-    http-server   1   1        54.173.203.169  2 mins ago  1x AWS(vCPU=2)  READY   us-east-1
-    http-server   2   1        52.87.241.103   2 mins ago  1x AWS(vCPU=2)  READY   us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED    INFRA                RESOURCES                                     STATUS  
+    http-server   1   1        http://54.173.203.169:8081  2 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY   
+    http-server   2   1        http://52.87.241.103:8081   2 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY   
 
 Service ``http-server`` has an initial version of 1.
 
@@ -102,12 +102,12 @@ SkyServe will trigger launching three new replicas.
     http-server  2        6m 15s  READY   2/5       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED     RESOURCES       STATUS        REGION
-    http-server   1   1        54.173.203.169  6 mins ago   1x AWS(vCPU=2)  READY         us-east-1
-    http-server   2   1        52.87.241.103   6 mins ago   1x AWS(vCPU=2)  READY         us-east-1
-    http-server   3   2        -               21 secs ago  1x AWS(vCPU=2)  PROVISIONING  us-east-1
-    http-server   4   2        -               21 secs ago  1x AWS(vCPU=2)  PROVISIONING  us-east-1
-    http-server   5   2        -               21 secs ago  1x AWS(vCPU=2)  PROVISIONING  us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                     STATUS       
+    http-server   1   1        http://54.173.203.169:8081  6 mins ago   AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY         
+    http-server   2   1        http://52.87.241.103:8081   6 mins ago   AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY         
+    http-server   3   2        -                           21 secs ago  AWS (us-east-1b)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING  
+    http-server   4   2        -                           21 secs ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING  
+    http-server   5   2        -                           21 secs ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING  
 
 
 Whenever a new replica is ready, the traffic will be redirected to both old and new replicas.
@@ -121,12 +121,12 @@ Whenever a new replica is ready, the traffic will be redirected to both old and
     http-server  1,2        10m 4s  READY   3/5       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED     RESOURCES       STATUS         REGION
-    http-server   1   1        54.173.203.169  10 mins ago  1x AWS(vCPU=2)  READY          us-east-1
-    http-server   2   1        52.87.241.103   10 mins ago  1x AWS(vCPU=2)  READY          us-east-1
-    http-server   3   2        3.93.241.163    1 min ago    1x AWS(vCPU=2)  READY          us-east-1
-    http-server   4   2        -               1 min ago    1x AWS(vCPU=2)  PROVISIONING   us-east-1
-    http-server   5   2        -               1 min ago    1x AWS(vCPU=2)  PROVISIONING   us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                     STATUS        
+    http-server   1   1        http://54.173.203.169:8081  10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   2   1        http://52.87.241.103:8081   10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   3   2        http://3.93.241.163:8081    1 min ago    AWS (us-east-1b)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   4   2        -                           1 min ago    AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING   
+    http-server   5   2        -                           1 min ago    AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING   
 
 
 Once the total number of both old and new replicas exceeds the requested number, old replicas will be scaled down.
@@ -140,12 +140,13 @@ Once the total number of both old and new replicas exceeds the requested number,
     http-server  1,2        10m 4s  READY   3/5       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED     RESOURCES       STATUS         REGION
-    http-server   1   1        54.173.203.169  10 mins ago  1x AWS(vCPU=2)  SHUTTING_DOWN  us-east-1
-    http-server   2   1        52.87.241.103   10 mins ago  1x AWS(vCPU=2)  READY          us-east-1
-    http-server   3   2        3.93.241.163    1 min ago    1x AWS(vCPU=2)  READY          us-east-1
-    http-server   4   2        18.206.226.82   1 min ago    1x AWS(vCPU=2)  READY          us-east-1
-    http-server   5   2        -               1 min ago    1x AWS(vCPU=2)  PROVISIONING   us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                     STATUS         
+    http-server   1   1        http://54.173.203.169:8081  10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   2   1        http://52.87.241.103:8081   10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   3   2        http://3.93.241.163:8081    1 min ago    AWS (us-east-1b)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   4   2        -                           1 min ago    AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING   
+    http-server   5   2        -                           1 min ago    AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING   
+
 
 Eventually, we will only have new replicas ready to serve user requests.
 
@@ -158,10 +159,10 @@ Eventually, we will only have new replicas ready to serve user requests.
     http-server  2        11m 42s  READY   3/3       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP             LAUNCHED    RESOURCES       STATUS  REGION
-    http-server   3   2        3.93.241.163   3 mins ago  1x AWS(vCPU=2)  READY   us-east-1
-    http-server   4   2        18.206.226.82  3 mins ago  1x AWS(vCPU=2)  READY   us-east-1
-    http-server   5   2        3.26.232.31    1 min ago   1x AWS(vCPU=2)  READY   us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED    INFRA                RESOURCES                                     STATUS  
+    http-server   3   2        http://3.93.241.163:8081    3 mins ago  AWS (us-east-1b)     1x(cpus=2, mem=8, type=m5.large, ...)         READY   
+    http-server   4   2        http://18.206.226.82:8081   3 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY   
+    http-server   5   2        http://3.26.232.31:8081     1 min ago   AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY
 
 
 
@@ -210,12 +211,12 @@ SkyServe will trigger launching three new replicas.
     http-server  2        6m 15s  READY   2/5       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED     RESOURCES       STATUS        REGION
-    http-server   1   1        54.173.203.169  6 mins ago   1x AWS(vCPU=2)  READY         us-east-1
-    http-server   2   1        52.87.241.103   6 mins ago   1x AWS(vCPU=2)  READY         us-east-1
-    http-server   3   2        -               21 secs ago  1x AWS(vCPU=2)  PROVISIONING  us-east-1
-    http-server   4   2        -               21 secs ago  1x AWS(vCPU=2)  PROVISIONING  us-east-1
-    http-server   5   2        -               21 secs ago  1x AWS(vCPU=2)  PROVISIONING  us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                     STATUS       
+    http-server   1   1        http://54.173.203.169:8081  6 mins ago   AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY         
+    http-server   2   1        http://52.87.241.103:8081   6 mins ago   AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY         
+    http-server   3   2        -                           21 secs ago  AWS (us-east-1b)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING  
+    http-server   4   2        -                           21 secs ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING  
+    http-server   5   2        -                           21 secs ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING  
 
 
 When a new replica is ready, the traffic will still be redirected to old replicas.
@@ -229,12 +230,12 @@ When a new replica is ready, the traffic will still be redirected to old replica
     http-server  1        10m 4s  READY   3/5       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED     RESOURCES       STATUS         REGION
-    http-server   1   1        54.173.203.169  10 mins ago  1x AWS(vCPU=2)  READY          us-east-1
-    http-server   2   1        52.87.241.103   10 mins ago  1x AWS(vCPU=2)  READY          us-east-1
-    http-server   3   2        3.93.241.163    1 min ago    1x AWS(vCPU=4)  READY          us-east-1
-    http-server   4   2        -               1 min ago    1x AWS(vCPU=4)  PROVISIONING   us-east-1
-    http-server   5   2        -               1 min ago    1x AWS(vCPU=4)  PROVISIONING   us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                      STATUS        
+    http-server   1   1        http://54.173.203.169:8081  10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         SHUTTING_DOWN  
+    http-server   2   1        http://52.87.241.103:8081   10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         READY          
+    http-server   3   2        http://3.93.241.163:8081    1 min ago    AWS (us-east-1b)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY          
+    http-server   4   2        http://18.206.226.82:8081   1 min ago    AWS (us-east-1a)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY          
+    http-server   5   2        -                           1 min ago    AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         PROVISIONING   
 
 
 Once the total number of new replicas satisfies the requirements, traffics will be redirected to new replicas and old replicas will be scaled down.
@@ -248,12 +249,12 @@ Once the total number of new replicas satisfies the requirements, traffics will
     http-server  2        10m 4s  READY   3/5       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP              LAUNCHED     RESOURCES       STATUS         REGION
-    http-server   1   1        54.173.203.169  10 mins ago  1x AWS(vCPU=2)  SHUTTING_DOWN  us-east-1
-    http-server   2   1        52.87.241.103   10 mins ago  1x AWS(vCPU=2)  SHUTTING_DOWN  us-east-1
-    http-server   3   2        3.93.241.163    1 min ago    1x AWS(vCPU=4)  READY          us-east-1
-    http-server   4   2        18.206.226.82   1 min ago    1x AWS(vCPU=4)  READY          us-east-1
-    http-server   5   2        3.26.232.31     1 min ago    1x AWS(vCPU=4)  READY          us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                      STATUS         
+    http-server   1   1        http://54.173.203.169:8081  10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         SHUTTING_DOWN  
+    http-server   2   1        http://52.87.241.103:8081   10 mins ago  AWS (us-east-1a)     1x(cpus=2, mem=8, type=m5.large, ...)         SHUTTING_DOWN  
+    http-server   3   2        http://3.93.241.163:8081    1 min ago    AWS (us-east-1b)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY          
+    http-server   4   2        http://18.206.226.82:8081   1 min ago    AWS (us-east-1a)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY          
+    http-server   5   2        http://3.26.232.31:8081     1 min ago    AWS (us-east-1a)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY
 
 Eventually, same as the rolling update, we will only have new replicas ready to serve user requests.
 
@@ -266,7 +267,7 @@ Eventually, same as the rolling update, we will only have new replicas ready to
     http-server  2        11m 42s  READY   3/3       44.206.240.249:30002
 
     Service Replicas
-    SERVICE_NAME  ID  VERSION  IP             LAUNCHED    RESOURCES       STATUS  REGION
-    http-server   3   2        3.93.241.163   3 mins ago  1x AWS(vCPU=4)  READY   us-east-1
-    http-server   4   2        18.206.226.82  3 mins ago  1x AWS(vCPU=4)  READY   us-east-1
-    http-server   5   2        3.26.232.31    1 min ago   1x AWS(vCPU=4)  READY   us-east-1
+    SERVICE_NAME  ID  VERSION  ENDPOINT                    LAUNCHED     INFRA                RESOURCES                                      STATUS        
+    http-server   3   2        http://3.93.241.163:8081    3 mins ago   AWS (us-east-1b)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY          
+    http-server   4   2        http://18.206.226.82:8081   3 mins ago   AWS (us-east-1a)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY          
+    http-server   5   2        http://3.26.232.31:8081     1 min ago    AWS (us-east-1a)     1x(cpus=4, mem=16, type=m5.xlarge, ...)       READY
diff --git a/examples/admin_policy/task.yaml b/examples/admin_policy/task.yaml
index 065b4cbfb11..d3d4789c7ee 100644
--- a/examples/admin_policy/task.yaml
+++ b/examples/admin_policy/task.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: aws
+  infra: aws
   cpus: 2
   labels:
     other_labels: test
diff --git a/examples/autogluon.yaml b/examples/autogluon.yaml
index 00e5804f809..66093004b5e 100644
--- a/examples/autogluon.yaml
+++ b/examples/autogluon.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: gcp
+  infra: gcp
 
 setup: |
   git clone https://github.com/autogluon/autogluon.git
diff --git a/examples/aws_efa/nccl_efa.yaml b/examples/aws_efa/nccl_efa.yaml
index de6212a1c52..c73f1c01beb 100644
--- a/examples/aws_efa/nccl_efa.yaml
+++ b/examples/aws_efa/nccl_efa.yaml
@@ -1,7 +1,7 @@
 name: nccl-test-efa
 
 resources:
-  cloud: kubernetes
+  infra: kubernetes
   accelerators: A100:8
   cpus: 90+
   image_id: docker:public.ecr.aws/hpc-cloud/nccl-tests:latest
diff --git a/examples/azure_start_stop.yaml b/examples/azure_start_stop.yaml
index f6337267c1c..33dcbdfd187 100644
--- a/examples/azure_start_stop.yaml
+++ b/examples/azure_start_stop.yaml
@@ -2,7 +2,7 @@
 name: azure-start-stop
 
 resources:
-  cloud: azure
+  infra: azure
 
 # Optimizing for smoke tests
 #   2 nodes: smoke tests ~37 mins
diff --git a/examples/containerized_app.py b/examples/containerized_app.py
index be58de3152b..2563aaaa9ba 100644
--- a/examples/containerized_app.py
+++ b/examples/containerized_app.py
@@ -22,6 +22,6 @@
 
 with sky.Dag() as dag:
     t = sky.Task(run=run_command, setup=setup_cmd)
-    t.set_resources(sky.Resources(sky.AWS(), accelerators='V100'))
+    t.set_resources(infra='aws', accelerators='V100')
 
 sky.launch(dag)
diff --git a/examples/custom_image.yaml b/examples/custom_image.yaml
index 535b91bfa4e..602985aa955 100644
--- a/examples/custom_image.yaml
+++ b/examples/custom_image.yaml
@@ -1,6 +1,5 @@
 resources:
-  cloud: aws
-  region: us-east-2
+  infra: aws/us-east-2
   # Nvidia image from
   # https://aws.amazon.com/marketplace/pp/prodview-rf7na2b2ttvdg
   image_id: ami-062ddd90fb6f8267a
diff --git a/examples/disk_size.yaml b/examples/disk_size.yaml
index 7384533b17c..eb97a978bc3 100644
--- a/examples/disk_size.yaml
+++ b/examples/disk_size.yaml
@@ -9,7 +9,7 @@
 name: minimal
 
 resources:
-  cloud: azure
+  infra: azure
   disk_size: 512
 
 setup: |
diff --git a/examples/dvc/dvc_pipeline.yaml b/examples/dvc/dvc_pipeline.yaml
index e3ff3bce8bb..1a377e55e7a 100644
--- a/examples/dvc/dvc_pipeline.yaml
+++ b/examples/dvc/dvc_pipeline.yaml
@@ -2,8 +2,8 @@
 name: dvc-pipeline
 resources:
   accelerators: T4:1
-  cloud: aws
-  region: us-east-2
+  infra: aws/us-east-2
+
 workdir: .
 file_mounts: 
   ~/.ssh/id_rsa: ~/.ssh/id_rsa
@@ -18,4 +18,4 @@ run: |
   # run DVC pipeline as an experiment
   dvc exp run --pull --allow-missing
   # push experiment results to DVC remote
-  dvc exp push origin 
\ No newline at end of file
+  dvc exp push origin 
diff --git a/examples/example_app.py b/examples/example_app.py
index 82162d11ac3..c86c123c13c 100644
--- a/examples/example_app.py
+++ b/examples/example_app.py
@@ -40,10 +40,12 @@ def make_application():
         train_op.set_outputs('CLOUD://my-model', estimated_size_gigabytes=0.1)
 
         train_op.set_resources({
-            sky.Resources(sky.AWS(), 'p3.2xlarge'),  # 1 V100, EC2.
-            sky.Resources(sky.AWS(), 'p3.8xlarge'),  # 4 V100s, EC2.
+            sky.Resources(infra='aws',
+                          instance_type='p3.2xlarge'),  # 1 V100, EC2.
+            sky.Resources(infra='aws',
+                          instance_type='p3.8xlarge'),  # 4 V100s, EC2.
             # Tuples mean all resources are required.
-            sky.Resources(sky.GCP(), accelerators='tpu-v3-8'),
+            sky.Resources(infra='gcp', accelerators='tpu-v3-8'),
         })
 
         train_op.set_time_estimator(time_estimators.resnet50_estimate_runtime)
@@ -58,10 +60,14 @@ def make_application():
                             estimated_size_gigabytes=0.1)
 
         infer_op.set_resources({
-            sky.Resources(sky.AWS(), 'inf1.2xlarge'),
-            sky.Resources(sky.AWS(), 'p3.2xlarge'),
-            sky.Resources(sky.GCP(), 'n1-standard-4', accelerators='T4'),
-            sky.Resources(sky.GCP(), 'n1-standard-8', accelerators='T4'),
+            sky.Resources(infra='aws', instance_type='inf1.2xlarge'),
+            sky.Resources(infra='aws', instance_type='p3.2xlarge'),
+            sky.Resources(infra='gcp',
+                          instance_type='n1-standard-4',
+                          accelerators='T4'),
+            sky.Resources(infra='gcp',
+                          instance_type='n1-standard-8',
+                          accelerators='T4'),
         })
 
         infer_op.set_time_estimator(
diff --git a/examples/gcp_gpu_direct_tcpx/gpu_direct_tcpx.yaml b/examples/gcp_gpu_direct_tcpx/gpu_direct_tcpx.yaml
index 23a205cf810..f74c5af7017 100644
--- a/examples/gcp_gpu_direct_tcpx/gpu_direct_tcpx.yaml
+++ b/examples/gcp_gpu_direct_tcpx/gpu_direct_tcpx.yaml
@@ -1,7 +1,7 @@
 name: nccl-gpu-direct-tcpx
 
 resources:
-  cloud: gcp
+  infra: gcp
   instance_type: a3-highgpu-8g
   image_id: docker:us-docker.pkg.dev/gce-ai-infra/gpudirect-tcpx/nccl-plugin-gpudirecttcpx
 
diff --git a/examples/gcp_start_stop.yaml b/examples/gcp_start_stop.yaml
index 507e75eb0ac..cdd833addbf 100644
--- a/examples/gcp_start_stop.yaml
+++ b/examples/gcp_start_stop.yaml
@@ -2,7 +2,7 @@
 name: gcp-start-stop
 
 resources:
-  cloud: gcp
+  infra: gcp
 
 num_nodes: 2
 
diff --git a/examples/horovod_distributed_tf_app.py b/examples/horovod_distributed_tf_app.py
index 273f653a710..9dd23485b3a 100644
--- a/examples/horovod_distributed_tf_app.py
+++ b/examples/horovod_distributed_tf_app.py
@@ -55,7 +55,7 @@ def run_fn(ip_list: List[IPAddr]) -> Dict[IPAddr, str]:
                      estimated_size_gigabytes=70)
     train.set_outputs('resnet-model-dir', estimated_size_gigabytes=0.1)
     train.set_resources({
-        sky.Resources(sky.AWS(), 'p3.2xlarge'),
+        sky.Resources(infra='aws', instance_type='p3.2xlarge'),
     })
 
 dag = sky.Optimizer.optimize(dag)
diff --git a/examples/huggingface_glue_imdb_grid_search_app.py b/examples/huggingface_glue_imdb_grid_search_app.py
index 89965f62fa2..4fc7b04dd9e 100644
--- a/examples/huggingface_glue_imdb_grid_search_app.py
+++ b/examples/huggingface_glue_imdb_grid_search_app.py
@@ -1,7 +1,7 @@
 """Grid search version of huggingface_glue_imdb_app.py."""
 import sky
 
-resources_to_launch = sky.Resources(sky.AWS(), accelerators={'V100': 4})
+resources_to_launch = sky.Resources(infra='aws', accelerators={'V100': 4})
 with sky.Dag() as dag:
     # Setup command, run once (pip, download dataset).
     common_setup = """\
diff --git a/examples/image_with_tag.yaml b/examples/image_with_tag.yaml
index 480cec69e99..7b18406bc38 100644
--- a/examples/image_with_tag.yaml
+++ b/examples/image_with_tag.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: aws
+  infra: aws
   image_id: skypilot:gpu-ubuntu-1804
 
 
diff --git a/examples/k8s_cloud_deploy/README.md b/examples/k8s_cloud_deploy/README.md
index efe2586a6ee..15fbe6182c0 100644
--- a/examples/k8s_cloud_deploy/README.md
+++ b/examples/k8s_cloud_deploy/README.md
@@ -21,7 +21,7 @@ pip install "skypilot-nightly[lambda,kubernetes]"
 1. Edit `cloud_k8s.yaml` to set the desired number of workers and GPUs per node. If using GCP, AWS or Azure, uncomment the ports line to allow inbound connections to the Kubernetes API server. 
 ```yaml
 resources:
-  cloud: lambda
+  infra: lambda
   accelerators: A10:1
   # ports: 6443
 
diff --git a/examples/k8s_cloud_deploy/cloud_k8s.yaml b/examples/k8s_cloud_deploy/cloud_k8s.yaml
index 2db46fb502b..dd8aeffe2f9 100644
--- a/examples/k8s_cloud_deploy/cloud_k8s.yaml
+++ b/examples/k8s_cloud_deploy/cloud_k8s.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: lambda
+  infra: lambda
   accelerators: A10:1
 #  Uncomment the following line to expose ports on a different cloud
 #  ports: 6443
diff --git a/examples/managed_job_with_storage.yaml b/examples/managed_job_with_storage.yaml
index 77b69485269..41d3648e074 100644
--- a/examples/managed_job_with_storage.yaml
+++ b/examples/managed_job_with_storage.yaml
@@ -7,7 +7,7 @@
 #   sky down spot-storage
 
 resources:
-  cloud: aws
+  infra: aws
   use_spot: true
   job_recovery: failover
 
diff --git a/examples/many_gpu_vms.yaml b/examples/many_gpu_vms.yaml
index 453392cdeb1..6a2789242a9 100644
--- a/examples/many_gpu_vms.yaml
+++ b/examples/many_gpu_vms.yaml
@@ -1,7 +1,7 @@
 name: many_gpu_vms
 
 resources:
-  cloud: aws
+  infra: aws
   accelerators: V100:8
   # use_spot: true
 
diff --git a/examples/minimal.yaml b/examples/minimal.yaml
index e76182c114a..89357a86112 100644
--- a/examples/minimal.yaml
+++ b/examples/minimal.yaml
@@ -9,7 +9,7 @@
 name: minimal
 
 resources:
-  cloud: aws
+  infra: aws
 
 setup: |
   echo "running setup"
diff --git a/examples/mpirun.yaml b/examples/mpirun.yaml
index 4ec7ce0107c..d002b63e985 100644
--- a/examples/mpirun.yaml
+++ b/examples/mpirun.yaml
@@ -1,7 +1,7 @@
 workdir: .
 
 resources:
-  cloud: aws
+  infra: aws
 
 num_nodes: 2  # Total number of nodes (1 head + 1 worker)
 
diff --git a/examples/multi_echo.py b/examples/multi_echo.py
index 2512fc3a437..1bab8bce523 100644
--- a/examples/multi_echo.py
+++ b/examples/multi_echo.py
@@ -9,7 +9,7 @@
 
 
 def run(cluster: Optional[str] = None,
-        cloud: Optional[str] = None,
+        infra: Optional[str] = None,
         use_spot: bool = True):
     if cluster is None:
         # (username, last 4 chars of hash of hostname): for uniquefying users on
@@ -19,14 +19,13 @@ def run(cluster: Optional[str] = None,
         _user_and_host = f'{getpass.getuser()}-{hostname_hash}'
         cluster = f'test-multi-echo-{_user_and_host}'
 
-    if cloud is None:
-        cloud = 'gcp'
-    cloud = sky.CLOUD_REGISTRY.from_str(cloud)
+    if infra is None:
+        infra = 'gcp'
 
     # Create the cluster.
     with sky.Dag() as dag:
         cluster_resources = sky.Resources(
-            cloud,
+            infra=infra,
             # We need to set CPUs to 5+ so that the total number of RUNNING jobs
             # is not limited by the number of CPU cores (5 x 2 x 2 = 20).
             cpus='5+',
@@ -56,13 +55,13 @@ def _exec(i):
 
 if __name__ == '__main__':
     cluster = None
-    cloud = None
+    infra = None
     use_spot = True
     if len(sys.argv) > 1:
         # For smoke test passing in a cluster name.
         cluster = sys.argv[1]
     if len(sys.argv) > 2:
-        cloud = sys.argv[2]
+        infra = sys.argv[2]
     if len(sys.argv) > 3:
         use_spot = sys.argv[3] == '1'
-    run(cluster, cloud, use_spot)
+    run(cluster, infra, use_spot)
diff --git a/examples/multi_hostname.py b/examples/multi_hostname.py
index 2c03a46fa19..e44a60e6d29 100644
--- a/examples/multi_hostname.py
+++ b/examples/multi_hostname.py
@@ -6,6 +6,6 @@
     #   My hostname: <host1>
     #   My hostname: <host2>
     sky.Task(run='echo My hostname: $(hostname)',
-             num_nodes=2).set_resources(sky.Resources(sky.AWS()))
+             num_nodes=2).set_resources(sky.Resources(infra='aws'))
 
 sky.launch(dag)
diff --git a/examples/multi_resources.yaml b/examples/multi_resources.yaml
index 56656b7cd1b..11f7c3eb23c 100644
--- a/examples/multi_resources.yaml
+++ b/examples/multi_resources.yaml
@@ -2,16 +2,16 @@ name: multi-resources
 
 resources:
   ordered:
-    - cloud: AWS
+    - infra: aws
       accelerators: A10g
-    - cloud: GCP
+    - infra: gcp
       accelerators: L4
 
 # resources:
 #   any_of:
-    # - cloud: AWS
+    # - infra: aws
     #   accelerators: A10g
-    # - cloud: GCP
+    # - infra: gcp
     #   accelerators: L4
 
 run: |
diff --git a/examples/oci/dataset-mount.yaml b/examples/oci/dataset-mount.yaml
index 1f62360a5a3..96a34c72af5 100644
--- a/examples/oci/dataset-mount.yaml
+++ b/examples/oci/dataset-mount.yaml
@@ -1,8 +1,7 @@
 name: cpu-task1
 
 resources:
-  cloud: oci
-  region: us-sanjose-1
+  infra: oci/us-sanjose-1
   cpus: 2
   disk_size: 256
   disk_tier: medium
diff --git a/examples/oci/dataset-upload-and-mount.yaml b/examples/oci/dataset-upload-and-mount.yaml
index 13ddc4d2b35..b28e754c126 100644
--- a/examples/oci/dataset-upload-and-mount.yaml
+++ b/examples/oci/dataset-upload-and-mount.yaml
@@ -1,8 +1,7 @@
 name: cpu-task1
 
 resources:
-  cloud: oci
-  region: us-sanjose-1
+  infra: oci/us-sanjose-1
   cpus: 2
   disk_size: 256
   disk_tier: medium
diff --git a/examples/oci/gpu-oraclelinux9.yaml b/examples/oci/gpu-oraclelinux9.yaml
index cc7b05ea0fc..4d24d6c9526 100644
--- a/examples/oci/gpu-oraclelinux9.yaml
+++ b/examples/oci/gpu-oraclelinux9.yaml
@@ -2,7 +2,7 @@ name: gpu-task
 
 resources:
   # Optional; if left out, automatically pick the cheapest cloud.
-  cloud: oci
+  infra: oci
 
   accelerators: A10:1
 
diff --git a/examples/oci/gpu-ubuntu-2204.yaml b/examples/oci/gpu-ubuntu-2204.yaml
index e0012a31a1a..b9fb1b35986 100644
--- a/examples/oci/gpu-ubuntu-2204.yaml
+++ b/examples/oci/gpu-ubuntu-2204.yaml
@@ -2,7 +2,7 @@ name: gpu-task
 
 resources:
   # Optional; if left out, automatically pick the cheapest cloud.
-  cloud: oci
+  infra: oci
 
   accelerators: A10:1
 
diff --git a/examples/oci/oci-mounts.yaml b/examples/oci/oci-mounts.yaml
index 6fd2aaf16eb..0d675fb3fe2 100644
--- a/examples/oci/oci-mounts.yaml
+++ b/examples/oci/oci-mounts.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: oci
+  infra: oci
 
 file_mounts:
   ~/tmpfile: ~/tmpfile
diff --git a/examples/oci/oci_cpu-sky-preemptible.yaml b/examples/oci/oci_cpu-sky-preemptible.yaml
index fb1c6e5f838..0d504a30ec4 100644
--- a/examples/oci/oci_cpu-sky-preemptible.yaml
+++ b/examples/oci/oci_cpu-sky-preemptible.yaml
@@ -2,12 +2,8 @@ name: cpu-task2
 
 resources:
   # Optional; if left out, automatically pick the cheapest cloud.
-  cloud: oci
+  infra: oci/ap-seoul-1
 
-  region: ap-seoul-1
-  
-  # zone: AP-SEOUL-1-AD-1
-  
   instance_type: VM.Standard.E4.Flex$_2_16
 
   cpus: 2
diff --git a/examples/oci/oci_cpu-sky.yaml b/examples/oci/oci_cpu-sky.yaml
index 41367a0700b..5a14f130ad8 100644
--- a/examples/oci/oci_cpu-sky.yaml
+++ b/examples/oci/oci_cpu-sky.yaml
@@ -2,12 +2,8 @@ name: cpu-task1
 
 resources:
   # Optional; if left out, automatically pick the cheapest cloud.
-  cloud: oci
+  infra: oci/ap-seoul-1
 
-  region: ap-seoul-1
-  
-  # zone: AP-SEOUL-1-AD-1
-  
   instance_type: VM.Standard.E4.Flex$_2_16
 
   cpus: 2
diff --git a/examples/oci/oci_gpu-sky.yaml b/examples/oci/oci_gpu-sky.yaml
index a3592145c89..ca05beb26a9 100644
--- a/examples/oci/oci_gpu-sky.yaml
+++ b/examples/oci/oci_gpu-sky.yaml
@@ -2,14 +2,10 @@ name: gpu-task1
 
 resources:
   # Optional; if left out, automatically pick the cheapest cloud.
-  cloud: oci
+  infra: oci/ap-seoul-1
 
   accelerators: A10:1  # 1x NVIDIA A10 GPU
 
-  region: ap-seoul-1
-  
-  # zone: AP-SEOUL-1-AD-1
-  
   # instance_type: VM.GPU.A10.1
 
   # image_id: skypilot:gpu-ubuntu-2004
diff --git a/examples/oci/serve-http-cpu.yaml b/examples/oci/serve-http-cpu.yaml
index 68e3d18c9e5..011b58ff10f 100644
--- a/examples/oci/serve-http-cpu.yaml
+++ b/examples/oci/serve-http-cpu.yaml
@@ -3,8 +3,7 @@ service:
   replicas: 2
 
 resources:
-  cloud: oci
-  region: us-sanjose-1
+  infra: oci/us-sanjose-1
   ports: 8080
   cpus: 2+
 
diff --git a/examples/oci/serve-qwen-7b.yaml b/examples/oci/serve-qwen-7b.yaml
index 004e912b088..d0a5d1f014d 100644
--- a/examples/oci/serve-qwen-7b.yaml
+++ b/examples/oci/serve-qwen-7b.yaml
@@ -5,8 +5,7 @@ service:
 
 # Fields below describe each replica.
 resources:
-  cloud: oci
-  region: us-sanjose-1
+  infra: oci/us-sanjose-1
   ports: 8080
   accelerators: {A10:1}
 
diff --git a/examples/per_region_images.yaml b/examples/per_region_images.yaml
index 99bc6e4f0c5..4e0e470969f 100644
--- a/examples/per_region_images.yaml
+++ b/examples/per_region_images.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: aws
+  infra: aws
   instance_type: g4dn.xlarge
   image_id:
     us-west-2: skypilot:gpu-ubuntu-1804
diff --git a/examples/perf/storage_rawperf.yaml b/examples/perf/storage_rawperf.yaml
index 982a1e7c43a..cc6263c712d 100644
--- a/examples/perf/storage_rawperf.yaml
+++ b/examples/perf/storage_rawperf.yaml
@@ -17,7 +17,7 @@
 name: storage-demo
 
 resources:
-  cloud: aws
+  infra: aws
   instance_type: m5.8xlarge
 
 file_mounts:
diff --git a/examples/playground/min_fail.yaml b/examples/playground/min_fail.yaml
index 215f3268855..bd64ba3b0fc 100644
--- a/examples/playground/min_fail.yaml
+++ b/examples/playground/min_fail.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: aws
+  infra: aws
 
 setup: |
   echo "running setup"
diff --git a/examples/playground/min_progress_bar.yaml b/examples/playground/min_progress_bar.yaml
index 06ba3b027e0..43499f0bf4c 100644
--- a/examples/playground/min_progress_bar.yaml
+++ b/examples/playground/min_progress_bar.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: aws
+  infra: aws
 
 setup: |
   echo "running setup"
diff --git a/examples/playground/symlink_playground.yaml b/examples/playground/symlink_playground.yaml
index 398373af85c..d53753f3efa 100644
--- a/examples/playground/symlink_playground.yaml
+++ b/examples/playground/symlink_playground.yaml
@@ -4,7 +4,7 @@
 name: symlink-playground
 
 resources:
-  cloud: aws
+  infra: aws
   instance_type: m5.2xlarge
 
 # Symlink: ln -s [data_path] ~/Downloads/temp1
diff --git a/examples/ray_tune_app.py b/examples/ray_tune_app.py
index 1993eb7e7d4..b08756ede0b 100644
--- a/examples/ray_tune_app.py
+++ b/examples/ray_tune_app.py
@@ -30,7 +30,7 @@ def run_fn(node_rank: int, ip_list: List[str]) -> Optional[str]:
     )
 
     train.set_resources({
-        sky.Resources(sky.AWS(), 'p3.2xlarge'),
+        sky.Resources(infra='aws', instance_type='p3.2xlarge'),
     })
 
 sky.launch(dag)
diff --git a/examples/ray_tune_app.yaml b/examples/ray_tune_app.yaml
index 96146b1ee2d..9c7bae9b099 100644
--- a/examples/ray_tune_app.yaml
+++ b/examples/ray_tune_app.yaml
@@ -1,5 +1,5 @@
 resources:
-    cloud: aws
+    infra: aws
     accelerators: V100
 
 num_nodes: 2
diff --git a/examples/resnet_app.py b/examples/resnet_app.py
index 17ebf9fa5d6..c7f43744ca3 100644
--- a/examples/resnet_app.py
+++ b/examples/resnet_app.py
@@ -68,10 +68,10 @@
 task.set_outputs('resnet-model-dir', estimated_size_gigabytes=0.1)
 task.set_resources({
     ##### Fully specified
-    # sky.Resources(sky.AWS(), 'p3.2xlarge'),
-    # sky.Resources(sky.GCP(), 'n1-standard-16'),
+    # sky.Resources(infra='aws', instance_type='p3.2xlarge'),
+    # sky.Resources(infra='gcp', instance_type='n1-standard-16'),
     # sky.Resources(
-    #     sky.GCP(),
+    #     infra='gcp',
     #     'n1-standard-8',
     #     # Options: 'V100', {'V100': <num>}.
     #     'V100',
@@ -79,16 +79,16 @@
     ##### Partially specified
     # sky.Resources(accelerators='T4'),
     # sky.Resources(accelerators={'T4': 8}, use_spot=True),
-    # sky.Resources(sky.AWS(), accelerators={'T4': 8}, use_spot=True),
-    # sky.Resources(sky.AWS(), accelerators='K80'),
-    # sky.Resources(sky.AWS(), accelerators='K80', use_spot=True),
+    # sky.Resources(infra='aws', accelerators={'T4': 8}, use_spot=True),
+    # sky.Resources(infra='aws', accelerators='K80'),
+    # sky.Resources(infra='aws', accelerators='K80', use_spot=True),
     # sky.Resources(accelerators='tpu-v3-8'),
     # sky.Resources(accelerators='V100', use_spot=True),
     # sky.Resources(accelerators={'T4': 4}),
-    sky.Resources(sky.AWS(), accelerators='V100'),
-    # sky.Resources(sky.GCP(), accelerators={'V100': 4}),
-    # sky.Resources(sky.AWS(), accelerators='V100', use_spot=True),
-    # sky.Resources(sky.AWS(), accelerators={'V100': 8}),
+    sky.Resources(infra='aws', accelerators='V100'),
+    # sky.Resources(infra='gcp', accelerators={'V100': 4}),
+    # sky.Resources(infra='aws', accelerators='V100', use_spot=True),
+    # sky.Resources(infra='aws', accelerators={'V100': 8}),
 })
 
 # Optionally, specify a time estimator: Resources -> time in seconds.
diff --git a/examples/resnet_app.yaml b/examples/resnet_app.yaml
index 4a37d332415..473dcea173c 100644
--- a/examples/resnet_app.yaml
+++ b/examples/resnet_app.yaml
@@ -1,7 +1,7 @@
 name: resnet-app
 
 resources:
-  cloud: aws
+  infra: aws
   accelerators:
     V100: 1
 
diff --git a/examples/resnet_app_storage.py b/examples/resnet_app_storage.py
index 9d8063ea6ab..707acecf11f 100644
--- a/examples/resnet_app_storage.py
+++ b/examples/resnet_app_storage.py
@@ -71,7 +71,7 @@
     train.set_inputs('s3://imagenet-bucket', estimated_size_gigabytes=150)
     train.set_outputs('resnet-model-dir', estimated_size_gigabytes=0.1)
     train.set_resources({
-        sky.Resources(sky.AWS(), 'p3.2xlarge'),
+        sky.Resources(infra='aws', instance_type='p3.2xlarge'),
     })
 
 sky.launch(dag)
diff --git a/examples/resnet_app_storage.yaml b/examples/resnet_app_storage.yaml
index 7a3ddd81b57..b6747ab5614 100644
--- a/examples/resnet_app_storage.yaml
+++ b/examples/resnet_app_storage.yaml
@@ -2,7 +2,7 @@ name: resnet-app-storage
 workdir: ~/Downloads/tpu
 
 resources:
-  cloud: aws
+  infra: aws
   instance_type: p3.2xlarge
 
 inputs: {
diff --git a/examples/resnet_app_storage_spot.yaml b/examples/resnet_app_storage_spot.yaml
index 0d4a3fec840..27ed558b4fc 100644
--- a/examples/resnet_app_storage_spot.yaml
+++ b/examples/resnet_app_storage_spot.yaml
@@ -1,7 +1,7 @@
 name: resnet-app-storage
 
 resources:
-  cloud: aws
+  infra: aws
   accelerators: V100
   use_spot: true
   spot_recovery: failover
diff --git a/examples/resnet_distributed_tf_app.py b/examples/resnet_distributed_tf_app.py
index 62befbbb313..2df1705e386 100644
--- a/examples/resnet_distributed_tf_app.py
+++ b/examples/resnet_distributed_tf_app.py
@@ -7,7 +7,7 @@
 import sky
 
 
-def run(cluster: Optional[str] = None, cloud: Optional[str] = None):
+def run(cluster: Optional[str] = None, infra: Optional[str] = None):
     if cluster is None:
         # (username, last 4 chars of hash of hostname): for uniquefying users on
         # shared-account cloud providers.
@@ -75,19 +75,17 @@ def run_fn(node_rank: int, ip_list: List[str]) -> Optional[str]:
         train.set_inputs('gs://cloud-tpu-test-datasets/fake_imagenet',
                          estimated_size_gigabytes=70)
         train.set_outputs('resnet-model-dir', estimated_size_gigabytes=0.1)
-        train.set_resources(
-            sky.Resources(sky.CLOUD_REGISTRY.from_str(cloud),
-                          accelerators='V100'))
+        train.set_resources(sky.Resources(infra=infra, accelerators='V100'))
 
     sky.launch(dag, cluster_name=cluster, retry_until_up=True)
 
 
 if __name__ == '__main__':
     cluster = None
-    cloud = None
+    infra = None
     if len(sys.argv) > 1:
         # For smoke test passing in a cluster name.
         cluster = sys.argv[1]
     if len(sys.argv) > 2:
-        cloud = sys.argv[2]
-    run(cluster, cloud)
+        infra = sys.argv[2]
+    run(cluster, infra)
diff --git a/examples/resnet_distributed_torch_app.py b/examples/resnet_distributed_torch_app.py
index 1bc38886536..9b31419cf85 100644
--- a/examples/resnet_distributed_torch_app.py
+++ b/examples/resnet_distributed_torch_app.py
@@ -35,19 +35,19 @@ def run_fn(node_rank: int, ip_list: List[str]) -> Optional[str]:
 
 train.set_resources({
     ##### Fully specified
-    sky.Resources(sky.AWS(), 'p3.2xlarge'),
-    # sky.Resources(sky.GCP(), 'n1-standard-16'),
+    sky.Resources(infra='aws', instance_type='p3.2xlarge'),
+    # sky.Resources(infra='gcp', instance_type='n1-standard-16'),
     #sky.Resources(
-    #     sky.GCP(),
-    #     'n1-standard-8',
+    #     infra='gcp',
+    #     instance_type='n1-standard-8',
     # Options: 'V100', {'V100': <num>}.
-    #     'V100',
+    #     accelerators='V100',
     #),
     ##### Partially specified
     #sky.Resources(accelerators='V100'),
     # sky.Resources(accelerators='tpu-v3-8'),
-    # sky.Resources(sky.AWS(), accelerators={'V100': 4}),
-    # sky.Resources(sky.AWS(), accelerators='V100'),
+    # sky.Resources(infra='aws', accelerators={'V100': 4}),
+    # sky.Resources(infra='aws', accelerators='V100'),
 })
 
 sky.launch(train, cluster_name='dth')
diff --git a/examples/resnet_distributed_torch_with_script.yaml b/examples/resnet_distributed_torch_with_script.yaml
index a492e4878b3..b3a07b227d9 100644
--- a/examples/resnet_distributed_torch_with_script.yaml
+++ b/examples/resnet_distributed_torch_with_script.yaml
@@ -2,7 +2,7 @@ name: resnet-distributed-app
 
 
 resources:
-    cloud: aws
+    infra: aws
     accelerators: V100
 
 num_nodes: 2
diff --git a/examples/serve/spot_policy/dynamic_on_demand_fallback.yaml b/examples/serve/spot_policy/dynamic_on_demand_fallback.yaml
index faa5e9f08ff..d5dd35a189a 100644
--- a/examples/serve/spot_policy/dynamic_on_demand_fallback.yaml
+++ b/examples/serve/spot_policy/dynamic_on_demand_fallback.yaml
@@ -11,8 +11,8 @@ service:
 
 resources:
   any_of:
-  - zone: us-central1-a
-  - region: us-east1
+  - infra: gcp/*/us-central1-a
+  - infra: gcp/us-east1
   ports: 8081
   cpus: 2+
   # use_spot is needed for ondemand fallback
diff --git a/examples/spot/lightning_cifar10.yaml b/examples/spot/lightning_cifar10.yaml
index 2a9aa0c761c..b8fbb11bb7e 100644
--- a/examples/spot/lightning_cifar10.yaml
+++ b/examples/spot/lightning_cifar10.yaml
@@ -2,7 +2,7 @@ name: lit
 
 resources:
     accelerators: V100:1
-    cloud: aws
+    infra: aws
     use_spot: true
     spot_recovery: FAILOVER
 
diff --git a/examples/spot/resnet.yaml b/examples/spot/resnet.yaml
index 54c13489f1a..7b439fff848 100644
--- a/examples/spot/resnet.yaml
+++ b/examples/spot/resnet.yaml
@@ -10,7 +10,7 @@ name: resnet
 
 resources:
     accelerators: V100
-    cloud: aws
+    infra: aws
     use_spot: true
     spot_recovery: FAILOVER
 
diff --git a/examples/storage/checkpointed_training.yaml b/examples/storage/checkpointed_training.yaml
index 7d96e9634ca..c0fc8c6b2bd 100644
--- a/examples/storage/checkpointed_training.yaml
+++ b/examples/storage/checkpointed_training.yaml
@@ -20,7 +20,7 @@ name: resnet-distributed-app
 
 resources:
     accelerators: V100
-    cloud: aws
+    infra: aws
 
 num_nodes: 1
 
diff --git a/examples/storage/hostname_echo_demo.yaml b/examples/storage/hostname_echo_demo.yaml
index d90edbc9ebd..1769593f601 100644
--- a/examples/storage/hostname_echo_demo.yaml
+++ b/examples/storage/hostname_echo_demo.yaml
@@ -10,7 +10,7 @@
 name: hostecho-demo
 
 resources:
-  cloud: aws
+  infra: aws
   instance_type: m5.2xlarge
 
 num_nodes: 2
diff --git a/examples/storage/pingpong.yaml b/examples/storage/pingpong.yaml
index fae72ab7f6a..345ade16162 100644
--- a/examples/storage/pingpong.yaml
+++ b/examples/storage/pingpong.yaml
@@ -14,7 +14,7 @@ name: pingpong
 num_nodes: 2
 
 resources:
-  cloud: gcp
+  infra: gcp
 
 file_mounts:
     /sharedfs:
diff --git a/examples/tensorboard_app.py b/examples/tensorboard_app.py
index a5432ee7a6f..e181c8dac7f 100644
--- a/examples/tensorboard_app.py
+++ b/examples/tensorboard_app.py
@@ -19,7 +19,7 @@
            cd models && pip install -e .)'
 
     task = sky.Task('setup', workdir=workdir, setup=setup)
-    task.set_resources(sky.Resources(sky.AWS(), accelerators={'V100': 1}))
+    task.set_resources(sky.Resources(infra='aws', accelerators={'V100': 1}))
 sky.stream_and_get(sky.launch(dag, cluster_name='tb'))
 
 # Run the training task.
diff --git a/examples/tensorflow_distributed/tf_distributed.yaml b/examples/tensorflow_distributed/tf_distributed.yaml
index beb6ad4b96e..0d59a538c30 100644
--- a/examples/tensorflow_distributed/tf_distributed.yaml
+++ b/examples/tensorflow_distributed/tf_distributed.yaml
@@ -7,7 +7,7 @@
 #   sky down myclus
 
 resources:
-  cloud: gcp
+  infra: gcp
   accelerators: V100:1 # Provision 1 V100 GPU per node
 
 # Provision 2 nodes, giving us a total of 2 GPUs in the cluster
diff --git a/examples/timm_app.py b/examples/timm_app.py
index 72ac53509c3..d3e9e4dd147 100644
--- a/examples/timm_app.py
+++ b/examples/timm_app.py
@@ -49,6 +49,6 @@ def clone_project():
         # Download from GCS.
         '/tmp/fake_imagenet': 'gs://cloud-tpu-test-datasets/fake_imagenet',
     })
-    train.set_resources({sky.Resources(sky.AWS(), accelerators='V100')})
+    train.set_resources({sky.Resources(infra='aws', accelerators='V100')})
 
 sky.launch(dag)
diff --git a/examples/torch_ddp_benchmark/torch_ddp_benchmark.yaml b/examples/torch_ddp_benchmark/torch_ddp_benchmark.yaml
index 36278961006..6c4c627aa4b 100644
--- a/examples/torch_ddp_benchmark/torch_ddp_benchmark.yaml
+++ b/examples/torch_ddp_benchmark/torch_ddp_benchmark.yaml
@@ -30,7 +30,7 @@ num_nodes: 2
 resources:
   accelerators: A100:8 # Make sure you use 8 GPU instances
   use_spot: True
-  cloud: gcp
+  infra: gcp
 
 file_mounts: 
   ./torch_ddp_benchmark.py: ./examples/torch_ddp_benchmark/torch_ddp_benchmark.py
diff --git a/examples/using_file_mounts.yaml b/examples/using_file_mounts.yaml
index fb7110ac705..5b6783efc9f 100644
--- a/examples/using_file_mounts.yaml
+++ b/examples/using_file_mounts.yaml
@@ -10,7 +10,7 @@
 #    commands may require flags to follow symlinks (e.g., ls -H; du -L).
 
 resources:
-  cloud: aws
+  infra: aws
   cpus: 2+
 
 workdir: .
diff --git a/examples/using_file_mounts_with_env_vars.yaml b/examples/using_file_mounts_with_env_vars.yaml
index 100aa3d15c9..11fcd99bb32 100644
--- a/examples/using_file_mounts_with_env_vars.yaml
+++ b/examples/using_file_mounts_with_env_vars.yaml
@@ -8,7 +8,7 @@ envs:
   MODEL_SIZE: 13b
 
 resources:
-  cloud: gcp
+  infra: gcp
 
 # You can use env vars in
 #   - the destination: source paths
diff --git a/llm/axolotl/axolotl-docker.yaml b/llm/axolotl/axolotl-docker.yaml
index b883ebdde46..25caf8ae408 100644
--- a/llm/axolotl/axolotl-docker.yaml
+++ b/llm/axolotl/axolotl-docker.yaml
@@ -5,7 +5,7 @@ name: axolotl
 
 resources:
   accelerators: L4:1
-  cloud: gcp # optional
+  infra: gcp # optional
 
 workdir: mistral
 
diff --git a/llm/axolotl/axolotl-spot.yaml b/llm/axolotl/axolotl-spot.yaml
index 0e04ba11992..e6c04f1bca7 100644
--- a/llm/axolotl/axolotl-spot.yaml
+++ b/llm/axolotl/axolotl-spot.yaml
@@ -10,7 +10,7 @@ name: axolotl
 
 resources:
   accelerators: A100:1
-  cloud: gcp # optional
+  infra: gcp # optional
   use_spot: True
   image_id: docker:winglian/axolotl:main-py3.10-cu118-2.0.1
 
diff --git a/llm/batch_inference/compute_text_vectors.yaml b/llm/batch_inference/compute_text_vectors.yaml
index 259bd685294..df197fcba80 100644
--- a/llm/batch_inference/compute_text_vectors.yaml
+++ b/llm/batch_inference/compute_text_vectors.yaml
@@ -6,7 +6,7 @@ resources:
   cpus: 4
   accelerators: 
     L4: 1
-  cloud: aws
+  infra: aws
   any_of:
     - use_spot: true
     - use_spot: false
@@ -83,4 +83,4 @@ run: |
   
   # Clean up vLLM service
   pkill -f "python -m vllm.entrypoints.openai.api_server"
-  echo "vLLM service has been stopped" 
\ No newline at end of file
+  echo "vLLM service has been stopped" 
diff --git a/llm/batch_inference/monitor_progress.yaml b/llm/batch_inference/monitor_progress.yaml
index 8f59b43325b..623d0df1dad 100644
--- a/llm/batch_inference/monitor_progress.yaml
+++ b/llm/batch_inference/monitor_progress.yaml
@@ -5,7 +5,7 @@ workdir: .
 resources:
   cpus: 2
   memory: 8+
-  cloud: aws
+  infra: aws
   ports:
     - 8000
 
@@ -26,4 +26,4 @@ setup: |
   pip install pandas pyarrow plotly
 
 run: |
-  python scripts/monitor_progress.py --metrics-dir /output/metrics 
\ No newline at end of file
+  python scripts/monitor_progress.py --metrics-dir /output/metrics 
diff --git a/llm/gpt-2/gpt2-pipeline.yaml b/llm/gpt-2/gpt2-pipeline.yaml
index e5ea05f7948..5d9b9d34164 100644
--- a/llm/gpt-2/gpt2-pipeline.yaml
+++ b/llm/gpt-2/gpt2-pipeline.yaml
@@ -46,13 +46,13 @@ resources:
   any_of:
     # Avoid using docker image for lambda due to the docker is not supported on
     # Lambda yet, but the base image works.
-    - cloud: lambda
+    - infra: lambda
       image_id: null
-    - cloud: aws
-    - cloud: gcp
-    - cloud: azure
-    - cloud: fluidstack
-    - cloud: kubernetes
+    - infra: aws
+    - infra: gcp
+    - infra: azure
+    - infra: fluidstack
+    - infra: kubernetes
   
 file_mounts:
   ~/.cache/huggingface:
diff --git a/llm/gpt-2/gpt2-train.yaml b/llm/gpt-2/gpt2-train.yaml
index 3a4e8c28d14..b3d48a67bd0 100644
--- a/llm/gpt-2/gpt2-train.yaml
+++ b/llm/gpt-2/gpt2-train.yaml
@@ -11,13 +11,13 @@ resources:
   any_of:
     # Avoid using docker image for lambda due to the docker is not supported on
     # Lambda yet, but the base image works.
-    - cloud: lambda
+    - infra: lambda
       image_id: null
-    - cloud: aws
-    - cloud: gcp
-    - cloud: azure
-    - cloud: fluidstack
-    - cloud: kubernetes
+    - infra: aws
+    - infra: gcp
+    - infra: azure
+    - infra: fluidstack
+    - infra: kubernetes
   
 file_mounts:
   ~/.cache/huggingface:
diff --git a/llm/gpt-2/gpt2.yaml b/llm/gpt-2/gpt2.yaml
index 8e203772128..6ede787d178 100644
--- a/llm/gpt-2/gpt2.yaml
+++ b/llm/gpt-2/gpt2.yaml
@@ -7,13 +7,13 @@ resources:
   any_of:
     # Avoid using docker image for lambda due to the docker is not supported on
     # Lambda yet, but the base image works.
-    - cloud: lambda
+    - infra: lambda
       image_id: null
-    - cloud: aws
-    - cloud: gcp
-    - cloud: azure
-    - cloud: fluidstack
-    - cloud: kubernetes
+    - infra: aws
+    - infra: gcp
+    - infra: azure
+    - infra: fluidstack
+    - infra: kubernetes
   
 
 setup: |
diff --git a/llm/rag/build_rag.yaml b/llm/rag/build_rag.yaml
index 9323f661903..ffd64911de0 100644
--- a/llm/rag/build_rag.yaml
+++ b/llm/rag/build_rag.yaml
@@ -4,7 +4,7 @@ workdir: .
 
 resources:
   memory: 32+  # Need more memory for merging embeddings
-  cloud: aws
+  infra: aws
 
 envs:
   EMBEDDINGS_BUCKET_NAME: sky-rag-embeddings
diff --git a/sky/backends/backend_utils.py b/sky/backends/backend_utils.py
index 3a1333e30ab..c33ffa80a88 100644
--- a/sky/backends/backend_utils.py
+++ b/sky/backends/backend_utils.py
@@ -2570,7 +2570,10 @@ def _update_record_with_credentials_and_resources_str(
         if handle is None:
             return
         record['resources_str'] = resources_utils.get_readable_resources_repr(
-            handle)
+            handle, simplify=True)
+        record[
+            'resources_str_full'] = resources_utils.get_readable_resources_repr(
+                handle, simplify=False)
         credentials = ssh_credential_from_yaml(handle.cluster_yaml,
                                                handle.docker_user,
                                                handle.ssh_user)
diff --git a/sky/backends/cloud_vm_ray_backend.py b/sky/backends/cloud_vm_ray_backend.py
index 48eb2ac7e0d..33dd56029c9 100644
--- a/sky/backends/cloud_vm_ray_backend.py
+++ b/sky/backends/cloud_vm_ray_backend.py
@@ -8,7 +8,6 @@
 import pathlib
 import re
 import shlex
-import shutil
 import signal
 import subprocess
 import sys
@@ -2157,11 +2156,18 @@ def provision_with_retries(
                 # possible resources or the requested resources is too
                 # restrictive. If we reach here, our failover logic finally
                 # ends here.
-                table = log_utils.create_table(['Resource', 'Reason'])
+                table = log_utils.create_table(['INFRA', 'RESOURCES', 'REASON'])
                 for (resource, exception) in resource_exceptions.items():
-                    table.add_row(
-                        [resources_utils.format_resource(resource), exception])
-                table.max_table_width = shutil.get_terminal_size().columns
+                    table.add_row([
+                        resource.infra.formatted_str(),
+                        resources_utils.format_resource(resource,
+                                                        simplify=True),
+                        exception
+                    ])
+                # Set the max width of REASON column to 80 to avoid the table
+                # being wrapped in a unreadable way.
+                # pylint: disable=protected-access
+                table._max_width = {'REASON': 80}
                 raise exceptions.ResourcesUnavailableError(
                     _RESOURCES_UNAVAILABLE_LOG + '\n' + table.get_string(),
                     failover_history=failover_history)
diff --git a/sky/check.py b/sky/check.py
index 65a3f92366e..6663e508748 100644
--- a/sky/check.py
+++ b/sky/check.py
@@ -34,7 +34,7 @@ def check_capabilities(
     echo = (lambda *_args, **_kwargs: None
            ) if quiet else lambda *args, **kwargs: click.echo(
                *args, **kwargs, color=True)
-    echo('Checking credentials to enable clouds for SkyPilot.')
+    echo('Checking credentials to enable infra for SkyPilot.')
     if capabilities is None:
         capabilities = sky_cloud.ALL_CAPABILITIES
     assert capabilities is not None
@@ -189,7 +189,7 @@ def get_all_clouds():
                                                   key=lambda item: item[0])
             ])
             echo(f'\n{colorama.Fore.GREEN}{PARTY_POPPER_EMOJI} '
-                 f'Enabled clouds {PARTY_POPPER_EMOJI}'
+                 f'Enabled infra {PARTY_POPPER_EMOJI}'
                  f'{colorama.Style.RESET_ALL}{enabled_clouds_str}')
     return enabled_clouds
 
diff --git a/sky/cli.py b/sky/cli.py
index bd067d95461..88e39fd398b 100644
--- a/sky/cli.py
+++ b/sky/cli.py
@@ -78,6 +78,7 @@
 from sky.utils import controller_utils
 from sky.utils import dag_utils
 from sky.utils import env_options
+from sky.utils import infra_utils
 from sky.utils import log_utils
 from sky.utils import registry
 from sky.utils import resources_utils
@@ -345,24 +346,39 @@ def return_option_decorator(func):
               'where the task will be invoked. '
               'Overrides the "workdir" config in the YAML if both are supplied.'
              )),
+    click.option(
+        '--infra',
+        required=False,
+        type=str,
+        help='Infrastructure to use. '
+        'Format: cloud, cloud/region, cloud/region/zone, '
+        'or kubernetes/context-name. '
+        'Examples: aws, aws/us-east-1, aws/us-east-1/us-east-1a, '
+        # TODO(zhwu): we have to use `\*` to make sure the docs build
+        # not complaining about the `*`, but this will cause `--help`
+        # to show `\*` instead of `*`.
+        'aws/\\*/us-east-1a, kubernetes/my-cluster-context.'),
     click.option(
         '--cloud',
         required=False,
         type=str,
         help=('The cloud to use. If specified, overrides the "resources.cloud" '
-              'config. Passing "none" resets the config.')),
+              'config. Passing "none" resets the config.'),
+        hidden=True),
     click.option(
         '--region',
         required=False,
         type=str,
         help=('The region to use. If specified, overrides the '
-              '"resources.region" config. Passing "none" resets the config.')),
+              '"resources.region" config. Passing "none" resets the config.'),
+        hidden=True),
     click.option(
         '--zone',
         required=False,
         type=str,
         help=('The zone to use. If specified, overrides the '
-              '"resources.zone" config. Passing "none" resets the config.')),
+              '"resources.zone" config. Passing "none" resets the config.'),
+        hidden=True),
     click.option(
         '--num-nodes',
         required=False,
@@ -1063,6 +1079,33 @@ def cli():
     pass
 
 
+def _handle_infra_cloud_region_zone_options(infra: Optional[str],
+                                            cloud: Optional[str],
+                                            region: Optional[str],
+                                            zone: Optional[str]):
+    """Handle the backward compatibility for --infra and --cloud/region/zone.
+
+    Returns:
+        cloud, region, zone
+    """
+    if cloud is not None or region is not None or zone is not None:
+        click.secho(
+            'The --cloud, --region, and --zone options are deprecated. '
+            'Use --infra instead.',
+            fg='yellow')
+        if infra is not None:
+            with ux_utils.print_exception_no_traceback():
+                raise ValueError('Cannot specify both --infra and '
+                                 '--cloud, --region, or --zone.')
+
+    if infra is not None:
+        infra_info = infra_utils.InfraInfo.from_str(infra)
+        cloud = infra_info.cloud
+        region = infra_info.region
+        zone = infra_info.zone
+    return cloud, region, zone
+
+
 @cli.command(cls=_DocumentedCodeCommand)
 @config_option(expose_value=True)
 @click.argument('entrypoint',
@@ -1172,6 +1215,7 @@ def launch(
         backend_name: Optional[str],
         name: Optional[str],
         workdir: Optional[str],
+        infra: Optional[str],
         cloud: Optional[str],
         region: Optional[str],
         zone: Optional[str],
@@ -1219,6 +1263,9 @@ def launch(
     if backend_name is None:
         backend_name = backends.CloudVmRayBackend.NAME
 
+    cloud, region, zone = _handle_infra_cloud_region_zone_options(
+        infra, cloud, region, zone)
+
     task_or_dag = _make_task_or_dag_from_entrypoint_with_overrides(
         entrypoint=entrypoint,
         name=name,
@@ -1336,6 +1383,7 @@ def exec(cluster: Optional[str],
          entrypoint: Tuple[str, ...],
          detach_run: bool,
          name: Optional[str],
+         infra: Optional[str],
          cloud: Optional[str],
          region: Optional[str],
          zone: Optional[str],
@@ -1427,6 +1475,9 @@ def exec(cluster: Optional[str],
     controller_utils.check_cluster_name_not_controller(
         cluster, operation_str='Executing task on it')
 
+    cloud, region, zone = _handle_infra_cloud_region_zone_options(
+        infra, cloud, region, zone)
+
     task_or_dag = _make_task_or_dag_from_entrypoint_with_overrides(
         entrypoint=entrypoint,
         name=name,
@@ -3265,7 +3316,7 @@ def _down_or_stop(name: str):
 
 @cli.command(cls=_DocumentedCodeCommand)
 @config_option(expose_value=False)
-@click.argument('clouds', required=False, type=str, nargs=-1)
+@click.argument('infra_list', required=False, type=str, nargs=-1)
 @click.option('--verbose',
               '-v',
               is_flag=True,
@@ -3273,7 +3324,7 @@ def _down_or_stop(name: str):
               help='Show the activated account for each cloud.')
 @usage_lib.entrypoint
 # pylint: disable=redefined-outer-name
-def check(clouds: Tuple[str], verbose: bool):
+def check(infra_list: Tuple[str], verbose: bool):
     """Check which clouds are available to use.
 
     This checks access credentials for all clouds supported by SkyPilot. If a
@@ -3295,8 +3346,8 @@ def check(clouds: Tuple[str], verbose: bool):
       # Check only specific clouds - AWS and GCP.
       sky check aws gcp
     """
-    clouds_arg = clouds if len(clouds) > 0 else None
-    request_id = sdk.check(clouds=clouds_arg, verbose=verbose)
+    infra_arg = infra_list if len(infra_list) > 0 else None
+    request_id = sdk.check(infra_list=infra_arg, verbose=verbose)
     sdk.stream_and_get(request_id)
     api_server_url = server_common.get_server_url()
     click.echo()
@@ -3312,10 +3363,15 @@ def check(clouds: Tuple[str], verbose: bool):
               is_flag=True,
               default=False,
               help='Show details of all GPU/TPU/accelerator offerings.')
+@click.option('--infra',
+              default=None,
+              type=str,
+              help='Infrastructure to query. Examples: "aws", "aws/us-east-1"')
 @click.option('--cloud',
               default=None,
               type=str,
-              help='Cloud provider to query.')
+              help='Cloud provider to query.',
+              hidden=True)
 @click.option(
     '--region',
     required=False,
@@ -3323,6 +3379,7 @@ def check(clouds: Tuple[str], verbose: bool):
     help=
     ('The region to use. If not specified, shows accelerators from all regions.'
     ),
+    hidden=True,
 )
 @click.option(
     '--all-regions',
@@ -3335,6 +3392,7 @@ def check(clouds: Tuple[str], verbose: bool):
 def show_gpus(
         accelerator_str: Optional[str],
         all: bool,  # pylint: disable=redefined-builtin
+        infra: Optional[str],
         cloud: Optional[str],
         region: Optional[str],
         all_regions: Optional[bool]):
@@ -3376,6 +3434,11 @@ def show_gpus(
     * ``UTILIZATION`` (Kubernetes only): Total number of GPUs free / available
       in the Kubernetes cluster.
     """
+    cloud, region, _ = _handle_infra_cloud_region_zone_options(infra,
+                                                               cloud,
+                                                               region,
+                                                               zone=None)
+
     # validation for the --region flag
     if region is not None and cloud is None:
         raise click.UsageError(
@@ -3991,6 +4054,7 @@ def jobs_launch(
     name: Optional[str],
     cluster: Optional[str],
     workdir: Optional[str],
+    infra: Optional[str],
     cloud: Optional[str],
     region: Optional[str],
     zone: Optional[str],
@@ -4032,6 +4096,8 @@ def jobs_launch(
                                    'Use one of the flags as they are alias.')
         name = cluster
     env = _merge_env_vars(env_file, env)
+    cloud, region, zone = _handle_infra_cloud_region_zone_options(
+        infra, cloud, region, zone)
     task_or_dag = _make_task_or_dag_from_entrypoint_with_overrides(
         entrypoint,
         name=name,
@@ -4509,6 +4575,7 @@ def serve_up(
     service_yaml: Tuple[str, ...],
     service_name: Optional[str],
     workdir: Optional[str],
+    infra: Optional[str],
     cloud: Optional[str],
     region: Optional[str],
     zone: Optional[str],
@@ -4555,6 +4622,8 @@ def serve_up(
 
         sky serve up service.yaml
     """
+    cloud, region, zone = _handle_infra_cloud_region_zone_options(
+        infra, cloud, region, zone)
     if service_name is None:
         service_name = serve_lib.generate_service_name()
 
@@ -4621,13 +4690,13 @@ def serve_up(
 @timeline.event
 @usage_lib.entrypoint
 def serve_update(service_name: str, service_yaml: Tuple[str, ...],
-                 workdir: Optional[str], cloud: Optional[str],
-                 region: Optional[str], zone: Optional[str],
-                 num_nodes: Optional[int], use_spot: Optional[bool],
-                 image_id: Optional[str], env_file: Optional[Dict[str, str]],
-                 env: List[Tuple[str, str]], gpus: Optional[str],
-                 instance_type: Optional[str], ports: Tuple[str],
-                 cpus: Optional[str], memory: Optional[str],
+                 workdir: Optional[str], infra: Optional[str],
+                 cloud: Optional[str], region: Optional[str],
+                 zone: Optional[str], num_nodes: Optional[int],
+                 use_spot: Optional[bool], image_id: Optional[str],
+                 env_file: Optional[Dict[str, str]], env: List[Tuple[str, str]],
+                 gpus: Optional[str], instance_type: Optional[str],
+                 ports: Tuple[str], cpus: Optional[str], memory: Optional[str],
                  disk_size: Optional[int], disk_tier: Optional[str], mode: str,
                  yes: bool, async_call: bool):
     """Update a SkyServe service.
@@ -4659,6 +4728,8 @@ def serve_update(service_name: str, service_yaml: Tuple[str, ...],
         sky serve update --mode blue_green sky-service-16aa new_service.yaml
 
     """
+    cloud, region, zone = _handle_infra_cloud_region_zone_options(
+        infra, cloud, region, zone)
     task = _generate_task_with_service(
         service_name=service_name,
         service_yaml_args=service_yaml,
@@ -5173,6 +5244,7 @@ def benchmark_launch(
     benchmark: str,
     name: Optional[str],
     workdir: Optional[str],
+    infra: Optional[str],
     cloud: Optional[str],
     region: Optional[str],
     zone: Optional[str],
@@ -5206,7 +5278,6 @@ def benchmark_launch(
         raise click.BadParameter(f'Benchmark {benchmark} already exists. '
                                  'To delete the previous benchmark result, '
                                  f'run `sky bench delete {benchmark}`.')
-
     entrypoint = ' '.join(entrypoint)
     if not entrypoint:
         raise click.BadParameter('Please specify a task yaml to benchmark.')
@@ -5217,6 +5288,8 @@ def benchmark_launch(
             'Sky Benchmark does not support command line tasks. '
             'Please provide a YAML file.')
     assert config is not None, (is_yaml, config)
+    cloud, region, zone = _handle_infra_cloud_region_zone_options(
+        infra, cloud, region, zone)
 
     click.secho('Benchmarking a task from YAML: ', fg='cyan', nl=False)
     click.secho(entrypoint, bold=True)
diff --git a/sky/client/sdk.py b/sky/client/sdk.py
index e5c38550aa8..a21cf70e811 100644
--- a/sky/client/sdk.py
+++ b/sky/client/sdk.py
@@ -42,6 +42,7 @@
 from sky.utils import common_utils
 from sky.utils import dag_utils
 from sky.utils import env_options
+from sky.utils import infra_utils
 from sky.utils import rich_utils
 from sky.utils import status_lib
 from sky.utils import subprocess_utils
@@ -87,12 +88,12 @@ def stream_response(request_id: Optional[str],
 @usage_lib.entrypoint
 @server_common.check_server_healthy_or_start
 @annotations.client_api
-def check(clouds: Optional[Tuple[str]],
+def check(infra_list: Optional[Tuple[str, ...]],
           verbose: bool) -> server_common.RequestId:
     """Checks the credentials to enable clouds.
 
     Args:
-        clouds: The clouds to check.
+        infra: The infra to check.
         verbose: Whether to show verbose output.
 
     Returns:
@@ -101,6 +102,22 @@ def check(clouds: Optional[Tuple[str]],
     Request Returns:
         None
     """
+    if infra_list is None:
+        clouds = None
+    else:
+        specified_clouds = []
+        for infra_str in infra_list:
+            infra = infra_utils.InfraInfo.from_str(infra_str)
+            if infra.cloud is None:
+                with ux_utils.print_exception_no_traceback():
+                    raise ValueError(f'Invalid infra to check: {infra_str}')
+            if infra.region is not None or infra.zone is not None:
+                region_zone = infra_str.partition('/')[-1]
+                logger.warning(f'Infra {infra_str} is specified, but `check` '
+                               f'only supports checking {infra.cloud}, '
+                               f'ignoring {region_zone}')
+            specified_clouds.append(infra.cloud)
+        clouds = tuple(specified_clouds)
     body = payloads.CheckBody(clouds=clouds, verbose=verbose)
     response = requests.post(f'{server_common.get_server_url()}/check',
                              json=json.loads(body.model_dump_json()),
@@ -344,7 +361,7 @@ def launch(
             import sky
             task = sky.Task(run='echo hello SkyPilot')
             task.set_resources(
-                sky.Resources(cloud=sky.AWS(), accelerators='V100:4'))
+                sky.Resources(infra='aws', accelerators='V100:4'))
             sky.launch(task, cluster_name='my-cluster')
 
 
diff --git a/sky/dashboard/src/components/clusters.jsx b/sky/dashboard/src/components/clusters.jsx
index e3b88e2cf46..804cdcba57e 100755
--- a/sky/dashboard/src/components/clusters.jsx
+++ b/sky/dashboard/src/components/clusters.jsx
@@ -7,7 +7,10 @@
 
 import React, { useState, useEffect } from 'react';
 import { CircularProgress } from '@mui/material';
-import { CustomTooltip as Tooltip } from '@/components/utils';
+import {
+  CustomTooltip as Tooltip,
+  NonCapitalizedTooltip,
+} from '@/components/utils';
 import Link from 'next/link';
 import { Button } from '@/components/ui/button';
 import { Card } from '@/components/ui/card';
@@ -228,15 +231,15 @@ export function ClusterTable({
               </TableHead>
               <TableHead
                 className="sortable whitespace-nowrap"
-                onClick={() => requestSort('resources_str')}
+                onClick={() => requestSort('infra')}
               >
-                Resources{getSortDirection('resources_str')}
+                Infra{getSortDirection('infra')}
               </TableHead>
               <TableHead
                 className="sortable whitespace-nowrap"
-                onClick={() => requestSort('region')}
+                onClick={() => requestSort('resources_str')}
               >
-                Region{getSortDirection('region')}
+                Resources{getSortDirection('resources_str')}
               </TableHead>
               <TableHead
                 className="sortable whitespace-nowrap"
@@ -277,8 +280,22 @@ export function ClusterTable({
                       </Link>
                     </TableCell>
                     <TableCell>{item.user}</TableCell>
-                    <TableCell>{item.resources_str}</TableCell>
-                    <TableCell>{item.region}</TableCell>
+                    <TableCell>
+                      <NonCapitalizedTooltip
+                        content={item.full_infra || item.infra}
+                        className="text-sm text-muted-foreground"
+                      >
+                        <span>{item.infra}</span>
+                      </NonCapitalizedTooltip>
+                    </TableCell>
+                    <TableCell>
+                      <NonCapitalizedTooltip
+                        content={item.resources_str_full || item.resources_str}
+                        className="text-sm text-muted-foreground"
+                      >
+                        <span>{item.resources_str}</span>
+                      </NonCapitalizedTooltip>
+                    </TableCell>
                     <TableCell>{relativeTime(item.time)}</TableCell>
                     <TableCell className="text-left">
                       <Status2Actions
diff --git a/sky/dashboard/src/components/jobs.jsx b/sky/dashboard/src/components/jobs.jsx
index 98b44e277dd..4ae9854f743 100755
--- a/sky/dashboard/src/components/jobs.jsx
+++ b/sky/dashboard/src/components/jobs.jsx
@@ -21,7 +21,11 @@ import { formatDuration } from '@/components/utils';
 import { getManagedJobs } from '@/data/connectors/jobs';
 import { getClusters } from '@/data/connectors/clusters';
 import { Layout } from '@/components/elements/layout';
-import { CustomTooltip as Tooltip, relativeTime } from '@/components/utils';
+import {
+  CustomTooltip as Tooltip,
+  NonCapitalizedTooltip,
+  relativeTime,
+} from '@/components/utils';
 import {
   FileSearchIcon,
   RotateCwIcon,
@@ -490,22 +494,23 @@ export function ManagedJobsTable({
               </TableHead>
               <TableHead
                 className="sortable whitespace-nowrap"
-                onClick={() => requestSort('resources')}
+                onClick={() => requestSort('resources_str')}
               >
-                Resources{getSortDirection('resources')}
+                Requested{getSortDirection('resources_str')}
               </TableHead>
               <TableHead
                 className="sortable whitespace-nowrap"
-                onClick={() => requestSort('cluster')}
+                onClick={() => requestSort('infra')}
               >
-                Cluster{getSortDirection('cluster')}
+                Infra{getSortDirection('infra')}
               </TableHead>
               <TableHead
                 className="sortable whitespace-nowrap"
-                onClick={() => requestSort('region')}
+                onClick={() => requestSort('cluster')}
               >
-                Region{getSortDirection('region')}
+                Resources{getSortDirection('cluster')}
               </TableHead>
+
               <TableHead
                 className="sortable whitespace-nowrap"
                 onClick={() => requestSort('recoveries')}
@@ -520,7 +525,7 @@ export function ManagedJobsTable({
             {loading && isInitialLoad ? (
               <TableRow>
                 <TableCell
-                  colSpan={12}
+                  colSpan={11}
                   className="text-center py-6 text-gray-500"
                 >
                   <div className="flex justify-center items-center">
@@ -556,9 +561,25 @@ export function ManagedJobsTable({
                       <TableCell>
                         <StatusBadge status={item.status} />
                       </TableCell>
-                      <TableCell>{item.resources}</TableCell>
-                      <TableCell>{item.cluster}</TableCell>
-                      <TableCell>{item.region}</TableCell>
+                      <TableCell>{item.requested_resources}</TableCell>
+                      <TableCell>
+                        <NonCapitalizedTooltip
+                          content={item.full_infra || item.infra}
+                          className="text-sm text-muted-foreground"
+                        >
+                          <span>{item.infra}</span>
+                        </NonCapitalizedTooltip>
+                      </TableCell>
+                      <TableCell>
+                        <NonCapitalizedTooltip
+                          content={
+                            item.resources_str_full || item.resources_str
+                          }
+                          className="text-sm text-muted-foreground"
+                        >
+                          <span>{item.resources_str}</span>
+                        </NonCapitalizedTooltip>
+                      </TableCell>
                       <TableCell>{item.recoveries}</TableCell>
                       <TableCell>
                         {item.details ? (
@@ -583,7 +604,7 @@ export function ManagedJobsTable({
                     {expandedRowId === item.id && (
                       <ExpandedDetailsRow
                         text={item.details}
-                        colSpan={12}
+                        colSpan={11}
                         innerRef={expandedRowRef}
                       />
                     )}
@@ -592,7 +613,7 @@ export function ManagedJobsTable({
               </>
             ) : (
               <TableRow>
-                <TableCell colSpan={12} className="text-center py-6">
+                <TableCell colSpan={11} className="text-center py-6">
                   <div className="flex flex-col items-center space-y-4">
                     {controllerLaunching && (
                       <div className="flex flex-col items-center space-y-2">
diff --git a/sky/dashboard/src/components/utils.jsx b/sky/dashboard/src/components/utils.jsx
index 8044abac9df..e4fe8a2b2e2 100644
--- a/sky/dashboard/src/components/utils.jsx
+++ b/sky/dashboard/src/components/utils.jsx
@@ -87,6 +87,24 @@ export const CustomTooltip = ({ children, ...props }) => {
   );
 };
 
+export const NonCapitalizedTooltip = ({ children, ...props }) => {
+  const content = props.content;
+  props.content = undefined;
+  return (
+    <Tooltip
+      {...DEFAULT_TOOLTIP_PROPS}
+      {...props}
+      content={
+        <span className="left-full w-max px-2 py-1 text-sm text-gray-100 bg-gray-500 text-sm rounded">
+          {content}
+        </span>
+      }
+    >
+      {children}
+    </Tooltip>
+  );
+};
+
 // Format duration from seconds to a readable format
 export function formatDuration(durationInSeconds) {
   if (!durationInSeconds && durationInSeconds !== 0) return '-';
diff --git a/sky/dashboard/src/data/connectors/clusters.jsx b/sky/dashboard/src/data/connectors/clusters.jsx
index 71944182602..8fb5052be52 100644
--- a/sky/dashboard/src/data/connectors/clusters.jsx
+++ b/sky/dashboard/src/data/connectors/clusters.jsx
@@ -4,6 +4,38 @@ import { useState, useEffect, useCallback } from 'react';
 import { showToast } from '@/data/connectors/toast';
 import { ENDPOINT } from '@/data/connectors/constants';
 
+/**
+ * Truncates a string in the middle, preserving parts from beginning and end.
+ * @param {string} str - The string to truncate
+ * @param {number} maxLength - Maximum length of the truncated string
+ * @return {string} - Truncated string
+ */
+function truncateMiddle(str, maxLength = 15) {
+  if (!str || str.length <= maxLength) return str;
+
+  // Reserve 3 characters for '...'
+  if (maxLength <= 3) return '...';
+
+  // Calculate how many characters to keep from beginning and end
+  const halfLength = Math.floor((maxLength - 3) / 2);
+  const remainder = (maxLength - 3) % 2;
+
+  // Keep one more character at the beginning if maxLength - 3 is odd
+  const startLength = halfLength + remainder;
+  const endLength = halfLength;
+
+  // When endLength is 0, just show the start part and '...'
+  if (endLength === 0) {
+    return str.substring(0, startLength) + '...';
+  }
+
+  return (
+    str.substring(0, startLength) +
+    '...' +
+    str.substring(str.length - endLength)
+  );
+}
+
 const clusterStatusMap = {
   UP: 'RUNNING',
   STOPPED: 'STOPPED',
@@ -31,16 +63,33 @@ export async function getClusters({ clusterNames = null } = {}) {
     const data = await fetchedData.json();
     const clusters = data.return_value ? JSON.parse(data.return_value) : [];
     const clusterData = clusters.map((cluster) => {
+      let region_or_zone = '';
+      if (cluster.zone) {
+        region_or_zone = cluster.zone;
+      } else {
+        region_or_zone = cluster.region;
+      }
+      // Store the full value before truncation
+      const full_region_or_zone = region_or_zone;
+      // Truncate region_or_zone in the middle if it's too long
+      if (region_or_zone && region_or_zone.length > 25) {
+        region_or_zone = truncateMiddle(region_or_zone, 25);
+      }
       return {
         status: clusterStatusMap[cluster.status],
         cluster: cluster.name,
         user: cluster.user_name,
-        infra: cluster.cloud,
-        region: cluster.region,
+        infra: region_or_zone
+          ? cluster.cloud + ' (' + region_or_zone + ')'
+          : cluster.cloud,
+        full_infra: full_region_or_zone
+          ? `${cluster.cloud} (${full_region_or_zone})`
+          : cluster.cloud,
         cpus: cluster.cpus,
         mem: cluster.memory,
         gpus: cluster.accelerators,
         resources_str: cluster.resources_str,
+        resources_str_full: cluster.resources_str_full,
         time: new Date(cluster.launched_at * 1000),
         num_nodes: cluster.nodes,
         jobs: [],
@@ -169,7 +218,7 @@ export function useClusterDetails({ cluster, job = null }) {
     if (cluster) {
       try {
         setLoadingClusterJobData(true);
-        const data = await getClusterJobs({ clusterName: cluster, job: job });
+        const data = await getClusterJobs({ clusterName: cluster });
         setClusterJobData(data);
       } catch (error) {
         console.error('Error fetching cluster job data:', error);
@@ -177,7 +226,7 @@ export function useClusterDetails({ cluster, job = null }) {
         setLoadingClusterJobData(false);
       }
     }
-  }, [cluster, job]);
+  }, [cluster]);
 
   const refreshData = useCallback(async () => {
     await Promise.all([fetchClusterData(), fetchClusterJobData()]);
diff --git a/sky/dashboard/src/data/connectors/jobs.jsx b/sky/dashboard/src/data/connectors/jobs.jsx
index 55cf10cf0f9..3f3728496fb 100644
--- a/sky/dashboard/src/data/connectors/jobs.jsx
+++ b/sky/dashboard/src/data/connectors/jobs.jsx
@@ -82,6 +82,49 @@ export async function getManagedJobs({ allUsers = true } = {}) {
       let endTime = job.end_at ? job.end_at : Date.now() / 1000;
       const total_duration = endTime - job.submitted_at;
 
+      // Extract cloud name if not available (backward compatibility)
+      // TODO(zhwu): remove this after 0.12.0
+      let cloud = job.cloud;
+      let cluster_resources = job.cluster_resources;
+      if (!cloud) {
+        // Backward compatibility for old jobs controller without cloud info
+        // Similar to the logic in sky/jobs/utils.py
+        if (job.cluster_resources && job.cluster_resources !== '-') {
+          try {
+            cloud = job.cluster_resources.split('(')[0].split('x').pop().trim();
+            cluster_resources = job.cluster_resources
+              .replace(`${cloud}(`, '(')
+              .replace('x ', 'x');
+          } catch (error) {
+            // If parsing fails, set a default value
+            cloud = 'Unknown';
+          }
+        } else {
+          cloud = 'Unknown';
+        }
+      }
+
+      let region_or_zone = '';
+      if (job.zone) {
+        region_or_zone = job.zone;
+      } else {
+        region_or_zone = job.region;
+      }
+
+      const full_region_or_zone = region_or_zone;
+      if (region_or_zone && region_or_zone.length > 15) {
+        region_or_zone = region_or_zone.substring(0, 15) + '...';
+      }
+
+      let infra = cloud + ' (' + region_or_zone + ')';
+      if (region_or_zone === '-') {
+        infra = cloud;
+      }
+      let full_infra = cloud + ' (' + full_region_or_zone + ')';
+      if (full_region_or_zone === '-') {
+        full_infra = cloud;
+      }
+
       return {
         id: job.job_id,
         task: job.task_name,
@@ -89,9 +132,11 @@ export async function getManagedJobs({ allUsers = true } = {}) {
         job_duration: job.job_duration,
         total_duration: total_duration,
         status: job.status,
-        resources: job.resources,
-        cluster: job.cluster_resources,
-        region: job.region,
+        requested_resources: job.resources,
+        resources_str: cluster_resources,
+        resources_str_full: job.cluster_resources_full || cluster_resources,
+        infra: infra,
+        full_infra: full_infra,
         recoveries: job.recovery_count,
         details: job.failure_reason,
         user: job.user_name,
diff --git a/sky/dashboard/src/pages/clusters/[cluster].js b/sky/dashboard/src/pages/clusters/[cluster].js
index 60549cbe30c..4d15ca6f71e 100644
--- a/sky/dashboard/src/pages/clusters/[cluster].js
+++ b/sky/dashboard/src/pages/clusters/[cluster].js
@@ -147,6 +147,14 @@ function ActiveTab({ clusterData, clusterJobData, loading }) {
           </div>
           <div className="p-4">
             <div className="grid grid-cols-2 gap-6">
+              <div>
+                <div className="text-gray-600 font-medium text-base">
+                  Status
+                </div>
+                <div className="text-base mt-1">
+                  <StatusBadge status={clusterData.status} />
+                </div>
+              </div>
               <div>
                 <div className="text-gray-600 font-medium text-base">
                   Cluster
@@ -158,11 +166,9 @@ function ActiveTab({ clusterData, clusterJobData, loading }) {
                 <div className="text-base mt-1">{clusterData.user}</div>
               </div>
               <div>
-                <div className="text-gray-600 font-medium text-base">
-                  Status
-                </div>
+                <div className="text-gray-600 font-medium text-base">Infra</div>
                 <div className="text-base mt-1">
-                  <StatusBadge status={clusterData.status} />
+                  {clusterData.full_infra || clusterData.infra || 'N/A'}
                 </div>
               </div>
               <div>
@@ -170,15 +176,19 @@ function ActiveTab({ clusterData, clusterJobData, loading }) {
                   Resources
                 </div>
                 <div className="text-base mt-1">
-                  {clusterData.resources_str || 'N/A'}
+                  {clusterData.resources_str_full ||
+                    clusterData.resources_str ||
+                    'N/A'}
                 </div>
               </div>
               <div>
                 <div className="text-gray-600 font-medium text-base">
-                  Region
+                  Started
                 </div>
                 <div className="text-base mt-1">
-                  {clusterData.region || 'N/A'}
+                  {clusterData.time
+                    ? new Date(clusterData.time).toLocaleString()
+                    : 'N/A'}
                 </div>
               </div>
             </div>
diff --git a/sky/dashboard/src/pages/clusters/[cluster]/[job].js b/sky/dashboard/src/pages/clusters/[cluster]/[job].js
index 54ea6f31b66..1d67b8c344a 100755
--- a/sky/dashboard/src/pages/clusters/[cluster]/[job].js
+++ b/sky/dashboard/src/pages/clusters/[cluster]/[job].js
@@ -228,7 +228,7 @@ export function JobDetailPage() {
                   {jobData.resources && (
                     <div>
                       <div className="text-gray-600 font-medium text-base">
-                        Resources
+                        Requested Resources
                       </div>
                       <div className="text-base mt-1">
                         {jobData.resources || 'N/A'}
diff --git a/sky/dashboard/src/pages/jobs/[job].js b/sky/dashboard/src/pages/jobs/[job].js
index 4b9b0c44cb2..5a079444a4c 100755
--- a/sky/dashboard/src/pages/jobs/[job].js
+++ b/sky/dashboard/src/pages/jobs/[job].js
@@ -450,12 +450,10 @@ function JobDetailsContent({
   return (
     <div className="grid grid-cols-2 gap-6">
       <div>
-        <div className="text-gray-600 font-medium text-base">Job ID</div>
-        <div className="text-base mt-1">{jobData.id}</div>
-      </div>
-      <div>
-        <div className="text-gray-600 font-medium text-base">Job Name</div>
-        <div className="text-base mt-1">{jobData.name}</div>
+        <div className="text-gray-600 font-medium text-base">Job ID (Name)</div>
+        <div className="text-base mt-1">
+          {jobData.id} {jobData.name ? `(${jobData.name})` : ''}
+        </div>
       </div>
       <div>
         <div className="text-gray-600 font-medium text-base">Status</div>
@@ -468,12 +466,22 @@ function JobDetailsContent({
         <div className="text-base mt-1">{jobData.user}</div>
       </div>
       <div>
-        <div className="text-gray-600 font-medium text-base">Resources</div>
-        <div className="text-base mt-1">{jobData.resources || 'N/A'}</div>
+        <div className="text-gray-600 font-medium text-base">
+          Requested Resources
+        </div>
+        <div className="text-base mt-1">
+          {jobData.requested_resources || 'N/A'}
+        </div>
+      </div>
+      <div>
+        <div className="text-gray-600 font-medium text-base">Infra</div>
+        <div className="text-base mt-1">{jobData.infra || '-'}</div>
       </div>
       <div>
-        <div className="text-gray-600 font-medium text-base">Cluster</div>
-        <div className="text-base mt-1">{jobData.cluster || '-'}</div>
+        <div className="text-gray-600 font-medium text-base">Resources</div>
+        <div className="text-base mt-1">
+          {jobData.resources_str_full || jobData.resources_str || '-'}
+        </div>
       </div>
     </div>
   );
diff --git a/sky/execution.py b/sky/execution.py
index 9d42ac11689..b173cc8b407 100644
--- a/sky/execution.py
+++ b/sky/execution.py
@@ -465,7 +465,7 @@ def launch(
             import sky
             task = sky.Task(run='echo hello SkyPilot')
             task.set_resources(
-                sky.Resources(cloud=sky.AWS(), accelerators='V100:4'))
+                sky.Resources(infra='aws', accelerators='V100:4'))
             sky.launch(task, cluster_name='my-cluster')
 
 
diff --git a/sky/jobs/server/core.py b/sky/jobs/server/core.py
index 09080c8a012..e64befc7488 100644
--- a/sky/jobs/server/core.py
+++ b/sky/jobs/server/core.py
@@ -395,7 +395,7 @@ def queue(refresh: bool,
     if returncode != 0:
         logger.error(job_table_payload + stderr)
         raise RuntimeError('Failed to fetch managed jobs with returncode: '
-                           f'{returncode}')
+                           f'{returncode}.\n{job_table_payload + stderr}')
 
     jobs = managed_job_utils.load_managed_job_queue(job_table_payload)
 
diff --git a/sky/jobs/utils.py b/sky/jobs/utils.py
index 73d96185c9e..c0eee370881 100644
--- a/sky/jobs/utils.py
+++ b/sky/jobs/utils.py
@@ -33,8 +33,10 @@
 from sky.skylet import log_lib
 from sky.usage import usage_lib
 from sky.utils import common_utils
+from sky.utils import infra_utils
 from sky.utils import log_utils
 from sky.utils import message_utils
+from sky.utils import resources_utils
 from sky.utils import rich_utils
 from sky.utils import subprocess_utils
 from sky.utils import ux_utils
@@ -911,15 +913,23 @@ def dump_managed_job_queue() -> str:
         cluster_name = generate_managed_job_cluster_name(
             job['task_name'], job['job_id'])
         handle = global_user_state.get_handle_from_cluster_name(cluster_name)
-        if handle is not None:
-            assert isinstance(handle, backends.CloudVmRayResourceHandle)
-            job['cluster_resources'] = (
-                f'{handle.launched_nodes}x {handle.launched_resources}')
+        if isinstance(handle, backends.CloudVmRayResourceHandle):
+            resources_str = resources_utils.get_readable_resources_repr(
+                handle, simplify=True)
+            resources_str_full = resources_utils.get_readable_resources_repr(
+                handle, simplify=False)
+            job['cluster_resources'] = resources_str
+            job['cluster_resources_full'] = resources_str_full
+            job['cloud'] = str(handle.launched_resources.cloud)
             job['region'] = handle.launched_resources.region
+            job['zone'] = handle.launched_resources.zone
         else:
             # FIXME(zongheng): display the last cached values for these.
             job['cluster_resources'] = '-'
+            job['cluster_resources_full'] = '-'
+            job['cloud'] = '-'
             job['region'] = '-'
+            job['zone'] = '-'
 
     return message_utils.encode_payload(jobs)
 
@@ -1026,7 +1036,7 @@ def get_hash(task):
         'TASK',
         'NAME',
         *user_cols,
-        'RESOURCES',
+        'REQUESTED',
         'SUBMITTED',
         'TOT. DURATION',
         'JOB DURATION',
@@ -1035,7 +1045,7 @@ def get_hash(task):
     ]
     if show_all:
         # TODO: move SCHED. STATE to a separate flag (e.g. --debug)
-        columns += ['STARTED', 'CLUSTER', 'REGION', 'SCHED. STATE', 'DETAILS']
+        columns += ['STARTED', 'INFRA', 'RESOURCES', 'SCHED. STATE', 'DETAILS']
     if tasks_have_k8s_user:
         columns.insert(0, 'USER')
     job_table = log_utils.create_table(columns)
@@ -1174,11 +1184,32 @@ def get_user_column_values(task: Dict[str, Any]) -> List[str]:
                 # more than one task, only display on the aggregated row.
                 schedule_state = (task['schedule_state']
                                   if len(job_tasks) == 1 else '-')
+                cloud = task.get('cloud')
+                if cloud is None:
+                    # Backward compatibility for old jobs controller without
+                    # cloud info returned, we parse it from the cluster
+                    # resources
+                    # TODO(zhwu): remove this after 0.12.0
+                    cloud = task['cluster_resources'].split('(')[0].split(
+                        'x')[-1]
+                    task['cluster_resources'] = task[
+                        'cluster_resources'].replace(f'{cloud}(',
+                                                     '(').replace('x ', 'x')
+                region = task['region']
+                zone = task.get('zone')
+                if cloud == '-':
+                    cloud = None
+                if region == '-':
+                    region = None
+                if zone == '-':
+                    zone = None
+
+                infra = infra_utils.InfraInfo(cloud, region, zone)
                 values.extend([
                     # STARTED
                     log_utils.readable_time_duration(task['start_at']),
+                    infra.formatted_str(),
                     task['cluster_resources'],
-                    task['region'],
                     schedule_state,
                     generate_details(task['failure_reason']),
                 ])
diff --git a/sky/optimizer.py b/sky/optimizer.py
index f4a9fa03553..453afb0b633 100644
--- a/sky/optimizer.py
+++ b/sky/optimizer.py
@@ -167,7 +167,7 @@ def _add_dummy_source_sink_nodes(dag: 'dag_lib.Dag'):
 
         def make_dummy(name):
             dummy = task_lib.Task(name)
-            dummy.set_resources({DummyResources(DummyCloud(), None)})
+            dummy.set_resources({DummyResources(cloud=DummyCloud())})
             dummy.set_time_estimator(lambda _: 0)
             return dummy
 
@@ -321,10 +321,10 @@ def get_reservations_available_resources(
                     estimated_runtime = 1 * 3600
                 else:
                     # We assume the time estimator takes in a partial resource
-                    #    Resources('V100')
+                    #    Resources(accelerators='V100')
                     # and treats their launchable versions
-                    #    Resources(AWS, 'p3.2xlarge'),
-                    #    Resources(GCP, '...', 'V100'),
+                    #    Resources(infra='aws', instance_type='p3.2xlarge'),
+                    #    Resources(infra='gcp', accelerators='V100'),
                     #    ...
                     # as having the same run time.
                     # FIXME(zongheng): take 'num_nodes' as an arg/into
@@ -772,6 +772,15 @@ def print_optimized_plan(
                     f'{colorama.Style.BRIGHT}Estimated total cost: '
                     f'{colorama.Style.RESET_ALL}${total_cost:.1f}\n')
 
+        def _instance_type_str(resources: 'resources_lib.Resources') -> str:
+            instance_type = resources.instance_type
+            assert instance_type is not None, 'Instance type must be specified'
+            if isinstance(resources.cloud, clouds.Kubernetes):
+                instance_type = '-'
+                if resources.use_spot:
+                    instance_type = ''
+            return instance_type
+
         def _get_resources_element_list(
                 resources: 'resources_lib.Resources') -> List[str]:
             accelerators = resources.get_accelerators_str()
@@ -794,22 +803,20 @@ def format_number(x: Optional[float]) -> str:
             vcpus = format_number(vcpus_)
             mem = format_number(mem_)
 
-            if resources.zone is None:
-                region_or_zone = resources.region
-            else:
-                region_or_zone = resources.zone
+            # Format infra as CLOUD (REGION/ZONE)
+            infra = resources.infra.formatted_str()
+
             return [
-                str(cloud),
-                resources.instance_type + spot,
+                infra,
+                _instance_type_str(resources) + spot,
                 vcpus,
                 mem,
                 str(accelerators),
-                str(region_or_zone),
             ]
 
         Row = collections.namedtuple('Row', [
-            'cloud', 'instance', 'vcpus', 'mem', 'accelerators',
-            'region_or_zone', 'cost_str', 'chosen_str'
+            'infra', 'instance', 'vcpus', 'mem', 'accelerators', 'cost_str',
+            'chosen_str'
         ])
 
         def _get_resources_named_tuple(resources: 'resources_lib.Resources',
@@ -833,18 +840,15 @@ def format_number(x: Optional[float]) -> str:
             vcpus = format_number(vcpus_)
             mem = format_number(mem_)
 
-            if resources.zone is None:
-                region_or_zone = resources.region
-            else:
-                region_or_zone = resources.zone
+            infra = resources.infra.formatted_str()
 
             chosen_str = ''
             if chosen:
                 chosen_str = (colorama.Fore.GREEN + '   ' + '\u2714' +
                               colorama.Style.RESET_ALL)
-            row = Row(cloud, resources.instance_type + spot, vcpus, mem,
-                      str(accelerators), str(region_or_zone), cost_str,
-                      chosen_str)
+            row = Row(infra,
+                      _instance_type_str(resources) + spot, vcpus, mem,
+                      str(accelerators), cost_str, chosen_str)
 
             return row
 
@@ -862,10 +866,7 @@ def _get_resource_group_hash(resources: 'resources_lib.Resources'):
             return json.dumps(resource_key_dict, sort_keys=True)
 
         # Print the list of resouces that the optimizer considered.
-        resource_fields = [
-            'CLOUD', 'INSTANCE', 'vCPUs', 'Mem(GB)', 'ACCELERATORS',
-            'REGION/ZONE'
-        ]
+        resource_fields = ['INFRA', 'INSTANCE', 'vCPUs', 'Mem(GB)', 'GPUS']
         if len(ordered_best_plan) > 1:
             best_plan_rows = []
             for t, r in ordered_best_plan.items():
@@ -993,13 +994,19 @@ def _print_candidates(node_to_candidate_map: _TaskToPerCloudCandidates):
                     if len(candidate_list) > 1:
                         is_multi_instances = True
                         instance_list = [
-                            res.instance_type for res in candidate_list
+                            res.instance_type
+                            for res in candidate_list
+                            if res.instance_type is not None
                         ]
+                        candidate_str = resources_utils.format_resource(
+                            candidate_list[0], simplify=True)
+
                         logger.info(
-                            f'Multiple {cloud} instances satisfy '
-                            f'{acc_name}:{int(acc_count)}. '
-                            f'The cheapest {candidate_list[0]!r} is considered '
-                            f'among:\n{instance_list}.')
+                            f'{colorama.Style.DIM}🔍 Multiple {cloud} instances '
+                            f'satisfy {acc_name}:{int(acc_count)}. '
+                            f'The cheapest {candidate_str} is considered '
+                            f'among: {", ".join(instance_list)}.'
+                            f'{colorama.Style.RESET_ALL}')
             if is_multi_instances:
                 logger.info(
                     f'To list more details, run: sky show-gpus {acc_name}\n')
diff --git a/sky/resources.py b/sky/resources.py
index f988c000548..cfbea41199f 100644
--- a/sky/resources.py
+++ b/sky/resources.py
@@ -6,6 +6,7 @@
 
 import colorama
 
+import sky
 from sky import check as sky_check
 from sky import clouds
 from sky import exceptions
@@ -20,6 +21,7 @@
 from sky.utils import annotations
 from sky.utils import common_utils
 from sky.utils import config_utils
+from sky.utils import infra_utils
 from sky.utils import log_utils
 from sky.utils import registry
 from sky.utils import resources_utils
@@ -106,6 +108,7 @@ def __init__(
         memory: Union[None, int, float, str] = None,
         accelerators: Union[None, str, Dict[str, Union[int, float]]] = None,
         accelerator_args: Optional[Dict[str, str]] = None,
+        infra: Optional[str] = None,
         use_spot: Optional[bool] = None,
         job_recovery: Optional[Union[Dict[str, Optional[Union[str, int]]],
                                      str]] = None,
@@ -134,9 +137,9 @@ def __init__(
           .. code-block:: python
 
             # Fully specified cloud and instance type (is_launchable() is True).
-            sky.Resources(clouds.AWS(), 'p3.2xlarge')
-            sky.Resources(clouds.GCP(), 'n1-standard-16')
-            sky.Resources(clouds.GCP(), 'n1-standard-8', 'V100')
+            sky.Resources(infra='aws', instance_type='p3.2xlarge')
+            sky.Resources(infra='k8s/my-cluster-ctx', accelerators='V100')
+            sky.Resources(infra='gcp/us-central1', accelerators='V100')
 
             # Specifying required resources; the system decides the
             # cloud/instance type. The below are equivalent:
@@ -145,8 +148,9 @@ def __init__(
             sky.Resources(accelerators={'V100': 1})
             sky.Resources(cpus='2+', memory='16+', accelerators='V100')
 
+
         Args:
-          cloud: the cloud to use.
+          cloud: the cloud to use. Deprecated. Use `infra` instead.
           instance_type: the instance type to use.
           cpus: the number of CPUs required for the task.
             If a str, must be a string of the form ``'2'`` or ``'2+'``, where
@@ -160,6 +164,11 @@ def __init__(
             dict of the form ``{'V100': 2}`` or ``{'tpu-v2-8': 1}``.
           accelerator_args: accelerator-specific arguments. For example,
             ``{'tpu_vm': True, 'runtime_version': 'tpu-vm-base'}`` for TPUs.
+          infra: a string specifying the infrastructure to use, in the format
+            of "cloud/region" or "cloud/region/zone". For example,
+            `aws/us-east-1` or `k8s/my-cluster-ctx`. This is an alternative to
+            specifying cloud, region, and zone separately. If provided, it
+            takes precedence over cloud, region, and zone parameters.
           use_spot: whether to use spot instances. If None, defaults to
             False.
           job_recovery: the job recovery strategy to use for the managed
@@ -172,8 +181,8 @@ def __init__(
             - max_restarts_on_errors: the max number of restarts on user code
               errors.
 
-          region: the region to use.
-          zone: the zone to use.
+          region: the region to use. Deprecated. Use `infra` instead.
+          zone: the zone to use. Deprecated. Use `infra` instead.
           image_id: the image ID to use. If a str, must be a string
             of the image id from the cloud, such as AWS:
             ``'ami-1234567890abcdef0'``, GCP:
@@ -218,6 +227,25 @@ def __init__(
             exceptions.NoCloudAccessError: if no public cloud is enabled.
         """
         self._version = self._VERSION
+
+        if infra is not None and (cloud is not None or region is not None or
+                                  zone is not None):
+            with ux_utils.print_exception_no_traceback():
+                raise ValueError('Cannot specify both `infra` and `cloud`, '
+                                 '`region`, or `zone` parameters. '
+                                 f'Got: infra={infra}, cloud={cloud}, '
+                                 f'region={region}, zone={zone}')
+
+        # Infra is user facing, and cloud, region, zone in parameters are for
+        # backward compatibility. Internally, we keep using cloud, region, zone
+        # for simplicity.
+        if infra is not None:
+            infra_info = infra_utils.InfraInfo.from_str(infra)
+            # Infra takes precedence over individually specified parameters
+            cloud = sky.CLOUD_REGISTRY.from_str(infra_info.cloud)
+            region = infra_info.region
+            zone = infra_info.zone
+
         self._cloud = cloud
         self._region: Optional[str] = region
         self._zone: Optional[str] = zone
@@ -431,6 +459,11 @@ def repr_with_region_zone(self) -> str:
             repr_str += f'{region_str}{zone_str}'
         return repr_str
 
+    @property
+    def infra(self) -> infra_utils.InfraInfo:
+        cloud = str(self.cloud) if self.cloud is not None else None
+        return infra_utils.InfraInfo(cloud, self.region, self.zone)
+
     @property
     def cloud(self) -> Optional[clouds.Cloud]:
         return self._cloud
@@ -486,9 +519,9 @@ def memory(self) -> Optional[str]:
     def accelerators(self) -> Optional[Dict[str, Union[int, float]]]:
         """Returns the accelerators field directly or by inferring.
 
-        For example, Resources(AWS, 'p3.2xlarge') has its accelerators field
-        set to None, but this function will infer {'V100': 1} from the instance
-        type.
+        For example, Resources(infra='aws', instance_type='p3.2xlarge') has its
+        accelerators field set to None, but this function will infer {'V100': 1}
+        from the instance type.
         """
         if self._accelerators is not None:
             return self._accelerators
@@ -1450,6 +1483,7 @@ def copy(self, **override) -> 'Resources':
             ports=override.pop('ports', self.ports),
             labels=override.pop('labels', self.labels),
             autostop=override.pop('autostop', current_autostop_config),
+            infra=override.pop('infra', None),
             _docker_login_config=override.pop('_docker_login_config',
                                               self._docker_login_config),
             _docker_username_for_runpod=override.pop(
@@ -1621,9 +1655,18 @@ def _override_resources(
     @classmethod
     def _from_yaml_config_single(cls, config: Dict[str, str]) -> 'Resources':
 
-        resources_fields = {}
+        resources_fields: Dict[str, Any] = {}
+
+        # Extract infra field if present
+        infra = config.pop('infra', None)
+        resources_fields['infra'] = infra
+
+        # Keep backward compatibility with cloud, region, zone
         resources_fields['cloud'] = registry.CLOUD_REGISTRY.from_str(
             config.pop('cloud', None))
+        resources_fields['region'] = config.pop('region', None)
+        resources_fields['zone'] = config.pop('zone', None)
+
         resources_fields['instance_type'] = config.pop('instance_type', None)
         resources_fields['cpus'] = config.pop('cpus', None)
         resources_fields['memory'] = config.pop('memory', None)
@@ -1641,8 +1684,6 @@ def _from_yaml_config_single(cls, config: Dict[str, str]) -> 'Resources':
             # exclusive by the schema validation.
             resources_fields['job_recovery'] = config.pop('job_recovery', None)
         resources_fields['disk_size'] = config.pop('disk_size', None)
-        resources_fields['region'] = config.pop('region', None)
-        resources_fields['zone'] = config.pop('zone', None)
         resources_fields['image_id'] = config.pop('image_id', None)
         resources_fields['disk_tier'] = config.pop('disk_tier', None)
         resources_fields['ports'] = config.pop('ports', None)
@@ -1679,7 +1720,10 @@ def add_if_not_none(key, value):
             if value is not None and value != 'None':
                 config[key] = value
 
-        add_if_not_none('cloud', str(self.cloud))
+        # Construct infra field if cloud is set
+        infra = self.infra.to_str()
+        add_if_not_none('infra', infra)
+
         add_if_not_none('instance_type', self.instance_type)
         add_if_not_none('cpus', self._cpus)
         add_if_not_none('memory', self.memory)
@@ -1690,8 +1734,6 @@ def add_if_not_none(key, value):
             add_if_not_none('use_spot', self.use_spot)
         add_if_not_none('job_recovery', self.job_recovery)
         add_if_not_none('disk_size', self.disk_size)
-        add_if_not_none('region', self.region)
-        add_if_not_none('zone', self.zone)
         add_if_not_none('image_id', self.image_id)
         if self.disk_tier is not None:
             config['disk_tier'] = self.disk_tier.value
diff --git a/sky/serve/serve_utils.py b/sky/serve/serve_utils.py
index a1b2b4a2b37..d1d510ff0d7 100644
--- a/sky/serve/serve_utils.py
+++ b/sky/serve/serve_utils.py
@@ -1027,11 +1027,9 @@ def _format_replica_table(replica_records: List[Dict[str, Any]],
         return 'No existing replicas.'
 
     replica_columns = [
-        'SERVICE_NAME', 'ID', 'VERSION', 'ENDPOINT', 'LAUNCHED', 'RESOURCES',
-        'STATUS', 'REGION'
+        'SERVICE_NAME', 'ID', 'VERSION', 'ENDPOINT', 'LAUNCHED', 'INFRA',
+        'RESOURCES', 'STATUS'
     ]
-    if show_all:
-        replica_columns.append('ZONE')
     replica_table = log_utils.create_table(replica_columns)
 
     truncate_hint = ''
@@ -1047,21 +1045,17 @@ def _format_replica_table(replica_records: List[Dict[str, Any]],
         version = (record['version'] if 'version' in record else '-')
         replica_endpoint = endpoint if endpoint else '-'
         launched_at = log_utils.readable_time_duration(record['launched_at'])
+        infra = '-'
         resources_str = '-'
         replica_status = record['status']
         status_str = replica_status.colored_str()
-        region = '-'
-        zone = '-'
 
         replica_handle: Optional['backends.CloudVmRayResourceHandle'] = record[
             'handle']
         if replica_handle is not None:
+            infra = replica_handle.launched_resources.infra.formatted_str()
             resources_str = resources_utils.get_readable_resources_repr(
                 replica_handle, simplify=not show_all)
-            if replica_handle.launched_resources.region is not None:
-                region = replica_handle.launched_resources.region
-            if replica_handle.launched_resources.zone is not None:
-                zone = replica_handle.launched_resources.zone
 
         replica_values = [
             service_name,
@@ -1069,12 +1063,10 @@ def _format_replica_table(replica_records: List[Dict[str, Any]],
             version,
             replica_endpoint,
             launched_at,
+            infra,
             resources_str,
             status_str,
-            region,
         ]
-        if show_all:
-            replica_values.append(zone)
         replica_table.add_row(replica_values)
 
     return f'{replica_table}{truncate_hint}'
diff --git a/sky/utils/cli_utils/status_utils.py b/sky/utils/cli_utils/status_utils.py
index 6e267770b87..246d3e19785 100644
--- a/sky/utils/cli_utils/status_utils.py
+++ b/sky/utils/cli_utils/status_utils.py
@@ -33,17 +33,15 @@ class StatusColumn:
     def __init__(self,
                  name: str,
                  calc_func: Callable,
-                 trunc_length: int = 0,
+                 truncate: bool = True,
                  show_by_default: bool = True):
         self.name = name
         self.calc_func = calc_func
-        self.trunc_length = trunc_length
+        self.truncate: bool = truncate
         self.show_by_default = show_by_default
 
     def calc(self, record):
-        val = self.calc_func(record)
-        if self.trunc_length != 0:
-            val = common_utils.truncate_long_string(str(val), self.trunc_length)
+        val = self.calc_func(record, self.truncate)
         return val
 
 
@@ -68,19 +66,20 @@ def show_status_table(cluster_records: List[_ClusterRecord],
             StatusColumn('USER_ID', _get_user_hash, show_by_default=False))
 
     status_columns += [
-        StatusColumn('LAUNCHED', _get_launched),
-        StatusColumn('RESOURCES',
-                     _get_resources,
-                     trunc_length=70 if not show_all else 0),
-        StatusColumn('REGION', _get_region, show_by_default=False),
-        StatusColumn('ZONE', _get_zone, show_by_default=False),
+        StatusColumn('INFRA', _get_infra, truncate=not show_all),
+        StatusColumn('RESOURCES', _get_resources, truncate=not show_all),
         StatusColumn('STATUS', _get_status_colored),
         StatusColumn('AUTOSTOP', _get_autostop),
-        StatusColumn('HEAD_IP', _get_head_ip, show_by_default=False),
-        StatusColumn('COMMAND',
-                     _get_command,
-                     trunc_length=COMMAND_TRUNC_LENGTH if not show_all else 0),
+        StatusColumn('LAUNCHED', _get_launched),
     ]
+    if show_all:
+        status_columns += [
+            StatusColumn('HEAD_IP', _get_head_ip, show_by_default=False),
+            StatusColumn('COMMAND',
+                         _get_command,
+                         truncate=not show_all,
+                         show_by_default=False),
+        ]
 
     columns = []
     for status_column in status_columns:
@@ -160,10 +159,10 @@ def show_cost_report_table(cluster_records: List[_ClusterCostReportRecord],
     status_columns = [
         StatusColumn('NAME', _get_name),
         StatusColumn('LAUNCHED', _get_launched),
-        StatusColumn('DURATION', _get_duration, trunc_length=20),
+        StatusColumn('DURATION', _get_duration, truncate=False),
         StatusColumn('RESOURCES',
                      _get_resources_for_cost_report,
-                     trunc_length=70 if not show_all else 0),
+                     truncate=False),
         StatusColumn('STATUS',
                      _get_status_for_cost_report,
                      show_by_default=True),
@@ -221,47 +220,68 @@ def show_cost_report_table(cluster_records: List[_ClusterCostReportRecord],
 # Some of these lambdas are invoked on both _ClusterRecord and
 # _ClusterCostReportRecord, which is okay as we guarantee the queried fields
 # exist in those cases.
-_get_name = (lambda cluster_record: cluster_record['name'])
-_get_user_hash = (lambda cluster_record: cluster_record['user_hash'])
-_get_user_name = (lambda cluster_record: cluster_record.get('user_name', '-'))
-_get_launched = (lambda cluster_record: log_utils.readable_time_duration(
+_get_name = (lambda cluster_record, _: cluster_record['name'])
+_get_user_hash = (lambda cluster_record, _: cluster_record['user_hash'])
+_get_user_name = (
+    lambda cluster_record, _: cluster_record.get('user_name', '-'))
+_get_launched = (lambda cluster_record, _: log_utils.readable_time_duration(
     cluster_record['launched_at']))
-_get_region = (
-    lambda clusters_status: clusters_status['handle'].launched_resources.region)
-_get_command = (lambda cluster_record: cluster_record['last_use'])
-_get_duration = (lambda cluster_record: log_utils.readable_time_duration(
+_get_duration = (lambda cluster_record, _: log_utils.readable_time_duration(
     0, cluster_record['duration'], absolute=True))
 
 
-def _get_status(cluster_record: _ClusterRecord) -> status_lib.ClusterStatus:
-    return cluster_record['status']
-
+def _get_command(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
+    command = cluster_record.get('last_use', '-')
+    if truncate:
+        return common_utils.truncate_long_string(command, COMMAND_TRUNC_LENGTH)
+    return command
 
-def _get_status_colored(cluster_record: _ClusterRecord) -> str:
-    return _get_status(cluster_record).colored_str()
 
+def _get_status(cluster_record: _ClusterRecord,
+                truncate: bool = True) -> status_lib.ClusterStatus:
+    del truncate
+    return cluster_record['status']
 
-def _get_resources(cluster_record: _ClusterRecord) -> str:
-    if 'resources_str' in cluster_record:
-        return cluster_record['resources_str']
-    handle = cluster_record['handle']
-    if isinstance(handle, backends.LocalDockerResourceHandle):
-        resources_str = 'docker'
-    elif isinstance(handle, backends.CloudVmRayResourceHandle):
-        resources_str = resources_utils.get_readable_resources_repr(handle)
-    else:
-        raise ValueError(f'Unknown handle type {type(handle)} encountered.')
-    return resources_str
 
+def _get_status_colored(cluster_record: _ClusterRecord,
+                        truncate: bool = True) -> str:
+    del truncate
+    return _get_status(cluster_record).colored_str()
 
-def _get_zone(cluster_record: _ClusterRecord) -> str:
-    zone_str = cluster_record['handle'].launched_resources.zone
-    if zone_str is None:
-        zone_str = '-'
-    return zone_str
 
+def _get_resources(cluster_record: _ClusterRecord,
+                   truncate: bool = True) -> str:
+    """Get the resources information for a cluster.
 
-def _get_autostop(cluster_record: _ClusterRecord) -> str:
+    Returns:
+        A string in one of the following formats:
+        - For cloud VMs: "Nx instance_type" (e.g., "1x m6i.2xlarge")
+        - For K8S/SSH: "Nx (...)"
+        - "-" if no resource information is available
+    """
+    handle = cluster_record['handle']
+    if isinstance(handle, backends.CloudVmRayResourceHandle):
+        launched_resources = handle.launched_resources
+        if launched_resources is None:
+            return '-'
+
+        # For cloud VMs, show instance type directly
+        # For K8S/SSH, show (...) as the resource type
+        resources_str = cluster_record.get('resources_str', None)
+        if not truncate:
+            resources_str_full = cluster_record.get('resources_str_full', None)
+            if resources_str_full is not None:
+                resources_str = resources_str_full
+        if resources_str is None:
+            resources_str = resources_utils.get_readable_resources_repr(
+                handle, simplify=truncate)
+
+        return resources_str
+    return '-'
+
+
+def _get_autostop(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
+    del truncate
     autostop_str = ''
     separation = ''
     if cluster_record['autostop'] >= 0:
@@ -276,7 +296,8 @@ def _get_autostop(cluster_record: _ClusterRecord) -> str:
     return autostop_str
 
 
-def _get_head_ip(cluster_record: _ClusterRecord) -> str:
+def _get_head_ip(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
+    del truncate  # Unused
     handle = cluster_record['handle']
     if not isinstance(handle, backends.CloudVmRayResourceHandle):
         return '-'
@@ -291,6 +312,25 @@ def _is_pending_autostop(cluster_record: _ClusterRecord) -> bool:
         cluster_record) != status_lib.ClusterStatus.STOPPED
 
 
+def _get_infra(cluster_record: _ClusterRecord, truncate: bool = True) -> str:
+    """Get the infrastructure information for a cluster.
+
+    Returns:
+        A string in one of the following formats:
+        - AWS/region (e.g., "AWS/us-east-1")
+        - K8S/context (e.g., "K8S/my-ctx")
+        - SSH/hostname (e.g., "SSH/my-tobi-box")
+        - "-" if no infrastructure information is available
+    """
+    handle = cluster_record['handle']
+    if isinstance(handle, backends.CloudVmRayResourceHandle):
+        if handle.launched_resources is None:
+            # If launched_resources is None, try to get infra from the record
+            return cluster_record.get('infra', '-')
+        return handle.launched_resources.infra.formatted_str(truncate)
+    return '-'
+
+
 # ---- 'sky cost-report' helper functions below ----
 
 
@@ -347,14 +387,13 @@ def show_kubernetes_cluster_status_table(
         show_all: bool) -> None:
     """Compute cluster table values and display for Kubernetes clusters."""
     status_columns = [
-        StatusColumn('USER', lambda c: c.user),
-        StatusColumn('NAME', lambda c: c.cluster_name),
-        StatusColumn('LAUNCHED',
-                     lambda c: log_utils.readable_time_duration(c.launched_at)),
-        StatusColumn('RESOURCES',
-                     lambda c: c.resources_str,
-                     trunc_length=70 if not show_all else 0),
-        StatusColumn('STATUS', lambda c: c.status.colored_str()),
+        StatusColumn('USER', lambda c, _: c.user),
+        StatusColumn('NAME', lambda c, _: c.cluster_name),
+        StatusColumn('RESOURCES', lambda c, _: c.resources_str, truncate=False),
+        StatusColumn('STATUS', lambda c, _: c.status.colored_str()),
+        StatusColumn(
+            'LAUNCHED',
+            lambda c, _: log_utils.readable_time_duration(c.launched_at)),
         # TODO(romilb): We should consider adding POD_NAME field here when --all
         #  is passed to help users fetch pod name programmatically.
     ]
diff --git a/sky/utils/common_utils.py b/sky/utils/common_utils.py
index 99f205e8e7a..00d9db4c756 100644
--- a/sky/utils/common_utils.py
+++ b/sky/utils/common_utils.py
@@ -723,10 +723,43 @@ def new_func(*args, **kwargs):
     return new_func
 
 
-def truncate_long_string(s: str, max_length: int = 35) -> str:
-    """Truncate a string to a maximum length, preserving whole words."""
+def truncate_long_string(s: str,
+                         max_length: int = 35,
+                         truncate_middle: bool = False) -> str:
+    """Truncate a string to a maximum length.
+
+    Args:
+        s: String to truncate.
+        max_length: Maximum length of the truncated string.
+        truncate_middle: Whether to truncate in the middle of the string.
+            If True, the middle part of the string is replaced with '...'.
+            If False, truncation happens at the end preserving whole words.
+
+    Returns:
+        Truncated string.
+    """
     if len(s) <= max_length:
         return s
+
+    if truncate_middle:
+        # Reserve 3 characters for '...'
+        if max_length <= 3:
+            return '...'
+
+        # Calculate how many characters to keep from beginning and end
+        half_length = (max_length - 3) // 2
+        remainder = (max_length - 3) % 2
+
+        # Keep one more character at the beginning if max_length - 3 is odd
+        start_length = half_length + remainder
+        end_length = half_length
+
+        # When end_length is 0, just show the start part and '...'
+        if end_length == 0:
+            return s[:start_length] + '...'
+        return s[:start_length] + '...' + s[-end_length:]
+
+    # Original end-truncation logic
     splits = s.split(' ')
     if len(splits[0]) > max_length:
         return splits[0][:max_length] + '...'  # Use '…'?
diff --git a/sky/utils/infra_utils.py b/sky/utils/infra_utils.py
new file mode 100644
index 00000000000..278475da51f
--- /dev/null
+++ b/sky/utils/infra_utils.py
@@ -0,0 +1,175 @@
+"""Utility functions for handling infrastructure specifications."""
+import dataclasses
+from typing import Optional
+
+from sky.utils import common_utils
+from sky.utils import ux_utils
+
+_REGION_OR_ZONE_TRUNCATION_LENGTH = 25
+
+
+@dataclasses.dataclass
+class InfraInfo:
+    """Infrastructure information parsed from infra string.
+
+    When a field is None, it means the field is not specified.
+    """
+    cloud: Optional[str] = None
+    region: Optional[str] = None
+    zone: Optional[str] = None
+
+    def __init__(self,
+                 cloud: Optional[str] = None,
+                 region: Optional[str] = None,
+                 zone: Optional[str] = None):
+        assert cloud not in ['none', 'None', 'NONE'], 'cloud must be specified'
+        if not cloud or cloud == '*':
+            cloud = None
+        if not region or region == '*':
+            region = None
+        if not zone or zone == '*':
+            zone = None
+
+        self.cloud = cloud
+        self.region = region
+        self.zone = zone
+
+    @staticmethod
+    def from_str(infra: Optional[str]) -> 'InfraInfo':
+        """Parse the infra string into cloud, region, and zone components.
+
+        The format of the infra string is `cloud`, `cloud/region`, or
+        `cloud/region/zone`. Examples: `aws`, `aws/us-east-1`,
+        `aws/us-east-1/us-east-1a`. For any field, you can use `*` to indicate
+        that any value is acceptable.
+
+        If `*` is used for any field, the InfraInfo will have None for that
+        field.
+
+        Args:
+            infra: A string in the format of `cloud`, `cloud/region`, or
+                `cloud/region/zone`. Examples: `aws`, `aws/us-east-1`,
+                `aws/us-east-1/us-east-1a`.
+
+        Returns:
+            An InfraInfo object containing cloud, region, and zone information.
+
+        Raises:
+            ValueError: If the infra string is malformed.
+        """
+        if infra is None or not infra.strip():
+            return InfraInfo()
+
+        infra = infra.strip().strip('/')
+
+        # Split on / to get cloud, region, zone
+        parts = [p.strip() for p in infra.strip().split('/')]
+
+        if '' in parts:
+            with ux_utils.print_exception_no_traceback():
+                raise ValueError(
+                    f'Invalid infra format: {infra}. Format should not contain '
+                    'empty parts (e.g., double slashes "//").')
+
+        if not parts or not parts[0]:
+            with ux_utils.print_exception_no_traceback():
+                raise ValueError(
+                    f'Invalid infra format: {infra}. Expected format is '
+                    '"cloud", "cloud/region", or "cloud/region/zone".')
+
+        cloud_name: Optional[str] = parts[0].lower()
+
+        # Handle Kubernetes contexts specially, as they can contain slashes
+        if cloud_name in ['k8s', 'kubernetes']:
+            # For Kubernetes, the entire string after "k8s/" is the
+            # context name (region)
+            cloud_name = 'kubernetes'  # Normalize k8s to kubernetes
+            region = '/'.join(parts[1:]) if len(parts) >= 2 else None
+            zone = None
+        else:
+            # For non-Kubernetes clouds, continue with regular parsing
+            # but be careful to only split into max 3 parts
+            region_zone_parts = parts[1:]
+            region = None
+            zone = None
+            if region_zone_parts:
+                region = region_zone_parts[0]
+                if len(region_zone_parts) > 1:
+                    zone = region_zone_parts[1]
+                if len(region_zone_parts) > 2:
+                    with ux_utils.print_exception_no_traceback():
+                        raise ValueError(
+                            f'Invalid infra format: {infra}. Expected format '
+                            'is "cloud", "cloud/region", or '
+                            '"cloud/region/zone".')
+
+        if cloud_name == '*':
+            cloud_name = None
+        if region == '*':
+            region = None
+        if zone == '*':
+            zone = None
+        return InfraInfo(cloud=cloud_name, region=region, zone=zone)
+
+    def to_str(self) -> Optional[str]:
+        """Formats cloud, region, and zone into an infra string.
+
+        Args:
+            cloud: The cloud object
+            region: The region name
+            zone: The zone name
+
+        Returns:
+            A formatted infra string, or None if cloud is None or '*'
+        """
+        cloud = self.cloud
+        region = self.region
+        zone = self.zone
+
+        if cloud is None:
+            cloud = '*'
+        if region is None:
+            region = '*'
+        if zone is None:
+            zone = '*'
+
+        # Build the parts list and filter out trailing wildcards
+        parts = [cloud.lower(), region, zone]
+        while parts and parts[-1] == '*':
+            parts.pop()
+
+        if not parts:
+            return None
+
+        # Join the parts with '/'
+        return '/'.join(parts)
+
+    def formatted_str(self, truncate: bool = True) -> str:
+        """Formats cloud, region, and zone into an infra string.
+
+        Args:
+            truncate: Whether to truncate the region or zone
+
+        Returns:
+            A formatted infra string, or None if cloud is None or '*'
+        """
+        if self.cloud is None or self.cloud == '*':
+            return '-'
+
+        region_or_zone = None
+        if self.zone is not None and self.zone != '*':
+            region_or_zone = self.zone
+        elif self.region is not None and self.region != '*':
+            region_or_zone = self.region
+
+        if region_or_zone is not None and truncate:
+            region_or_zone = common_utils.truncate_long_string(
+                region_or_zone,
+                _REGION_OR_ZONE_TRUNCATION_LENGTH,
+                truncate_middle=True)
+
+        formatted_str = f'{self.cloud}'
+        if region_or_zone is not None:
+            formatted_str += f' ({region_or_zone})'
+
+        return formatted_str
diff --git a/sky/utils/resources_utils.py b/sky/utils/resources_utils.py
index 60556e95b68..6c6fb38a374 100644
--- a/sky/utils/resources_utils.py
+++ b/sky/utils/resources_utils.py
@@ -4,11 +4,11 @@
 import itertools
 import json
 import math
-import re
 import typing
 from typing import Dict, List, Optional, Set, Union
 
 from sky import skypilot_config
+from sky.utils import common_utils
 from sky.utils import registry
 from sky.utils import ux_utils
 
@@ -139,34 +139,54 @@ def simplify_ports(ports: List[str]) -> List[str]:
 
 def format_resource(resource: 'resources_lib.Resources',
                     simplify: bool = False) -> str:
+    resource = resource.assert_launchable()
+    vcpu, mem = resource.cloud.get_vcpus_mem_from_instance_type(
+        resource.instance_type)
+
+    components = []
+
+    if resource.accelerators is not None:
+        acc, count = list(resource.accelerators.items())[0]
+        components.append(f'gpus={acc}:{count}')
+
+    is_k8s = str(resource.cloud).lower() == 'kubernetes'
+    if (resource.accelerators is None or is_k8s or not simplify):
+        if vcpu is not None:
+            components.append(f'cpus={int(vcpu)}')
+        if mem is not None:
+            components.append(f'mem={int(mem)}')
+
+    instance_type = resource.instance_type
     if simplify:
-        resource = resource.assert_launchable()
-        cloud = resource.cloud
-        if resource.accelerators is None:
-            vcpu, _ = cloud.get_vcpus_mem_from_instance_type(
-                resource.instance_type)
-            assert vcpu is not None, 'vCPU must be specified'
-            hardware = f'vCPU={int(vcpu)}'
-        else:
-            hardware = f'{resource.accelerators}'
-        spot = '[Spot]' if resource.use_spot else ''
-        return f'{cloud}({spot}{hardware})'
+        instance_type = common_utils.truncate_long_string(instance_type, 15)
+    if not is_k8s:
+        components.append(f'type={instance_type}')
+    if simplify:
+        components.append('...')
     else:
-        # accelerator_args is way too long.
-        # Convert from:
-        #  GCP(n1-highmem-8, {'tpu-v2-8': 1}, accelerator_args={'runtime_version': '2.12.0'}  # pylint: disable=line-too-long
-        # to:
-        #  GCP(n1-highmem-8, {'tpu-v2-8': 1}...)
-        pattern = ', accelerator_args={.*}'
-        launched_resource_str = re.sub(pattern, '...', str(resource))
-        return launched_resource_str
+        image_id = resource.image_id
+        if image_id is not None:
+            if None in image_id:
+                components.append(f'image_id={image_id[None]}')
+            else:
+                components.append(f'image_id={image_id}')
+        components.append(f'disk={resource.disk_size}')
+        disk_tier = resource.disk_tier
+        if disk_tier is not None:
+            components.append(f'disk_tier={disk_tier.value}')
+        ports = resource.ports
+        if ports is not None:
+            components.append(f'ports={ports}')
+
+    spot = '[spot]' if resource.use_spot else ''
+    return f'{spot}({"" if not components else ", ".join(components)})'
 
 
 def get_readable_resources_repr(handle: 'backends.CloudVmRayResourceHandle',
                                 simplify: bool = False) -> str:
     if (handle.launched_nodes is not None and
             handle.launched_resources is not None):
-        return (f'{handle.launched_nodes}x '
+        return (f'{handle.launched_nodes}x'
                 f'{format_resource(handle.launched_resources, simplify)}')
     return _DEFAULT_MESSAGE_HANDLE_INITIALIZING
 
diff --git a/sky/utils/schemas.py b/sky/utils/schemas.py
index 479c23cb7e1..7b263460eeb 100644
--- a/sky/utils/schemas.py
+++ b/sky/utils/schemas.py
@@ -85,6 +85,19 @@ def _get_single_resources_schema():
             'zone': {
                 'type': 'string',
             },
+            'infra': {
+                'type': 'string',
+                'description':
+                    ('Infrastructure specification in format: '
+                     'cloud[/region[/zone]]. Use "*" as a wildcard.'),
+                # Create a pattern validator that uses a big regex to match all
+                # valid formats. This allows us to maintain JSON Schema
+                # validation while supporting all formats
+                'pattern':
+                    ('^(?:(?i:(' + '|'.join(list(service_catalog.ALL_CLOUDS)) +
+                     '))(?:/[^/]+(?:/[^/]+)?)?|\\*(?:/[^/]+(?:/[^/]+)?|/\\*'
+                     '(?:/[^/]+)?)?|(?i:k8s|kubernetes)/.+)$')
+            },
             'cpus': {
                 'anyOf': [{
                     'type': 'string',
diff --git a/tests/common_test_fixtures.py b/tests/common_test_fixtures.py
index 10de8e89766..6ad96fb2531 100644
--- a/tests/common_test_fixtures.py
+++ b/tests/common_test_fixtures.py
@@ -133,6 +133,59 @@ def mock_get(url, *args, **kwargs):
     monkeypatch.setattr(requests, "get", mock_get)
 
 
+# Define helper functions at module level for pickleability
+def get_cached_enabled_clouds_mock(enabled_clouds, *_, **__):
+    return enabled_clouds
+
+
+def dummy_function(*_, **__):
+    return None
+
+
+def get_az_mappings(*_, **__):
+    return pd.read_csv('tests/default_aws_az_mappings.csv')
+
+
+def list_empty_reservations(*_, **__):
+    return []
+
+
+def get_kubernetes_label_formatter(*_, **__):
+    return [kubernetes_utils.SkyPilotLabelFormatter, {}]
+
+
+def detect_accelerator_resource_mock(*_, **__):
+    return [True, []]
+
+
+def check_instance_fits_mock(*_, **__):
+    return [True, '']
+
+
+def get_spot_label_mock(*_, **__):
+    return [None, None]
+
+
+def is_kubeconfig_exec_auth_mock(*_, **__):
+    return [False, None]
+
+
+def regions_with_offering_mock(*_, **__):
+    return [sky.clouds.Region('my-k8s-cluster-context')]
+
+
+def check_quota_available_mock(*_, **__):
+    return True
+
+
+def mock_redirect_output(*_, **__):
+    return (None, None)
+
+
+def mock_restore_output(*_, **__):
+    return None
+
+
 @pytest.fixture
 def enable_all_clouds(monkeypatch, request, mock_client_requests):
     """Create mock context managers for cloud configurations."""
@@ -143,40 +196,43 @@ def enable_all_clouds(monkeypatch, request, mock_client_requests):
     config_file = tempfile.NamedTemporaryFile(prefix='tmp_config_default',
                                               delete=False).name
 
+    # Use a function that takes enabled_clouds as an argument
+    def get_clouds_factory(*args, **kwargs):
+        return get_cached_enabled_clouds_mock(enabled_clouds, *args, **kwargs)
+
     # Mock all the functions
     monkeypatch.setattr('sky.check.get_cached_enabled_clouds_or_refresh',
-                        lambda *_, **__: enabled_clouds)
-    monkeypatch.setattr('sky.check.check_capability', lambda *_, **__: None)
+                        get_clouds_factory)
+    monkeypatch.setattr('sky.check.check_capability', dummy_function)
     monkeypatch.setattr(
         'sky.clouds.service_catalog.aws_catalog._get_az_mappings',
-        lambda *_, **__: pd.read_csv('tests/default_aws_az_mappings.csv'))
+        get_az_mappings)
     monkeypatch.setattr('sky.backends.backend_utils.check_owner_identity',
-                        lambda *_, **__: None)
+                        dummy_function)
     monkeypatch.setattr(
         'sky.clouds.utils.gcp_utils.list_reservations_for_instance_type_in_zone',
-        lambda *_, **__: [])
+        list_empty_reservations)
 
     # Kubernetes mocks
-    monkeypatch.setattr('sky.adaptors.kubernetes._load_config',
-                        lambda *_, **__: None)
+    monkeypatch.setattr('sky.adaptors.kubernetes._load_config', dummy_function)
     monkeypatch.setattr(
         'sky.provision.kubernetes.utils.detect_gpu_label_formatter',
-        lambda *_, **__: [kubernetes_utils.SkyPilotLabelFormatter, {}])
+        get_kubernetes_label_formatter)
     monkeypatch.setattr(
         'sky.provision.kubernetes.utils.detect_accelerator_resource',
-        lambda *_, **__: [True, []])
+        detect_accelerator_resource_mock)
     monkeypatch.setattr('sky.provision.kubernetes.utils.check_instance_fits',
-                        lambda *_, **__: [True, ''])
+                        check_instance_fits_mock)
     monkeypatch.setattr('sky.provision.kubernetes.utils.get_spot_label',
-                        lambda *_, **__: [None, None])
+                        get_spot_label_mock)
     monkeypatch.setattr('sky.clouds.kubernetes.kubernetes_utils.get_spot_label',
-                        lambda *_, **__: [None, None])
+                        get_spot_label_mock)
     monkeypatch.setattr(
         'sky.provision.kubernetes.utils.is_kubeconfig_exec_auth',
-        lambda *_, **__: [False, None])
+        is_kubeconfig_exec_auth_mock)
     monkeypatch.setattr(
         'sky.clouds.kubernetes.Kubernetes.regions_with_offering',
-        lambda *_, **__: [sky.clouds.Region('my-k8s-cluster-context')])
+        regions_with_offering_mock)
 
     # VSphere catalog mock
     monkeypatch.setattr(vsphere_catalog, '_LOCAL_CATALOG',
@@ -186,7 +242,7 @@ def enable_all_clouds(monkeypatch, request, mock_client_requests):
     for cloud in enabled_clouds:
         if hasattr(cloud, 'check_quota_available'):
             monkeypatch.setattr(cloud, 'check_quota_available',
-                                lambda *_, **__: True)
+                                check_quota_available_mock)
 
     # Environment variables
     monkeypatch.setattr(
@@ -326,9 +382,9 @@ def mock_get_queue(schedule_type):
 @pytest.fixture
 def mock_redirect_log_file(monkeypatch):
     monkeypatch.setattr('sky.server.requests.executor._redirect_output',
-                        lambda *_, **__: (None, None))
+                        mock_redirect_output)
     monkeypatch.setattr('sky.server.requests.executor._restore_output',
-                        lambda *_, **__: None)
+                        mock_restore_output)
 
 
 @pytest.fixture
diff --git a/tests/load_tests/test_distribute_load_on_server.py b/tests/load_tests/test_distribute_load_on_server.py
index 60c2dc26ed5..e2511b4e67c 100644
--- a/tests/load_tests/test_distribute_load_on_server.py
+++ b/tests/load_tests/test_distribute_load_on_server.py
@@ -79,9 +79,7 @@ def stream_log(req_id):
         task = sky.Task(setup=setup, run=run)
         task.set_file_mounts(file_mounts)
         task.set_resources(
-            sky.Resources(clouds.Kubernetes(),
-                          cpus=args.cpus,
-                          memory=args.memory))
+            sky.Resources(infra='k8s', cpus=args.cpus, memory=args.memory))
         # Use launch instead of jobs launch for predictable client parallelism
         resps.append(sky.launch(task, f'benchmark-{i}'))
     try:
diff --git a/tests/skyserve/auto_restart.yaml b/tests/skyserve/auto_restart.yaml
index 5fd26ea8acd..6369034f08e 100644
--- a/tests/skyserve/auto_restart.yaml
+++ b/tests/skyserve/auto_restart.yaml
@@ -7,7 +7,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/cancel/cancel.yaml b/tests/skyserve/cancel/cancel.yaml
index 2683e7dbb7b..883814fb03d 100644
--- a/tests/skyserve/cancel/cancel.yaml
+++ b/tests/skyserve/cancel/cancel.yaml
@@ -8,7 +8,7 @@ service:
 
 resources:
   ports: 9000
-  cloud: gcp
+  infra: gcp
 
 workdir: examples/serve/misc/cancel
 
diff --git a/tests/skyserve/high_availability/config.yaml b/tests/skyserve/high_availability/config.yaml
index 836894e2067..49100f4eff4 100644
--- a/tests/skyserve/high_availability/config.yaml
+++ b/tests/skyserve/high_availability/config.yaml
@@ -1,6 +1,6 @@
 serve:
   controller:
     resources:
-      cloud: kubernetes
+      infra: kubernetes
       cpus: 2
     high_availability: true
diff --git a/tests/skyserve/high_availability/service.yaml b/tests/skyserve/high_availability/service.yaml
index a33761535b6..b9b875fd830 100644
--- a/tests/skyserve/high_availability/service.yaml
+++ b/tests/skyserve/high_availability/service.yaml
@@ -7,7 +7,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp
   cpus: 2+
 
 workdir: examples/serve/http_server
diff --git a/tests/skyserve/http/aws.yaml b/tests/skyserve/http/aws.yaml
index edb562a5273..73b1da6bba9 100644
--- a/tests/skyserve/http/aws.yaml
+++ b/tests/skyserve/http/aws.yaml
@@ -6,7 +6,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: aws
+  infra: aws
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/http/azure.yaml b/tests/skyserve/http/azure.yaml
index 2f111a7d610..b0e869e9b13 100644
--- a/tests/skyserve/http/azure.yaml
+++ b/tests/skyserve/http/azure.yaml
@@ -6,7 +6,7 @@ service:
 
 resources:
   ports: 8081
-  cloud: azure
+  infra: azure
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/http/gcp.yaml b/tests/skyserve/http/gcp.yaml
index b61f0c29fe3..81c2e24eaf4 100644
--- a/tests/skyserve/http/gcp.yaml
+++ b/tests/skyserve/http/gcp.yaml
@@ -6,7 +6,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/http/kubernetes.yaml b/tests/skyserve/http/kubernetes.yaml
index 987304bb2d7..64a9033ead2 100644
--- a/tests/skyserve/http/kubernetes.yaml
+++ b/tests/skyserve/http/kubernetes.yaml
@@ -6,7 +6,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: kubernetes
+  infra: kubernetes
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/http/oci.yaml b/tests/skyserve/http/oci.yaml
index d7d98c18ab4..c9451634438 100644
--- a/tests/skyserve/http/oci.yaml
+++ b/tests/skyserve/http/oci.yaml
@@ -3,8 +3,8 @@ service:
   replicas: 2
 
 resources:
-  cloud: oci
+  infra: oci
   ports: 8080
   cpus: 2+
 
-run: python -m http.server 8080
\ No newline at end of file
+run: python -m http.server 8080
diff --git a/tests/skyserve/llm/service.yaml b/tests/skyserve/llm/service.yaml
index dde5c9313b0..a848889ea9d 100644
--- a/tests/skyserve/llm/service.yaml
+++ b/tests/skyserve/llm/service.yaml
@@ -15,7 +15,7 @@ envs:
 
 resources:
   ports: 8087
-  cloud: gcp
+  infra: gcp
   accelerators: T4
   cpus: 7+
   memory: 20+
diff --git a/tests/skyserve/spot/dynamic_ondemand_fallback.yaml b/tests/skyserve/spot/dynamic_ondemand_fallback.yaml
index 2e8d692ecbd..00cb905eaa6 100644
--- a/tests/skyserve/spot/dynamic_ondemand_fallback.yaml
+++ b/tests/skyserve/spot/dynamic_ondemand_fallback.yaml
@@ -11,9 +11,8 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp/*/us-central1-a
   cpus: 2+
-  zone: us-central1-a
   use_spot: true
 
 workdir: examples/serve/http_server
diff --git a/tests/skyserve/spot/recovery.yaml b/tests/skyserve/spot/recovery.yaml
index 81cae7e1fc7..5efc467c6d6 100644
--- a/tests/skyserve/spot/recovery.yaml
+++ b/tests/skyserve/spot/recovery.yaml
@@ -7,8 +7,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
-  zone: us-central1-a
+  infra: gcp/*/us-central1-a
   use_spot: true
 
 workdir: examples/serve/http_server
diff --git a/tests/skyserve/spot/spot_hedge.yaml b/tests/skyserve/spot/spot_hedge.yaml
index 88bbfeda052..f9dcb5e16c7 100644
--- a/tests/skyserve/spot/spot_hedge.yaml
+++ b/tests/skyserve/spot/spot_hedge.yaml
@@ -21,14 +21,12 @@ envs:
   HF_TOKEN: # TODO: Fill with your own huggingface token, or use --env to pass.
 
 resources:
-  cloud: aws
+  infra: aws
   any_of:
     # Enable all region in AWS.
-    - cloud: aws
+    - infra: aws
     # Enable one in GCP.
-    - cloud: gcp
-      region: asia-northeast3
-      zone: asia-northeast3-a
+    - infra: gcp/*/asia-northeast3-a
   use_spot: true
   accelerators: L4
   ports: 9000  # Expose to internet traffic.
diff --git a/tests/skyserve/spot/spot_hedge_T4.yaml b/tests/skyserve/spot/spot_hedge_T4.yaml
index af949ee8904..641fd245428 100644
--- a/tests/skyserve/spot/spot_hedge_T4.yaml
+++ b/tests/skyserve/spot/spot_hedge_T4.yaml
@@ -23,13 +23,11 @@ envs:
 resources:
   any_of:
     # Enable all region in AWS.
-    - cloud: aws
+    - infra: aws
       # region: us-east-1
       # zone: us-east-1f
     # Enable one zone in GCP.
-    - cloud: gcp
-      region: europe-west2
-      zone: europe-west2-a
+    - infra: gcp/*/europe-west2-a
   use_spot: true
   accelerators: T4
   ports: 9000  # Expose to internet traffic.
diff --git a/tests/skyserve/update/new.yaml b/tests/skyserve/update/new.yaml
index 4317af1b146..15982656d0e 100644
--- a/tests/skyserve/update/new.yaml
+++ b/tests/skyserve/update/new.yaml
@@ -7,7 +7,7 @@ service:
 
 resources:
   ports: 8081
-  cloud: gcp
+  infra: gcp
 
 workdir: tests/skyserve/update
 
diff --git a/tests/skyserve/update/num_min_one.yaml b/tests/skyserve/update/num_min_one.yaml
index e168af69af3..9dd84f38091 100644
--- a/tests/skyserve/update/num_min_one.yaml
+++ b/tests/skyserve/update/num_min_one.yaml
@@ -7,7 +7,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/update/num_min_two.yaml b/tests/skyserve/update/num_min_two.yaml
index d4f26fdee8c..457ddd5849e 100644
--- a/tests/skyserve/update/num_min_two.yaml
+++ b/tests/skyserve/update/num_min_two.yaml
@@ -7,7 +7,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp
 
 workdir: examples/serve/http_server
 
diff --git a/tests/skyserve/update/old.yaml b/tests/skyserve/update/old.yaml
index 38ef1cdcb60..666f8ff231a 100644
--- a/tests/skyserve/update/old.yaml
+++ b/tests/skyserve/update/old.yaml
@@ -7,7 +7,7 @@ service:
 
 resources:
   ports: 8080
-  cloud: gcp
+  infra: gcp
 
 workdir: tests/skyserve/update
 
diff --git a/tests/smoke_tests/smoke_tests_utils.py b/tests/smoke_tests/smoke_tests_utils.py
index 4396a73b4a9..d79026b6d14 100644
--- a/tests/smoke_tests/smoke_tests_utils.py
+++ b/tests/smoke_tests/smoke_tests_utils.py
@@ -37,10 +37,10 @@
 # different job id.
 test_id = str(uuid.uuid4())[-2:]
 
-LAMBDA_TYPE = '--cloud lambda --gpus A10'
-FLUIDSTACK_TYPE = '--cloud fluidstack --gpus RTXA4000'
+LAMBDA_TYPE = '--infra lambda --gpus A10'
+FLUIDSTACK_TYPE = '--infra fluidstack --gpus RTXA4000'
 
-SCP_TYPE = '--cloud scp'
+SCP_TYPE = '--infra scp'
 SCP_GPU_V100 = '--gpus V100-32GB'
 
 STORAGE_SETUP_COMMANDS = [
@@ -490,7 +490,7 @@ def get_aws_region_for_quota_failover() -> Optional[str]:
                                                   use_spot=True,
                                                   region=None,
                                                   zone=None)
-    original_resources = sky.Resources(cloud=sky.AWS(),
+    original_resources = sky.Resources(infra='aws',
                                        instance_type='p3.16xlarge',
                                        use_spot=True)
 
@@ -517,7 +517,7 @@ def get_gcp_region_for_quota_failover() -> Optional[str]:
                                                   region=None,
                                                   zone=None)
 
-    original_resources = sky.Resources(cloud=sky.GCP(),
+    original_resources = sky.Resources(infra='gcp',
                                        instance_type='a2-ultragpu-1g',
                                        accelerators={'A100-80GB': 1},
                                        use_spot=True)
@@ -611,7 +611,7 @@ def launch_cluster_for_cloud_cmd(cloud: str, test_cluster_name: str) -> str:
         return 'true'
     else:
         return (
-            f'sky launch -y -c {cluster_name} --cloud {cloud} {LOW_RESOURCE_ARG} --async'
+            f'sky launch -y -c {cluster_name} --infra {cloud} {LOW_RESOURCE_ARG} --async'
         )
 
 
diff --git a/tests/smoke_tests/test_basic.py b/tests/smoke_tests/test_basic.py
index 21ed1d37940..7cb87c5582a 100644
--- a/tests/smoke_tests/test_basic.py
+++ b/tests/smoke_tests/test_basic.py
@@ -55,12 +55,12 @@ def test_minimal(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'minimal',
         [
-            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
+            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
             # Output validation done.
             f'sky logs {name} 1 --status',
             f'sky logs {name} --status | grep "Job 1: SUCCEEDED"',  # Equivalent.
             # Test launch output again on existing cluster
-            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
+            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
             f'sky logs {name} 2 --status',
             f'sky logs {name} --status | grep "Job 2: SUCCEEDED"',  # Equivalent.
             # Check the logs downloading
@@ -103,7 +103,7 @@ def test_launch_fast(generic_cloud: str):
         'test_launch_fast',
         [
             # First launch to create the cluster
-            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --cloud {generic_cloud} --fast {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
+            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --infra {generic_cloud} --fast {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
             f'sky logs {name} 1 --status',
 
             # Second launch to test fast launch - should not reprovision
@@ -138,7 +138,7 @@ def test_launch_fast_with_autostop(generic_cloud: str):
         'test_launch_fast_with_autostop',
         [
             # First launch to create the cluster with a short autostop
-            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --cloud {generic_cloud} --fast -i 1 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
+            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --infra {generic_cloud} --fast -i 1 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
             f'sky logs {name} 1 --status',
             f'sky status -r {name} | grep UP',
 
@@ -172,7 +172,7 @@ def test_launch_fast_with_cluster_changes(generic_cloud: str, tmp_path):
         'test_launch_fast_with_cluster_changes',
         [
             # Initial launch
-            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --cloud {generic_cloud} --fast {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
+            f's=$(SKYPILOT_DEBUG=0 sky launch -y -c {name} --infra {generic_cloud} --fast {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml) && {smoke_tests_utils.VALIDATE_LAUNCH_OUTPUT}',
             f'sky logs {name} 1 --status',
 
             # Launch again - setup and provisioning should be skipped
@@ -209,14 +209,14 @@ def test_stale_job(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'stale_job',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo hi"',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo hi"',
             f'sky exec {name} -d "echo start; sleep 10000"',
-            f'sky stop {name} -y',
+            f'sky stop -y {name}',
             smoke_tests_utils.get_cmd_wait_until_cluster_status_contains(
                 cluster_name=name,
                 cluster_status=[sky.ClusterStatus.STOPPED],
                 timeout=100),
-            f'sky start {name} -y',
+            f'sky start -y {name}',
             f'sky logs {name} 1 --status',
             f's=$(sky queue {name}); echo "$s"; echo; echo; echo "$s" | grep FAILED_DRIVER',
         ],
@@ -236,7 +236,7 @@ def test_aws_stale_job_manual_restart():
         'aws_stale_job_manual_restart',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('aws', name),
-            f'sky launch -y -c {name} --cloud aws --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo hi"',
+            f'sky launch -y -c {name} --infra aws/us-east-2 {smoke_tests_utils.LOW_RESOURCE_ARG} "echo hi"',
             f'sky exec {name} -d "echo start; sleep 10000"',
             # Stop the cluster manually.
             smoke_tests_utils.run_cloud_cmd_on_cluster(
@@ -283,7 +283,7 @@ def test_gcp_stale_job_manual_restart():
         'gcp_stale_job_manual_restart',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
-            f'sky launch -y -c {name} --cloud gcp --zone {zone} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo hi"',
+            f'sky launch -y -c {name} --infra gcp/*/us-central1-a {smoke_tests_utils.LOW_RESOURCE_ARG} "echo hi"',
             f'sky exec {name} -d "echo start; sleep 10000"',
             # Stop the cluster manually.
             smoke_tests_utils.run_cloud_cmd_on_cluster(name, cmd=stop_cmd),
@@ -313,7 +313,7 @@ def test_env_check(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'env_check',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/env_check.yaml',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/env_check.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             # Test with only setup.
             f'sky launch -y -c {name} tests/test_yamls/test_only_setup.yaml',
@@ -337,7 +337,7 @@ def test_cli_logs(generic_cloud: str):
         num_nodes = 1
     timestamp = time.time()
     test = smoke_tests_utils.Test('cli_logs', [
-        f'sky launch -y -c {name} --cloud {generic_cloud} --num-nodes {num_nodes} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo {timestamp} 1"',
+        f'sky launch -y -c {name} --infra {generic_cloud} --num-nodes {num_nodes} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo {timestamp} 1"',
         f'sky exec {name} "echo {timestamp} 2"',
         f'sky exec {name} "echo {timestamp} 3"',
         f'sky exec {name} "echo {timestamp} 4"',
@@ -377,21 +377,23 @@ def test_scp_logs():
 # These tests are for testing the return value of the APIs not fully used in CLI.
 def test_core_api_sky_launch_exec(generic_cloud: str):
     name = smoke_tests_utils.get_cluster_name()
-    cloud = sky.CLOUD_REGISTRY.from_str(generic_cloud)
     task = sky.Task(run="whoami")
     task.set_resources(
-        sky.Resources(cloud=cloud, **smoke_tests_utils.LOW_RESOURCE_PARAM))
+        sky.Resources(infra=generic_cloud,
+                      **smoke_tests_utils.LOW_RESOURCE_PARAM))
     try:
         job_id, handle = sky.get(sky.launch(task, cluster_name=name))
         assert job_id == 1
         assert handle is not None
         assert handle.cluster_name == name
-        assert handle.launched_resources.cloud.is_same_cloud(cloud)
+        assert str(
+            handle.launched_resources.cloud).lower() == generic_cloud.lower()
         job_id_exec, handle_exec = sky.get(sky.exec(task, cluster_name=name))
         assert job_id_exec == 2
         assert handle_exec is not None
         assert handle_exec.cluster_name == name
-        assert handle_exec.launched_resources.cloud.is_same_cloud(cloud)
+        assert str(handle_exec.launched_resources.cloud).lower(
+        ) == generic_cloud.lower()
         # For dummy task (i.e. task.run is None), the job won't be submitted.
         dummy_task = sky.Task()
         job_id_dummy, _ = sky.get(sky.exec(dummy_task, cluster_name=name))
@@ -416,10 +418,10 @@ def test_core_api_sky_launch_exec(generic_cloud: str):
 @pytest.mark.no_kubernetes
 def test_core_api_sky_launch_fast(generic_cloud: str):
     name = smoke_tests_utils.get_cluster_name()
-    cloud = sky.CLOUD_REGISTRY.from_str(generic_cloud)
     try:
         task = sky.Task(run="whoami").set_resources(
-            sky.Resources(cloud=cloud, **smoke_tests_utils.LOW_RESOURCE_PARAM))
+            sky.Resources(infra=generic_cloud,
+                          **smoke_tests_utils.LOW_RESOURCE_PARAM))
         sky.launch(task,
                    cluster_name=name,
                    idle_minutes_to_autostop=1,
@@ -444,9 +446,9 @@ def test_jobs_launch_and_logs(generic_cloud: str):
             smoke_tests_utils.LOW_CONTROLLER_RESOURCE_OVERRIDE_CONFIG):
         name = smoke_tests_utils.get_cluster_name()
         task = sky.Task(run="echo start job; sleep 30; echo end job")
-        cloud = sky.CLOUD_REGISTRY.from_str(generic_cloud)
         task.set_resources(
-            sky.Resources(cloud=cloud, **smoke_tests_utils.LOW_RESOURCE_PARAM))
+            sky.Resources(infra=generic_cloud,
+                          **smoke_tests_utils.LOW_RESOURCE_PARAM))
         job_id, handle = sky.stream_and_get(sky.jobs.launch(task, name=name))
         assert handle is not None
         # Check the job status from the dashboard
@@ -558,7 +560,7 @@ def test_multiple_accelerators_ordered_with_default():
         [
             f'sky launch -y -c {name} tests/test_yamls/test_multiple_accelerators_ordered_with_default.yaml | grep "Using user-specified accelerators list"',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
-            f'sky status {name} | grep Spot',
+            f'sky status {name} | grep spot',
         ],
         f'sky down -y {name}',
     )
@@ -593,7 +595,7 @@ def test_multiple_accelerators_unordered_with_default():
         [
             f'sky launch -y -c {name} tests/test_yamls/test_multiple_accelerators_unordered_with_default.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
-            f'sky status {name} | grep Spot',
+            f'sky status {name} | grep spot',
         ],
         f'sky down -y {name}',
     )
@@ -627,7 +629,7 @@ def test_sky_bench(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'sky-bench',
         [
-            f'sky bench launch -y -b {name} --cloud {generic_cloud} -i0 tests/test_yamls/minimal.yaml',
+            f'sky bench launch -y -b {name} --infra {generic_cloud} -i0 tests/test_yamls/minimal.yaml',
             'sleep 120',
             f'sky bench show {name} | grep sky-bench-{name} | grep FINISHED',
         ],
@@ -738,40 +740,34 @@ def test_kubernetes_context_failover(unreachable_context):
                 'kubectl get namespaces --context kind-skypilot | grep test-namespace || '
                 '{ echo "Should set the namespace to test-namespace for kind-skypilot. Check the instructions in '
                 'tests/test_smoke.py::test_kubernetes_context_failover." && exit 1; }',
-                'sky show-gpus --cloud kubernetes --region kind-skypilot | grep H100 | grep "1, 2, 4, 8"',
+                'sky show-gpus --infra kubernetes/kind-skypilot | grep H100 | grep "1, 2, 4, 8"',
                 # Get contexts and set current context to the other cluster that is not kind-skypilot
                 f'kubectl config use-context {context}',
                 # H100 should not be in the current context
-                f'! sky show-gpus --cloud kubernetes --region {context} | grep H100',
+                f'! sky show-gpus --infra kubernetes/{context} | grep H100',
                 # H100 should be displayed as long as it is available in one of the contexts
-                'sky show-gpus --cloud kubernetes | grep H100',
+                'sky show-gpus --infra kubernetes | grep H100',
                 f'sky launch -y -c {name}-1 --cpus 1 echo hi',
                 f'sky logs {name}-1 --status',
                 # It should be launched not on kind-skypilot
                 f'sky status -v {name}-1 | grep "{context}"',
                 # Test failure for launching H100 on other cluster
-                f'sky launch -y -c {name}-2 --gpus H100 --cpus 1 --cloud kubernetes --region {context} echo hi && exit 1 || true',
+                f'sky launch -y -c {name}-2 --gpus H100 --cpus 1 --infra kubernetes/{context} echo hi && exit 1 || true',
                 # Test failover
-                f'sky launch -y -c {name}-3 --gpus H100 --cpus 1 --cloud kubernetes echo hi',
+                f'sky launch -y -c {name}-3 --gpus H100 --cpus 1 --infra kubernetes echo hi',
                 f'sky logs {name}-3 --status',
                 # Test pods
                 f'kubectl get pods --context kind-skypilot | grep "{name}-3"',
                 # It should be launched on kind-skypilot
                 f'sky status -v {name}-3 | grep "kind-skypilot"',
                 # Should be 7 free GPUs
-                f'sky show-gpus --cloud kubernetes --region kind-skypilot | grep H100 | grep "  7"',
+                f'sky show-gpus --infra kubernetes/kind-skypilot | grep H100 | grep "  7"',
                 # Remove the line with "kind-skypilot"
                 f'sed -i "/kind-skypilot/d" {f.name}',
-                # Should still be able to exec and launch on existing cluster
-                f'sky exec {name}-3 "echo hi"',
-                f'sky logs {name}-3 --status',
-                f'sky status -r {name}-3 | grep UP',
-                f'sky launch -c {name}-3 --gpus h100 echo hi',
-                f'sky logs {name}-3 --status',
-                f'sky status -r {name}-3 | grep UP',
+                f'export KUBECONFIG={f.name}',
                 # Test failure for launching on unreachable context
                 f'kubectl config use-context {unreachable_context}',
-                f'sky launch -y -c {name}-4 --gpus H100 --cpus 1 --cloud kubernetes --region {unreachable_context} echo hi && exit 1 || true',
+                f'sky launch -y -c {name}-4 --gpus H100 --cpus 1 --infra kubernetes/{unreachable_context} echo hi && exit 1 || true',
                 # Test failover from unreachable context
                 f'sky launch -y -c {name}-5 --cpus 1 echo hi',
             ],
@@ -836,7 +832,7 @@ def test_cancel_launch_and_exec_async(generic_cloud: str):
     wait_cmd = wait_cmd.replace('sleep 10', 'sleep 1')
     test = smoke_tests_utils.Test(
         'cancel_launch_and_exec_async', [
-            f'sky launch -c {name} -y --cloud {generic_cloud} --async',
+            f'sky launch -c {name} -y --infra {generic_cloud} --async',
             (f's=$(sky exec {name} echo --async) && '
              'echo "$s" && '
              'logs_cmd=$(echo "$s" | grep "Check logs with" | '
@@ -861,7 +857,7 @@ def test_cli_exit_codes(generic_cloud: str):
         'cli_exit_codes',
         [
             # Test successful job exit code (0)
-            f'sky launch -y -c {name} --cloud {generic_cloud} "echo success" && echo "Exit code: $?"',
+            f'sky launch -y -c {name} --infra {generic_cloud} "echo success" && echo "Exit code: $?"',
             f'sky logs {name} 1 --status | grep SUCCEEDED',
 
             # Test that sky logs with successful job returns 0
diff --git a/tests/smoke_tests/test_cluster_job.py b/tests/smoke_tests/test_cluster_job.py
index 0437d35bd2b..8bf3f635bf9 100644
--- a/tests/smoke_tests/test_cluster_job.py
+++ b/tests/smoke_tests/test_cluster_job.py
@@ -54,7 +54,7 @@ def test_job_queue(generic_cloud: str, accelerator: Dict[str, str]):
     test = smoke_tests_utils.Test(
         'job_queue',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/job_queue/cluster.yaml',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/job_queue/cluster.yaml',
             f'sky exec {name} -n {name}-1 -d --gpus {accelerator}:0.5 examples/job_queue/job.yaml',
             f'sky exec {name} -n {name}-2 -d --gpus {accelerator}:0.5 examples/job_queue/job.yaml',
             f'sky exec {name} -n {name}-3 -d --gpus {accelerator}:0.5 examples/job_queue/job.yaml',
@@ -115,7 +115,7 @@ def test_job_queue_with_docker(generic_cloud: str, image_id: str,
     test = smoke_tests_utils.Test(
         'job_queue_with_docker',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} --image-id {image_id} examples/job_queue/cluster_docker.yaml',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} --image-id {image_id} examples/job_queue/cluster_docker.yaml',
             f'sky exec {name} -n {name}-1 -d --gpus {accelerator}:0.5 --image-id {image_id} --env TIME_TO_SLEEP={time_to_sleep*2} examples/job_queue/job_docker.yaml',
             f'sky exec {name} -n {name}-2 -d --gpus {accelerator}:0.5 --image-id {image_id} --env TIME_TO_SLEEP={time_to_sleep} examples/job_queue/job_docker.yaml',
             f'sky exec {name} -n {name}-3 -d --gpus {accelerator}:0.5 --image-id {image_id} --env TIME_TO_SLEEP={time_to_sleep} examples/job_queue/job_docker.yaml',
@@ -180,10 +180,10 @@ def test_ibm_job_queue():
     test = smoke_tests_utils.Test(
         'ibm_job_queue',
         [
-            f'sky launch -y -c {name} --cloud ibm --gpus v100',
-            f'sky exec {name} -n {name}-1 --cloud ibm -d examples/job_queue/job_ibm.yaml',
-            f'sky exec {name} -n {name}-2 --cloud ibm -d examples/job_queue/job_ibm.yaml',
-            f'sky exec {name} -n {name}-3 --cloud ibm -d examples/job_queue/job_ibm.yaml',
+            f'sky launch -y -c {name} --infra ibm --gpus v100',
+            f'sky exec {name} -n {name}-1 --infra ibm -d examples/job_queue/job_ibm.yaml',
+            f'sky exec {name} -n {name}-2 --infra ibm -d examples/job_queue/job_ibm.yaml',
+            f'sky exec {name} -n {name}-3 --infra ibm -d examples/job_queue/job_ibm.yaml',
             f'sky queue {name} | grep {name}-1 | grep RUNNING',
             f'sky queue {name} | grep {name}-2 | grep RUNNING',
             f'sky queue {name} | grep {name}-3 | grep PENDING',
@@ -239,7 +239,7 @@ def test_job_queue_multinode(generic_cloud: str, accelerator: Dict[str, str]):
     test = smoke_tests_utils.Test(
         'job_queue_multinode',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/job_queue/cluster_multinode.yaml',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/job_queue/cluster_multinode.yaml',
             f'sky exec {name} -n {name}-1 -d --gpus {accelerator}:0.5 examples/job_queue/job_multinode.yaml',
             f'sky exec {name} -n {name}-2 -d --gpus {accelerator}:0.5 examples/job_queue/job_multinode.yaml',
             f'sky launch -c {name} -n {name}-3 -d --gpus {accelerator}:0.5 examples/job_queue/job_multinode.yaml',
@@ -280,7 +280,7 @@ def test_large_job_queue(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'large_job_queue',
         [
-            f'sky launch -y -c {name} --cpus 8 --cloud {generic_cloud}',
+            f'sky launch -y -c {name} --cpus 8 --infra {generic_cloud}',
             f'for i in `seq 1 75`; do sky exec {name} -n {name}-$i -d "echo $i; sleep 100000000"; done',
             f'sky cancel -y {name} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16',
             'sleep 90',
@@ -328,7 +328,7 @@ def test_fast_large_job_queue(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'fast_large_job_queue',
         [
-            f'sky launch -y -c {name} --cpus 8 --cloud {generic_cloud}',
+            f'sky launch -y -c {name} --cpus 8 --infra {generic_cloud}',
             f'for i in `seq 1 32`; do sky exec {name} -n {name}-$i -d "echo $i"; done',
             'sleep 60',
             f's=$(sky queue {name}); echo "$s"; echo; echo; echo "$s" | grep -v grep | grep SUCCEEDED | wc -l | grep 32',
@@ -346,7 +346,7 @@ def test_ibm_job_queue_multinode():
     test = smoke_tests_utils.Test(
         'ibm_job_queue_multinode',
         [
-            f'sky launch -y -c {name} --cloud ibm --gpus v100 --num-nodes 2',
+            f'sky launch -y -c {name} --infra ibm --gpus v100 --num-nodes 2',
             f'sky exec {name} -n {name}-1 -d {task_file}',
             f'sky exec {name} -n {name}-2 -d {task_file}',
             f'sky launch -y -c {name} -n {name}-3 -d {task_file}',
@@ -391,7 +391,7 @@ def test_docker_preinstalled_package(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'docker_with_preinstalled_package',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id docker:nginx',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id docker:nginx',
             f'sky exec {name} "nginx -V"',
             f'sky logs {name} 1 --status',
             f'sky exec {name} whoami | grep root',
@@ -471,7 +471,7 @@ def test_huggingface(generic_cloud: str, accelerator: Dict[str, str]):
     test = smoke_tests_utils.Test(
         'huggingface_glue_imdb_app',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/huggingface_glue_imdb_app.yaml',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/huggingface_glue_imdb_app.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky exec {name} --gpus {accelerator} examples/huggingface_glue_imdb_app.yaml',
             f'sky logs {name} 2 --status',  # Ensure the job succeeded.
@@ -623,7 +623,7 @@ def test_multi_hostname(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'multi_hostname',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/multi_hostname.yaml',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/multi_hostname.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky logs {name} 1 | grep "My hostname:" | wc -l | grep 2',  # Ensure there are 2 hosts.
             f'sky exec {name} examples/multi_hostname.yaml',
@@ -643,7 +643,7 @@ def test_multi_node_failure(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'multi_node_failure',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/failed_worker_setup.yaml || [ $? -eq 100 ]',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/failed_worker_setup.yaml || [ $? -eq 100 ]',
             f'sky logs {name} 1 --status | grep FAILED_SETUP',  # Ensure the job setup failed.
             f'sky exec {name} tests/test_yamls/failed_worker_run.yaml || [ $? -eq 100 ]',
             f'sky logs {name} 2 --status | grep FAILED',  # Ensure the job failed.
@@ -661,7 +661,7 @@ def test_gcp_http_server_with_custom_ports():
     test = smoke_tests_utils.Test(
         'gcp_http_server_with_custom_ports',
         [
-            f'sky launch -y -d -c {name} --cloud gcp {smoke_tests_utils.LOW_RESOURCE_ARG} examples/http_server_with_custom_ports/task.yaml',
+            f'sky launch -y -d -c {name} --infra gcp {smoke_tests_utils.LOW_RESOURCE_ARG} examples/http_server_with_custom_ports/task.yaml',
             f'until SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}; do sleep 10; done',
             # Retry a few times to avoid flakiness in ports being open.
             f'ip=$(SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}); success=false; for i in $(seq 1 5); do if curl $ip | grep "<h1>This is a demo HTML page.</h1>"; then success=true; break; fi; sleep 10; done; if [ "$success" = false ]; then exit 1; fi',
@@ -678,7 +678,7 @@ def test_aws_http_server_with_custom_ports():
     test = smoke_tests_utils.Test(
         'aws_http_server_with_custom_ports',
         [
-            f'sky launch -y -d -c {name} --cloud aws {smoke_tests_utils.LOW_RESOURCE_ARG} examples/http_server_with_custom_ports/task.yaml',
+            f'sky launch -y -d -c {name} --infra aws {smoke_tests_utils.LOW_RESOURCE_ARG} examples/http_server_with_custom_ports/task.yaml',
             f'until SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}; do sleep 10; done',
             # Retry a few times to avoid flakiness in ports being open.
             f'ip=$(SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}); success=false; for i in $(seq 1 5); do if curl $ip | grep "<h1>This is a demo HTML page.</h1>"; then success=true; break; fi; sleep 10; done; if [ "$success" = false ]; then exit 1; fi'
@@ -695,7 +695,7 @@ def test_azure_http_server_with_custom_ports():
     test = smoke_tests_utils.Test(
         'azure_http_server_with_custom_ports',
         [
-            f'sky launch -y -d -c {name} --cloud azure {smoke_tests_utils.LOW_RESOURCE_ARG} examples/http_server_with_custom_ports/task.yaml',
+            f'sky launch -y -d -c {name} --infra azure {smoke_tests_utils.LOW_RESOURCE_ARG} examples/http_server_with_custom_ports/task.yaml',
             f'until SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}; do sleep 10; done',
             # Retry a few times to avoid flakiness in ports being open.
             f'ip=$(SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}); success=false; for i in $(seq 1 5); do if curl $ip | grep "<h1>This is a demo HTML page.</h1>"; then success=true; break; fi; sleep 10; done; if [ "$success" = false ]; then exit 1; fi'
@@ -713,7 +713,7 @@ def test_kubernetes_http_server_with_custom_ports():
     test = smoke_tests_utils.Test(
         'kubernetes_http_server_with_custom_ports',
         [
-            f'sky launch -y -d -c {name} --cloud kubernetes examples/http_server_with_custom_ports/task.yaml',
+            f'sky launch -y -d -c {name} --infra kubernetes examples/http_server_with_custom_ports/task.yaml',
             f'until SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}; do sleep 10; done',
             # Retry a few times to avoid flakiness in ports being open.
             f'ip=$(SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}); success=false; for i in $(seq 1 100); do if curl $ip | grep "<h1>This is a demo HTML page.</h1>"; then success=true; break; fi; sleep 5; done; if [ "$success" = false ]; then exit 1; fi'
@@ -730,7 +730,7 @@ def test_paperspace_http_server_with_custom_ports():
     test = smoke_tests_utils.Test(
         'paperspace_http_server_with_custom_ports',
         [
-            f'sky launch -y -d -c {name} --cloud paperspace examples/http_server_with_custom_ports/task.yaml',
+            f'sky launch -y -d -c {name} --infra paperspace examples/http_server_with_custom_ports/task.yaml',
             f'until SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}; do sleep 10; done',
             # Retry a few times to avoid flakiness in ports being open.
             f'ip=$(SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}); success=false; for i in $(seq 1 5); do if curl $ip | grep "<h1>This is a demo HTML page.</h1>"; then success=true; break; fi; sleep 10; done; if [ "$success" = false ]; then exit 1; fi',
@@ -747,7 +747,7 @@ def test_runpod_http_server_with_custom_ports():
     test = smoke_tests_utils.Test(
         'runpod_http_server_with_custom_ports',
         [
-            f'sky launch -y -d -c {name} --cloud runpod examples/http_server_with_custom_ports/task.yaml',
+            f'sky launch -y -d -c {name} --infra runpod examples/http_server_with_custom_ports/task.yaml',
             f'until SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}; do sleep 10; done',
             # Retry a few times to avoid flakiness in ports being open.
             f'ip=$(SKYPILOT_DEBUG=0 sky status --endpoint 33828 {name}); success=false; for i in $(seq 1 5); do if curl $ip | grep "<h1>This is a demo HTML page.</h1>"; then success=true; break; fi; sleep 10; done; if [ "$success" = false ]; then exit 1; fi',
@@ -862,7 +862,7 @@ def test_add_pod_annotations_for_autodown_with_launch():
             smoke_tests_utils.launch_cluster_for_cloud_cmd('kubernetes', name),
             # Launch Kubernetes cluster with two nodes, each being head node and worker node.
             # Autodown is set.
-            f'sky launch -y -c {name} -i 10 --down --num-nodes 2 --cpus=1 --cloud kubernetes',
+            f'sky launch -y -c {name} -i 10 --down --num-nodes 2 --cpus=1 --infra kubernetes',
             # Get names of the pods containing cluster name.
             smoke_tests_utils.run_cloud_cmd_on_cluster(
                 name,
@@ -894,7 +894,7 @@ def test_add_and_remove_pod_annotations_with_autostop():
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('kubernetes', name),
             # Launch Kubernetes cluster with two nodes, each being head node and worker node.
-            f'sky launch -y -c {name} --num-nodes 2 --cpus=1 --cloud kubernetes',
+            f'sky launch -y -c {name} --num-nodes 2 --cpus=1 --infra kubernetes',
             # Set autodown on the cluster with 'autostop' command.
             f'sky autostop -y {name} -i 20 --down',
             # Get names of the pods containing cluster name.
@@ -1216,7 +1216,7 @@ def test_autostop(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'autostop',
         [
-            f'sky launch -y -d -c {name} --num-nodes 2 --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
+            f'sky launch -y -d -c {name} --num-nodes 2 --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
             f'sky autostop -y {name} -i 1',
 
             # Ensure autostop is set.
@@ -1285,7 +1285,7 @@ def test_autodown(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'autodown',
         [
-            f'sky launch -y -d -c {name} --num-nodes 2 --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
+            f'sky launch -y -d -c {name} --num-nodes 2 --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
             f'sky autostop -y {name} --down -i 1',
             check_autostop_set,
             # Ensure the cluster is not terminated early.
@@ -1294,14 +1294,14 @@ def test_autodown(generic_cloud: str):
             # Ensure the cluster is terminated.
             f'sleep {autodown_timeout}',
             f's=$(SKYPILOT_DEBUG=0 sky status {name} --refresh) && echo "$s" && {{ echo "$s" | grep {name} | grep "Autodowned cluster\|Cluster \'{name}\' not found"; }} || {{ echo "$s" | grep {name} && exit 1 || exit 0; }}',
-            f'sky launch -y -d -c {name} --cloud {generic_cloud} --num-nodes 2 --down {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
+            f'sky launch -y -d -c {name} --infra {generic_cloud} --num-nodes 2 --down {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
             f'sky status | grep {name} | grep UP',  # Ensure the cluster is UP.
-            f'sky exec {name} --cloud {generic_cloud} tests/test_yamls/minimal.yaml',
+            f'sky exec {name} --infra {generic_cloud} tests/test_yamls/minimal.yaml',
             check_autostop_set,
             f'sleep {autodown_timeout}',
             # Ensure the cluster is terminated.
             f's=$(SKYPILOT_DEBUG=0 sky status {name} --refresh) && echo "$s" && {{ echo "$s" | grep {name} | grep "Autodowned cluster\|Cluster \'{name}\' not found"; }} || {{ echo "$s" | grep {name} && exit 1 || exit 0; }}',
-            f'sky launch -y -d -c {name} --cloud {generic_cloud} --num-nodes 2 --down {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
+            f'sky launch -y -d -c {name} --infra {generic_cloud} --num-nodes 2 --down {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
             f'sky autostop -y {name} --cancel',
             f'sleep {autodown_timeout}',
             # Ensure the cluster is still UP.
@@ -1352,7 +1352,7 @@ def _get_cancel_task_with_cloud(name, cloud, timeout=15 * 60):
     test = smoke_tests_utils.Test(
         f'{cloud}-cancel-task',
         [
-            f'sky launch -c {name} examples/resnet_app.yaml --cloud {cloud} -y -d',
+            f'sky launch -c {name} examples/resnet_app.yaml --infra {cloud} -y -d',
             # Wait the job to be scheduled and finished setup.
             f'until sky queue {name} | grep "RUNNING"; do sleep 10; done',
             # Wait the setup and initialize before the GPU process starts.
@@ -1407,7 +1407,7 @@ def test_cancel_pytorch(generic_cloud: str, accelerator: Dict[str, str]):
     test = smoke_tests_utils.Test(
         'cancel-pytorch',
         [
-            f'sky launch -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/resnet_distributed_torch.yaml -y -d',
+            f'sky launch -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus {accelerator} examples/resnet_distributed_torch.yaml -y -d',
             # Wait until the setup finishes.
             smoke_tests_utils.
             get_cmd_wait_until_job_status_contains_matching_job_id(
@@ -1444,7 +1444,7 @@ def test_cancel_ibm():
     test = smoke_tests_utils.Test(
         'ibm-cancel-task',
         [
-            f'sky launch -y -c {name} --cloud ibm examples/minimal.yaml',
+            f'sky launch -y -c {name} --infra ibm examples/minimal.yaml',
             f'sky exec {name} -n {name}-1 -d  "while true; do echo \'Hello SkyPilot\'; sleep 2; done"',
             'sleep 20',
             f'sky queue {name} | grep {name}-1 | grep RUNNING',
@@ -1472,7 +1472,7 @@ def test_use_spot(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'use-spot',
         [
-            f'sky launch -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml --use-spot -y',
+            f'sky launch -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml --use-spot -y',
             f'sky logs {name} 1 --status',
             f'sky exec {name} echo hi',
             f'sky logs {name} 2 --status',
@@ -1493,7 +1493,7 @@ def test_azure_spot_instance_verification():
     test = smoke_tests_utils.Test(
         'azure-spot-verification',
         [
-            f'sky launch -c {name} --cloud azure {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml --use-spot -y',
+            f'sky launch -c {name} --infra azure {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml --use-spot -y',
             f'sky logs {name} 1 --status', f'TARGET_VM_NAME="{name}"; '
             'VM_INFO=$(az vm list --query "[?contains(name, \'$TARGET_VM_NAME\')].{Name:name, ResourceGroup:resourceGroup}" -o tsv); '
             '[[ -z "$VM_INFO" ]] && exit 1; '
@@ -1517,7 +1517,7 @@ def test_stop_gcp_spot():
     test = smoke_tests_utils.Test(
         'stop_gcp_spot',
         [
-            f'sky launch -c {name} --cloud gcp {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot -y -- touch myfile',
+            f'sky launch -c {name} --infra gcp {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot -y -- touch myfile',
             # stop should go through:
             f'sky stop {name} -y',
             f'sky start {name} -y',
@@ -1550,7 +1550,7 @@ def test_inline_env(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-inline-env',
         [
-            f'sky launch -c {name} -y --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --env TEST_ENV="hello world" -- "([[ ! -z \\"\$TEST_ENV\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_IPS}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_RANK}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NUM_NODES}\\" ]]) || exit 1"',
+            f'sky launch -c {name} -y --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --env TEST_ENV="hello world" -- "([[ ! -z \\"\$TEST_ENV\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_IPS}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_RANK}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NUM_NODES}\\" ]]) || exit 1"',
             'sleep 20',
             f'sky logs {name} 1 --status',
             f'sky exec {name} --env TEST_ENV2="success" "([[ ! -z \\"\$TEST_ENV2\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_IPS}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_RANK}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NUM_NODES}\\" ]]) || exit 1"',
@@ -1569,7 +1569,7 @@ def test_inline_env_file(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-inline-env-file',
         [
-            f'sky launch -c {name} -y --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --env TEST_ENV="hello world" -- "([[ ! -z \\"\$TEST_ENV\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_IPS}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_RANK}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NUM_NODES}\\" ]]) || exit 1"',
+            f'sky launch -c {name} -y --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --env TEST_ENV="hello world" -- "([[ ! -z \\"\$TEST_ENV\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_IPS}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_RANK}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NUM_NODES}\\" ]]) || exit 1"',
             f'sky logs {name} 1 --status',
             f'sky exec {name} --env-file examples/sample_dotenv "([[ ! -z \\"\$TEST_ENV2\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_IPS}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NODE_RANK}\\" ]] && [[ ! -z \\"\${constants.SKYPILOT_NUM_NODES}\\" ]]) || exit 1"',
             f'sky logs {name} 2 --status',
@@ -1588,7 +1588,7 @@ def test_aws_custom_image():
     test = smoke_tests_utils.Test(
         'test-aws-custom-image',
         [
-            f'sky launch -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --retry-until-up -y tests/test_yamls/test_custom_image.yaml --cloud aws --region us-east-2 --image-id ami-062ddd90fb6f8267a',  # Nvidia image
+            f'sky launch -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --retry-until-up -y tests/test_yamls/test_custom_image.yaml --infra aws/us-east-2 --image-id ami-062ddd90fb6f8267a',  # Nvidia image
             f'sky logs {name} 1 --status',
         ],
         f'sky down -y {name}',
@@ -1616,10 +1616,10 @@ def test_kubernetes_custom_image(image_id):
     test = smoke_tests_utils.Test(
         'test-kubernetes-custom-image',
         [
-            f'sky launch -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --retry-until-up -y tests/test_yamls/test_custom_image.yaml --cloud kubernetes --image-id {image_id} --region None --gpus T4:1',
+            f'sky launch -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --retry-until-up -y tests/test_yamls/test_custom_image.yaml --infra kubernetes/none --image-id {image_id} --gpus T4:1',
             f'sky logs {name} 1 --status',
             # Try exec to run again and check if the logs are printed
-            f'sky exec {name} tests/test_yamls/test_custom_image.yaml --cloud kubernetes --image-id {image_id} --region None --gpus T4:1 | grep "Hello 100"',
+            f'sky exec {name} tests/test_yamls/test_custom_image.yaml --infra kubernetes/none --image-id {image_id} --gpus T4:1 | grep "Hello 100"',
             # Make sure ssh is working with custom username
             f'ssh {name} echo hi | grep hi',
         ],
@@ -1677,7 +1677,7 @@ def _get_aws_query_command(region: str, instance_id: str, field: str,
             'aws-disk-tier-' + disk_tier.value,
             [
                 smoke_tests_utils.launch_cluster_for_cloud_cmd('aws', name),
-                f'sky launch -y -c {name} --cloud aws --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
+                f'sky launch -y -c {name} --infra aws/{region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
                 f'--disk-tier {disk_tier.value} echo "hello sky"',
                 smoke_tests_utils.run_cloud_cmd_on_cluster(
                     name,
@@ -1736,7 +1736,7 @@ def test_gcp_disk_tier(instance_types: List[str]):
                 'gcp-disk-tier-' + disk_tier.value,
                 [
                     smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
-                    f'sky launch -y -c {name} --cloud gcp --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
+                    f'sky launch -y -c {name} --infra gcp/{region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
                     f'--disk-tier {disk_tier.value} {instance_type_option} ',
                     smoke_tests_utils.run_cloud_cmd_on_cluster(
                         name,
@@ -1766,7 +1766,7 @@ def test_azure_disk_tier():
         test = smoke_tests_utils.Test(
             'azure-disk-tier-' + disk_tier.value,
             [
-                f'sky launch -y -c {name} --cloud azure --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
+                f'sky launch -y -c {name} --infra azure/{region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
                 f'--disk-tier {disk_tier.value} echo "hello sky"',
                 f'az resource list --tag ray-cluster-name={name_on_cloud} --query '
                 f'"[?type==\'Microsoft.Compute/disks\'].sku.name" '
@@ -1788,7 +1788,7 @@ def test_azure_best_tier_failover():
     test = smoke_tests_utils.Test(
         'azure-best-tier-failover',
         [
-            f'sky launch -y -c {name} --cloud azure --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
+            f'sky launch -y -c {name} --infra azure/{region} {smoke_tests_utils.LOW_RESOURCE_ARG} '
             f'--disk-tier best --instance-type Standard_D8_v5 echo "hello sky"',
             f'az resource list --tag ray-cluster-name={name_on_cloud} --query '
             f'"[?type==\'Microsoft.Compute/disks\'].sku.name" '
@@ -1817,7 +1817,7 @@ def test_aws_zero_quota_failover():
     test = smoke_tests_utils.Test(
         'aws-zero-quota-failover',
         [
-            f'sky launch -y -c {name} --cloud aws --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus V100:8 --use-spot | grep "Found no quota"',
+            f'sky launch -y -c {name} --infra aws/{region} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus V100:8 --use-spot | grep "Found no quota"',
         ],
         f'sky down -y {name}',
     )
@@ -1840,7 +1840,7 @@ def test_gcp_zero_quota_failover():
     test = smoke_tests_utils.Test(
         'gcp-zero-quota-failover',
         [
-            f'sky launch -y -c {name} --cloud gcp --region {region} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus A100-80GB:1 --use-spot | grep "Found no quota"',
+            f'sky launch -y -c {name} --infra gcp/{region} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus A100-80GB:1 --use-spot | grep "Found no quota"',
         ],
         f'sky down -y {name}',
     )
@@ -1872,7 +1872,7 @@ def test_long_setup_run_script(generic_cloud: str):
         test = smoke_tests_utils.Test(
             'long-setup-run-script',
             [
-                f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {f.name}',
+                f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {f.name}',
                 f'sky exec {name} "echo hello"',
                 f'sky exec {name} {f.name}',
                 f'sky logs {name} --status 1',
@@ -1909,7 +1909,7 @@ def test_min_gpt_kubernetes():
         test = smoke_tests_utils.Test(
             'min_gpt_kubernetes',
             [
-                f'sky launch -y -c {name} --cloud kubernetes {f.name}',
+                f'sky launch -y -c {name} --infra kubernetes {f.name}',
                 f'sky logs {name} 1 --status',
             ],
             f'sky down -y {name}',
diff --git a/tests/smoke_tests/test_images.py b/tests/smoke_tests/test_images.py
index f27bef7a5f6..b7825982508 100644
--- a/tests/smoke_tests/test_images.py
+++ b/tests/smoke_tests/test_images.py
@@ -57,10 +57,10 @@ def test_gcp_images():
     test = smoke_tests_utils.Test(
         'gcp_images',
         [
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:gpu-debian-10 --cloud gcp tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:gpu-debian-10 --infra gcp tests/test_yamls/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
-            f'sky launch -c {name} --image-id skypilot:cpu-debian-10 --cloud gcp tests/test_yamls/minimal.yaml && exit 1 || true',
-            f'sky launch -y -c {name} tests/test_yamls/minimal.yaml',
+            f'sky launch -c {name} --image-id skypilot:cpu-debian-10 --infra gcp tests/test_yamls/minimal.yaml && exit 1 || true',
+            f'sky launch -y -c {name} --infra gcp tests/test_yamls/minimal.yaml',
             f'sky logs {name} 2 --status',
             f'sky logs {name} --status | grep "Job 2: SUCCEEDED"',  # Equivalent.
             f'sky exec {name} \'echo $SKYPILOT_CLUSTER_INFO | jq .cloud | grep -i gcp\'',
@@ -77,9 +77,9 @@ def test_azure_images():
     test = smoke_tests_utils.Test(
         'azure_images',
         [
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:gpu-ubuntu-2204 --cloud azure tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:gpu-ubuntu-2204 --infra azure tests/test_yamls/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
-            f'sky launch -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:v1-ubuntu-2004 --cloud azure tests/test_yamls/minimal.yaml && exit 1 || true',
+            f'sky launch -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:v1-ubuntu-2004 --infra azure tests/test_yamls/minimal.yaml && exit 1 || true',
             f'sky launch -y -c {name} tests/test_yamls/minimal.yaml',
             f'sky logs {name} 2 --status',
             f'sky logs {name} --status | grep "Job 2: SUCCEEDED"',  # Equivalent.
@@ -140,9 +140,9 @@ def test_aws_image_id_dict_region():
             #       us-west-2: skypilot:gpu-ubuntu-1804
             #       us-east-2: skypilot:gpu-ubuntu-2004
             # Use region to filter image_id dict.
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-1 examples/per_region_images.yaml && exit 1 || true',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra aws/us-east-1 examples/per_region_images.yaml && exit 1 || true',
             f'sky status | grep {name} && exit 1 || true',  # Ensure the cluster is not created.
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-2 examples/per_region_images.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra aws/us-east-2 examples/per_region_images.yaml',
             # Should success because the image id match for the region.
             f'sky launch -c {name} --image-id skypilot:gpu-ubuntu-2004 examples/minimal.yaml',
             f'sky exec {name} --image-id skypilot:gpu-ubuntu-2004 examples/minimal.yaml',
@@ -152,9 +152,9 @@ def test_aws_image_id_dict_region():
             f'sky logs {name} 3 --status',
             f'sky status -v | grep {name} | grep us-east-2',  # Ensure the region is correct.
             # Ensure exec works.
-            f'sky exec {name} --region us-east-2 examples/per_region_images.yaml',
+            f'sky exec {name} --infra aws/us-east-2 examples/per_region_images.yaml',
             f'sky exec {name} examples/per_region_images.yaml',
-            f'sky exec {name} --cloud aws --region us-east-2 "ls ~"',
+            f'sky exec {name} --infra aws/us-east-2 "ls ~"',
             f'sky exec {name} "ls ~"',
             f'sky logs {name} 4 --status',
             f'sky logs {name} 5 --status',
@@ -173,21 +173,21 @@ def test_gcp_image_id_dict_region():
         'gcp_image_id_dict_region',
         [
             # Use region to filter image_id dict.
-            f'sky launch -y -c {name} --region us-east1 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml && exit 1 || true',
+            f'sky launch -y -c {name} --infra gcp/us-east1 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml && exit 1 || true',
             f'sky status | grep {name} && exit 1 || true',  # Ensure the cluster is not created.
-            f'sky launch -y -c {name} --region us-west3 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml',
+            f'sky launch -y -c {name} --infra gcp/us-west3 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml',
             # Should success because the image id match for the region.
-            f'sky launch -c {name} --cloud gcp --image-id projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20230112 tests/test_yamls/minimal.yaml',
-            f'sky exec {name} --cloud gcp --image-id projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20230112 tests/test_yamls/minimal.yaml',
-            f'sky exec {name} --cloud gcp --image-id skypilot:cpu-debian-10 tests/test_yamls/minimal.yaml && exit 1 || true',
+            f'sky launch -c {name} --infra gcp --image-id projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20230112 tests/test_yamls/minimal.yaml',
+            f'sky exec {name} --infra gcp --image-id projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20230112 tests/test_yamls/minimal.yaml',
+            f'sky exec {name} --infra gcp --image-id skypilot:cpu-debian-10 tests/test_yamls/minimal.yaml && exit 1 || true',
             f'sky logs {name} 1 --status',
             f'sky logs {name} 2 --status',
             f'sky logs {name} 3 --status',
             f'sky status -v | grep {name} | grep us-west3',  # Ensure the region is correct.
             # Ensure exec works.
-            f'sky exec {name} --region us-west3 tests/test_yamls/gcp_per_region_images.yaml',
+            f'sky exec {name} --infra gcp/us-west3 tests/test_yamls/gcp_per_region_images.yaml',
             f'sky exec {name} tests/test_yamls/gcp_per_region_images.yaml',
-            f'sky exec {name} --cloud gcp --region us-west3 "ls ~"',
+            f'sky exec {name} --infra gcp/us-west3 "ls ~"',
             f'sky exec {name} "ls ~"',
             f'sky logs {name} 4 --status',
             f'sky logs {name} 5 --status',
@@ -210,9 +210,9 @@ def test_aws_image_id_dict_zone():
             #       us-west-2: skypilot:gpu-ubuntu-1804
             #       us-east-2: skypilot:gpu-ubuntu-2004
             # Use zone to filter image_id dict.
-            f'sky launch -y -c {name} --zone us-east-1b {smoke_tests_utils.LOW_RESOURCE_ARG} examples/per_region_images.yaml && exit 1 || true',
+            f'sky launch -y -c {name} --infra aws/*/us-east-1b {smoke_tests_utils.LOW_RESOURCE_ARG} examples/per_region_images.yaml && exit 1 || true',
             f'sky status | grep {name} && exit 1 || true',  # Ensure the cluster is not created.
-            f'sky launch -y -c {name} --zone us-east-2a {smoke_tests_utils.LOW_RESOURCE_ARG} examples/per_region_images.yaml',
+            f'sky launch -y -c {name} --infra aws/*/us-east-2a {smoke_tests_utils.LOW_RESOURCE_ARG} examples/per_region_images.yaml',
             # Should success because the image id match for the zone.
             f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id skypilot:gpu-ubuntu-2004 examples/minimal.yaml',
             f'sky exec {name} --image-id skypilot:gpu-ubuntu-2004 examples/minimal.yaml',
@@ -223,9 +223,9 @@ def test_aws_image_id_dict_zone():
             f'sky logs {name} 3 --status',
             f'sky status -v | grep {name} | grep us-east-2a',  # Ensure the zone is correct.
             # Ensure exec works.
-            f'sky exec {name} --zone us-east-2a examples/per_region_images.yaml',
+            f'sky exec {name} --infra aws/*/us-east-2a examples/per_region_images.yaml',
             f'sky exec {name} examples/per_region_images.yaml',
-            f'sky exec {name} --cloud aws --region us-east-2 "ls ~"',
+            f'sky exec {name} --infra aws/us-east-2 "ls ~"',
             f'sky exec {name} "ls ~"',
             f'sky logs {name} 4 --status',
             f'sky logs {name} 5 --status',
@@ -244,22 +244,22 @@ def test_gcp_image_id_dict_zone():
         'gcp_image_id_dict_zone',
         [
             # Use zone to filter image_id dict.
-            f'sky launch -y -c {name} --zone us-east1-a {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml && exit 1 || true',
+            f'sky launch -y -c {name} --infra */*/us-east1-a {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml && exit 1 || true',
             f'sky status | grep {name} && exit 1 || true',  # Ensure the cluster is not created.
-            f'sky launch -y -c {name} --zone us-central1-a {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml',
+            f'sky launch -y -c {name} --infra */*/us-central1-a {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/gcp_per_region_images.yaml',
             # Should success because the image id match for the zone.
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud gcp --image-id skypilot:cpu-debian-10 tests/test_yamls/minimal.yaml',
-            f'sky exec {name} --cloud gcp --image-id skypilot:cpu-debian-10 tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra gcp --image-id skypilot:cpu-debian-10 tests/test_yamls/minimal.yaml',
+            f'sky exec {name} --infra gcp --image-id skypilot:cpu-debian-10 tests/test_yamls/minimal.yaml',
             # Fail due to image id mismatch.
-            f'sky exec {name} --cloud gcp --image-id skypilot:gpu-debian-10 tests/test_yamls/minimal.yaml && exit 1 || true',
+            f'sky exec {name} --infra gcp --image-id skypilot:gpu-debian-10 tests/test_yamls/minimal.yaml && exit 1 || true',
             f'sky logs {name} 1 --status',
             f'sky logs {name} 2 --status',
             f'sky logs {name} 3 --status',
             f'sky status -v | grep {name} | grep us-central1',  # Ensure the zone is correct.
             # Ensure exec works.
-            f'sky exec {name} --cloud gcp --zone us-central1-a tests/test_yamls/gcp_per_region_images.yaml',
+            f'sky exec {name} --infra gcp/*/us-central1-a tests/test_yamls/gcp_per_region_images.yaml',
             f'sky exec {name} tests/test_yamls/gcp_per_region_images.yaml',
-            f'sky exec {name} --cloud gcp --region us-central1 "ls ~"',
+            f'sky exec {name} --infra gcp/us-central1 "ls ~"',
             f'sky exec {name} "ls ~"',
             f'sky logs {name} 4 --status',
             f'sky logs {name} 5 --status',
@@ -279,7 +279,7 @@ def test_clone_disk_aws():
     test = smoke_tests_utils.Test(
         'clone_disk_aws',
         [
-            f'sky launch -y -c {name} --cloud aws --region us-east-2 --retry-until-up "echo hello > ~/user_file.txt"',
+            f'sky launch -y -c {name} --infra aws/us-east-2 --retry-until-up "echo hello > ~/user_file.txt"',
             f'sky launch --clone-disk-from {name} -y -c {name}-clone && exit 1 || true',
             f'sky stop {name} -y',
             smoke_tests_utils.get_cmd_wait_until_cluster_status_contains(
@@ -289,8 +289,8 @@ def test_clone_disk_aws():
             # Wait for EC2 instance to be in stopped state.
             # TODO: event based wait.
             'sleep 60',
-            f'sky launch --clone-disk-from {name} -y -c {name}-clone --cloud aws -d --region us-east-2 "cat ~/user_file.txt | grep hello"',
-            f'sky launch --clone-disk-from {name} -y -c {name}-clone-2 --cloud aws -d --region us-east-2 "cat ~/user_file.txt | grep hello"',
+            f'sky launch --clone-disk-from {name} -y -c {name}-clone --infra aws/us-east-2 -d "cat ~/user_file.txt | grep hello"',
+            f'sky launch --clone-disk-from {name} -y -c {name}-clone-2 --infra aws/us-east-2 -d "cat ~/user_file.txt | grep hello"',
             f'sky logs {name}-clone 1 --status',
             f'sky logs {name}-clone-2 1 --status',
         ],
@@ -308,11 +308,11 @@ def test_clone_disk_gcp():
     test = smoke_tests_utils.Test(
         'clone_disk_gcp',
         [
-            f'sky launch -y -c {name} --cloud gcp --zone us-east1-b --retry-until-up "echo hello > ~/user_file.txt"',
+            f'sky launch -y -c {name} --infra gcp/*/us-east1-b --retry-until-up "echo hello > ~/user_file.txt"',
             f'sky launch --clone-disk-from {name} -y -c {name}-clone && exit 1 || true',
             f'sky stop {name} -y',
-            f'sky launch --clone-disk-from {name} -y -c {name}-clone --cloud gcp --zone us-central1-a "cat ~/user_file.txt | grep hello"',
-            f'sky launch --clone-disk-from {name} -y -c {name}-clone-2 --cloud gcp --zone us-east1-b "cat ~/user_file.txt | grep hello"',
+            f'sky launch --clone-disk-from {name} -y -c {name}-clone --infra gcp/*/us-central1-a "cat ~/user_file.txt | grep hello"',
+            f'sky launch --clone-disk-from {name} -y -c {name}-clone-2 --infra gcp/*/us-east1-b "cat ~/user_file.txt | grep hello"',
             f'sky logs {name}-clone 1 --status',
             f'sky logs {name}-clone-2 1 --status',
         ],
@@ -331,9 +331,9 @@ def test_gcp_mig():
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
             # Launch a CPU instance asynchronously.
-            f'sky launch -y -c {name}-cpu {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud gcp --zone {zone} --async tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name}-cpu {smoke_tests_utils.LOW_RESOURCE_ARG} --infra gcp/*/us-central1-a --async tests/test_yamls/minimal.yaml',
             # Launch a GPU instance.
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus l4 --num-nodes 2 --image-id skypilot:gpu-debian-10 --cloud gcp --region {region} tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --gpus l4 --num-nodes 2 --image-id skypilot:gpu-debian-10 --infra gcp/{region} tests/test_yamls/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
             f'sky logs {name} 2 --status',
@@ -354,7 +354,7 @@ def test_gcp_mig():
                 )),
             # Launch again with the same region. The original instance template
             # should be removed.
-            f'sky launch -y -c {name} --gpus L4 --num-nodes 2 --region {region} nvidia-smi',
+            f'sky launch -y -c {name} --gpus L4 --num-nodes 2 --infra gcp/{region} nvidia-smi',
             f'sky logs {name} 1 | grep "L4"',
             f'sky down -y {name}',
             f'sky status | grep {name}-cpu | grep UP',
@@ -408,7 +408,7 @@ def test_gcp_force_enable_external_ips():
 
     test_commands = [
         is_on_gcp_command,
-        f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud gcp --cpus 2 tests/test_yamls/minimal.yaml',
+        f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra gcp --cpus 2 tests/test_yamls/minimal.yaml',
         # Check network of vm is "default"
         (f'gcloud compute instances list --filter=name~"{name}" --format='
          '"value(networkInterfaces.network)" | grep "networks/default"'),
@@ -438,7 +438,7 @@ def test_image_no_conda():
         'image_no_conda',
         [
             # Use image id dict.
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-2 examples/per_region_images.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra aws/us-east-2 examples/per_region_images.yaml',
             f'sky logs {name} 1 --status',
             f'sky stop {name} -y',
             f'sky start {name} -y',
@@ -459,7 +459,7 @@ def test_custom_default_conda_env(generic_cloud: str):
         timeout *= 3
     name = smoke_tests_utils.get_cluster_name()
     test = smoke_tests_utils.Test('custom_default_conda_env', [
-        f'sky launch -c {name} -y {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} tests/test_yamls/test_custom_default_conda_env.yaml',
+        f'sky launch -c {name} -y {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} tests/test_yamls/test_custom_default_conda_env.yaml',
         f'sky status -r {name} | grep "UP"',
         f'sky logs {name} 1 --status',
         f'sky logs {name} 1 --no-follow | grep -E "myenv\\s+\\*"',
diff --git a/tests/smoke_tests/test_managed_job.py b/tests/smoke_tests/test_managed_job.py
index 69915e3e89e..9a874f5a6da 100644
--- a/tests/smoke_tests/test_managed_job.py
+++ b/tests/smoke_tests/test_managed_job.py
@@ -51,8 +51,8 @@ def test_managed_jobs_basic(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'managed-jobs',
         [
-            f'sky jobs launch -n {name}-1 --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/managed_job.yaml -y -d',
-            f'sky jobs launch -n {name}-2 --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/managed_job.yaml -y -d',
+            f'sky jobs launch -n {name}-1 --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/managed_job.yaml -y -d',
+            f'sky jobs launch -n {name}-2 --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} examples/managed_job.yaml -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=f'{name}-1',
@@ -97,7 +97,7 @@ def test_managed_jobs_cli_exit_codes(generic_cloud: str):
         'managed_jobs_exit_codes',
         [
             # Test jobs launch with successful job
-            f'sky jobs launch -y -n jobs-{name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo jobs success" && echo "Jobs launch exit code: $?"',
+            f'sky jobs launch -y -n jobs-{name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo jobs success" && echo "Jobs launch exit code: $?"',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=f'jobs-{name}',
@@ -112,7 +112,7 @@ def test_managed_jobs_cli_exit_codes(generic_cloud: str):
             f'sky jobs logs $JOB_ID && echo "Jobs logs exit code: $?"',
 
             # Test jobs launch with failing job
-            f'sky jobs launch -y -n jobs-fail-{name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "exit 1" || echo "Jobs launch failed exit code: $?" | grep "Jobs launch failed exit code: 100"',
+            f'sky jobs launch -y -n jobs-fail-{name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "exit 1" || echo "Jobs launch failed exit code: $?" | grep "Jobs launch failed exit code: 100"',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=f'jobs-fail-{name}',
@@ -149,7 +149,7 @@ def test_job_pipeline(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'job_pipeline',
         [
-            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/pipeline.yaml --cloud {generic_cloud} -y -d',
+            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} tests/test_yamls/pipeline.yaml -y -d',
             # Need to wait for setup and job initialization.
             'sleep 30',
             rf'{smoke_tests_utils.GET_JOB_QUEUE} | grep {name} | head -n1 | grep "STARTING\|RUNNING"',
@@ -194,7 +194,7 @@ def test_managed_jobs_failed_setup(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'managed_jobs_failed_setup',
         [
-            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} -y -d tests/test_yamls/failed_setup.yaml',
+            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} -y -d tests/test_yamls/failed_setup.yaml',
             # Make sure the job failed quickly.
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
@@ -225,7 +225,7 @@ def test_managed_jobs_pipeline_failed_setup(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'managed_jobs_pipeline_failed_setup',
         [
-            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} -y -d tests/test_yamls/failed_setup_pipeline.yaml',
+            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} -y -d tests/test_yamls/failed_setup_pipeline.yaml',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -265,7 +265,7 @@ def test_managed_jobs_recovery_aws(aws_config_region):
         'managed_jobs_recovery_aws',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('aws', name),
-            rf'sky jobs launch --cloud aws --region {region} --use-spot -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
+            rf'sky jobs launch --infra aws/{region} --use-spot -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -316,7 +316,7 @@ def test_managed_jobs_recovery_gcp():
         'managed_jobs_recovery_gcp',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
-            rf'sky jobs launch --cloud gcp --zone {zone} -n {name} --use-spot {smoke_tests_utils.LOW_RESOURCE_ARG} "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
+            rf'sky jobs launch --infra gcp/*/{zone} -n {name} --use-spot {smoke_tests_utils.LOW_RESOURCE_ARG} "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -468,7 +468,7 @@ def test_managed_jobs_recovery_default_resources(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'managed-spot-recovery-default-resources',
         [
-            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} --use-spot "sleep 30 && sudo shutdown now && sleep 1000" -y -d',
+            f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} --use-spot "sleep 30 && sudo shutdown now && sleep 1000" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -497,7 +497,7 @@ def test_managed_jobs_recovery_multi_node_aws(aws_config_region):
         'managed_jobs_recovery_multi_node_aws',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('aws', name),
-            rf'sky jobs launch --cloud aws --region {region} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot --num-nodes 2 "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
+            rf'sky jobs launch --infra aws/{region} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot --num-nodes 2 "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -549,7 +549,7 @@ def test_managed_jobs_recovery_multi_node_gcp():
         'managed_jobs_recovery_multi_node_gcp',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
-            rf'sky jobs launch --cloud gcp --zone {zone} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot --num-nodes 2 "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
+            rf'sky jobs launch --infra gcp/*/{zone} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot --num-nodes 2 "echo SKYPILOT_TASK_ID: \$SKYPILOT_TASK_ID; sleep 1800" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -591,7 +591,7 @@ def test_managed_jobs_cancellation_aws(aws_config_region):
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('aws', name),
             # Test cancellation during spot cluster being launched.
-            f'sky jobs launch --cloud aws --region {region} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
+            f'sky jobs launch --infra aws/{region} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -614,7 +614,7 @@ def test_managed_jobs_cancellation_aws(aws_config_region):
                  '--output text) && echo "$s" && echo; [[ -z "$s" ]] || [[ "$s" = "terminated" ]] || [[ "$s" = "shutting-down" ]]'
                 )),
             # Test cancelling the spot cluster during spot job being setup.
-            f'sky jobs launch --cloud aws --region {region} -n {name}-2 {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot tests/test_yamls/test_long_setup.yaml -y -d',
+            f'sky jobs launch --infra aws/{region} -n {name}-2 {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot tests/test_yamls/test_long_setup.yaml -y -d',
             # The job is set up in the cluster, will shown as RUNNING.
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
@@ -636,7 +636,7 @@ def test_managed_jobs_cancellation_aws(aws_config_region):
                  '--output text) && echo "$s" && echo; [[ -z "$s" ]] || [[ "$s" = "terminated" ]] || [[ "$s" = "shutting-down" ]]'
                 )),
             # Test cancellation during spot job is recovering.
-            f'sky jobs launch --cloud aws --region {region} -n {name}-3 {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
+            f'sky jobs launch --infra aws/{region} -n {name}-3 {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
             # The job is running in the cluster, will shown as RUNNING.
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
@@ -701,7 +701,7 @@ def test_managed_jobs_cancellation_gcp():
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
             # Test cancellation during spot cluster being launched.
-            f'sky jobs launch --cloud gcp --zone {zone} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
+            f'sky jobs launch --infra gcp/*/{zone} -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -714,7 +714,7 @@ def test_managed_jobs_cancellation_gcp():
                 job_status=[sky.ManagedJobStatus.CANCELLED],
                 timeout=155),
             # Test cancelling the spot cluster during spot job being setup.
-            f'sky jobs launch --cloud gcp --zone {zone} -n {name}-2 {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot tests/test_yamls/test_long_setup.yaml -y -d',
+            f'sky jobs launch --infra gcp/*/{zone} -n {name}-2 {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot tests/test_yamls/test_long_setup.yaml -y -d',
             # The job is set up in the cluster, will shown as RUNNING.
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
@@ -728,7 +728,7 @@ def test_managed_jobs_cancellation_gcp():
                 job_status=[sky.ManagedJobStatus.CANCELLED],
                 timeout=155),
             # Test cancellation during spot job is recovering.
-            f'sky jobs launch --cloud gcp --zone {zone} -n {name_3} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
+            f'sky jobs launch --infra gcp/*/{zone} -n {name_3} {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot "sleep 1000" -y -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name_3,
@@ -821,16 +821,20 @@ def test_managed_jobs_storage(generic_cloud: str):
     storage_name = f'sky-test-{timestamp}'
     output_storage_name = f'sky-test-output-{timestamp}'
 
+    # First, add an initialization for region
+    region = None
+    region_flag = ''
+    region_validation_cmd = 'true'
+    use_spot = ' --use-spot'
+    output_check_cmd = None
+
     # Also perform region testing for bucket creation to validate if buckets are
     # created in the correct region and correctly mounted in managed jobs.
     # However, we inject this testing only for AWS and GCP since they are the
     # supported object storage providers in SkyPilot.
-    region_flag = ''
-    region_validation_cmd = 'true'
-    use_spot = ' --use-spot'
     if generic_cloud == 'aws':
         region = 'eu-central-1'
-        region_flag = f' --region {region}'
+        region_flag = f'/{region}'
         region_cmd = test_mount_and_storage.TestStorageWithCredentials.cli_region_cmd(
             storage_lib.StoreType.S3, bucket_name=output_storage_name)
         region_validation_cmd = f's=$({region_cmd}) && echo "$s" && echo; echo "$s" | grep {region}'
@@ -847,7 +851,7 @@ def test_managed_jobs_storage(generic_cloud: str):
             f'{non_persistent_bucket_removed_check_cmd} && exit 1 || true')
     elif generic_cloud == 'gcp':
         region = 'us-west2'
-        region_flag = f' --region {region}'
+        region_flag = f'/{region}'
         region_cmd = test_mount_and_storage.TestStorageWithCredentials.cli_region_cmd(
             storage_lib.StoreType.GCS, bucket_name=output_storage_name)
         region_validation_cmd = f'{region_cmd} | grep {region}'
@@ -863,12 +867,12 @@ def test_managed_jobs_storage(generic_cloud: str):
             name,
             f'{non_persistent_bucket_removed_check_cmd} && exit 1 || true')
     elif generic_cloud == 'azure':
-        region = 'centralus'
-        # Region centralus seems don't have the quota for low resource.
+        # Azure instances with smaller than 7G memory can have flaky performance,
         # so we keep the default resource to avoid flakiness.
         low_resource_arg = ""
-        region_flag = f' --region {region}'
-        storage_account_name = test_mount_and_storage.TestStorageWithCredentials. \
+        region = 'centralus'
+        region_flag = f'/{region}'
+        storage_account_name = test_mount_and_storage.TestStorageWithCredentials.\
             get_az_storage_account_name(region)
         region_cmd = test_mount_and_storage.TestStorageWithCredentials.cli_region_cmd(
             storage_lib.StoreType.AZURE,
@@ -948,7 +952,7 @@ def test_managed_jobs_storage(generic_cloud: str):
                     generic_cloud, name),
                 # Override CPU/memory requirements to relax resource constraints
                 # and reduce the chance of out-of-stock
-                f'sky jobs launch -n {name}{use_spot} {low_resource_arg} --cloud {generic_cloud}{region_flag} {file_path} -y -d',
+                f'sky jobs launch -n {name}{use_spot} {low_resource_arg} --infra {generic_cloud}{region_flag} {file_path} -y -d',
                 region_validation_cmd,  # Check if the bucket is created in the correct region
                 smoke_tests_utils.
                 get_cmd_wait_until_managed_job_status_contains_matching_job_name(
@@ -1010,7 +1014,7 @@ def test_managed_jobs_intermediate_storage(generic_cloud: str):
                     *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
                     # Verify command fails with correct error - run only once
                     # In API server, we don't error out if the bucket does not exist, instead we create it.
-                    # f'err=$(sky jobs launch -n {name} --cloud {generic_cloud} {file_path} -y 2>&1); '
+                    # f'err=$(sky jobs launch -n {name} --infra {generic_cloud} {file_path} -y 2>&1); '
                     # f'ret=$?; if [ $ret -ne 0 ] && echo "$err" | grep -q "StorageBucketCreateError: '
                     # f'Jobs bucket \'{intermediate_storage_name}\' does not exist."; then exit 0; '
                     # f'else exit 1; fi',
@@ -1019,7 +1023,7 @@ def test_managed_jobs_intermediate_storage(generic_cloud: str):
                         cmd=
                         f'aws s3api create-bucket --bucket {intermediate_storage_name}'
                     ),
-                    f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} {file_path} -y',
+                    f'sky jobs launch -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} {file_path} -y',
                     # fail because the bucket does not exist
                     smoke_tests_utils.
                     get_cmd_wait_until_managed_job_status_contains_matching_job_name(
@@ -1090,7 +1094,7 @@ def test_managed_jobs_inline_env(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-managed-jobs-inline-env',
         [
-            rf'sky jobs launch -n {name} -y --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --env TEST_ENV="hello world" -- "echo "\$TEST_ENV"; ([[ ! -z \"\$TEST_ENV\" ]] && [[ ! -z \"\${constants.SKYPILOT_NODE_IPS}\" ]] && [[ ! -z \"\${constants.SKYPILOT_NODE_RANK}\" ]] && [[ ! -z \"\${constants.SKYPILOT_NUM_NODES}\" ]]) || exit 1"',
+            rf'sky jobs launch -n {name} -y --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --env TEST_ENV="hello world" -- "echo "\$TEST_ENV"; ([[ ! -z \"\$TEST_ENV\" ]] && [[ ! -z \"\${constants.SKYPILOT_NODE_IPS}\" ]] && [[ ! -z \"\${constants.SKYPILOT_NODE_RANK}\" ]] && [[ ! -z \"\${constants.SKYPILOT_NUM_NODES}\" ]]) || exit 1"',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=name,
@@ -1120,7 +1124,7 @@ def test_managed_jobs_logs_sync_down(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-managed-jobs-logs-sync-down',
         [
-            f'sky jobs launch -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y examples/managed_job.yaml -d',
+            f'sky jobs launch -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y examples/managed_job.yaml -d',
             smoke_tests_utils.
             get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                 job_name=f'{name}',
diff --git a/tests/smoke_tests/test_mount_and_storage.py b/tests/smoke_tests/test_mount_and_storage.py
index e2f45ede9e4..91249affd8d 100644
--- a/tests/smoke_tests/test_mount_and_storage.py
+++ b/tests/smoke_tests/test_mount_and_storage.py
@@ -71,7 +71,7 @@ def test_file_mounts(generic_cloud: str):
         extra_flags = '--num-nodes 1'
     test_commands = [
         *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-        f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {extra_flags} examples/using_file_mounts.yaml',
+        f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {extra_flags} examples/using_file_mounts.yaml',
         f'sky logs {name} 1 --status',  # Ensure the job succeeded.
     ]
     test = smoke_tests_utils.Test(
@@ -105,7 +105,7 @@ def test_oci_mounts():
     name = smoke_tests_utils.get_cluster_name()
     test_commands = [
         *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-        f'sky launch -y -c {name} --cloud oci --num-nodes 2 examples/oci/oci-mounts.yaml',
+        f'sky launch -y -c {name} --infra oci --num-nodes 2 examples/oci/oci-mounts.yaml',
         f'sky logs {name} 1 --status',  # Ensure the job succeeded.
     ]
     test = smoke_tests_utils.Test(
@@ -124,12 +124,12 @@ def test_using_file_mounts_with_env_vars(generic_cloud: str):
     storage_name = TestStorageWithCredentials.generate_bucket_name()
     test_commands = [
         *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-        (f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} '
+        (f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} '
          'examples/using_file_mounts_with_env_vars.yaml '
          f'--env MY_BUCKET={storage_name}'),
         f'sky logs {name} 1 --status',  # Ensure the job succeeded.
         # Override with --env:
-        (f'sky launch -y -c {name}-2 {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} '
+        (f'sky launch -y -c {name}-2 {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} '
          'examples/using_file_mounts_with_env_vars.yaml '
          f'--env MY_BUCKET={storage_name} '
          '--env MY_LOCAL_PATH=tmpfile'),
@@ -176,7 +176,7 @@ def _storage_mounts_commands_generator(f: TextIO, cluster_name: str,
     test_commands = [
         smoke_tests_utils.launch_cluster_for_cloud_cmd(cloud, cluster_name),
         *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-        f'sky launch -y -c {cluster_name} --cloud {cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {file_path}',
+        f'sky launch -y -c {cluster_name} --infra {cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {file_path}',
         f'sky logs {cluster_name} 1 --status',  # Ensure job succeeded.
         smoke_tests_utils.run_cloud_cmd_on_cluster(cluster_name,
                                                    cmd=ls_hello_command),
@@ -336,7 +336,7 @@ def test_kubernetes_context_switch():
 
     test_commands = [
         # Launch a cluster and run a simple task
-        f'sky launch -y -c {name} --cloud kubernetes "echo Hello from original context"',
+        f'sky launch -y -c {name} --infra kubernetes "echo Hello from original context"',
         f'sky logs {name} 1 --status',  # Ensure job succeeded
 
         # Get current context details and save to a file for later use in cleanup
@@ -420,7 +420,7 @@ def test_docker_storage_mounts(generic_cloud: str, image_id: str):
     # If azure is used, the azure blob storage checking assumes the bucket is
     # created in the centralus region when getting the storage account. We
     # should set the cluster to be launched in the same region.
-    region_str = '--region centralus' if generic_cloud == 'azure' else ''
+    region_str = f'/centralus' if generic_cloud == 'azure' else ''
     if azure_mount_unsupported_ubuntu_version in image_id:
         # The store for mount_private_mount is not specified in the template.
         # If we're running on Azure, the private mount will be created on
@@ -449,7 +449,7 @@ def test_docker_storage_mounts(generic_cloud: str, image_id: str):
         file_path = f.name
         test_commands = [
             *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} {region_str} --image-id {image_id} {file_path}',
+            f'sky launch -y -c {name} --infra {generic_cloud}{region_str} {smoke_tests_utils.LOW_RESOURCE_ARG} --image-id {image_id} {file_path}',
             f'sky logs {name} 1 --status',  # Ensure job succeeded.
             # Check AWS, GCP, or Azure storage mount.
             f'sky exec {name} {quoted_check}',
@@ -479,7 +479,7 @@ def test_cloudflare_storage_mounts(generic_cloud: str):
         file_path = f.name
         test_commands = [
             *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-            f'sky launch -y -c {name} --cloud {generic_cloud} {file_path}',
+            f'sky launch -y -c {name} --infra {generic_cloud} {file_path}',
             f'sky logs {name} 1 --status',  # Ensure job succeeded.
             f'AWS_SHARED_CREDENTIALS_FILE={cloudflare.R2_CREDENTIALS_PATH} aws s3 ls s3://{storage_name}/hello.txt --endpoint {endpoint_url} --profile=r2'
         ]
@@ -507,7 +507,7 @@ def test_nebius_storage_mounts(generic_cloud: str):
         file_path = f.name
         test_commands = [
             *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-            f'sky launch -y -c {name} --cloud {generic_cloud} {file_path}',
+            f'sky launch -y -c {name} --infra {generic_cloud} {file_path}',
             f'sky logs {name} 1 --status',  # Ensure job succeeded.
             f'aws s3 ls s3://{storage_name}/hello.txt --profile={nebius.NEBIUS_PROFILE_NAME}'
         ]
@@ -537,7 +537,7 @@ def test_ibm_storage_mounts():
         file_path = f.name
         test_commands = [
             *smoke_tests_utils.STORAGE_SETUP_COMMANDS,
-            f'sky launch -y -c {name} --cloud ibm {file_path}',
+            f'sky launch -y -c {name} --infra ibm {file_path}',
             f'sky logs {name} 1 --status',  # Ensure job succeeded.
             f'rclone ls {rclone_profile_name}:{storage_name}/hello.txt',
         ]
@@ -617,7 +617,7 @@ def test_ignore_exclusions(generic_cloud: str, ignore_file: str):
         # Run test commands
         test_commands = [
             # Test with sky launch
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --workdir {temp_dir} {yaml_path}',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --workdir {temp_dir} {yaml_path}',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded
 
             # Test with sky jobs launch
diff --git a/tests/smoke_tests/test_region_and_zone.py b/tests/smoke_tests/test_region_and_zone.py
index ebc8d65ca5a..3d6091bd767 100644
--- a/tests/smoke_tests/test_region_and_zone.py
+++ b/tests/smoke_tests/test_region_and_zone.py
@@ -37,7 +37,7 @@ def test_aws_region():
     test = smoke_tests_utils.Test(
         'aws_region',
         [
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-2 examples/minimal.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra */us-east-2 examples/minimal.yaml',
             f'sky exec {name} examples/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky status -v | grep {name} | grep us-east-2',  # Ensure the region is correct.
@@ -70,15 +70,15 @@ def test_aws_with_ssh_proxy_command():
         test = smoke_tests_utils.Test(
             'aws_with_ssh_proxy_command',
             [
-                f'sky launch -y -c jump-{name} --cloud aws {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-1',
+                f'sky launch -y -c jump-{name} --infra aws/us-east-1 {smoke_tests_utils.LOW_RESOURCE_ARG}',
                 # Use jump config
                 f'export {skypilot_config.ENV_VAR_SKYPILOT_CONFIG}={f.name}; '
-                f'sky launch -y -c {name} --cloud aws {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-1 echo hi',
+                f'sky launch -y -c {name} --infra aws/us-east-1 {smoke_tests_utils.LOW_RESOURCE_ARG} echo hi',
                 f'sky logs {name} 1 --status',
                 f'export {skypilot_config.ENV_VAR_SKYPILOT_CONFIG}={f.name}; sky exec {name} echo hi',
                 f'sky logs {name} 2 --status',
                 # Start a small job to make sure the controller is created.
-                f'sky jobs launch -n {name}-0 --cloud aws {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot -y echo hi',
+                f'sky jobs launch -n {name}-0 --infra aws {smoke_tests_utils.LOW_RESOURCE_ARG} --use-spot -y echo hi',
                 # Wait other tests to create the job controller first, so that
                 # the job controller is not launched with proxy command.
                 smoke_tests_utils.
@@ -86,7 +86,7 @@ def test_aws_with_ssh_proxy_command():
                     cluster_name_wildcard='sky-jobs-controller-*',
                     cluster_status=[sky.ClusterStatus.UP],
                     timeout=300),
-                f'export {skypilot_config.ENV_VAR_SKYPILOT_CONFIG}={f.name}; sky jobs launch -n {name} --cloud aws {smoke_tests_utils.LOW_RESOURCE_ARG} --region us-east-1 -yd echo hi',
+                f'export {skypilot_config.ENV_VAR_SKYPILOT_CONFIG}={f.name}; sky jobs launch -n {name} --infra aws/us-east-1 {smoke_tests_utils.LOW_RESOURCE_ARG} -yd echo hi',
                 smoke_tests_utils.
                 get_cmd_wait_until_managed_job_status_contains_matching_job_name(
                     job_name=name,
@@ -109,7 +109,7 @@ def test_gcp_region_and_service_account():
     test = smoke_tests_utils.Test(
         'gcp_region',
         [
-            f'sky launch -y -c {name} --region us-central1 {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud gcp tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} --infra gcp/us-central1 {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
             f'sky exec {name} tests/test_yamls/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky exec {name} \'curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?format=standard&audience=gcp"\'',
@@ -133,8 +133,8 @@ def test_ibm_region():
     test = smoke_tests_utils.Test(
         'region',
         [
-            f'sky launch -y -c {name} --cloud ibm --region {region} examples/minimal.yaml',
-            f'sky exec {name} --cloud ibm examples/minimal.yaml',
+            f'sky launch -y -c {name} --infra ibm/{region} examples/minimal.yaml',
+            f'sky exec {name} --infra ibm examples/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky status -v | grep {name} | grep {region}',  # Ensure the region is correct.
         ],
@@ -149,7 +149,7 @@ def test_azure_region():
     test = smoke_tests_utils.Test(
         'azure_region',
         [
-            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --region eastus2 --cloud azure tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra azure/eastus2 tests/test_yamls/minimal.yaml',
             f'sky exec {name} tests/test_yamls/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky status -v | grep {name} | grep eastus2',  # Ensure the region is correct.
@@ -173,8 +173,8 @@ def test_aws_zone():
     test = smoke_tests_utils.Test(
         'aws_zone',
         [
-            f'sky launch -y -c {name} examples/minimal.yaml {smoke_tests_utils.LOW_RESOURCE_ARG} --zone us-east-2b',
-            f'sky exec {name} examples/minimal.yaml --zone us-east-2b',
+            f'sky launch -y -c {name} examples/minimal.yaml {smoke_tests_utils.LOW_RESOURCE_ARG} --infra */*/us-east-2b',
+            f'sky exec {name} examples/minimal.yaml --infra */*/us-east-2b',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky status -v | grep {name} | grep us-east-2b',  # Ensure the zone is correct.
         ],
@@ -190,8 +190,8 @@ def test_ibm_zone():
     test = smoke_tests_utils.Test(
         'zone',
         [
-            f'sky launch -y -c {name} --cloud ibm examples/minimal.yaml {smoke_tests_utils.LOW_RESOURCE_ARG} --zone {zone}',
-            f'sky exec {name} --cloud ibm examples/minimal.yaml --zone {zone}',
+            f'sky launch -y -c {name} --infra ibm/*/{zone} examples/minimal.yaml {smoke_tests_utils.LOW_RESOURCE_ARG}',
+            f'sky exec {name} --infra ibm/*/{zone} examples/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky status -v | grep {name} | grep {zone}',  # Ensure the zone is correct.
         ],
@@ -206,8 +206,8 @@ def test_gcp_zone():
     test = smoke_tests_utils.Test(
         'gcp_zone',
         [
-            f'sky launch -y -c {name} --zone us-central1-a {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud gcp tests/test_yamls/minimal.yaml',
-            f'sky exec {name} --zone us-central1-a --cloud gcp tests/test_yamls/minimal.yaml',
+            f'sky launch -y -c {name} --infra gcp/*/us-central1-a {smoke_tests_utils.LOW_RESOURCE_ARG} tests/test_yamls/minimal.yaml',
+            f'sky exec {name} --infra gcp/*/us-central1-a tests/test_yamls/minimal.yaml',
             f'sky logs {name} 1 --status',  # Ensure the job succeeded.
             f'sky status -v | grep {name} | grep us-central1-a',  # Ensure the zone is correct.
         ],
diff --git a/tests/smoke_tests/test_sky_serve.py b/tests/smoke_tests/test_sky_serve.py
index f766a277f8b..62064148b94 100644
--- a/tests/smoke_tests/test_sky_serve.py
+++ b/tests/smoke_tests/test_sky_serve.py
@@ -113,13 +113,13 @@ def _get_service_name() -> str:
     'echo "$s"')
 
 _WAIT_PROVISION_REPR = (
-    # Once controller is ready, check provisioning vs. vCPU=2. This is for
-    # the `_check_replica_in_status`, which will check number of `vCPU=2` in the
+    # Once controller is ready, check provisioning vs. cpus=2. This is for
+    # the `_check_replica_in_status`, which will check number of `cpus=2` in the
     # `sky serve status` output and use that to suggest the number of replicas.
     # However, replicas in provisioning state is possible to have a repr of `-`,
     # since the desired `launched_resources` is not decided yet. This would
     # cause an error when counting desired number of replicas. We wait for the
-    # representation of `vCPU=2` the same with number of provisioning replicas
+    # representation of `cpus=2` the same with number of provisioning replicas
     # to avoid this error.
     # NOTE(tian): This assumes the replica will not do failover, as the
     # requested resources is only 2 vCPU and likely to be immediately available
@@ -127,7 +127,7 @@ def _get_service_name() -> str:
     # failover
     # Check #4565 for more information.
     'num_provisioning=$(echo "$s" | grep "PROVISIONING" | wc -l); '
-    'num_vcpu_in_provision=$(echo "$s" | grep "PROVISIONING" | grep "vCPU=2" | wc -l); '
+    'num_vcpu_in_provision=$(echo "$s" | grep "PROVISIONING" | grep "x(cpus=2, " | wc -l); '
     'until [ "$num_provisioning" -eq "$num_vcpu_in_provision" ]; '
     'do '
     '    echo "Waiting for provisioning resource repr ready..."; '
@@ -135,10 +135,10 @@ def _get_service_name() -> str:
     '    sleep 2; '
     '    s=$(sky serve status {name}); '
     '    num_provisioning=$(echo "$s" | grep "PROVISIONING" | wc -l); '
-    '    num_vcpu_in_provision=$(echo "$s" | grep "PROVISIONING" | grep "vCPU=2" | wc -l); '
+    '    num_vcpu_in_provision=$(echo "$s" | grep "PROVISIONING" | grep "x(cpus=2, " | wc -l); '
     'done; '
     # Provisioning is complete
-    'echo "Provisioning complete. PROVISIONING: $num_provisioning, vCPU=2: $num_vcpu_in_provision"'
+    'echo "Provisioning complete. PROVISIONING: $num_provisioning, cpus=2: $num_cpus_in_provision"'
 )
 
 # Shell script snippet to monitor and wait for resolution of NOT_READY status:
@@ -197,7 +197,7 @@ def _check_replica_in_status(name: str,
                              timeout_seconds: int = 0) -> str:
     """Check replicas' status and count in sky serve status
 
-    We will check vCPU=2, as all our tests use vCPU=2.
+    We will check cpus=2, as all our tests use cpus=2.
 
     Args:
         name: the name of the service
@@ -216,8 +216,8 @@ def _check_replica_in_status(name: str,
                          ] and not status.startswith('FAILED'):
             spot_str = ''
             if is_spot:
-                spot_str = r'\[Spot\]'
-            resource_str = f'({spot_str}vCPU=2)'
+                spot_str = r'\[spot\]'
+            resource_str = f'x{spot_str}(cpus=2, '
         check_conditions.append(
             f'echo "$s" | grep "{resource_str}" | grep "{status}" | wc -l | '
             f'grep {count}')
@@ -342,7 +342,7 @@ def generate_llm_test_command(prompt: str, expected_output: str) -> str:
     test = smoke_tests_utils.Test(
         'test-skyserve-llm',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} --gpus {accelerator} -y tests/skyserve/llm/service.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} --gpus {accelerator} -y tests/skyserve/llm/service.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=1),
             *[
                 generate_llm_test_command(prompt, output)
@@ -395,7 +395,7 @@ def test_skyserve_base_ondemand_fallback(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-base-ondemand-fallback',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/spot/base_ondemand_fallback.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/spot/base_ondemand_fallback.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=2),
             _check_replica_in_status(name, [(1, True, 'READY'),
                                             (1, False, 'READY')]),
@@ -417,7 +417,7 @@ def test_skyserve_dynamic_ondemand_fallback():
         'test-skyserve-dynamic-ondemand-fallback',
         [
             smoke_tests_utils.launch_cluster_for_cloud_cmd('gcp', name),
-            f'sky serve up -n {name} --cloud gcp {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/spot/dynamic_ondemand_fallback.yaml',
+            f'sky serve up -n {name} --infra gcp {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/spot/dynamic_ondemand_fallback.yaml',
             f'sleep 40',
             # 2 on-demand (provisioning) + 2 Spot (provisioning).
             f'{_SERVE_STATUS_WAIT.format(name=name)}; echo "$s";'
@@ -475,7 +475,7 @@ def test_skyserve_user_bug_restart(generic_cloud: str):
             'test-skyserve-user-bug-restart',
             [
                 increase_initial_delay_seconds(
-                    f'sky serve up -n {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/restart/user_bug.yaml'
+                    f'sky serve up -n {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/restart/user_bug.yaml'
                 ),
                 f's=$(sky serve status {name}); echo "$s";'
                 'until echo "$s" | grep -A 100 "Service Replicas" | grep "SHUTTING_DOWN"; '
@@ -490,7 +490,7 @@ def test_skyserve_user_bug_restart(generic_cloud: str):
                 f'echo "$s" | grep -A 100 "Service Replicas" | grep "{name}" | wc -l | grep 1; '
                 f'echo "$s" | grep -B 100 "NO_REPLICA" | grep "0/0"',
                 increase_initial_delay_seconds(
-                    f'sky serve update {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/auto_restart.yaml'
+                    f'sky serve update {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/auto_restart.yaml'
                 ),
                 f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
                 'until curl --connect-timeout 10 --max-time 10 $endpoint | grep "Hi, SkyPilot here"; do sleep 1; done; sleep 2; '
@@ -513,7 +513,7 @@ def test_skyserve_load_balancer(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-load-balancer',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/load_balancer/service.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/load_balancer/service.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=3),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
             f'{_SERVE_STATUS_WAIT.format(name=name)}; '
@@ -587,7 +587,7 @@ def test_skyserve_cancel(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-cancel',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/cancel/cancel.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/cancel/cancel.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=1),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; python3 '
             'tests/skyserve/cancel/send_cancel_request.py '
@@ -616,7 +616,7 @@ def test_skyserve_streaming(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-streaming',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/streaming/streaming.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/streaming/streaming.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=1),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
             'python3 tests/skyserve/streaming/send_streaming_request.py '
@@ -637,7 +637,7 @@ def test_skyserve_readiness_timeout_fail(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-readiness-timeout-fail',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/readiness_timeout/task.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/readiness_timeout/task.yaml',
             # None of the readiness probe will pass, so the service will be
             # terminated after the initial delay.
             f's=$(sky serve status {name}); '
@@ -663,7 +663,7 @@ def test_skyserve_large_readiness_timeout(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-large-readiness-timeout',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/readiness_timeout/task_large_timeout.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/readiness_timeout/task_large_timeout.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=1),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
             'request_output=$(curl $endpoint); echo "$request_output"; echo "$request_output" | grep "Hi, SkyPilot here"',
@@ -689,10 +689,10 @@ def test_skyserve_update(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-update',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/update/old.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/update/old.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=2),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; curl $endpoint | grep "Hi, SkyPilot here"',
-            f'sky serve update {name} --cloud {generic_cloud} {resource_arg} --mode blue_green -y tests/skyserve/update/new.yaml',
+            f'sky serve update {name} --infra {generic_cloud} {resource_arg} --mode blue_green -y tests/skyserve/update/new.yaml',
             # sleep before update is registered.
             'sleep 20',
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
@@ -733,12 +733,12 @@ def test_skyserve_rolling_update(generic_cloud: str):
             f'test-skyserve-rolling-update',
             [
                 increase_initial_delay_seconds(
-                    f'sky serve up -n {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/update/old.yaml'
+                    f'sky serve up -n {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/update/old.yaml'
                 ),
                 _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=2),
                 f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; curl $endpoint | grep "Hi, SkyPilot here"',
                 increase_initial_delay_seconds(
-                    f'sky serve update {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/update/new.yaml'
+                    f'sky serve update {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/update/new.yaml'
                 ),
                 # Make sure the traffic is mixed across two versions, the replicas
                 # with even id will sleep 120 seconds before being ready, so we
@@ -785,10 +785,10 @@ def test_skyserve_fast_update(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-fast-update',
         [
-            f'sky serve up -n {name} -y {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} tests/skyserve/update/bump_version_before.yaml',
+            f'sky serve up -n {name} -y {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} tests/skyserve/update/bump_version_before.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=2),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; curl $endpoint | grep "Hi, SkyPilot here"',
-            f'sky serve update {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --mode blue_green -y tests/skyserve/update/bump_version_after.yaml',
+            f'sky serve update {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --mode blue_green -y tests/skyserve/update/bump_version_after.yaml',
             # sleep to wait for update to be registered.
             'sleep 40',
             # 2 on-deamnd (ready) + 1 on-demand (provisioning).
@@ -802,7 +802,7 @@ def test_skyserve_fast_update(generic_cloud: str):
             _check_service_version(name, "2"),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; curl $endpoint | grep "Hi, SkyPilot here"',
             # Test rolling update
-            f'sky serve update {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/update/bump_version_before.yaml',
+            f'sky serve update {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/update/bump_version_before.yaml',
             # sleep to wait for update to be registered.
             'sleep 25',
             # 2 on-deamnd (ready) + 1 on-demand (shutting down).
@@ -833,14 +833,14 @@ def test_skyserve_update_autoscale(generic_cloud: str):
             f'test-skyserve-update-autoscale',
             [
                 increase_initial_delay_seconds(
-                    f'sky serve up -n {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/update/num_min_two.yaml'
+                    f'sky serve up -n {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/update/num_min_two.yaml'
                 ),
                 _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=2) +
                 _check_service_version(name, "1"),
                 f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
                 'curl $endpoint | grep "Hi, SkyPilot here"',
                 increase_initial_delay_seconds(
-                    f'sky serve update {name} --cloud {generic_cloud} {resource_arg} --mode blue_green -y tests/skyserve/update/num_min_one.yaml'
+                    f'sky serve update {name} --infra {generic_cloud} {resource_arg} --mode blue_green -y tests/skyserve/update/num_min_one.yaml'
                 ),
                 # sleep before update is registered.
                 'sleep 20',
@@ -851,7 +851,7 @@ def test_skyserve_update_autoscale(generic_cloud: str):
                 'curl $endpoint | grep "Hi, SkyPilot here!"',
                 # Rolling Update
                 increase_initial_delay_seconds(
-                    f'sky serve update {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/update/num_min_two.yaml'
+                    f'sky serve update {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/update/num_min_two.yaml'
                 ),
                 # sleep before update is registered.
                 'sleep 20',
@@ -909,12 +909,12 @@ def test_skyserve_new_autoscaler_update(mode: str, generic_cloud: str):
     test = smoke_tests_utils.Test(
         f'test-skyserve-new-autoscaler-update-{mode}',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/update/new_autoscaler_before.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/update/new_autoscaler_before.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=2) +
             _check_service_version(name, "1"),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
             's=$(curl $endpoint); echo "$s"; echo "$s" | grep "Hi, SkyPilot here"',
-            f'sky serve update {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --mode {mode} -y tests/skyserve/update/new_autoscaler_after.yaml',
+            f'sky serve update {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} --mode {mode} -y tests/skyserve/update/new_autoscaler_after.yaml',
             # Wait for update to be registered
             'sleep 90',
             wait_until_no_pending,
@@ -953,7 +953,7 @@ def test_skyserve_failures(generic_cloud: str):
             'test-skyserve-failures',
             [
                 increase_initial_delay_seconds(
-                    f'sky serve up -n {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/failures/initial_delay.yaml'
+                    f'sky serve up -n {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/failures/initial_delay.yaml'
                 ),
                 f's=$(sky serve status {name}); '
                 f'until echo "$s" | grep "FAILED_INITIAL_DELAY"; do '
@@ -964,7 +964,7 @@ def test_skyserve_failures(generic_cloud: str):
                 # Make sure no new replicas are started for early failure.
                 f'echo "$s" | grep -A 100 "Service Replicas" | grep "{name}" | wc -l | grep 2;',
                 increase_initial_delay_seconds(
-                    f'sky serve update {name} --cloud {generic_cloud} {resource_arg} -y tests/skyserve/failures/probing.yaml'
+                    f'sky serve update {name} --infra {generic_cloud} {resource_arg} -y tests/skyserve/failures/probing.yaml'
                 ),
                 f's=$(sky serve status {name}); '
                 # Wait for replica to be ready.
@@ -1012,7 +1012,7 @@ def test_skyserve_https(generic_cloud: str):
         test = smoke_tests_utils.Test(
             'test-skyserve-https',
             [
-                f'sky serve up -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --cloud {generic_cloud} -y tests/skyserve/https/service.yaml '
+                f'sky serve up -n {name} {smoke_tests_utils.LOW_RESOURCE_ARG} --infra {generic_cloud} -y tests/skyserve/https/service.yaml '
                 f'--env TLS_KEYFILE_ENV_VAR={keyfile} --env TLS_CERTFILE_ENV_VAR={certfile}',
                 _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=1),
                 f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
@@ -1043,7 +1043,7 @@ def test_skyserve_multi_ports(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'test-skyserve-multi-ports',
         [
-            f'sky serve up -n {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/multi_ports.yaml',
+            f'sky serve up -n {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} -y tests/skyserve/multi_ports.yaml',
             _SERVE_WAIT_UNTIL_READY.format(name=name, replica_num=1),
             f'{_SERVE_ENDPOINT_WAIT.format(name=name)}; '
             'curl $replica_endpoint | grep "Hi, SkyPilot here"; '
@@ -1069,7 +1069,7 @@ def test_user_dependencies(generic_cloud: str):
     test = smoke_tests_utils.Test(
         'user-dependencies',
         [
-            f'sky launch -y -c {name} --cloud {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "pip install ray>2.11; ray start --head"',
+            f'sky launch -y -c {name} --infra {generic_cloud} {smoke_tests_utils.LOW_RESOURCE_ARG} "pip install ray>2.11; ray start --head"',
             f'sky logs {name} 1 --status',
             f'sky exec {name} "echo hi"',
             f'sky logs {name} 2 --status',
diff --git a/tests/stress/mountedstorage/mount_stress.yaml b/tests/stress/mountedstorage/mount_stress.yaml
index 41b9f19656b..8caa3f49f1c 100644
--- a/tests/stress/mountedstorage/mount_stress.yaml
+++ b/tests/stress/mountedstorage/mount_stress.yaml
@@ -10,7 +10,7 @@
 name: stress
 
 resources:
-  cloud: aws
+  infra: aws
 
 workdir: .
 
diff --git a/tests/test_failover.py b/tests/test_failover.py
index c8159213d85..8b50115e58e 100644
--- a/tests/test_failover.py
+++ b/tests/test_failover.py
@@ -80,7 +80,7 @@ def mock_create_instances(ec2_fail_fast, cluster_name, node_config, tags,
         monkeypatch.setattr(aws_instance, '_create_instances',
                             mock_create_instances)
         task = sky.Task(run='echo hi')
-        task.set_resources(sky.Resources(sky.AWS(), instance_type='t2.micro'))
+        task.set_resources(sky.Resources(infra='aws', instance_type='t2.micro'))
 
         with unittest.mock.patch.object(
                 cloud_vm_ray_backend.FailoverCloudErrorHandlerV2,
diff --git a/tests/test_jobs.py b/tests/test_jobs.py
index a5cebd0c3d1..1ac2e76be72 100644
--- a/tests/test_jobs.py
+++ b/tests/test_jobs.py
@@ -38,10 +38,8 @@ def _mock_cluster_state(_mock_db_conn, enable_all_clouds):
         cluster_name_on_cloud='test-cluster1',
         cluster_yaml='/tmp/cluster1.yaml',
         launched_nodes=2,
-        launched_resources=sky.Resources(sky.AWS(),
-                                         instance_type='p4d.24xlarge',
-                                         region='us-east-1',
-                                         zone='us-east-1a'),
+        launched_resources=sky.Resources(infra='aws/us-east-1/us-east-1a',
+                                         instance_type='p4d.24xlarge'),
     )
     global_user_state.add_or_update_cluster(
         'test-cluster1',
@@ -53,11 +51,9 @@ def _mock_cluster_state(_mock_db_conn, enable_all_clouds):
         cluster_name_on_cloud='test-cluster2',
         cluster_yaml='/tmp/cluster2.yaml',
         launched_nodes=1,
-        launched_resources=sky.Resources(sky.GCP(),
+        launched_resources=sky.Resources(infra='gcp/us-west1/us-west1-a',
                                          instance_type='n1-highmem-64',
-                                         accelerators='V100:4',
-                                         region='us-west1',
-                                         zone='us-west1-a'),
+                                         accelerators='V100:4'),
     )
     global_user_state.add_or_update_cluster(
         'test-cluster2',
@@ -69,9 +65,8 @@ def _mock_cluster_state(_mock_db_conn, enable_all_clouds):
         cluster_name_on_cloud='test-cluster3',
         cluster_yaml='/tmp/cluster3.yaml',
         launched_nodes=1,
-        launched_resources=sky.Resources(sky.Azure(),
-                                         instance_type='Standard_D4s_v3',
-                                         region='eastus'),
+        launched_resources=sky.Resources(infra='azure/eastus',
+                                         instance_type='Standard_D4s_v3'),
     )
     global_user_state.add_or_update_cluster(
         'test-cluster3',
@@ -84,10 +79,8 @@ def _mock_cluster_state(_mock_db_conn, enable_all_clouds):
         cluster_yaml='/tmp/disk-tier1.yaml',
         launched_nodes=1,
         launched_resources=sky.Resources(
-            sky.AWS(),
+            infra='aws/us-east-1/us-east-1a',
             instance_type='m6i.2xlarge',
-            region='us-east-1',
-            zone='us-east-1a',
             disk_tier=resources_utils.DiskTier.BEST))
     global_user_state.add_or_update_cluster(
         'test-disk-tier1',
@@ -100,10 +93,8 @@ def _mock_cluster_state(_mock_db_conn, enable_all_clouds):
         cluster_yaml='/tmp/disk-tier2.yaml',
         launched_nodes=1,
         launched_resources=sky.Resources(
-            sky.GCP(),
+            infra='gcp/us-west1/us-west1-a',
             instance_type='n2-standard-8',
-            region='us-west1',
-            zone='us-west1-a',
             disk_tier=resources_utils.DiskTier.MEDIUM))
     global_user_state.add_or_update_cluster(
         'test-disk-tier2',
@@ -150,9 +141,8 @@ def test_launch_exec(self):
             sky.exec(task, cluster_name='test-cluster1', dryrun=True))
         task.set_resources(
             sky.Resources(
-                sky.AWS(),
+                infra='aws/us-east-1',
                 accelerators='A100:1',
-                region='us-east-1',
             ))
         sky.stream_and_get(
             sky.launch(task, cluster_name='test-cluster1', dryrun=True))
@@ -166,7 +156,7 @@ def test_launch_exec(self):
         sky.stream_and_get(
             sky.exec(task, cluster_name='test-cluster2', dryrun=True))
         task.set_resources(
-            sky.Resources(sky.GCP(), accelerators='V100:3', region='us-west1'))
+            sky.Resources(infra='gcp/us-west1', accelerators='V100:3'))
         sky.stream_and_get(
             sky.launch(task, cluster_name='test-cluster2', dryrun=True))
         sky.stream_and_get(
@@ -217,10 +207,10 @@ def test_launch_exec_mismatch(self):
         self._run_launch_exec_with_error(task, 'test-cluster3')
 
         # Cloud mismatch
-        task.set_resources(sky.Resources(sky.AWS(), accelerators='V100'))
+        task.set_resources(sky.Resources(infra='aws', accelerators='V100'))
         self._run_launch_exec_with_error(task, 'test-cluster2')
 
-        task.set_resources(sky.Resources(sky.GCP()))
+        task.set_resources(sky.Resources(infra='gcp'))
         self._run_launch_exec_with_error(task, 'test-cluster1')
 
         # Disk tier mismatch
diff --git a/tests/test_jobs_and_serve.py b/tests/test_jobs_and_serve.py
index 21e369e26f5..3f8d7344c21 100644
--- a/tests/test_jobs_and_serve.py
+++ b/tests/test_jobs_and_serve.py
@@ -74,9 +74,8 @@ def _mock_cluster_state(_mock_db_conn, tmp_path):
         cluster_name_on_cloud='test-cluster1',
         cluster_yaml=_generate_tmp_yaml(tmp_path, 'cluster1.yaml'),
         launched_nodes=2,
-        launched_resources=sky.Resources(sky.AWS(),
-                                         instance_type='p3.2xlarge',
-                                         region='us-east-1'),
+        launched_resources=sky.Resources(infra='aws/us-east-1',
+                                         instance_type='p3.2xlarge'),
     )
     global_user_state.add_or_update_cluster(
         'test-cluster1',
@@ -88,10 +87,9 @@ def _mock_cluster_state(_mock_db_conn, tmp_path):
         cluster_name_on_cloud='test-cluster2',
         cluster_yaml=_generate_tmp_yaml(tmp_path, 'cluster2.yaml'),
         launched_nodes=1,
-        launched_resources=sky.Resources(sky.GCP(),
+        launched_resources=sky.Resources(infra='gcp/us-west1',
                                          instance_type='a2-highgpu-4g',
-                                         accelerators={'A100': 4},
-                                         region='us-west1'),
+                                         accelerators={'A100': 4}),
     )
     global_user_state.add_or_update_cluster(
         'test-cluster2',
@@ -103,9 +101,8 @@ def _mock_cluster_state(_mock_db_conn, tmp_path):
         cluster_name_on_cloud='test-cluster3',
         cluster_yaml=_generate_tmp_yaml(tmp_path, 'cluster3.yaml'),
         launched_nodes=4,
-        launched_resources=sky.Resources(sky.Azure(),
-                                         instance_type='Standard_D4s_v3',
-                                         region='eastus'),
+        launched_resources=sky.Resources(infra='AZURE/eastus',
+                                         instance_type='Standard_D4s_v3'),
     )
     global_user_state.add_or_update_cluster(
         'test-cluster3',
@@ -121,9 +118,8 @@ def _mock_jobs_controller(_mock_db_conn, tmp_path):
         cluster_name_on_cloud=common.JOB_CONTROLLER_NAME,
         cluster_yaml=_generate_tmp_yaml(tmp_path, 'jobs_controller.yaml'),
         launched_nodes=1,
-        launched_resources=sky.Resources(sky.AWS(),
-                                         instance_type='m4.2xlarge',
-                                         region='us-west-1'),
+        launched_resources=sky.Resources(infra='aws/us-west-1',
+                                         instance_type='m4.2xlarge'),
     )
     global_user_state.add_or_update_cluster(
         common.JOB_CONTROLLER_NAME,
@@ -140,9 +136,8 @@ def _mock_serve_controller(_mock_db_conn, tmp_path):
         cluster_name_on_cloud=common.SKY_SERVE_CONTROLLER_NAME,
         cluster_yaml=yaml_path,
         launched_nodes=1,
-        launched_resources=sky.Resources(sky.AWS(),
-                                         instance_type='m4.2xlarge',
-                                         region='us-west-1'),
+        launched_resources=sky.Resources(infra='aws/us-west-1',
+                                         instance_type='m4.2xlarge'),
         stable_internal_external_ips=[('1.2.3.4', '4.3.2.1')],
         stable_ssh_ports=[22],
     )
diff --git a/tests/test_optimizer_dryruns.py b/tests/test_optimizer_dryruns.py
index 2de21695bd9..4e594025287 100644
--- a/tests/test_optimizer_dryruns.py
+++ b/tests/test_optimizer_dryruns.py
@@ -86,16 +86,16 @@ def _test_resources_launch(*resources_args,
 
 
 def test_resources_aws(enable_all_clouds):
-    _test_resources_launch(sky.AWS(), 'p3.2xlarge')
+    _test_resources_launch(infra='aws', instance_type='p3.2xlarge')
 
 
 def test_resources_azure(enable_all_clouds):
-    _test_resources_launch(sky.Azure(), 'Standard_NC24s_v3')
+    _test_resources_launch(infra='azure', instance_type='Standard_NC24s_v3')
 
 
 def test_resources_gcp(enable_all_clouds):
-    _test_resources_launch(sky.GCP(), 'n1-standard-16')
-    _test_resources_launch(sky.GCP(), 'a3-highgpu-8g')
+    _test_resources_launch(infra='gcp', instance_type='n1-standard-16')
+    _test_resources_launch(infra='gcp', instance_type='a3-highgpu-8g')
 
 
 def test_partial_cpus(enable_all_clouds):
@@ -419,20 +419,15 @@ def test_invalid_image(enable_all_clouds):
 
 
 def test_valid_image(enable_all_clouds):
-    _test_resources(cloud=sky.AWS(),
-                    region='us-east-1',
-                    image_id='ami-0868a20f5a3bf9702')
+    _test_resources(infra='aws/us-east-1', image_id='ami-0868a20f5a3bf9702')
     _test_resources(
-        cloud=sky.GCP(),
-        region='us-central1',
+        infra='gcp/us-central1',
         image_id=
-        'projects/deeplearning-platform-release/global/images/family/common-cpu-v20230126'
-    )
+        'projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20240927')
     _test_resources(
-        cloud=sky.GCP(),
+        infra='gcp',
         image_id=
-        'projects/deeplearning-platform-release/global/images/family/common-cpu-v20230126'
-    )
+        'projects/ubuntu-os-cloud/global/images/ubuntu-2204-jammy-v20240927')
 
 
 def test_parse_cpus_from_yaml():
@@ -566,9 +561,8 @@ def test_invalid_accelerators_regions(enable_all_clouds):
     task = sky.Task(run='echo hi')
     task.set_resources(
         sky.Resources(
-            sky.AWS(),
+            infra='aws/us-west-1',
             accelerators='A100:8',
-            region='us-west-1',
         ))
     with pytest.raises(exceptions.ResourcesUnavailableError) as e:
         sky.stream_and_get(
@@ -591,7 +585,7 @@ def _test_optimize_speed(resources: sky.Resources):
 def test_optimize_speed(enable_all_clouds):
     _test_optimize_speed(sky.Resources(cpus=4))
     for cloud in registry.CLOUD_REGISTRY.values():
-        _test_optimize_speed(sky.Resources(cloud, cpus='4+'))
+        _test_optimize_speed(sky.Resources(infra=str(cloud), cpus='4+'))
     _test_optimize_speed(sky.Resources(cpus='4+', memory='4+'))
     _test_optimize_speed(
         sky.Resources(cpus='4+', memory='4+', accelerators='V100:1'))
diff --git a/tests/test_optimizer_random_dag.py b/tests/test_optimizer_random_dag.py
index 1a848097ab7..8efaaf098e4 100644
--- a/tests/test_optimizer_random_dag.py
+++ b/tests/test_optimizer_random_dag.py
@@ -83,7 +83,7 @@ def generate_random_dag(
                     if 'tpu' in candidate.accelerator_name:
                         instance_type = 'TPU-VM'
                 resources = sky.Resources(
-                    cloud=registry.CLOUD_REGISTRY.from_str(candidate.cloud),
+                    infra=candidate.cloud,
                     instance_type=instance_type,
                     accelerators={
                         candidate.accelerator_name: candidate.accelerator_count
diff --git a/tests/test_yamls/failed_setup_pipeline.yaml b/tests/test_yamls/failed_setup_pipeline.yaml
index 81e5f2bde34..3d4b3885b18 100644
--- a/tests/test_yamls/failed_setup_pipeline.yaml
+++ b/tests/test_yamls/failed_setup_pipeline.yaml
@@ -9,8 +9,8 @@ resources:
   cpus: 2
   memory: 4+
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 
 setup: |
@@ -27,8 +27,8 @@ resources:
   cpus: 2
   memory: 4+
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 
 setup: |
@@ -47,8 +47,8 @@ resources:
   cpus: 2
   memory: 4+
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 setup: |
   echo setup for eval
@@ -67,8 +67,8 @@ resources:
   cpus: 2
   memory: 4+
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 setup: |
   echo setup for eval
diff --git a/tests/test_yamls/gcp_per_region_images.yaml b/tests/test_yamls/gcp_per_region_images.yaml
index db07061d5d9..8d309ca8b8f 100644
--- a/tests/test_yamls/gcp_per_region_images.yaml
+++ b/tests/test_yamls/gcp_per_region_images.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: gcp
+  infra: gcp
   image_id: 
     us-central1: skypilot:cpu-debian-10
     us-west3: projects/ubuntu-os-cloud/global/images/ubuntu-1804-bionic-v20230112
diff --git a/tests/test_yamls/minimal_test_quick_tests_core.yaml b/tests/test_yamls/minimal_test_quick_tests_core.yaml
index 15857e972dd..9159f22ad00 100644
--- a/tests/test_yamls/minimal_test_quick_tests_core.yaml
+++ b/tests/test_yamls/minimal_test_quick_tests_core.yaml
@@ -1,5 +1,5 @@
 resources:
-  cloud: aws
+  infra: aws
   instance_type: t3.small
 
 file_mounts:
diff --git a/tests/test_yamls/pipeline.yaml b/tests/test_yamls/pipeline.yaml
index 3f3e0b1e563..14514c6c736 100644
--- a/tests/test_yamls/pipeline.yaml
+++ b/tests/test_yamls/pipeline.yaml
@@ -8,8 +8,8 @@ resources:
   memory: 4+
   use_spot: true
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 setup: |
   echo setup for train
@@ -27,8 +27,8 @@ resources:
   cpus: 2+
   memory: 4+
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 setup: |
   echo setup for train
diff --git a/tests/test_yamls/pipeline_aws.yaml b/tests/test_yamls/pipeline_aws.yaml
index fe074f74128..f11afae5bbd 100644
--- a/tests/test_yamls/pipeline_aws.yaml
+++ b/tests/test_yamls/pipeline_aws.yaml
@@ -4,8 +4,7 @@ name: pipeline
 name: a
 
 resources:
-  cloud: aws
-  region: us-east-2
+  infra: aws/us-east-2
   cpus: 2+
   memory: 4+
 
@@ -21,7 +20,7 @@ run: |
 name: b
 
 resources:
-  cloud: aws
+  infra: aws
   cpus: 2+
   memory: 4+
 
@@ -39,7 +38,7 @@ run: |
 name: eval1
 
 resources:
-  cloud: aws
+  infra: aws
   cpus: 2+
   memory: 4+
 
@@ -57,7 +56,7 @@ run: |
 name: eval2
 
 resources:
-  cloud: aws
+  infra: aws
   cpus: 2+
   memory: 4+
 
diff --git a/tests/test_yamls/pipeline_gcp.yaml b/tests/test_yamls/pipeline_gcp.yaml
index c32b423a171..5d3cb1ba142 100644
--- a/tests/test_yamls/pipeline_gcp.yaml
+++ b/tests/test_yamls/pipeline_gcp.yaml
@@ -4,8 +4,7 @@ name: pipeline
 name: a
 
 resources:
-  cloud: gcp
-  zone: us-east4-b
+  infra: gcp/*/us-east4-b
   cpus: 2+
   memory: 4+
 
@@ -21,7 +20,7 @@ run: |
 name: b
 
 resources:
-  cloud: gcp
+  infra: gcp
   cpus: 2+
   memory: 4+
 
@@ -39,7 +38,7 @@ run: |
 name: eval1
 
 resources:
-  cloud: gcp
+  infra: gcp
   cpus: 2+
   memory: 4+
 
@@ -57,7 +56,7 @@ run: |
 name: eval2
 
 resources:
-  cloud: gcp
+  infra: gcp
   cpus: 2+
   memory: 4+
 
diff --git a/tests/test_yamls/test_custom_image.yaml b/tests/test_yamls/test_custom_image.yaml
index 2b304c73bca..6479d25fb65 100644
--- a/tests/test_yamls/test_custom_image.yaml
+++ b/tests/test_yamls/test_custom_image.yaml
@@ -1,6 +1,5 @@
 resources:
-  cloud: aws
-  region: us-east-2
+  infra: aws/us-east-2
   # Nvidia image from
   # https://aws.amazon.com/marketplace/pp/prodview-rf7na2b2ttvdg
   image_id: ami-062ddd90fb6f8267a
diff --git a/tests/test_yamls/test_multiple_accelerators_unordered_with_default.yaml b/tests/test_yamls/test_multiple_accelerators_unordered_with_default.yaml
index f6e143d8378..233ba8e4caf 100644
--- a/tests/test_yamls/test_multiple_accelerators_unordered_with_default.yaml
+++ b/tests/test_yamls/test_multiple_accelerators_unordered_with_default.yaml
@@ -4,8 +4,8 @@ resources:
   use_spot: true
   accelerators: {'A100:1', 'T4:1', 'V100:1'}
   any_of:
-    - cloud: aws
-    - cloud: gcp
+    - infra: aws
+    - infra: gcp
 
 run: |
   nvidia-smi
diff --git a/tests/test_yamls/test_multiple_resources.yaml b/tests/test_yamls/test_multiple_resources.yaml
index 37c0c25e867..6771c52a70c 100644
--- a/tests/test_yamls/test_multiple_resources.yaml
+++ b/tests/test_yamls/test_multiple_resources.yaml
@@ -2,12 +2,11 @@ name: multi-resources
 
 resources:
   any_of:
-    - cloud: aws
-      region: us-east-1
+    - infra: aws/us-east-1
       accelerators: A100:8
-    - cloud: gcp
+    - infra: gcp
       accelerators: T4:4
-    - cloud: aws
+    - infra: aws
 
 run:
-  echo hi
\ No newline at end of file
+  echo hi
diff --git a/tests/unit_tests/test_controller_utils.py b/tests/unit_tests/test_controller_utils.py
index d3704afcd78..54050be9be7 100644
--- a/tests/unit_tests/test_controller_utils.py
+++ b/tests/unit_tests/test_controller_utils.py
@@ -7,6 +7,7 @@
 from sky.jobs import constants as managed_job_constants
 from sky.serve import constants as serve_constants
 from sky.utils import controller_utils
+from sky.utils import registry
 
 _DEFAULT_AUTOSTOP = {
     'down': False,
@@ -73,21 +74,17 @@ def get_custom_controller_resources(keys, default):
 
 
 def _check_controller_resources(
-        controller_resources: Set[sky.Resources],
-        expected_combinations: Set[Tuple[Optional[str], Optional[str],
-                                         Optional[str]]],
+        controller_resources: Set[sky.Resources], expected_infra_list: Set[str],
         default_controller_resources: Dict[str, Any]) -> None:
     """Helper function to check that the controller resources match the
     expected combinations."""
     for r in controller_resources:
         config = r.to_yaml_config()
-        cloud = config.pop('cloud')
-        region = config.pop('region', None)
-        zone = config.pop('zone', None)
-        assert (cloud, region, zone) in expected_combinations
-        expected_combinations.remove((cloud, region, zone))
+        infra = config.pop('infra')
+        assert infra in expected_infra_list
+        expected_infra_list.remove(infra)
         assert config == default_controller_resources, config
-    assert not expected_combinations
+    assert not expected_infra_list
 
 
 @pytest.mark.parametrize(('controller_type', 'default_controller_resources'), [
@@ -107,28 +104,23 @@ def test_get_controller_resources_with_task_resources(
     # 1. All resources has cloud specified. All of them
     # could host controllers. Return a set, each item has
     # one cloud specified plus the default resources.
-    all_clouds = {sky.AWS(), sky.GCP(), sky.Azure()}
-    expected_combinations = {(str(c), None, None) for c in all_clouds}
+    all_clouds = {'aws', 'gcp', 'azure'}
+    expected_infra_set = all_clouds
     controller_resources = controller_utils.get_controller_resources(
         controller=controller_utils.Controllers.from_type(controller_type),
-        task_resources=[sky.Resources(cloud=c) for c in all_clouds])
-    _check_controller_resources(controller_resources, expected_combinations,
+        task_resources=[sky.Resources(infra=c) for c in all_clouds])
+    _check_controller_resources(controller_resources, expected_infra_set,
                                 default_controller_resources)
 
     # 2. All resources has cloud specified. Some of them
     # could NOT host controllers. Return a set, only
     # containing those could host controllers.
     all_clouds = {
-        sky.AWS(),
-        sky.GCP(),
-        sky.Azure(),
-        sky.Fluidstack(),
-        sky.Kubernetes(),
-        sky.Lambda(),
-        sky.RunPod()
+        'aws', 'gcp', 'azure', 'fluidstack', 'kubernetes', 'lambda', 'runpod'
     }
 
-    def _could_host_controllers(cloud: sky.clouds.Cloud) -> bool:
+    def _could_host_controllers(cloud_str: str) -> bool:
+        cloud = registry.CLOUD_REGISTRY.from_str(cloud_str)
         try:
             cloud.check_features_are_supported(
                 sky.Resources(),
@@ -137,13 +129,11 @@ def _could_host_controllers(cloud: sky.clouds.Cloud) -> bool:
             return False
         return True
 
-    expected_combinations = {
-        (str(c), None, None) for c in all_clouds if _could_host_controllers(c)
-    }
+    expected_infra_set = {c for c in all_clouds if _could_host_controllers(c)}
     controller_resources = controller_utils.get_controller_resources(
         controller=controller_utils.Controllers.from_type(controller_type),
-        task_resources=[sky.Resources(cloud=c) for c in all_clouds])
-    _check_controller_resources(controller_resources, expected_combinations,
+        task_resources=[sky.Resources(infra=c) for c in all_clouds])
+    _check_controller_resources(controller_resources, expected_infra_set,
                                 default_controller_resources)
 
     # 3. Some resources does not have cloud specified.
@@ -152,7 +142,7 @@ def _could_host_controllers(cloud: sky.clouds.Cloud) -> bool:
         controller=controller_utils.Controllers.from_type(controller_type),
         task_resources=[
             sky.Resources(accelerators='L4'),
-            sky.Resources(cloud=sky.RunPod(), accelerators='A40'),
+            sky.Resources(infra='runpod', accelerators='A40'),
         ])
     assert len(controller_resources) == 1
     config = list(controller_resources)[0].to_yaml_config()
@@ -170,16 +160,18 @@ def _could_host_controllers(cloud: sky.clouds.Cloud) -> bool:
                       zone='us-central1-a'),
         sky.Resources(cloud=sky.GCP(),
                       region='europe-west1',
-                      zone='europe-west1-b')
+                      zone='europe-west1-b'),
     ]
-    expected_combinations = {('AWS', 'us-east-1', 'us-east-1a'),
-                             ('AWS', 'ap-south-1', 'ap-south-1b'),
-                             ('GCP', 'us-central1', 'us-central1-a'),
-                             ('GCP', 'europe-west1', 'europe-west1-b')}
+    expected_infra_set = {
+        'aws/us-east-1/us-east-1a',
+        'aws/ap-south-1/ap-south-1b',
+        'gcp/us-central1/us-central1-a',
+        'gcp/europe-west1/europe-west1-b',
+    }
     controller_resources = controller_utils.get_controller_resources(
         controller=controller_utils.Controllers.from_type(controller_type),
         task_resources=all_cloud_regions_zones)
-    _check_controller_resources(controller_resources, expected_combinations,
+    _check_controller_resources(controller_resources, expected_infra_set,
                                 default_controller_resources)
 
     # 5. Clouds and regions are specified, but zones are partially specified.
@@ -190,17 +182,15 @@ def _could_host_controllers(cloud: sky.clouds.Cloud) -> bool:
     controller_resources = controller_utils.get_controller_resources(
         controller=controller_utils.Controllers.from_type(controller_type),
         task_resources=[
-            sky.Resources(cloud=sky.AWS(), region='us-west-2'),
-            sky.Resources(cloud=sky.AWS(),
-                          region='us-west-2',
-                          zone='us-west-2b'),
-            sky.Resources(cloud=sky.GCP(),
-                          region='us-central1',
-                          zone='us-central1-a')
+            sky.Resources(infra='aws/us-west-2'),
+            sky.Resources(infra='aws/us-west-2/us-west-2b'),
+            sky.Resources(infra='gcp/us-central1/us-central1-a')
         ])
-    expected_combinations = {('AWS', 'us-west-2', None),
-                             ('GCP', 'us-central1', 'us-central1-a')}
-    _check_controller_resources(controller_resources, expected_combinations,
+    expected_infra_set = {
+        'aws/us-west-2',
+        'gcp/us-central1/us-central1-a',
+    }
+    _check_controller_resources(controller_resources, expected_infra_set,
                                 default_controller_resources)
 
     # 6. Mixed case: Some resources have clouds and regions or zones, others do
@@ -219,11 +209,11 @@ def _could_host_controllers(cloud: sky.clouds.Cloud) -> bool:
             sky.Resources(cloud=sky.AWS(), region='ap-south-1'),
             sky.Resources(cloud=sky.Azure()),
         ])
-    expected_combinations = {
-        ('AWS', 'eu-north-1', None),
-        ('AWS', 'ap-south-1', None),
-        ('GCP', None, None),
-        ('Azure', None, None),
+    expected_infra_set = {
+        'aws/eu-north-1',
+        'aws/ap-south-1',
+        'gcp',
+        'azure',
     }
-    _check_controller_resources(controller_resources, expected_combinations,
+    _check_controller_resources(controller_resources, expected_infra_set,
                                 default_controller_resources)
diff --git a/tests/unit_tests/test_resources.py b/tests/unit_tests/test_resources.py
index b79211c9a9f..4c2a0bc5039 100644
--- a/tests/unit_tests/test_resources.py
+++ b/tests/unit_tests/test_resources.py
@@ -50,8 +50,9 @@ def _run_label_test(allowed_labels: Dict[str, str],
         r = Resources(cloud=cloud, labels=l)
         with pytest.raises(ValueError):
             r.validate()
-            assert False, (f'Resources were initialized with '
-                           f'invalid label {invalid_label}={value}')
+            assert False, (f'Resources {r.to_yaml_config()} were initialized '
+                           f'with invalid label {invalid_label}={value} but no '
+                           'error was raised.')
 
 
 def test_gcp_labels_resources():
@@ -208,3 +209,283 @@ def test_aws_make_deploy_variables(*mocks) -> None:
                                             dryrun=True)
     assert config == expected_config, ('unexpected resource '
                                        'variables generated')
+
+
+@pytest.mark.parametrize(['resources_kwargs', 'expected_yaml_config'], [
+    ({
+        'infra': '*/*/us-east-1b',
+        'accelerators': 'A10'
+    }, {
+        'infra': '*/*/us-east-1b',
+        'accelerators': {
+            'A10': 1
+        },
+        'disk_size': 256,
+    }),
+    ({
+        'infra': 'gcp/*/us-east1-b',
+        'accelerators': 'A10:8',
+        'labels': {
+            'key': 'value'
+        }
+    }, {
+        'infra': 'gcp/*/us-east1-b',
+        'accelerators': {
+            'A10': 8
+        },
+        'labels': {
+            'key': 'value'
+        },
+        'disk_size': 256,
+    }),
+])
+def test_to_yaml_and_load(resources_kwargs, expected_yaml_config):
+    r = Resources(**resources_kwargs)
+    yaml_config = r.to_yaml_config()
+    assert yaml_config == expected_yaml_config
+
+    loaded_r = list(Resources.from_yaml_config(yaml_config))[0]
+    assert loaded_r.cloud == r.cloud
+    assert loaded_r.region == r.region
+    assert loaded_r.zone == r.zone
+    original_accelerators = r.accelerators
+    assert loaded_r.accelerators == original_accelerators
+    assert original_accelerators == r.accelerators
+    assert loaded_r.labels == r.labels
+
+
+def test_resources_any_of():
+    """Test Resources creation with any_of option."""
+    # Test any_of with different resources options
+    config = {
+        'any_of': [
+            {
+                'cpus': 8,
+                'memory': 16
+            },
+            {
+                'cpus': 4,
+                'memory': 32
+            },
+            {
+                'accelerators': 'V100:1'
+            },
+        ]
+    }
+    resources_set = Resources.from_yaml_config(config)
+
+    # Verify it returns a set of resources
+    assert isinstance(resources_set, set)
+    assert len(resources_set) == 3
+
+    # Validate the resources options are correctly created
+    resources_list = list(resources_set)
+
+    # Find resources by properties (order may not be preserved)
+    r_cpus8 = next((r for r in resources_list if r.cpus == '8'), None)
+    r_cpus4 = next((r for r in resources_list if r.cpus == '4'), None)
+    r_gpu = next((r for r in resources_list if r.accelerators is not None),
+                 None)
+
+    assert r_cpus8 is not None
+    assert r_cpus8.memory == '16'
+
+    assert r_cpus4 is not None
+    assert r_cpus4.memory == '32'
+
+    assert r_gpu is not None
+    assert r_gpu.accelerators == {'V100': 1}
+
+
+def test_resources_ordered():
+    """Test Resources creation with ordered option."""
+    # Test ordered with different resources options
+    config = {
+        'ordered': [
+            {
+                'infra': 'gcp',
+                'accelerators': 'A100:8'
+            },
+            {
+                'infra': 'aws',
+                'accelerators': 'V100:8'
+            },
+            {
+                'accelerators': 'T4:8'
+            },
+        ]
+    }
+    resources_list = Resources.from_yaml_config(config)
+
+    # Verify it returns a list of resources
+    assert isinstance(resources_list, list)
+    assert len(resources_list) == 3
+
+    # Ordered resources should preserve order
+    assert resources_list[0].infra.cloud.lower() == 'gcp'
+    assert resources_list[0].accelerators == {'A100': 8}
+
+    assert resources_list[1].infra.cloud.lower() == 'aws'
+    assert resources_list[1].accelerators == {'V100': 8}
+
+    assert resources_list[2].accelerators == {'T4': 8}
+
+
+def test_resources_any_of_spot_flag():
+    """Test Resources with any_of option including spot flag variations."""
+    config = {
+        'accelerators': 'A100:8',
+        'any_of': [{
+            'use_spot': True
+        }, {
+            'use_spot': False
+        }]
+    }
+    resources_set = Resources.from_yaml_config(config)
+
+    # Verify it returns a set of resources
+    assert isinstance(resources_set, set)
+    assert len(resources_set) == 2
+
+    # Find spot and on-demand resources
+    resources_list = list(resources_set)
+    r_spot = next((r for r in resources_list if r.use_spot), None)
+    r_ondemand = next((r for r in resources_list if not r.use_spot), None)
+
+    assert r_spot is not None
+    assert r_spot.accelerators == {'A100': 8}
+    assert r_spot.use_spot is True
+
+    assert r_ondemand is not None
+    assert r_ondemand.accelerators == {'A100': 8}
+    assert r_ondemand.use_spot is False
+
+
+def test_resources_ordered_preference():
+    """Test Resources creation with ordered preference correctly preserves order."""
+    config = {
+        'ordered': [
+            {
+                'infra': 'aws/us-east-1',
+                'accelerators': 'A100:8'
+            },
+            {
+                'infra': 'gcp/us-central1',
+                'accelerators': 'A100:8'
+            },
+            {
+                'infra': 'azure/eastus',
+                'accelerators': 'A100:8'
+            },
+        ]
+    }
+    resources_list = Resources.from_yaml_config(config)
+
+    # Verify order matches the input order
+    assert resources_list[0].infra.cloud.lower() == 'aws'
+    assert resources_list[0].infra.region == 'us-east-1'
+
+    assert resources_list[1].infra.cloud.lower() == 'gcp'
+    assert resources_list[1].infra.region == 'us-central1'
+
+    assert resources_list[2].infra.cloud.lower() == 'azure'
+    assert resources_list[2].infra.region == 'eastus'
+
+
+def test_resources_any_of_ordered_exclusive():
+    """Test that Resources raises ValueError if both any_of and ordered are specified."""
+    config = {'any_of': [{'cpus': 8}], 'ordered': [{'cpus': 4}]}
+
+    # Should raise ValueError because both any_of and ordered are specified
+    with pytest.raises(ValueError,
+                       match='Cannot specify both "any_of" and "ordered"'):
+        Resources.from_yaml_config(config)
+
+
+def test_resources_any_of_with_base_infra():
+    """Test Resources creation with any_of option and base infra."""
+    # Test any_of with base infra and additional infra specifications
+    config = {
+        'infra': 'aws',  # Base infra
+        'cpus': 8,
+        'any_of': [
+            {
+                'infra': 'aws/us-east-1'
+            },  # Override with specific region
+            {
+                'infra': 'aws/us-west-2'
+            },  # Different region
+            {
+                'infra': 'gcp/us-central1'
+            },  # Different cloud
+        ]
+    }
+    resources_set = Resources.from_yaml_config(config)
+
+    # Verify it returns a set of resources
+    assert isinstance(resources_set, set)
+    assert len(resources_set) == 3
+
+    # Validate the resources are correctly created with proper infra
+    resources_list = list(resources_set)
+
+    # All resources should have cpus=8 from the base config
+    for r in resources_list:
+        assert r.cpus == '8'
+
+    # Find resources by infra properties
+    r_east = next((r for r in resources_list if r.infra.region == 'us-east-1'),
+                  None)
+    r_west = next((r for r in resources_list if r.infra.region == 'us-west-2'),
+                  None)
+    r_gcp = next((r for r in resources_list if r.infra.cloud.lower() == 'gcp'),
+                 None)
+
+    assert r_east is not None
+    assert str(r_east.cloud).lower() == 'aws'
+
+    assert r_west is not None
+    assert str(r_west.cloud).lower() == 'aws'
+
+    assert r_gcp is not None
+    assert r_gcp.infra.region == 'us-central1'
+
+
+def test_resources_ordered_with_base_infra():
+    """Test Resources creation with ordered option and base infra."""
+    # Test ordered with base infra and additional infra specifications
+    config = {
+        'infra': 'azure',  # Base infra
+        'accelerators': 'A100:8',  # Base accelerator
+        'ordered': [
+            {
+                'infra': 'gcp/us-central1'
+            },  # Specific region in same cloud
+            {
+                'infra': 'aws/us-east-1'
+            },  # Different cloud
+            {
+                'accelerators': 'T4:8'
+            },  # Another cloud
+        ]
+    }
+    resources_list = Resources.from_yaml_config(config)
+
+    # Verify it returns a list of resources with right length
+    assert isinstance(resources_list, list)
+    assert len(resources_list) == 3
+
+    # All resources should have A100:8 from the base config
+    assert resources_list[0].accelerators == {'A100': 8}
+    assert resources_list[1].accelerators == {'A100': 8}
+    assert resources_list[2].accelerators == {'T4': 8}
+
+    # Ordered resources should preserve order and have correct infra
+    assert str(resources_list[0].cloud).lower() == 'gcp'
+    assert resources_list[0].region == 'us-central1'
+
+    assert str(resources_list[1].cloud).lower() == 'aws'
+    assert resources_list[1].region == 'us-east-1'
+
+    assert str(resources_list[2].cloud).lower() == 'azure'
+    assert resources_list[2].region is None
diff --git a/tests/unit_tests/test_sky/utils/test_cli_utils.py b/tests/unit_tests/test_sky/utils/test_cli_utils.py
new file mode 100644
index 00000000000..c1c302e634e
--- /dev/null
+++ b/tests/unit_tests/test_sky/utils/test_cli_utils.py
@@ -0,0 +1,378 @@
+"""Tests for CLI utilities.
+
+This module contains tests for the CLI utilities in sky.utils.cli_utils.
+"""
+import time
+
+import pytest
+
+import sky
+from sky import backends
+from sky.resources import Resources
+from sky.utils import status_lib
+from sky.utils.cli_utils import status_utils
+
+
+def test_status_table_format():
+    """Test the status table format."""
+    # Test AWS case
+    mock_resources = Resources(infra='aws/us-east-1',
+                               instance_type='m6i.2xlarge')
+    mock_handle = backends.CloudVmRayResourceHandle(
+        cluster_name='test-cluster',
+        cluster_name_on_cloud='test-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=1,
+        launched_resources=mock_resources)
+    mock_record = {
+        'name': 'test-cluster',
+        'handle': mock_handle,
+        'launched_at': int(time.time()) - 3600,  # 1 hour ago
+        'status': status_lib.ClusterStatus.UP,
+        'autostop': 300,  # 5 minutes
+        'to_down': False,
+    }
+
+    # Test the infra format
+    infra_str = status_utils._get_infra(mock_record)
+    assert infra_str == 'AWS (us-east-1)'
+
+    # Test the resources format
+    resources_str = status_utils._get_resources(mock_record)
+    assert resources_str == '1x(cpus=8, mem=32, type=m6i.2xlarge, ...)'
+
+    # Test Kubernetes case
+    mock_k8s_resources = Resources(infra='k8s/my-ctx', cpus='2+', memory='4+')
+    mock_k8s_handle = backends.CloudVmRayResourceHandle(
+        cluster_name='test-k8s-cluster',
+        cluster_name_on_cloud='test-k8s-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=2,
+        launched_resources=mock_k8s_resources)
+    mock_k8s_record = {
+        'name': 'test-k8s-cluster',
+        'handle': mock_k8s_handle,
+        'launched_at': int(time.time()) - 3600,  # 1 hour ago
+        'status': status_lib.ClusterStatus.UP,
+        'autostop': -1,  # No autostop
+        'to_down': False,
+        'resources_str': '2x (...)',
+    }
+
+    # Test K8S infra format
+    k8s_infra_str = status_utils._get_infra(mock_k8s_record)
+    assert k8s_infra_str == 'Kubernetes (my-ctx)'
+
+    # Test K8S resources format
+    k8s_resources_str = status_utils._get_resources(mock_k8s_record)
+    assert k8s_resources_str == '2x (...)'
+
+    # For test purposes, override _get_resources to avoid trying to call
+    # resources_utils.get_readable_resources_repr on a Resources object
+    orig_get_resources = status_utils._get_resources
+
+    def mock_get_resources(cluster_record, truncate=True):
+        return cluster_record.get('resources_str', '-')
+
+    status_utils._get_resources = mock_get_resources
+
+    try:
+        # Test SSH case
+        mock_ssh_handle = backends.CloudVmRayResourceHandle(
+            cluster_name='test-ssh-cluster',
+            cluster_name_on_cloud='test-ssh-cluster-cloud',
+            cluster_yaml=None,
+            launched_nodes=1,
+            launched_resources=None)
+        mock_ssh_record = {
+            'name': 'test-ssh-cluster',
+            'handle': mock_ssh_handle,
+            'launched_at': int(time.time()) - 3600,  # 1 hour ago
+            'status': status_lib.ClusterStatus.UP,
+            'autostop': -1,  # No autostop
+            'to_down': False,
+            'resources_str': '1x (...)',
+            'infra': 'SSH/my-tobi-box',
+        }
+
+        # Test SSH infra format
+        ssh_infra_str = status_utils._get_infra(mock_ssh_record)
+        assert ssh_infra_str == 'SSH/my-tobi-box'
+
+        # Test SSH resources format
+        ssh_resources_str = status_utils._get_resources(mock_ssh_record)
+        assert ssh_resources_str == '1x (...)'
+    finally:
+        # Restore original function
+        status_utils._get_resources = orig_get_resources
+
+
+def test_show_status_table():
+    """Test the full status table output."""
+    mock_resources = Resources(infra='aws/us-east-1',
+                               instance_type='m6i.2xlarge')
+    mock_handle = backends.CloudVmRayResourceHandle(
+        cluster_name='test-cluster',
+        cluster_name_on_cloud='test-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=1,
+        launched_resources=mock_resources)
+
+    # Test different cluster statuses
+    statuses = [
+        status_lib.ClusterStatus.UP,
+        status_lib.ClusterStatus.INIT,
+        status_lib.ClusterStatus.STOPPED,
+    ]
+
+    for status in statuses:
+        mock_record = {
+            'name': 'test-cluster',
+            'handle': mock_handle,
+            'launched_at': int(time.time()) - 3600,  # 1 hour ago
+            'status': status,
+            'autostop': 300,  # 5 minutes
+            'to_down': False,
+            'last_use': 'sky launch test.yaml',
+            'user_name': 'test_user',
+            'user_hash': 'abc123',
+            'head_ip': '1.2.3.4',
+            'resources_str': '1x(cpus=8, mem=32, type=m6i.2xlarge, ...)',
+            'resources_str_full': ('1x(cpus=8, mem=32, type=m6i.2xlarge, '
+                                   'disk=50)'),
+        }
+
+        # Test basic table
+        num_pending = status_utils.show_status_table([mock_record],
+                                                     show_all=False,
+                                                     show_user=False)
+        assert num_pending == (1 if status != status_lib.ClusterStatus.STOPPED
+                               else 0)
+
+        # Test with user info
+        num_pending = status_utils.show_status_table([mock_record],
+                                                     show_all=False,
+                                                     show_user=True)
+        assert num_pending == (1 if status != status_lib.ClusterStatus.STOPPED
+                               else 0)
+
+        # Test with show_all
+        num_pending = status_utils.show_status_table([mock_record],
+                                                     show_all=True,
+                                                     show_user=True)
+        assert num_pending == (1 if status != status_lib.ClusterStatus.STOPPED
+                               else 0)
+
+        # Test with query_clusters
+        num_pending = status_utils.show_status_table(
+            [mock_record],
+            show_all=False,
+            show_user=False,
+            query_clusters=['test-cluster'])
+        assert num_pending == (1 if status != status_lib.ClusterStatus.STOPPED
+                               else 0)
+
+        # Test with non-existent query_clusters
+        num_pending = status_utils.show_status_table(
+            [mock_record],
+            show_all=False,
+            show_user=False,
+            query_clusters=['non-existent'])
+        assert num_pending == (1 if status != status_lib.ClusterStatus.STOPPED
+                               else 0)
+
+
+def test_get_command():
+    """Test command display in status table."""
+    mock_record = {
+        'last_use': 'sky launch test.yaml --env FOO=bar',
+    }
+
+    # Test normal command
+    cmd_str = status_utils._get_command(mock_record)
+    assert cmd_str == 'sky launch test.yaml --env...'
+
+    # Test command without truncation
+    cmd_str = status_utils._get_command(mock_record, truncate=False)
+    assert cmd_str == 'sky launch test.yaml --env FOO=bar'
+
+    # Test short command
+    mock_record['last_use'] = 'sky status'
+    cmd_str = status_utils._get_command(mock_record)
+    assert cmd_str == 'sky status'
+
+
+def test_get_autostop():
+    """Test autostop display in status table."""
+    mock_record = {
+        'autostop': 300,  # 5 minutes
+        'to_down': False,
+    }
+
+    # Test normal autostop
+    autostop_str = status_utils._get_autostop(mock_record)
+    assert autostop_str == '300m'
+
+    # Test autostop with to_down
+    mock_record['to_down'] = True
+    autostop_str = status_utils._get_autostop(mock_record)
+    assert autostop_str == '300m (down)'
+
+    # Test no autostop
+    mock_record['autostop'] = -1
+    autostop_str = status_utils._get_autostop(mock_record)
+    assert autostop_str == '(down)'
+
+    # Test no autostop and no to_down
+    mock_record['to_down'] = False
+    autostop_str = status_utils._get_autostop(mock_record)
+    assert autostop_str == '-'
+
+
+def test_get_resources():
+    """Test resources display in status table."""
+    mock_resources = Resources(infra='aws/us-east-1',
+                               instance_type='m6i.2xlarge')
+    mock_handle = backends.CloudVmRayResourceHandle(
+        cluster_name='test-cluster',
+        cluster_name_on_cloud='test-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=1,
+        launched_resources=mock_resources)
+    mock_record = {
+        'handle': mock_handle,
+        'resources_str': '1x(cpus=8, mem=32, type=m6i.2xlarge, ...)',
+        'resources_str_full': '1x(cpus=8, mem=32, type=m6i.2xlarge, disk=50)',
+    }
+
+    # Test normal resources
+    resources_str = status_utils._get_resources(mock_record)
+    assert resources_str == '1x(cpus=8, mem=32, type=m6i.2xlarge, ...)'
+
+    # Test full resources
+    resources_str = status_utils._get_resources(mock_record, truncate=False)
+    assert resources_str == '1x(cpus=8, mem=32, type=m6i.2xlarge, disk=50)'
+
+    # Test no resources
+    mock_record['handle'].launched_resources = None
+    resources_str = status_utils._get_resources(mock_record)
+    assert resources_str == '-'
+
+
+def test_get_resources_gpu():
+    """Test resources display for clusters with GPUs."""
+    # Test AWS with GPU resources
+    mock_resources_aws_gpu = Resources(infra='aws/us-east-1',
+                                       instance_type='p3.2xlarge',
+                                       accelerators='V100')
+    mock_handle_aws_gpu = backends.CloudVmRayResourceHandle(
+        cluster_name='test-gpu-cluster',
+        cluster_name_on_cloud='test-gpu-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=1,
+        launched_resources=mock_resources_aws_gpu)
+    mock_record_aws_gpu = {
+        'handle': mock_handle_aws_gpu,
+        'resources_str': '1x(V100:1, cpus=8, mem=61, ...)',
+        'resources_str_full': '1x(V100:1, cpus=8, mem=61, disk=50)',
+    }
+
+    # Test GPU resources
+    resources_str = status_utils._get_resources(mock_record_aws_gpu)
+    assert resources_str == '1x(V100:1, cpus=8, mem=61, ...)'
+
+    # Test full GPU resources
+    resources_str = status_utils._get_resources(mock_record_aws_gpu,
+                                                truncate=False)
+    assert resources_str == '1x(V100:1, cpus=8, mem=61, disk=50)'
+
+    # Test GCP with multiple GPUs
+    mock_resources_gcp_multi_gpu = Resources(infra='gcp/us-central1',
+                                             instance_type='a2-highgpu-4g',
+                                             accelerators='A100:4')
+    mock_handle_gcp_multi_gpu = backends.CloudVmRayResourceHandle(
+        cluster_name='test-gcp-multi-gpu',
+        cluster_name_on_cloud='test-gcp-multi-gpu-cloud',
+        cluster_yaml=None,
+        launched_nodes=2,
+        launched_resources=mock_resources_gcp_multi_gpu)
+    mock_record_gcp_multi_gpu = {
+        'handle': mock_handle_gcp_multi_gpu,
+        'resources_str': '2x(gpus=A100:4, cpus=12, mem=85, ...)',
+        'resources_str_full': '2x(gpus=A100:4, cpus=12, mem=85, disk=50)',
+    }
+
+    # Test multiple GPU resources
+    resources_str = status_utils._get_resources(mock_record_gcp_multi_gpu)
+    assert resources_str == '2x(gpus=A100:4, cpus=12, mem=85, ...)'
+
+
+def test_get_resources_kubernetes():
+    """Test resources display for Kubernetes clusters."""
+    # Test Kubernetes with CPU resources
+    mock_resources_k8s_cpu = Resources(infra='k8s/my-cluster-ctx',
+                                       cpus=4,
+                                       memory=16)
+    mock_handle_k8s_cpu = backends.CloudVmRayResourceHandle(
+        cluster_name='test-k8s-cluster',
+        cluster_name_on_cloud='test-k8s-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=1,
+        launched_resources=mock_resources_k8s_cpu)
+    mock_record_k8s_cpu = {
+        'handle': mock_handle_k8s_cpu,
+        'resources_str': '1x(cpus=4, mem=16, ...)',
+        'resources_str_full': '1x(cpus=4, mem=16, disk=50)',
+    }
+
+    # Test K8s CPU resources
+    resources_str = status_utils._get_resources(mock_record_k8s_cpu)
+    assert resources_str == '1x(cpus=4, mem=16, ...)'
+
+    # Test Kubernetes with GPU resources
+    mock_resources_k8s_gpu = Resources(infra='k8s/gpu-cluster-ctx',
+                                       cpus=8,
+                                       memory=32,
+                                       accelerators='A100:2')
+    mock_handle_k8s_gpu = backends.CloudVmRayResourceHandle(
+        cluster_name='test-k8s-gpu-cluster',
+        cluster_name_on_cloud='test-k8s-gpu-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=2,
+        launched_resources=mock_resources_k8s_gpu)
+    mock_record_k8s_gpu = {
+        'handle': mock_handle_k8s_gpu,
+        'resources_str': '2x(gpus=A100:2, cpus=8, mem=32, ...)',
+        'resources_str_full': '2x(gpus=A100:2, cpus=8, mem=32, disk=50)',
+    }
+
+    # Test K8s GPU resources
+    resources_str = status_utils._get_resources(mock_record_k8s_gpu)
+    assert resources_str == '2x(gpus=A100:2, cpus=8, mem=32, ...)'
+
+    # Test full K8s GPU resources
+    resources_str = status_utils._get_resources(mock_record_k8s_gpu,
+                                                truncate=False)
+    assert resources_str == '2x(gpus=A100:2, cpus=8, mem=32, disk=50)'
+
+    # Test K8s with TPU resources
+    mock_resources_k8s_tpu = Resources(infra='k8s/gke-tpu-cluster',
+                                       cpus=8,
+                                       memory=32,
+                                       accelerators='tpu-v4-8')
+    mock_handle_k8s_tpu = backends.CloudVmRayResourceHandle(
+        cluster_name='test-k8s-tpu-cluster',
+        cluster_name_on_cloud='test-k8s-tpu-cluster-cloud',
+        cluster_yaml=None,
+        launched_nodes=1,
+        launched_resources=mock_resources_k8s_tpu)
+    mock_record_k8s_tpu = {
+        'handle': mock_handle_k8s_tpu,
+        'resources_str': '1x(gpus=tpu-v4-8:1, cpus=8, mem=32, ...)',
+        'resources_str_full': ('1x(gpus=tpu-v4-8:1, cpus=8, mem=32, '
+                               'disk=50)'),
+    }
+
+    # Test K8s TPU resources
+    resources_str = status_utils._get_resources(mock_record_k8s_tpu)
+    assert resources_str == '1x(gpus=tpu-v4-8:1, cpus=8, mem=32, ...)'
diff --git a/tests/unit_tests/test_common_utils.py b/tests/unit_tests/test_sky/utils/test_common_utils.py
similarity index 79%
rename from tests/unit_tests/test_common_utils.py
rename to tests/unit_tests/test_sky/utils/test_common_utils.py
index f8a0a17498a..d42d908c579 100644
--- a/tests/unit_tests/test_common_utils.py
+++ b/tests/unit_tests/test_sky/utils/test_common_utils.py
@@ -9,6 +9,64 @@
 MOCKED_USER_HASH = 'ab12cd34'
 
 
+class TestTruncateLongString:
+
+    def test_no_truncation_needed(self):
+        s = "short string"
+        result = common_utils.truncate_long_string(s, 15)
+        assert result == s
+
+    def test_end_truncation(self):
+        s = "this is a very long string that needs truncation"
+        result = common_utils.truncate_long_string(s, 20)
+        assert len(result) <= 20 + 3  # +3 for '...'
+        assert result.endswith('...')
+        assert result.startswith('this is a very')
+
+    def test_middle_truncation(self):
+        s = "us-west-2-availability-zone-1"
+        result = common_utils.truncate_long_string(s, 20, truncate_middle=True)
+        assert len(result) <= 20
+        assert '...' in result
+        assert result.startswith('us-west')
+        assert result.endswith('zone-1')
+
+    def test_middle_truncation_odd_length(self):
+        s = "us-west-2-availability-zone-1"
+        result = common_utils.truncate_long_string(s, 15, truncate_middle=True)
+        assert len(result) <= 15
+        assert '...' in result
+        assert result.startswith('us-w')
+        assert result.endswith('ne-1')
+
+    def test_middle_truncation_very_short(self):
+        s = "us-west-2-availability-zone-1"
+        result = common_utils.truncate_long_string(s, 3, truncate_middle=True)
+        assert result == '...'
+
+    def test_empty_string(self):
+        assert common_utils.truncate_long_string('', 10) == ''
+
+    def test_exact_length_no_truncation(self):
+        assert common_utils.truncate_long_string(
+            'abcde', 5, truncate_middle=True) == 'abcde'
+
+    def test_one_less_than_length(self):
+        assert common_utils.truncate_long_string('abcde',
+                                                 4,
+                                                 truncate_middle=True) == 'a...'
+
+    def test_middle_truncation_even_length(self):
+        assert common_utils.truncate_long_string(
+            'abcdefghijklmnopqrstuvwxyz', 10,
+            truncate_middle=True) == 'abcd...xyz'
+
+    def test_middle_truncation_odd_max_length(self):
+        assert common_utils.truncate_long_string(
+            'abcdefghijklmnopqrstuvwxyz', 11,
+            truncate_middle=True) == 'abcd...wxyz'
+
+
 class TestCheckClusterNameIsValid:
 
     def test_check(self):
diff --git a/tests/unit_tests/test_sky/utils/test_infra_utils.py b/tests/unit_tests/test_sky/utils/test_infra_utils.py
new file mode 100644
index 00000000000..c164a24ddfc
--- /dev/null
+++ b/tests/unit_tests/test_sky/utils/test_infra_utils.py
@@ -0,0 +1,163 @@
+"""Tests for infra_utils.py"""
+import unittest
+
+from sky.utils import infra_utils
+
+
+class TestInfraUtils(unittest.TestCase):
+    """Tests for infra_utils.py"""
+
+    def test_from_str(self):
+        """Test the from_str function with various inputs."""
+        test_cases = [
+            # Format: (infra_str, expected_cloud, expected_region, expected_zone)
+            ('aws/us-east-1', 'aws', 'us-east-1', None),
+            ('aws/us-east-1/us-east-1a', 'aws', 'us-east-1', 'us-east-1a'),
+            ('gcp/us-central1', 'gcp', 'us-central1', None),
+            ('k8s/my-cluster-ctx', 'kubernetes', 'my-cluster-ctx', None),
+            ('kubernetes/my-cluster-ctx', 'kubernetes', 'my-cluster-ctx', None),
+            # Test Kubernetes context with slashes
+            ('k8s/my/cluster/ctx', 'kubernetes', 'my/cluster/ctx', None),
+            # Test AWS with empty zone
+            ('aws/us-east-1/', 'aws', 'us-east-1', None),
+            # Test with just cloud
+            ('aws', 'aws', None, None),
+            # Test with asterisk
+            ('*/us-east-1', None, 'us-east-1', None),
+            ('aws/*/us-east-1a', 'aws', None, 'us-east-1a'),
+            ('aws/*', 'aws', None, None),
+            ('*/*/us-east-1a', None, None, 'us-east-1a'),
+            (None, None, None, None),
+            ('*', None, None, None),
+            # Test case sensitivity
+            ('AWS/US-EAST-1', 'aws', 'US-EAST-1', None),
+            ('GCP/US-CENTRAL1', 'gcp', 'US-CENTRAL1', None),
+            ('K8S/MY-CLUSTER', 'kubernetes', 'MY-CLUSTER', None),
+            # Test whitespace handling
+            ('  aws/us-east-1  ', 'aws', 'us-east-1', None),
+            (' aws / us-east-1 / us-east-1a ', 'aws', 'us-east-1',
+             'us-east-1a'),
+            # Test local and lambda clouds
+            ('local', 'local', None, None),
+            ('lambda', 'lambda', None, None),
+        ]
+
+        for infra_str, expected_cloud, expected_region, expected_zone in test_cases:
+            info = infra_utils.InfraInfo.from_str(infra_str)
+            cloud_str = info.cloud
+
+            self.assertEqual(
+                cloud_str, expected_cloud,
+                f'Failed on {infra_str}: Expected cloud={expected_cloud}, got {cloud_str}'
+            )
+            self.assertEqual(
+                info.region, expected_region,
+                f'Failed on {infra_str}: Expected region={expected_region}, got {info.region}'
+            )
+            self.assertEqual(
+                info.zone, expected_zone,
+                f'Failed on {infra_str}: Expected zone={expected_zone}, got {info.zone}'
+            )
+
+    def test_from_str_errors(self):
+        """Test the from_str function with invalid inputs."""
+        error_test_cases = [
+            # Too many segments
+            'aws/us-east-1/us-east-1a/extra',
+            # Invalid format
+            'aws//us-east-1',
+            # Just slashes
+            '///',
+            # Multiple consecutive slashes
+            'aws///us-east-1',
+        ]
+
+        for infra_str in error_test_cases:
+            with self.assertRaises((ValueError, TypeError),
+                                   msg=f'Expected error for {infra_str!r}'):
+                infra_utils.InfraInfo.from_str(infra_str)
+
+    def test_to_str(self):
+        """Test the to_str function with various inputs."""
+        test_cases = [
+            # Format: (cloud, region, zone, expected)
+            ('aws', 'us-east-1', None, 'aws/us-east-1'),
+            ('aws', 'us-east-1', 'us-east-1a', 'aws/us-east-1/us-east-1a'),
+            ('gcp', 'us-central1', None, 'gcp/us-central1'),
+            ('kubernetes', 'my-cluster-ctx', None, 'kubernetes/my-cluster-ctx'),
+            # Test with slashes in Kubernetes context
+            ('kubernetes', 'my/cluster/ctx', None, 'kubernetes/my/cluster/ctx'),
+            # Test with zone in Kubernetes
+            ('kubernetes', 'my-cluster-ctx', 'some-zone',
+             'kubernetes/my-cluster-ctx/some-zone'),
+            # Test with just cloud
+            ('aws', None, None, 'aws'),
+            # Test with None cloud
+            (None, 'us-east-1', None, '*/us-east-1'),
+            # Additional test cases for simplified implementation
+            ('aws', '*', '*', 'aws'),
+            ('gcp', 'us-central1', '*', 'gcp/us-central1'),
+            ('aws', '*', 'us-east-1a', 'aws/*/us-east-1a'),
+            (None, None, None, None),
+            ('*', '*', '*', None),
+            ('*', 'us-east-1', None, '*/us-east-1'),
+            # Test case sensitivity preservation
+            ('aws', 'US-EAST-1', 'US-EAST-1A', 'aws/US-EAST-1/US-EAST-1A'),
+            # Test local and lambda clouds
+            ('local', None, None, 'local'),
+            ('lambda', 'region-name', None, 'lambda/region-name'),
+        ]
+
+        for cloud, region, zone, expected in test_cases:
+            result = infra_utils.InfraInfo(cloud, region, zone).to_str()
+            self.assertEqual(result, expected,
+                             f'Failed: Expected {expected}, got {result}')
+
+    def test_formatted_str(self):
+        """Test the formatted_str function with various inputs."""
+        test_cases = [
+            # Format: (cloud, region, zone, truncate, expected)
+            ('aws', 'us-east-1', None, True, 'aws (us-east-1)'),
+            ('aws', 'us-east-1', 'us-east-1a', True, 'aws (us-east-1a)'),
+            ('gcp', 'us-central1', None, True, 'gcp (us-central1)'),
+            ('kubernetes', 'my-cluster-ctx', None, True,
+             'kubernetes (my-cluster-ctx)'),
+            # Test with slashes in Kubernetes context
+            ('kubernetes', 'my/cluster/ctx', None, True,
+             'kubernetes (my/cluster/ctx)'),
+            # Test with just cloud
+            ('aws', None, None, True, 'aws'),
+            # Test with None cloud
+            (None, 'us-east-1', None, True, '-'),
+            # Test with long region/zone (truncation)
+            ('aws', 'us-east-1-very-long-region', None, True,
+             'aws (us-east-1-v...long-region)'),
+            ('aws', 'us-east-1-very-very-very-long-region', None, True,
+             'aws (us-east-1-v...long-region)'),
+            ('aws', 'us-east-1-very-long-region', None, False,
+             'aws (us-east-1-very-long-region)'),
+            # Test with asterisk
+            ('*', '*', '*', True, '-'),
+            ('aws', '*', '*', True, 'aws'),
+            ('aws', '*', 'us-east-1a', True, 'aws (us-east-1a)'),
+            ('*', 'us-east-1', None, True, '-'),
+            # Test truncation boundary cases
+            ('aws', 'x' * 25, None, True, 'aws (' + 'x' * 25 + ')'),
+            ('aws', 'x' * 26, None, True,
+             'aws (' + 'x' * 11 + '...' + 'x' * 11 + ')'),
+            ('aws', 'x' * 24, None, True, 'aws (' + 'x' * 24 + ')'),
+            # Test with empty strings
+            ('aws', '', None, True, 'aws'),
+            ('aws', '', '', True, 'aws'),
+            # Test local and lambda clouds
+            ('local', None, None, True, 'local'),
+            ('lambda', 'region-name', None, True, 'lambda (region-name)'),
+        ]
+
+        for cloud, region, zone, truncate, expected in test_cases:
+            result = infra_utils.InfraInfo(
+                cloud, region, zone).formatted_str(truncate=truncate)
+            self.assertEqual(
+                result, expected, f'Failed: Expected {expected}, got {result}, '
+                f'cloud={cloud}, region={region}, zone={zone}, '
+                f'truncate={truncate}')
diff --git a/tests/unit_tests/test_sky/utils/test_schemas.py b/tests/unit_tests/test_sky/utils/test_schemas.py
new file mode 100644
index 00000000000..39c5bb4f2a1
--- /dev/null
+++ b/tests/unit_tests/test_sky/utils/test_schemas.py
@@ -0,0 +1,113 @@
+"""Tests for schemas.py"""
+import unittest
+
+import jsonschema
+
+from sky.utils import schemas
+
+
+class TestResourcesSchema(unittest.TestCase):
+    """Tests for the resources schema in schemas.py"""
+
+    def test_valid_infra_configs(self):
+        """Test validation of valid infra field configs."""
+        resources_schema = schemas.get_resources_schema()
+
+        # Valid infra configurations
+        valid_infra_configs = [
+            {
+                'infra': 'aws'
+            },
+            {
+                'infra': 'gcp'
+            },
+            {
+                'infra': 'azure'
+            },
+            {
+                'infra': 'kubernetes'
+            },
+            {
+                'infra': 'aws/us-east-1'
+            },
+            {
+                'infra': 'aws/us-east-1/us-east-1a'
+            },
+            {
+                'infra': 'gcp/us-central1'
+            },
+            {
+                'infra': 'k8s/my-cluster-ctx'
+            },
+            {
+                'infra': 'kubernetes/my/complex/context/path'
+            },
+            {
+                'infra': '*'
+            },
+            {
+                'infra': '*/us-east-1'
+            },
+            {
+                'infra': '*/us-east-1/us-east-1a'
+            },
+            {
+                'infra': '*/*'
+            },
+            {
+                'infra': '*/*/us-east-1a'
+            },
+        ]
+
+        for config in valid_infra_configs:
+            # Should not raise an exception
+            jsonschema.validate(instance=config, schema=resources_schema)
+
+    def test_invalid_infra_type(self):
+        """Test validation rejects invalid infra field types."""
+        resources_schema = schemas.get_resources_schema()
+
+        # Invalid infra configurations - wrong type
+        invalid_type_config = {'infra': 123}  # Not a string
+        with self.assertRaises(jsonschema.exceptions.ValidationError):
+            jsonschema.validate(instance=invalid_type_config,
+                                schema=resources_schema)
+
+    def test_invalid_infra_format(self):
+        """Test validation rejects invalid infra field formats."""
+        resources_schema = schemas.get_resources_schema()
+
+        # Invalid formats
+        invalid_formats = [
+            {
+                'infra': 'aws/'
+            },  # Trailing slash without region
+            {
+                'infra': 'aws//us-east-1a'
+            },  # Empty region
+            {
+                'infra': '/us-east-1'
+            },  # Missing cloud
+            {
+                'infra': 'aws/us-east-1/zone/extra'
+            },  # Too many segments
+            {
+                'infra': 'invalid-cloud/us-east-1'
+            },  # Invalid cloud name
+            {
+                'infra': 'invalid-cloud'
+            },  # Invalid cloud name without region
+            {
+                'infra': '**/us-east-1'
+            },  # Multiple asterisks (invalid syntax)
+        ]
+
+        for config in invalid_formats:
+            with self.assertRaises(
+                    jsonschema.exceptions.ValidationError,
+                    msg=f"Expected '{config['infra']}' to be rejected"):
+                jsonschema.validate(instance=config, schema=resources_schema)
+
+
+if __name__ == "__main__":
+    unittest.main()


Code review comments:
diff_hunk: @@ -85,6 +85,19 @@ def _get_single_resources_schema():
             'zone': {
                 'type': 'string',
             },
+            'infra': {
+                'type': 'string',
+                'description':
+                    ('Infrastructure specification in format: '
+                     'cloud[/region[/zone]]. Use "*" as a wildcard.'),
+                # Create a pattern validator that uses a big regex to match all
+                # valid formats. This allows us to maintain JSON Schema
+                # validation while supporting all formats
+                'pattern':
+                    ('^(?:(?i:(' + '|'.join(list(service_catalog.ALL_CLOUDS)) +
+                     '))(?:/[^/]+(?:/[^/]+)?)?|\\*(?:/[^/]+(?:/[^/]+)?|/\\*'
+                     '(?:/[^/]+)?)?|(?i:k8s|kubernetes)/.+)$')
path: sky/utils/schemas.py
text: @user1:
Human interpretation for future reference:
- `cloud/x?/x?` format:
  - Cloud name
  - Optionally /region
  - Optionally /zone (must have /region)
  - (Note: technically I believe this could be simplified e.g. `(?:/[^/]){0,2}` but it's equivalent)
- `*/x/x?`
  - */region - region is required
  - Optionally /zone
- k8s/anything or kubernetes/anything
  - context name can include slashes
  - this will need to be updated to include ssh

@user1:
Review comment: `*/*` matches which seems unintentional.
Either way, the `|/\*'(?:/[^/]+)?` after `*` cloud seems completely redundant to the simpler `/[^/]`... match before

@author:
Updated the comments and fixed the schemas : )

diff_hunk: @@ -22,6 +22,6 @@
 
 with sky.Dag() as dag:
     t = sky.Task(run=run_command, setup=setup_cmd)
-    t.set_resources(sky.Resources(sky.AWS(), accelerators='V100'))
+    t.set_resources(infra='aws', accelerators='V100')
path: examples/containerized_app.py
text: @user1:
Seems wrong - Task.set_resources isn't changed in the PR?

```suggestion
    t.set_resources(sky.Resources(infra='aws', accelerators='V100'))
```

@author:
Oops, good catch! Added