Repository: grafana/terraform-provider-grafana

Issue:
Unable to Import `grafana_frontend_o11y_app` Resources
### Terraform Version

1.11.3

### Terraform Grafana Provider Version

3.22.2

### Grafana Version

Cloud

### Affected Resource(s)

* `grafana_frontend_o11y_app`

### Terraform Configuration Files

```hcl
resource "grafana_frontend_o11y_app" "grafna" {
  provider        = grafana.cloud
  stack_id        = data.grafana_cloud_stack.stack.id
  name            = "Grafana"
  allowed_origins = ["https://grafana.com"]

  settings = {
    "combineLabData" : "1"
  }
}
```

### Expected Behavior

The Docs state Import is possible, so I would expect the Resource to be Imported into Terraforms State

### Actual Behavior

It Errors, saying its not Implemeneted;

```
Error: Resource Import Not Implemented

This resource does not support import. Please contact the provider developer
for additional information.
```

### Steps to Reproduce

Run `terraform import grafana_frontend_o11y_app.grafana "STACKID:Grafana"`

### Important Factoids

[Docs](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/frontend_o11y_app) State;

```
Import is supported using the following syntax:

terraform import grafana_frontend_o11y_app.name "{{ stack_id }}:{{ name }}"
```

### References

_No response_

Hints from issue comments:
@kpelelis: I have verified the issue and can confirm import is not supported. I am working on a fix

Solution patch:
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 8be7d14a..a53a28d5 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -8,6 +8,7 @@
 /internal/resources/connections/*                   @grafana/platform-monitoring @grafana/middleware-apps
 /internal/resources/fleetmanagement/*               @grafana/platform-monitoring @grafana/fleet-management-backend
 /internal/resources/frontendo11y/*                  @grafana/platform-monitoring @grafana/frontend-o11y
+/internal/resources/k6/*                            @grafana/platform-monitoring @grafana/k6-core
 /internal/resources/machinelearning/*               @grafana/platform-monitoring @grafana/machine-learning
 /internal/resources/oncall/*                        @grafana/platform-monitoring @grafana/grafana-irm-backend
 /internal/resources/slo/*                           @grafana/platform-monitoring @grafana/slo-squad
diff --git a/.github/workflows/acc-tests.yml b/.github/workflows/acc-tests.yml
index 6d12dafc..07df9070 100644
--- a/.github/workflows/acc-tests.yml
+++ b/.github/workflows/acc-tests.yml
@@ -47,8 +47,10 @@ jobs:
         with:
           repo_secrets: |
             GRAFANA_AUTH=cloud-instance-tests:auth
+            GRAFANA_STACK_ID=cloud-instance-k6-tests:stack-id
             GRAFANA_ONCALL_ACCESS_TOKEN=cloud-instance-tests:oncall-token
             GRAFANA_SM_ACCESS_TOKEN=cloud-instance-tests:sm-token
+            GRAFANA_K6_ACCESS_TOKEN=cloud-instance-k6-tests:k6-token
             GRAFANA_SM_URL=cloud-instance-tests:sm-url
             GRAFANA_URL=cloud-instance-tests:url
             GRAFANA_CLOUD_PROVIDER_URL=cloudprovider-tests:url
diff --git a/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_job.md b/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_job.md
index 9082912a..c6aef709 100644
--- a/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_job.md
+++ b/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_job.md
@@ -83,21 +83,21 @@ data "grafana_cloud_provider_aws_cloudwatch_scrape_job" "test" {
 
 ### Required
 
-- `name` (String) The name of the CloudWatch Scrape Job. Part of the Terraform Resource ID.
+- `name` (String) The name of the AWS CloudWatch Scrape Job. Part of the Terraform Resource ID.
 - `stack_id` (String) The Stack ID of the Grafana Cloud instance. Part of the Terraform Resource ID.
 
 ### Read-Only
 
-- `aws_account_resource_id` (String) The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.
-- `custom_namespace` (Block List) Zero or more configuration blocks to configure custom namespaces for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--custom_namespace))
-- `disabled_reason` (String) When the CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.
-- `enabled` (Boolean) Whether the CloudWatch Scrape Job is enabled or not.
+- `aws_account_resource_id` (String) The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this AWS CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.
+- `custom_namespace` (Block List) Zero or more configuration blocks to configure custom namespaces for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--custom_namespace))
+- `disabled_reason` (String) When the AWS CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.
+- `enabled` (Boolean) Whether the AWS CloudWatch Scrape Job is enabled or not.
 - `export_tags` (Boolean) When enabled, AWS resource tags are exported as Prometheus labels to metrics formatted as `aws_<service_name>_info`.
 - `id` (String) The Terraform Resource ID. This has the format "{{ stack_id }}:{{ name }}".
-- `regions` (Set of String) The set of AWS region names that this CloudWatch Scrape Job is configured to scrape.
-- `regions_subset_override_used` (Boolean) When true, the `regions` attribute will be the set of regions configured in the override. When false, the `regions` attribute will be the set of regions belonging to the AWS Account resource that is associated with this CloudWatch Scrape Job.
-- `role_arn` (String) The AWS ARN of the IAM role associated with the AWS Account resource that is being used by this CloudWatch Scrape Job.
-- `service` (Block List) One or more configuration blocks to dictate what this CloudWatch Scrape Job should scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--service))
+- `regions` (Set of String) The set of AWS region names that this AWS CloudWatch Scrape Job is configured to scrape.
+- `regions_subset_override_used` (Boolean) When true, the `regions` attribute will be the set of regions configured in the override. When false, the `regions` attribute will be the set of regions belonging to the AWS Account resource that is associated with this AWS CloudWatch Scrape Job.
+- `role_arn` (String) The AWS ARN of the IAM role associated with the AWS Account resource that is being used by this AWS CloudWatch Scrape Job.
+- `service` (Block List) One or more configuration blocks to dictate what this AWS CloudWatch Scrape Job should scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--service))
 - `static_labels` (Map of String) A set of static labels to add to all metrics exported by this scrape job.
 
 <a id="nestedblock--custom_namespace"></a>
diff --git a/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_jobs.md b/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_jobs.md
index b5d2a3a1..6fb372e5 100644
--- a/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_jobs.md
+++ b/docs/data-sources/cloud_provider_aws_cloudwatch_scrape_jobs.md
@@ -39,17 +39,17 @@ data "grafana_cloud_provider_aws_cloudwatch_scrape_jobs" "test" {
 
 Read-Only:
 
-- `aws_account_resource_id` (String) The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.
-- `custom_namespace` (Block List) Zero or more configuration blocks to configure custom namespaces for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--scrape_job--custom_namespace))
-- `disabled_reason` (String) When the CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.
-- `enabled` (Boolean) Whether the CloudWatch Scrape Job is enabled or not.
+- `aws_account_resource_id` (String) The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this AWS CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.
+- `custom_namespace` (Block List) Zero or more configuration blocks to configure custom namespaces for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--scrape_job--custom_namespace))
+- `disabled_reason` (String) When the AWS CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.
+- `enabled` (Boolean) Whether the AWS CloudWatch Scrape Job is enabled or not.
 - `export_tags` (Boolean) When enabled, AWS resource tags are exported as Prometheus labels to metrics formatted as `aws_<service_name>_info`.
 - `id` (String) The Terraform Resource ID. This has the format "{{ stack_id }}:{{ name }}".
-- `name` (String) The name of the CloudWatch Scrape Job. Part of the Terraform Resource ID.
-- `regions` (Set of String) The set of AWS region names that this CloudWatch Scrape Job is configured to scrape.
-- `regions_subset_override_used` (Boolean) When true, the `regions` attribute will be the set of regions configured in the override. When false, the `regions` attribute will be the set of regions belonging to the AWS Account resource that is associated with this CloudWatch Scrape Job.
-- `role_arn` (String) The AWS ARN of the IAM role associated with the AWS Account resource that is being used by this CloudWatch Scrape Job.
-- `service` (Block List) One or more configuration blocks to dictate what this CloudWatch Scrape Job should scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--scrape_job--service))
+- `name` (String) The name of the AWS CloudWatch Scrape Job. Part of the Terraform Resource ID.
+- `regions` (Set of String) The set of AWS region names that this AWS CloudWatch Scrape Job is configured to scrape.
+- `regions_subset_override_used` (Boolean) When true, the `regions` attribute will be the set of regions configured in the override. When false, the `regions` attribute will be the set of regions belonging to the AWS Account resource that is associated with this AWS CloudWatch Scrape Job.
+- `role_arn` (String) The AWS ARN of the IAM role associated with the AWS Account resource that is being used by this AWS CloudWatch Scrape Job.
+- `service` (Block List) One or more configuration blocks to dictate what this AWS CloudWatch Scrape Job should scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--scrape_job--service))
 - `stack_id` (String) The Stack ID of the Grafana Cloud instance. Part of the Terraform Resource ID.
 - `static_labels` (Map of String) A set of static labels to add to all metrics exported by this scrape job.
 
diff --git a/docs/data-sources/k6_load_test.md b/docs/data-sources/k6_load_test.md
new file mode 100644
index 00000000..9dc5e8c4
--- /dev/null
+++ b/docs/data-sources/k6_load_test.md
@@ -0,0 +1,49 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_load_test Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves a k6 load test.
+---
+
+# grafana_k6_load_test (Data Source)
+
+Retrieves a k6 load test.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
+
+data "grafana_k6_load_test" "from_id" {
+  id = grafana_k6_load_test.test_load_test.id
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `id` (Number) Numeric identifier of the load test.
+
+### Read-Only
+
+- `baseline_test_run_id` (Number) Identifier of a baseline test run used for results comparison.
+- `created` (String) The date when the load test was created.
+- `name` (String) Human-friendly identifier of the load test.
+- `project_id` (Number) The identifier of the project this load test belongs to.
+- `script` (String) The k6 test script content.
+- `updated` (String) The date when the load test was last updated.
diff --git a/docs/data-sources/k6_load_tests.md b/docs/data-sources/k6_load_tests.md
new file mode 100644
index 00000000..03f15f16
--- /dev/null
+++ b/docs/data-sources/k6_load_tests.md
@@ -0,0 +1,94 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_load_tests Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves all k6 load tests that belong to a project.
+---
+
+# grafana_k6_load_tests (Data Source)
+
+Retrieves all k6 load tests that belong to a project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_project.load_test_project,
+  ]
+}
+
+resource "grafana_k6_load_test" "test_load_test_2" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test (2)"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+  ]
+}
+
+data "grafana_k6_load_tests" "from_project_id" {
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+    grafana_k6_load_test.test_load_test_2
+  ]
+}
+
+data "grafana_k6_load_tests" "filter_by_name" {
+  name       = "Terraform Test Load Test (2)"
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test_2,
+  ]
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `project_id` (Number) The identifier of the project the load tests belong to.
+
+### Optional
+
+- `name` (String) Human-friendly identifier of the load test.
+
+### Read-Only
+
+- `id` (Number) The identifier of the project the load tests belong to. This is set to the same as the project_id.
+- `load_tests` (List of Object) (see [below for nested schema](#nestedatt--load_tests))
+
+<a id="nestedatt--load_tests"></a>
+### Nested Schema for `load_tests`
+
+Read-Only:
+
+- `baseline_test_run_id` (Number)
+- `created` (String)
+- `id` (Number)
+- `name` (String)
+- `project_id` (Number)
+- `script` (String)
+- `updated` (String)
diff --git a/docs/data-sources/k6_project.md b/docs/data-sources/k6_project.md
new file mode 100644
index 00000000..c7de1b10
--- /dev/null
+++ b/docs/data-sources/k6_project.md
@@ -0,0 +1,41 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves a k6 project.
+---
+
+# grafana_k6_project (Data Source)
+
+Retrieves a k6 project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_project" "from_id" {
+  depends_on = [
+    grafana_k6_project.test
+  ]
+  id = grafana_k6_project.test.id
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `id` (Number) Numeric identifier of the project.
+
+### Read-Only
+
+- `created` (String) The date when the project was created.
+- `grafana_folder_uid` (String) The Grafana folder uid.
+- `is_default` (Boolean) Whether this project is the default for running tests when no explicit project identifier is provided.
+- `name` (String) Human-friendly identifier of the project.
+- `updated` (String) The date when the project was last updated.
diff --git a/docs/data-sources/k6_project_limits.md b/docs/data-sources/k6_project_limits.md
new file mode 100644
index 00000000..0f7092d0
--- /dev/null
+++ b/docs/data-sources/k6_project_limits.md
@@ -0,0 +1,38 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project_limits Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves a k6 project limits.
+---
+
+# grafana_k6_project_limits (Data Source)
+
+Retrieves a k6 project limits.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+data "grafana_k6_project_limits" "from_project_id" {
+  project_id = grafana_k6_project.test_project_limits.id
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `project_id` (Number) The identifier of the project to get limits for.
+
+### Read-Only
+
+- `duration_max_per_test` (Number) Maximum duration of a test in seconds.
+- `id` (Number) The identifier of the project limits. This is set to the same as the project_id.
+- `vu_browser_max_per_test` (Number) Maximum number of concurrent browser virtual users (VUs) used in one test.
+- `vu_max_per_test` (Number) Maximum number of concurrent virtual users (VUs) used in one test.
+- `vuh_max_per_month` (Number) Maximum amount of virtual user hours (VU/h) used per one calendar month.
diff --git a/docs/data-sources/k6_projects.md b/docs/data-sources/k6_projects.md
new file mode 100644
index 00000000..d3347524
--- /dev/null
+++ b/docs/data-sources/k6_projects.md
@@ -0,0 +1,57 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_projects Data Source - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Retrieves all k6 projects with the given name.
+---
+
+# grafana_k6_projects (Data Source)
+
+Retrieves all k6 projects with the given name.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "project" {
+  name = "Terraform Test Project"
+}
+
+resource "grafana_k6_project" "project_2" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_projects" "from_name" {
+  name = "Terraform Test Project"
+
+  depends_on = [
+    grafana_k6_project.project,
+    grafana_k6_project.project_2
+  ]
+
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Optional
+
+- `name` (String) Human-friendly identifier of the project.
+
+### Read-Only
+
+- `id` (String) Human-friendly identifier of the project. This is set to the same as name.
+- `projects` (List of Object) (see [below for nested schema](#nestedatt--projects))
+
+<a id="nestedatt--projects"></a>
+### Nested Schema for `projects`
+
+Read-Only:
+
+- `created` (String)
+- `grafana_folder_uid` (String)
+- `id` (Number)
+- `is_default` (Boolean)
+- `name` (String)
+- `updated` (String)
diff --git a/docs/index.md b/docs/index.md
index 468be594..0f05c04c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -262,6 +262,29 @@ provider "grafana" {
 }
 ```
 
+#### Import existing Frontend Observability apps into Terraform
+
+To manage an existing Frontend Observability app with Terraform, you can import it into your Terraform state.
+
+##### Before you begin
+
+You will need the following:
+
+- **Stack slug** – This is the first part of your Grafana Cloud URL. For example, in `https://mystack.grafana.net`, the stack slug is `mystack`.
+
+- **App ID** – Navigate to the app in your Grafana Cloud instance and copy the ID from the URL. It will look like this: `https://<stack-slug>.grafana.net/a/grafana-kowalski-app/apps/<app-id>`
+
+
+##### Import command
+
+Run the following command to import the app into your Terraform state:
+
+```bash
+terraform import '<stack-slug>:<app-id>' my-app
+```
+
+Replace <stack-slug> and <app-id> with the appropriate values for your environment.
+
 <!-- schema generated by tfplugindocs -->
 ## Schema
 
@@ -280,6 +303,8 @@ provider "grafana" {
 - `frontend_o11y_api_access_token` (String, Sensitive) A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.
 - `http_headers` (Map of String, Sensitive) Optional. HTTP headers mapping keys to values used for accessing the Grafana and Grafana Cloud APIs. May alternatively be set via the `GRAFANA_HTTP_HEADERS` environment variable in JSON format.
 - `insecure_skip_verify` (Boolean) Skip TLS certificate verification. May alternatively be set via the `GRAFANA_INSECURE_SKIP_VERIFY` environment variable.
+- `k6_access_token` (String, Sensitive) The k6 Cloud API token. May alternatively be set via the `GRAFANA_K6_ACCESS_TOKEN` environment variable.
+- `k6_url` (String) The k6 Cloud API url. May alternatively be set via the `GRAFANA_K6_URL` environment variable.
 - `oncall_access_token` (String, Sensitive) A Grafana OnCall access token. May alternatively be set via the `GRAFANA_ONCALL_ACCESS_TOKEN` environment variable.
 - `oncall_url` (String) An Grafana OnCall backend address. May alternatively be set via the `GRAFANA_ONCALL_URL` environment variable.
 - `org_id` (Number) The Grafana org ID, if you are using a self-hosted OSS or enterprise Grafana instance. May alternatively be set via the `GRAFANA_ORG_ID` environment variable.
diff --git a/docs/resources/apps_dashboard_dashboard_v1beta1.md b/docs/resources/apps_dashboard_dashboard_v1beta1.md
index 79b1c45e..768004a4 100644
--- a/docs/resources/apps_dashboard_dashboard_v1beta1.md
+++ b/docs/resources/apps_dashboard_dashboard_v1beta1.md
@@ -3,12 +3,16 @@
 page_title: "grafana_apps_dashboard_dashboard_v1beta1 Resource - terraform-provider-grafana"
 subcategory: "Grafana Apps"
 description: |-
-  Manages Grafana dashboards via the new Grafana App Platform API. This resource is currently EXPERIMENTAL and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+  Manages Grafana dashboards using the new Grafana APIs.
+  Official documentation https://grafana.com/docs/grafana/latest/dashboards/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis
 ---
 
 # grafana_apps_dashboard_dashboard_v1beta1 (Resource)
 
-Manages Grafana dashboards via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana dashboards using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis)
 
 
 
diff --git a/docs/resources/apps_playlist_playlist_v0alpha1.md b/docs/resources/apps_playlist_playlist_v0alpha1.md
index 80bdfb3d..b21ee941 100644
--- a/docs/resources/apps_playlist_playlist_v0alpha1.md
+++ b/docs/resources/apps_playlist_playlist_v0alpha1.md
@@ -3,12 +3,16 @@
 page_title: "grafana_apps_playlist_playlist_v0alpha1 Resource - terraform-provider-grafana"
 subcategory: "Grafana Apps"
 description: |-
-  Manages Grafana playlists via the new Grafana App Platform API. This resource is currently EXPERIMENTAL and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+  Manages Grafana playlists using the new Grafana APIs.
+  Official documentation https://grafana.com/docs/grafana/latest/dashboards/create-manage-playlists/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/apis/
 ---
 
 # grafana_apps_playlist_playlist_v0alpha1 (Resource)
 
-Manages Grafana playlists via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana playlists using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/create-manage-playlists/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/apis/)
 
 
 
diff --git a/docs/resources/cloud_provider_aws_cloudwatch_scrape_job.md b/docs/resources/cloud_provider_aws_cloudwatch_scrape_job.md
index 0ef4b1e5..3c08e58c 100644
--- a/docs/resources/cloud_provider_aws_cloudwatch_scrape_job.md
+++ b/docs/resources/cloud_provider_aws_cloudwatch_scrape_job.md
@@ -77,21 +77,21 @@ resource "grafana_cloud_provider_aws_cloudwatch_scrape_job" "test" {
 ### Required
 
 - `aws_account_resource_id` (String) The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.
-- `name` (String) The name of the CloudWatch Scrape Job. Part of the Terraform Resource ID.
+- `name` (String) The name of the AWS CloudWatch Scrape Job. Part of the Terraform Resource ID.
 - `stack_id` (String) The Stack ID of the Grafana Cloud instance. Part of the Terraform Resource ID.
 
 ### Optional
 
-- `custom_namespace` (Block List) Zero or more configuration blocks to configure custom namespaces for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--custom_namespace))
-- `enabled` (Boolean) Whether the CloudWatch Scrape Job is enabled or not.
+- `custom_namespace` (Block List) Zero or more configuration blocks to configure custom namespaces for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--custom_namespace))
+- `enabled` (Boolean) Whether the AWS CloudWatch Scrape Job is enabled or not.
 - `export_tags` (Boolean) When enabled, AWS resource tags are exported as Prometheus labels to metrics formatted as `aws_<service_name>_info`.
 - `regions_subset_override` (Set of String) A subset of the regions that are configured in the associated AWS Account resource to apply to this scrape job. If not set or empty, all of the Account resource's regions are scraped.
-- `service` (Block List) One or more configuration blocks to configure AWS services for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--service))
+- `service` (Block List) One or more configuration blocks to configure AWS services for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--service))
 - `static_labels` (Map of String) A set of static labels to add to all metrics exported by this scrape job.
 
 ### Read-Only
 
-- `disabled_reason` (String) When the CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.
+- `disabled_reason` (String) When the AWS CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.
 - `id` (String) The Terraform Resource ID. This has the format "{{ stack_id }}:{{ name }}".
 
 <a id="nestedblock--custom_namespace"></a>
diff --git a/docs/resources/cloud_provider_aws_resource_metadata_scrape_job.md b/docs/resources/cloud_provider_aws_resource_metadata_scrape_job.md
new file mode 100644
index 00000000..60467007
--- /dev/null
+++ b/docs/resources/cloud_provider_aws_resource_metadata_scrape_job.md
@@ -0,0 +1,102 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_cloud_provider_aws_resource_metadata_scrape_job Resource - terraform-provider-grafana"
+subcategory: "Cloud Provider"
+description: |-
+  
+---
+
+# grafana_cloud_provider_aws_resource_metadata_scrape_job (Resource)
+
+
+
+## Example Usage
+
+```terraform
+data "grafana_cloud_stack" "test" {
+  slug = "gcloudstacktest"
+}
+
+data "aws_iam_role" "test" {
+  name = "my-role"
+}
+
+resource "grafana_cloud_provider_aws_account" "test" {
+  stack_id = data.grafana_cloud_stack.test.id
+  role_arn = data.aws_iam_role.test.arn
+  regions = [
+    "us-east-1",
+    "us-east-2",
+    "us-west-1"
+  ]
+}
+
+resource "grafana_cloud_provider_aws_resource_metadata_scrape_job" "test" {
+  stack_id                = data.grafana_cloud_stack.test.id
+  name                    = "my-aws-resource-metadata-scrape-job"
+  aws_account_resource_id = grafana_cloud_provider_aws_account.test.resource_id
+
+  service {
+    name                    = "AWS/EC2"
+    scrape_interval_seconds = 300
+    resource_discovery_tag_filter {
+      key   = "k8s.io/cluster-autoscaler/enabled"
+      value = "true"
+    }
+  }
+
+  static_labels = {
+    "label1" = "value1"
+    "label2" = "value2"
+  }
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `aws_account_resource_id` (String) The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this Resource Metadata Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.
+- `name` (String) The name of the AWS Resource Metadata Scrape Job. Part of the Terraform Resource ID.
+- `stack_id` (String) The Stack ID of the Grafana Cloud instance. Part of the Terraform Resource ID.
+
+### Optional
+
+- `enabled` (Boolean) Whether the AWS Resource Metadata Scrape Job is enabled or not.
+- `regions_subset_override` (Set of String) A subset of the regions that are configured in the associated AWS Account resource to apply to this scrape job. If not set or empty, all of the Account resource's regions are scraped.
+- `service` (Block List) One or more configuration blocks to configure AWS services for the Resource Metadata Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--service))
+- `static_labels` (Map of String) A set of static labels to add to all metrics exported by this scrape job.
+
+### Read-Only
+
+- `disabled_reason` (String) When the AWS Resource Metadata Scrape Job is disabled, this will show the reason that it is in that state.
+- `id` (String) The Terraform Resource ID. This has the format "{{ stack_id }}:{{ name }}".
+
+<a id="nestedblock--service"></a>
+### Nested Schema for `service`
+
+Required:
+
+- `name` (String) The name of the service to scrape. See https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/aws/cloudwatch-metrics/services/ for supported services.
+
+Optional:
+
+- `resource_discovery_tag_filter` (Block List) One or more configuration blocks to configure tag filters applied to discovery of resource entities in the associated AWS account. When accessing this as an attribute reference, it is a list of objects. (see [below for nested schema](#nestedblock--service--resource_discovery_tag_filter))
+- `scrape_interval_seconds` (Number) The interval in seconds to scrape the service. See https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/aws/cloudwatch-metrics/services/ for supported scrape intervals.
+
+<a id="nestedblock--service--resource_discovery_tag_filter"></a>
+### Nested Schema for `service.resource_discovery_tag_filter`
+
+Required:
+
+- `key` (String) The key of the tag filter.
+- `value` (String) The value of the tag filter.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_cloud_provider_aws_resource_metadata_scrape_job.name "{{ stack_id }}:{{ name }}"
+```
diff --git a/docs/resources/k6_installation.md b/docs/resources/k6_installation.md
new file mode 100644
index 00000000..298a82f1
--- /dev/null
+++ b/docs/resources/k6_installation.md
@@ -0,0 +1,114 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_installation Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Sets up the k6 App on a Grafana Cloud instance and generates a token.
+  Once a Grafana Cloud stack is created, a user can either use this resource or go into the UI to install k6.
+  This resource cannot be imported but it can be used on an existing k6 App installation without issues.
+  Note that this resource must be used on a provider configured with Grafana Cloud credentials.
+  Official documentation https://grafana.com/docs/grafana-cloud/testing/k6/
+  Required access policy scopes:
+  stacks:readstacks:writesubscriptions:readorgs:read
+---
+
+# grafana_k6_installation (Resource)
+
+Sets up the k6 App on a Grafana Cloud instance and generates a token. 
+Once a Grafana Cloud stack is created, a user can either use this resource or go into the UI to install k6.
+This resource cannot be imported but it can be used on an existing k6 App installation without issues.
+
+**Note that this resource must be used on a provider configured with Grafana Cloud credentials.**
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/testing/k6/)
+
+Required access policy scopes:
+
+* stacks:read
+* stacks:write
+* subscriptions:read
+* orgs:read
+
+## Example Usage
+
+```terraform
+variable "cloud_access_policy_token" {
+  description = "Cloud Access Policy token for Grafana Cloud with the following scopes: stacks:read|write|delete, stack-service-accounts:write"
+}
+variable "stack_slug" {}
+variable "cloud_region" {
+  default = "us"
+}
+
+// Step 1: Create a stack in Grafana Cloud
+provider "grafana" {
+  alias                     = "cloud"
+  cloud_access_policy_token = var.cloud_access_policy_token
+}
+
+resource "grafana_cloud_stack" "k6_stack" {
+  provider = grafana.cloud
+
+  name        = var.stack_slug
+  slug        = var.stack_slug
+  region_slug = var.cloud_region
+}
+
+// Step 2: Create a Service Account and a token to install the k6 App
+resource "grafana_cloud_stack_service_account" "k6_sa" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name        = "${var.stack_slug}-k6-app"
+  role        = "Admin"
+  is_disabled = false
+}
+
+resource "grafana_cloud_stack_service_account_token" "k6_sa_token" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name               = "${var.stack_slug}-k6-app-token"
+  service_account_id = grafana_cloud_stack_service_account.k6_sa.id
+}
+
+// Step 3: Install the k6 App on the stack
+resource "grafana_k6_installation" "k6_installation" {
+  provider = grafana.cloud
+
+  cloud_access_policy_token = var.cloud_access_policy_token
+  stack_id                  = grafana_cloud_stack.k6_stack.id
+  grafana_sa_token          = grafana_cloud_stack_service_account_token.k6_sa_token.key
+  grafana_user              = "admin"
+}
+
+// Step 4: Interact with the k6 App: create a new project
+provider "grafana" {
+  alias = "k6"
+
+  stack_id        = grafana_cloud_stack.k6_stack.id
+  k6_access_token = grafana_k6_installation.k6_installation.k6_access_token
+}
+
+resource "grafana_k6_project" "my_k6_project" {
+  provider = grafana.k6
+
+  name = "k6 Project created with TF"
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `cloud_access_policy_token` (String, Sensitive) The [Grafana Cloud access policy](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/).
+- `grafana_sa_token` (String, Sensitive) The [service account](https://grafana.com/docs/grafana/latest/administration/service-accounts/) token.
+- `grafana_user` (String) The user to use for the installation.
+- `stack_id` (String) The identifier of the stack to install k6 on.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+- `k6_access_token` (String, Sensitive) Generated token to access the k6 API.
+- `k6_organization` (String) The identifier of the k6 organization.
diff --git a/docs/resources/k6_load_test.md b/docs/resources/k6_load_test.md
new file mode 100644
index 00000000..2f4a69c6
--- /dev/null
+++ b/docs/resources/k6_load_test.md
@@ -0,0 +1,56 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_load_test Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Manages a k6 load test.
+---
+
+# grafana_k6_load_test (Resource)
+
+Manages a k6 load test.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `name` (String) Human-friendly identifier of the load test.
+- `project_id` (Number) The identifier of the project this load test belongs to.
+- `script` (String) The k6 test script content. Can be provided inline or via the `file()` function.
+
+### Optional
+
+- `baseline_test_run_id` (Number) Identifier of a baseline test run used for results comparison.
+
+### Read-Only
+
+- `created` (String) The date when the load test was created.
+- `id` (Number) Numeric identifier of the load test.
+- `updated` (String) The date when the load test was last updated.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_k6_load_test.name "{{ id }}"
+```
diff --git a/docs/resources/k6_project.md b/docs/resources/k6_project.md
new file mode 100644
index 00000000..33f78294
--- /dev/null
+++ b/docs/resources/k6_project.md
@@ -0,0 +1,42 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Manages a k6 project.
+---
+
+# grafana_k6_project (Resource)
+
+Manages a k6 project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test_project" {
+  name = "Terraform Test Project"
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `name` (String) Human-friendly identifier of the project.
+
+### Read-Only
+
+- `created` (String) The date when the project was created.
+- `grafana_folder_uid` (String) The Grafana folder uid.
+- `id` (Number) Numeric identifier of the project.
+- `is_default` (Boolean) Use this project as default for running tests when no explicit project identifier is provided.
+- `updated` (String) The date when the project was last updated.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_k6_project.name "{{ id }}"
+```
diff --git a/docs/resources/k6_project_limits.md b/docs/resources/k6_project_limits.md
new file mode 100644
index 00000000..65f33237
--- /dev/null
+++ b/docs/resources/k6_project_limits.md
@@ -0,0 +1,53 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_k6_project_limits Resource - terraform-provider-grafana"
+subcategory: "k6"
+description: |-
+  Manages limits for a k6 project.
+---
+
+# grafana_k6_project_limits (Resource)
+
+Manages limits for a k6 project.
+
+## Example Usage
+
+```terraform
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+resource "grafana_k6_project_limits" "test_limits" {
+  project_id              = grafana_k6_project.test_project_limits.id
+  vuh_max_per_month       = 1000
+  vu_max_per_test         = 100
+  vu_browser_max_per_test = 10
+  duration_max_per_test   = 3600
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `project_id` (Number) The identifier of the project to manage limits for.
+
+### Optional
+
+- `duration_max_per_test` (Number) Maximum duration of a test in seconds.
+- `vu_browser_max_per_test` (Number) Maximum number of concurrent browser virtual users (VUs) used in one test.
+- `vu_max_per_test` (Number) Maximum number of concurrent virtual users (VUs) used in one test.
+- `vuh_max_per_month` (Number) Maximum amount of virtual user hours (VU/h) used per one calendar month.
+
+### Read-Only
+
+- `id` (Number) The identifier of the project limits. This is the same as the project_id.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_k6_project_limits.name "{{ project_id }}"
+```
diff --git a/docs/resources/synthetic_monitoring_check_alerts.md b/docs/resources/synthetic_monitoring_check_alerts.md
new file mode 100644
index 00000000..8b817d17
--- /dev/null
+++ b/docs/resources/synthetic_monitoring_check_alerts.md
@@ -0,0 +1,75 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "grafana_synthetic_monitoring_check_alerts Resource - terraform-provider-grafana"
+subcategory: "Synthetic Monitoring"
+description: |-
+  Manages alerts for a check in Grafana Synthetic Monitoring.
+  Official documentation https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/configure-alerts/configure-per-check-alerts/
+---
+
+# grafana_synthetic_monitoring_check_alerts (Resource)
+
+Manages alerts for a check in Grafana Synthetic Monitoring.
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/configure-alerts/configure-per-check-alerts/)
+
+## Example Usage
+
+```terraform
+resource "grafana_synthetic_monitoring_check" "main" {
+  job     = "Check Alert Test"
+  target  = "https://grafana.com"
+  enabled = true
+  probes  = [1]
+  labels  = {}
+  settings {
+    http {
+      ip_version = "V4"
+      method     = "GET"
+    }
+  }
+}
+
+resource "grafana_synthetic_monitoring_check_alerts" "main" {
+  check_id = grafana_synthetic_monitoring_check.main.id
+  alerts = [{
+    name      = "ProbeFailedExecutionsTooHigh"
+    threshold = 1
+    period    = "15m"
+    },
+    {
+      name      = "TLSTargetCertificateCloseToExpiring"
+      threshold = 14
+      period    = ""
+  }]
+}
+```
+
+<!-- schema generated by tfplugindocs -->
+## Schema
+
+### Required
+
+- `alerts` (Set of Object) List of alerts for the check. (see [below for nested schema](#nestedatt--alerts))
+- `check_id` (Number) The ID of the check to manage alerts for.
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+<a id="nestedatt--alerts"></a>
+### Nested Schema for `alerts`
+
+Required:
+
+- `name` (String)
+- `period` (String)
+- `threshold` (Number)
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+terraform import grafana_synthetic_monitoring_check_alerts.name "{{ check_id }}"
+```
diff --git a/examples/data-sources/grafana_k6_project/data-source.tf b/examples/data-sources/grafana_k6_project/data-source.tf
new file mode 100644
index 00000000..fcecc558
--- /dev/null
+++ b/examples/data-sources/grafana_k6_project/data-source.tf
@@ -0,0 +1,12 @@
+resource "grafana_k6_project" "test" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_project" "from_id" {
+  depends_on = [
+    grafana_k6_project.test
+  ]
+  id = grafana_k6_project.test.id
+}
+
+
diff --git a/examples/data-sources/grafana_k6_project_limits/data-source.tf b/examples/data-sources/grafana_k6_project_limits/data-source.tf
new file mode 100644
index 00000000..625a72bd
--- /dev/null
+++ b/examples/data-sources/grafana_k6_project_limits/data-source.tf
@@ -0,0 +1,7 @@
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+data "grafana_k6_project_limits" "from_project_id" {
+  project_id = grafana_k6_project.test_project_limits.id
+}
\ No newline at end of file
diff --git a/examples/data-sources/grafana_k6_projects/data-source.tf b/examples/data-sources/grafana_k6_projects/data-source.tf
new file mode 100644
index 00000000..2708f97c
--- /dev/null
+++ b/examples/data-sources/grafana_k6_projects/data-source.tf
@@ -0,0 +1,19 @@
+resource "grafana_k6_project" "project" {
+  name = "Terraform Test Project"
+}
+
+resource "grafana_k6_project" "project_2" {
+  name = "Terraform Test Project"
+}
+
+data "grafana_k6_projects" "from_name" {
+  name = "Terraform Test Project"
+
+  depends_on = [
+    grafana_k6_project.project,
+    grafana_k6_project.project_2
+  ]
+
+}
+
+
diff --git a/examples/resources/grafana_cloud_provider_aws_resource_metadata_scrape_job/import.sh b/examples/resources/grafana_cloud_provider_aws_resource_metadata_scrape_job/import.sh
new file mode 100644
index 00000000..2b74f370
--- /dev/null
+++ b/examples/resources/grafana_cloud_provider_aws_resource_metadata_scrape_job/import.sh
@@ -0,0 +1 @@
+terraform import grafana_cloud_provider_aws_resource_metadata_scrape_job.name "{{ stack_id }}:{{ name }}"
diff --git a/examples/resources/grafana_cloud_provider_aws_resource_metadata_scrape_job/resource.tf b/examples/resources/grafana_cloud_provider_aws_resource_metadata_scrape_job/resource.tf
new file mode 100644
index 00000000..e6df2f25
--- /dev/null
+++ b/examples/resources/grafana_cloud_provider_aws_resource_metadata_scrape_job/resource.tf
@@ -0,0 +1,37 @@
+data "grafana_cloud_stack" "test" {
+  slug = "gcloudstacktest"
+}
+
+data "aws_iam_role" "test" {
+  name = "my-role"
+}
+
+resource "grafana_cloud_provider_aws_account" "test" {
+  stack_id = data.grafana_cloud_stack.test.id
+  role_arn = data.aws_iam_role.test.arn
+  regions = [
+    "us-east-1",
+    "us-east-2",
+    "us-west-1"
+  ]
+}
+
+resource "grafana_cloud_provider_aws_resource_metadata_scrape_job" "test" {
+  stack_id                = data.grafana_cloud_stack.test.id
+  name                    = "my-aws-resource-metadata-scrape-job"
+  aws_account_resource_id = grafana_cloud_provider_aws_account.test.resource_id
+
+  service {
+    name                    = "AWS/EC2"
+    scrape_interval_seconds = 300
+    resource_discovery_tag_filter {
+      key   = "k8s.io/cluster-autoscaler/enabled"
+      value = "true"
+    }
+  }
+
+  static_labels = {
+    "label1" = "value1"
+    "label2" = "value2"
+  }
+}
diff --git a/examples/resources/grafana_k6_installation/resource.tf b/examples/resources/grafana_k6_installation/resource.tf
new file mode 100644
index 00000000..15560f02
--- /dev/null
+++ b/examples/resources/grafana_k6_installation/resource.tf
@@ -0,0 +1,63 @@
+variable "cloud_access_policy_token" {
+  description = "Cloud Access Policy token for Grafana Cloud with the following scopes: stacks:read|write|delete, stack-service-accounts:write"
+}
+variable "stack_slug" {}
+variable "cloud_region" {
+  default = "us"
+}
+
+// Step 1: Create a stack in Grafana Cloud
+provider "grafana" {
+  alias                     = "cloud"
+  cloud_access_policy_token = var.cloud_access_policy_token
+}
+
+resource "grafana_cloud_stack" "k6_stack" {
+  provider = grafana.cloud
+
+  name        = var.stack_slug
+  slug        = var.stack_slug
+  region_slug = var.cloud_region
+}
+
+// Step 2: Create a Service Account and a token to install the k6 App
+resource "grafana_cloud_stack_service_account" "k6_sa" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name        = "${var.stack_slug}-k6-app"
+  role        = "Admin"
+  is_disabled = false
+}
+
+resource "grafana_cloud_stack_service_account_token" "k6_sa_token" {
+  provider   = grafana.cloud
+  stack_slug = var.stack_slug
+
+  name               = "${var.stack_slug}-k6-app-token"
+  service_account_id = grafana_cloud_stack_service_account.k6_sa.id
+}
+
+// Step 3: Install the k6 App on the stack
+resource "grafana_k6_installation" "k6_installation" {
+  provider = grafana.cloud
+
+  cloud_access_policy_token = var.cloud_access_policy_token
+  stack_id                  = grafana_cloud_stack.k6_stack.id
+  grafana_sa_token          = grafana_cloud_stack_service_account_token.k6_sa_token.key
+  grafana_user              = "admin"
+}
+
+// Step 4: Interact with the k6 App: create a new project
+provider "grafana" {
+  alias = "k6"
+
+  stack_id        = grafana_cloud_stack.k6_stack.id
+  k6_access_token = grafana_k6_installation.k6_installation.k6_access_token
+}
+
+resource "grafana_k6_project" "my_k6_project" {
+  provider = grafana.k6
+
+  name = "k6 Project created with TF"
+}
diff --git a/examples/resources/grafana_k6_project/import.sh b/examples/resources/grafana_k6_project/import.sh
new file mode 100644
index 00000000..c34c9681
--- /dev/null
+++ b/examples/resources/grafana_k6_project/import.sh
@@ -0,0 +1 @@
+terraform import grafana_k6_project.name "{{ id }}"
diff --git a/examples/resources/grafana_k6_project/resource.tf b/examples/resources/grafana_k6_project/resource.tf
new file mode 100644
index 00000000..6683457b
--- /dev/null
+++ b/examples/resources/grafana_k6_project/resource.tf
@@ -0,0 +1,4 @@
+resource "grafana_k6_project" "test_project" {
+  name = "Terraform Test Project"
+}
+
diff --git a/examples/resources/grafana_k6_project_limits/import.sh b/examples/resources/grafana_k6_project_limits/import.sh
new file mode 100644
index 00000000..24bb07f6
--- /dev/null
+++ b/examples/resources/grafana_k6_project_limits/import.sh
@@ -0,0 +1 @@
+terraform import grafana_k6_project_limits.name "{{ project_id }}"
diff --git a/examples/resources/grafana_k6_project_limits/resource.tf b/examples/resources/grafana_k6_project_limits/resource.tf
new file mode 100644
index 00000000..e05c15a7
--- /dev/null
+++ b/examples/resources/grafana_k6_project_limits/resource.tf
@@ -0,0 +1,11 @@
+resource "grafana_k6_project" "test_project_limits" {
+  name = "Terraform Project Test Limits"
+}
+
+resource "grafana_k6_project_limits" "test_limits" {
+  project_id              = grafana_k6_project.test_project_limits.id
+  vuh_max_per_month       = 1000
+  vu_max_per_test         = 100
+  vu_browser_max_per_test = 10
+  duration_max_per_test   = 3600
+}
\ No newline at end of file
diff --git a/examples/resources/grafana_synthetic_monitoring_check_alerts/import.sh b/examples/resources/grafana_synthetic_monitoring_check_alerts/import.sh
new file mode 100644
index 00000000..624af98a
--- /dev/null
+++ b/examples/resources/grafana_synthetic_monitoring_check_alerts/import.sh
@@ -0,0 +1 @@
+terraform import grafana_synthetic_monitoring_check_alerts.name "{{ check_id }}"
diff --git a/examples/resources/grafana_synthetic_monitoring_check_alerts/resource.tf b/examples/resources/grafana_synthetic_monitoring_check_alerts/resource.tf
new file mode 100644
index 00000000..a6008e7a
--- /dev/null
+++ b/examples/resources/grafana_synthetic_monitoring_check_alerts/resource.tf
@@ -0,0 +1,27 @@
+resource "grafana_synthetic_monitoring_check" "main" {
+  job     = "Check Alert Test"
+  target  = "https://grafana.com"
+  enabled = true
+  probes  = [1]
+  labels  = {}
+  settings {
+    http {
+      ip_version = "V4"
+      method     = "GET"
+    }
+  }
+}
+
+resource "grafana_synthetic_monitoring_check_alerts" "main" {
+  check_id = grafana_synthetic_monitoring_check.main.id
+  alerts = [{
+    name      = "ProbeFailedExecutionsTooHigh"
+    threshold = 1
+    period    = "15m"
+    },
+    {
+      name      = "TLSTargetCertificateCloseToExpiring"
+      threshold = 14
+      period    = ""
+  }]
+} 
\ No newline at end of file
diff --git a/examples/resources/grafana_synthetic_monitoring_check_alerts/resource_update.tf b/examples/resources/grafana_synthetic_monitoring_check_alerts/resource_update.tf
new file mode 100644
index 00000000..493b2a38
--- /dev/null
+++ b/examples/resources/grafana_synthetic_monitoring_check_alerts/resource_update.tf
@@ -0,0 +1,27 @@
+resource "grafana_synthetic_monitoring_check" "main" {
+  job     = "Check Alert Test Updated"
+  target  = "https://grafana.com"
+  enabled = true
+  probes  = [1]
+  labels  = {}
+  settings {
+    http {
+      ip_version = "V4"
+      method     = "GET"
+    }
+  }
+}
+
+resource "grafana_synthetic_monitoring_check_alerts" "main" {
+  check_id = grafana_synthetic_monitoring_check.main.id
+  alerts = [{
+    name      = "ProbeFailedExecutionsTooHigh"
+    threshold = 2
+    period    = "10m"
+    },
+    {
+      name      = "TLSTargetCertificateCloseToExpiring"
+      threshold = 7
+      period    = ""
+  }]
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 52ab784f..4b0c3edd 100644
--- a/go.mod
+++ b/go.mod
@@ -17,11 +17,12 @@ require (
 	github.com/grafana/grafana/apps/dashboard v0.0.0-20250424064802-2fbb2d6f5d27
 	github.com/grafana/grafana/apps/playlist v0.0.0-20250424064802-2fbb2d6f5d27
 	github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250424064802-2fbb2d6f5d27
+	github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20250416134020-d958828152cd
 	github.com/grafana/machine-learning-go-client v0.8.2
 	github.com/grafana/river v0.3.0
 	github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6
-	github.com/grafana/synthetic-monitoring-agent v0.34.1
-	github.com/grafana/synthetic-monitoring-api-go-client v0.11.0
+	github.com/grafana/synthetic-monitoring-agent v0.34.4
+	github.com/grafana/synthetic-monitoring-api-go-client v0.14.0
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
 	github.com/hashicorp/go-retryablehttp v0.7.7
 	github.com/hashicorp/go-uuid v1.0.3
diff --git a/go.sum b/go.sum
index 3e3be5ec..a43a8e31 100644
--- a/go.sum
+++ b/go.sum
@@ -190,6 +190,8 @@ github.com/grafana/grafana/apps/playlist v0.0.0-20250424064802-2fbb2d6f5d27 h1:a
 github.com/grafana/grafana/apps/playlist v0.0.0-20250424064802-2fbb2d6f5d27/go.mod h1:9U44mptAJW8bkvgPgCxsnki58/nz3wKPgDayeyeFWJs=
 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250424064802-2fbb2d6f5d27 h1:2Q158xAoyGgPVw7BIWF5cPYTkjjdsn9c8TpNA38gKzM=
 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250424064802-2fbb2d6f5d27/go.mod h1:kzjpaBODMbCSS2kvAnV43Pwxoq4lOxrgw/TGKqq8oTA=
+github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20250416134020-d958828152cd h1:UOHbve+keUInhIBSqA4m7GtF2wEiKyW8f/kUt9S0+zM=
+github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20250416134020-d958828152cd/go.mod h1:nBtSyJ0NTfJvEPSrRSKc1u7Vn0mB/kttEZdClHwTqi0=
 github.com/grafana/machine-learning-go-client v0.8.2 h1:TvU4e+Kgg4GhwBNYTMjBUNq4tbhcxe0L8w1eo/UfV2M=
 github.com/grafana/machine-learning-go-client v0.8.2/go.mod h1:GQKDn10CZqG11l1Qtc6BZ5V6e54fSv5Vi8wskWn3BWs=
 github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
@@ -200,10 +202,10 @@ github.com/grafana/river v0.3.0 h1:6TsaR/vkkcppUM9I0muGbPIUedCtpPu6OWreE5+CE6g=
 github.com/grafana/river v0.3.0/go.mod h1:icSidCSHYXJUYy6TjGAi/D+X7FsP7Gc7cxvBUIwYMmY=
 github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6 h1:7jbuz2MCqHlIc+vC3geOEzMZolm30v3K1bFTaTJ4mGI=
 github.com/grafana/slo-openapi-client/go/slo v0.0.0-20250218172929-ab9cae090da6/go.mod h1:a9idYds6valDrBwBdcFTIp+QzlERgf2CFDZgEfmp9pA=
-github.com/grafana/synthetic-monitoring-agent v0.34.1 h1:igtwIr61lFopRzSRL1uBAmmbaQ5OebsgKXw9dpjOefY=
-github.com/grafana/synthetic-monitoring-agent v0.34.1/go.mod h1:PM7+XQSxiXAklTbTmpK8OvbcvUvGF8BOdRWhd7V/39o=
-github.com/grafana/synthetic-monitoring-api-go-client v0.11.0 h1:C/LMuhY9USNXRVhzQ3S09C0rSyAwCUssCp3WOShLN5I=
-github.com/grafana/synthetic-monitoring-api-go-client v0.11.0/go.mod h1:eapoDTSZLpj+p//C15C/IUJY4PnIWWf8jb/Ui2rj4Pg=
+github.com/grafana/synthetic-monitoring-agent v0.34.4 h1:2wlf6qJifAMIfc3JnmPamGc16t5b3OVmjv42TVsq60Q=
+github.com/grafana/synthetic-monitoring-agent v0.34.4/go.mod h1:V/Wj1omTCRzsK++f9UZ7QK/neZXpP1ESOFkWJ62g5ik=
+github.com/grafana/synthetic-monitoring-api-go-client v0.14.0 h1:XBqG4F5SON1Bz6DwnK4xw2bPny3Y8iv+DF/RYAhL+zQ=
+github.com/grafana/synthetic-monitoring-api-go-client v0.14.0/go.mod h1:+58mGFzv0T/wcBCHPK1NpKpGtsYkeSKRBLH5M89hgBE=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 h1:KcFzXwzM/kGhIRHvc8jdixfIJjVzuUJdnv+5xsPutog=
diff --git a/internal/common/client.go b/internal/common/client.go
index fc02f3bb..e1838590 100644
--- a/internal/common/client.go
+++ b/internal/common/client.go
@@ -10,17 +10,19 @@ import (
 	"github.com/grafana/grafana-app-sdk/k8s"
 	"github.com/grafana/grafana-com-public-clients/go/gcom"
 	goapi "github.com/grafana/grafana-openapi-client-go/client"
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
 	"github.com/grafana/machine-learning-go-client/mlapi"
 	"github.com/grafana/slo-openapi-client/go/slo"
 	SMAPI "github.com/grafana/synthetic-monitoring-api-go-client"
-	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
-	"github.com/grafana/terraform-provider-grafana/v3/internal/common/fleetmanagementapi"
 
-	"github.com/grafana/terraform-provider-grafana/v3/internal/common/frontendo11yapi"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/connectionsapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/fleetmanagementapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/frontendo11yapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
 )
 
 type Client struct {
@@ -43,6 +45,9 @@ type Client struct {
 	FleetManagementClient *fleetmanagementapi.Client
 	FrontendO11yAPIClient *frontendo11yapi.Client
 
+	K6APIClient *k6.APIClient
+	K6APIConfig *k6providerapi.K6APIConfig
+
 	alertingMutex sync.Mutex
 }
 
diff --git a/internal/common/cloudproviderapi/client.go b/internal/common/cloudproviderapi/client.go
index b17754a5..e50625ac 100644
--- a/internal/common/cloudproviderapi/client.go
+++ b/internal/common/cloudproviderapi/client.go
@@ -199,6 +199,87 @@ func (c *Client) DeleteAWSCloudWatchScrapeJob(ctx context.Context, stackID strin
 	return nil
 }
 
+type AWSResourceMetadataScrapeJobRequest struct {
+	Name                  string                       `json:"name"`
+	Enabled               bool                         `json:"enabled"`
+	AWSAccountResourceID  string                       `json:"awsAccountResourceID"`
+	RegionsSubsetOverride []string                     `json:"regionsSubsetOverride"`
+	Services              []AWSResourceMetadataService `json:"services"`
+	StaticLabels          map[string]string            `json:"staticLabels"`
+}
+type AWSResourceMetadataScrapeJobResponse struct {
+	Name                 string                       `json:"name"`
+	Enabled              bool                         `json:"enabled"`
+	AWSAccountResourceID string                       `json:"awsAccountResourceID"`
+	Services             []AWSResourceMetadataService `json:"services"`
+	StaticLabels         map[string]string            `json:"staticLabels"`
+
+	// computed fields beyond the original request
+	RoleARN                   string   `json:"roleARN"`
+	Regions                   []string `json:"regions"`
+	RegionsSubsetOverrideUsed bool     `json:"regionsSubsetOverrideUsed"`
+	DisabledReason            string   `json:"disabledReason"`
+	Provenance                string   `json:"provenance"`
+}
+type AWSResourceMetadataService struct {
+	Name                        string                         `json:"name"`
+	ScrapeIntervalSeconds       int64                          `json:"scrapeIntervalSeconds"`
+	ResourceDiscoveryTagFilters []AWSResourceMetadataTagFilter `json:"resourceDiscoveryTagFilters"`
+}
+type AWSResourceMetadataTagFilter struct {
+	Key   string `json:"key"`
+	Value string `json:"value"`
+}
+
+func (c *Client) CreateAWSResourceMetadataScrapeJob(ctx context.Context, stackID string, jobData AWSResourceMetadataScrapeJobRequest) (AWSResourceMetadataScrapeJobResponse, error) {
+	path := fmt.Sprintf("/api/v2/stacks/%s/aws/jobs/resources", stackID)
+	respData := apiResponseWrapper[AWSResourceMetadataScrapeJobResponse]{}
+	err := c.doAPIRequest(ctx, http.MethodPost, path, &jobData, &respData)
+	if err != nil {
+		return AWSResourceMetadataScrapeJobResponse{}, fmt.Errorf("failed to create AWS Resource Metadata scrape job: %w", err)
+	}
+	return respData.Data, nil
+}
+
+func (c *Client) GetAWSResourceMetadataScrapeJob(ctx context.Context, stackID string, jobName string) (AWSResourceMetadataScrapeJobResponse, error) {
+	path := fmt.Sprintf("/api/v2/stacks/%s/aws/jobs/resources/%s", stackID, jobName)
+	respData := apiResponseWrapper[AWSResourceMetadataScrapeJobResponse]{}
+	err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &respData)
+	if err != nil {
+		return AWSResourceMetadataScrapeJobResponse{}, fmt.Errorf("failed to get AWS Resource Metadata scrape job: %w", err)
+	}
+	return respData.Data, nil
+}
+
+func (c *Client) ListAWSResourceMetadataScrapeJobs(ctx context.Context, stackID string) ([]AWSResourceMetadataScrapeJobResponse, error) {
+	path := fmt.Sprintf("/api/v2/stacks/%s/aws/jobs/resources", stackID)
+	respData := apiResponseWrapper[[]AWSResourceMetadataScrapeJobResponse]{}
+	err := c.doAPIRequest(ctx, http.MethodGet, path, nil, &respData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get AWS Resource Metadata scrape job: %w", err)
+	}
+	return respData.Data, nil
+}
+
+func (c *Client) UpdateAWSResourceMetadataScrapeJob(ctx context.Context, stackID string, jobName string, jobData AWSResourceMetadataScrapeJobRequest) (AWSResourceMetadataScrapeJobResponse, error) {
+	path := fmt.Sprintf("/api/v2/stacks/%s/aws/jobs/resources/%s", stackID, jobName)
+	respData := apiResponseWrapper[AWSResourceMetadataScrapeJobResponse]{}
+	err := c.doAPIRequest(ctx, http.MethodPut, path, &jobData, &respData)
+	if err != nil {
+		return AWSResourceMetadataScrapeJobResponse{}, fmt.Errorf("failed to update AWS Resource Metadata scrape job: %w", err)
+	}
+	return respData.Data, nil
+}
+
+func (c *Client) DeleteAWSResourceMetadataScrapeJob(ctx context.Context, stackID string, jobName string) error {
+	path := fmt.Sprintf("/api/v2/stacks/%s/aws/jobs/resources/%s", stackID, jobName)
+	err := c.doAPIRequest(ctx, http.MethodDelete, path, nil, nil)
+	if err != nil {
+		return fmt.Errorf("failed to delete AWS Resource Metadata scrape job: %w", err)
+	}
+	return nil
+}
+
 type AzureCredential struct {
 	// ID is the unique identifier for the Azure credential in our systems.
 	ID string `json:"id"`
diff --git a/internal/common/k6providerapi/config.go b/internal/common/k6providerapi/config.go
new file mode 100644
index 00000000..d63055e2
--- /dev/null
+++ b/internal/common/k6providerapi/config.go
@@ -0,0 +1,9 @@
+package k6providerapi
+
+type K6APIConfig struct {
+	// Token is the k6 Cloud API token.
+	Token string
+
+	// StackID is the k6 Cloud API stack id.
+	StackID int32
+}
diff --git a/internal/common/resource.go b/internal/common/resource.go
index ab51ef4d..6b5b323e 100644
--- a/internal/common/resource.go
+++ b/internal/common/resource.go
@@ -26,6 +26,7 @@ var (
 	CategoryConnections         ResourceCategory = "Connections"
 	CategoryFleetManagement     ResourceCategory = "Fleet Management"
 	CategoryFrontendO11y        ResourceCategory = "Frontend Observability"
+	CategoryK6                  ResourceCategory = "k6"
 )
 
 type ResourceCommon struct {
diff --git a/internal/common/types.go b/internal/common/types.go
new file mode 100644
index 00000000..18516fa9
--- /dev/null
+++ b/internal/common/types.go
@@ -0,0 +1,13 @@
+package common
+
+import (
+	"errors"
+	"math"
+)
+
+func ToInt32[T ~int | ~int64](val T) (int32, error) {
+	if val < math.MinInt32 || val > math.MaxInt32 {
+		return 0, errors.New("value overflows int32")
+	}
+	return int32(val), nil
+}
diff --git a/internal/resources/appplatform/dashboard_resource.go b/internal/resources/appplatform/dashboard_resource.go
index aee99936..6efdc67e 100644
--- a/internal/resources/appplatform/dashboard_resource.go
+++ b/internal/resources/appplatform/dashboard_resource.go
@@ -30,9 +30,11 @@ func Dashboard() NamedResource {
 			Schema: ResourceSpecSchema{
 				Description: "Manages Grafana dashboards.",
 				MarkdownDescription: `
-Manages Grafana dashboards via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana dashboards using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/dashboard/#new-dashboard-apis)
 	`,
-				DeprecationMessage: "This resource is currently EXPERIMENTAL and may be subject to change.",
 				SpecAttributes: map[string]schema.Attribute{
 					"json": schema.StringAttribute{
 						Required:    true,
diff --git a/internal/resources/appplatform/playlist_resource.go b/internal/resources/appplatform/playlist_resource.go
index 05bbed2b..8dc8d2ea 100644
--- a/internal/resources/appplatform/playlist_resource.go
+++ b/internal/resources/appplatform/playlist_resource.go
@@ -45,9 +45,11 @@ func Playlist() NamedResource {
 			Schema: ResourceSpecSchema{
 				Description: "Manages Grafana playlists.",
 				MarkdownDescription: `
-Manages Grafana playlists via the new Grafana App Platform API. This resource is currently **EXPERIMENTAL** and may be subject to change. It requires a development build of Grafana with specific feature flags enabled.
+Manages Grafana playlists using the new Grafana APIs.
+
+* [Official documentation](https://grafana.com/docs/grafana/latest/dashboards/create-manage-playlists/)
+* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/apis/)
 `,
-				DeprecationMessage: "This resource is currently EXPERIMENTAL and may be subject to change.",
 				SpecAttributes: map[string]schema.Attribute{
 					"title": schema.StringAttribute{
 						Required:    true,
diff --git a/internal/resources/cloud/resource_k6_installation.go b/internal/resources/cloud/resource_k6_installation.go
new file mode 100644
index 00000000..1ce613c7
--- /dev/null
+++ b/internal/resources/cloud/resource_k6_installation.go
@@ -0,0 +1,175 @@
+package cloud
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+	"github.com/grafana/grafana-com-public-clients/go/gcom"
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+func resourceK6Installation() *common.Resource {
+	schema := &schema.Resource{
+
+		Description: `
+Sets up the k6 App on a Grafana Cloud instance and generates a token. 
+Once a Grafana Cloud stack is created, a user can either use this resource or go into the UI to install k6.
+This resource cannot be imported but it can be used on an existing k6 App installation without issues.
+
+**Note that this resource must be used on a provider configured with Grafana Cloud credentials.**
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/testing/k6/)
+
+Required access policy scopes:
+
+* stacks:read
+* stacks:write
+* subscriptions:read
+* orgs:read
+`,
+		CreateContext: withClient[schema.CreateContextFunc](resourceK6InstallationCreate),
+		ReadContext:   resourceK6InstallationRead,
+		DeleteContext: resourceK6InstallationDelete,
+
+		Schema: map[string]*schema.Schema{
+			"cloud_access_policy_token": {
+				Type:        schema.TypeString,
+				Sensitive:   true,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The [Grafana Cloud access policy](https://grafana.com/docs/grafana-cloud/account-management/authentication-and-permissions/access-policies/).",
+			},
+			"stack_id": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The identifier of the stack to install k6 on.",
+			},
+			"grafana_sa_token": {
+				Type:        schema.TypeString,
+				Sensitive:   true,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The [service account](https://grafana.com/docs/grafana/latest/administration/service-accounts/) token.",
+			},
+			"grafana_user": {
+				Type:        schema.TypeString,
+				Required:    true,
+				ForceNew:    true,
+				Description: "The user to use for the installation.",
+			},
+			"k6_access_token": {
+				Type:        schema.TypeString,
+				Sensitive:   true,
+				Computed:    true,
+				Description: "Generated token to access the k6 API.",
+			},
+			"k6_organization": {
+				Type:        schema.TypeString,
+				Computed:    true,
+				Description: "The identifier of the k6 organization.",
+			},
+		},
+	}
+
+	return common.NewLegacySDKResource(
+		common.CategoryK6,
+		"grafana_k6_installation",
+		nil,
+		schema,
+	)
+}
+
+func resourceK6InstallationCreate(ctx context.Context, d *schema.ResourceData, cloudClient *gcom.APIClient) diag.Diagnostics {
+	const url = "https://api.k6.io/v3/account/grafana-app/start"
+	req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, nil)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	stackID, ok := d.Get("stack_id").(string)
+	if !ok || len(stackID) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid stack_id")
+	}
+
+	cloudAccessPolicyToken, ok := d.Get("cloud_access_policy_token").(string)
+	if !ok || len(cloudAccessPolicyToken) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid cloud_access_policy_token")
+	}
+
+	grafanaServiceAccountToken, ok := d.Get("grafana_sa_token").(string)
+	if !ok || len(grafanaServiceAccountToken) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid grafana_sa_token")
+	}
+
+	grafanaUser, ok := d.Get("grafana_user").(string)
+	if !ok || len(grafanaUser) == 0 {
+		return diag.Errorf("the grafana_k6_installation must have a valid grafana_user")
+	}
+
+	req.Header.Set("X-Stack-Id", stackID)
+	req.Header.Set("X-Grafana-Key", cloudAccessPolicyToken)
+	req.Header.Set("X-Grafana-Service-Token", grafanaServiceAccountToken)
+	req.Header.Set("X-Grafana-User", grafanaUser)
+
+	resp, err := cloudClient.GetConfig().HTTPClient.Do(req)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	var installationRes struct {
+		V3GrafanaToken string `json:"v3_grafana_token"`
+		OrganizationID string `json:"organization_id"`
+	}
+
+	err = json.NewDecoder(resp.Body).Decode(&installationRes)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	d.SetId(installationRes.OrganizationID)
+
+	if err := d.Set("k6_access_token", installationRes.V3GrafanaToken); err != nil {
+		return diag.FromErr(err)
+	}
+
+	if err := d.Set("k6_organization", installationRes.OrganizationID); err != nil {
+		return diag.FromErr(err)
+	}
+
+	return resourceK6InstallationRead(ctx, d, nil)
+}
+
+// Management of the installation is a one-off operation. The state cannot be updated through a read operation.
+// This read function will only invalidate the state (forcing recreation) if the installation has been deleted.
+func resourceK6InstallationRead(ctx context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
+	var stackID int32
+	if intStackID, err := strconv.Atoi(d.Get("stack_id").(string)); err != nil {
+		return diag.Errorf("could not convert stack_id to integer: %s", err.Error())
+	} else if stackID, err = common.ToInt32(intStackID); err != nil {
+		return diag.Errorf("could not convert stack_id to int32: %s", err.Error())
+	}
+
+	tempClient := k6.NewAPIClient(k6.NewConfiguration())
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.Get("k6_access_token").(string))
+	if _, _, err := tempClient.ProjectsAPI.ProjectsList(ctx).XStackId(stackID).Execute(); err != nil {
+		return common.WarnMissing("k6 installation", d)
+	}
+
+	return nil
+}
+
+func resourceK6InstallationDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
+	// To be implemented, not supported yet
+	return nil
+}
diff --git a/internal/resources/cloud/resources.go b/internal/resources/cloud/resources.go
index f91dfa8c..ab8bea67 100644
--- a/internal/resources/cloud/resources.go
+++ b/internal/resources/cloud/resources.go
@@ -20,6 +20,7 @@ var Resources = []*common.Resource{
 	resourceStack(),
 	resourceStackServiceAccount(),
 	resourceStackServiceAccountToken(),
+	resourceK6Installation(),
 	resourceSyntheticMonitoringInstallation(),
 	resourcePDCNetwork(),
 	resourcePDCNetworkToken(),
diff --git a/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_job.go b/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_job.go
index a4e67a15..f8850b70 100644
--- a/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_job.go
+++ b/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_job.go
@@ -25,28 +25,28 @@ var (
 				Required:    true,
 			},
 			"name": schema.StringAttribute{
-				Description: "The name of the CloudWatch Scrape Job. Part of the Terraform Resource ID.",
+				Description: "The name of the AWS CloudWatch Scrape Job. Part of the Terraform Resource ID.",
 				Required:    true,
 			},
 			"enabled": schema.BoolAttribute{
-				Description: "Whether the CloudWatch Scrape Job is enabled or not.",
+				Description: "Whether the AWS CloudWatch Scrape Job is enabled or not.",
 				Computed:    true,
 			},
 			"aws_account_resource_id": schema.StringAttribute{
-				Description: "The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.",
+				Description: "The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this AWS CloudWatch Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.",
 				Computed:    true,
 			},
 			"role_arn": schema.StringAttribute{
-				Description: "The AWS ARN of the IAM role associated with the AWS Account resource that is being used by this CloudWatch Scrape Job.",
+				Description: "The AWS ARN of the IAM role associated with the AWS Account resource that is being used by this AWS CloudWatch Scrape Job.",
 				Computed:    true,
 			},
 			"regions": schema.SetAttribute{
-				Description: "The set of AWS region names that this CloudWatch Scrape Job is configured to scrape.",
+				Description: "The set of AWS region names that this AWS CloudWatch Scrape Job is configured to scrape.",
 				Computed:    true,
 				ElementType: types.StringType,
 			},
 			"regions_subset_override_used": schema.BoolAttribute{
-				Description: "When true, the `regions` attribute will be the set of regions configured in the override. When false, the `regions` attribute will be the set of regions belonging to the AWS Account resource that is associated with this CloudWatch Scrape Job.",
+				Description: "When true, the `regions` attribute will be the set of regions configured in the override. When false, the `regions` attribute will be the set of regions belonging to the AWS Account resource that is associated with this AWS CloudWatch Scrape Job.",
 				Computed:    true,
 			},
 			"export_tags": schema.BoolAttribute{
@@ -54,7 +54,7 @@ var (
 				Computed:    true,
 			},
 			"disabled_reason": schema.StringAttribute{
-				Description: "When the CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.",
+				Description: "When the AWS CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.",
 				Computed:    true,
 			},
 			"static_labels": schema.MapAttribute{
@@ -65,7 +65,7 @@ var (
 		},
 		Blocks: map[string]schema.Block{
 			"service": schema.ListNestedBlock{
-				Description: "One or more configuration blocks to dictate what this CloudWatch Scrape Job should scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
+				Description: "One or more configuration blocks to dictate what this AWS CloudWatch Scrape Job should scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
 				NestedObject: schema.NestedBlockObject{
 					Attributes: map[string]schema.Attribute{
 						"name": schema.StringAttribute{
@@ -118,7 +118,7 @@ var (
 				},
 			},
 			"custom_namespace": schema.ListNestedBlock{
-				Description: "Zero or more configuration blocks to configure custom namespaces for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
+				Description: "Zero or more configuration blocks to configure custom namespaces for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
 				NestedObject: schema.NestedBlockObject{
 					Attributes: map[string]schema.Attribute{
 						"name": schema.StringAttribute{
@@ -189,7 +189,7 @@ func (r *datasourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req datas
 }
 
 func (r *datasourceAWSCloudWatchScrapeJob) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
-	var data awsCWScrapeJobTFDataSourceModel
+	var data awsCloudWatchScrapeJobTFDataSourceModel
 	diags := req.Config.Get(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
@@ -202,11 +202,11 @@ func (r *datasourceAWSCloudWatchScrapeJob) Read(ctx context.Context, req datasou
 		data.Name.ValueString(),
 	)
 	if err != nil {
-		resp.Diagnostics.AddError("Failed to get AWS CloudWatch scrape job", err.Error())
+		resp.Diagnostics.AddError("Failed to get AWS CloudWatch Scrape Job", err.Error())
 		return
 	}
 
-	jobTF, diags := generateCloudWatchScrapeJobDataSourceTFModel(ctx, data.StackID.ValueString(), jobResp)
+	jobTF, diags := generateAWSCloudWatchScrapeJobDataSourceTFModel(ctx, data.StackID.ValueString(), jobResp)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
 		return
diff --git a/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_jobs.go b/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_jobs.go
index 7afd3757..248c9648 100644
--- a/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_jobs.go
+++ b/internal/resources/cloudprovider/data_source_aws_cloudwatch_scrape_jobs.go
@@ -16,9 +16,9 @@ var (
 )
 
 type datasourceAWSCloudWatchScrapeJobsModel struct {
-	ID         types.String                      `tfsdk:"id"`
-	StackID    types.String                      `tfsdk:"stack_id"`
-	ScrapeJobs []awsCWScrapeJobTFDataSourceModel `tfsdk:"scrape_job"`
+	ID         types.String                              `tfsdk:"id"`
+	StackID    types.String                              `tfsdk:"stack_id"`
+	ScrapeJobs []awsCloudWatchScrapeJobTFDataSourceModel `tfsdk:"scrape_job"`
 }
 
 type datasourceAWSCloudWatchScrapeJobs struct {
@@ -77,7 +77,7 @@ func (r datasourceAWSCloudWatchScrapeJobs) Schema(ctx context.Context, req datas
 							Computed:    true,
 						}
 						attrs["name"] = schema.StringAttribute{
-							Description: "The name of the CloudWatch Scrape Job. Part of the Terraform Resource ID.",
+							Description: "The name of the AWS CloudWatch Scrape Job. Part of the Terraform Resource ID.",
 							Computed:    true,
 						}
 						return attrs
@@ -106,9 +106,9 @@ func (r datasourceAWSCloudWatchScrapeJobs) Read(ctx context.Context, req datasou
 		return
 	}
 
-	scrapeJobs := make([]awsCWScrapeJobTFDataSourceModel, len(jobs))
+	scrapeJobs := make([]awsCloudWatchScrapeJobTFDataSourceModel, len(jobs))
 	for i, jobResp := range jobs {
-		jobTF, diags := generateCloudWatchScrapeJobDataSourceTFModel(ctx, data.StackID.ValueString(), jobResp)
+		jobTF, diags := generateAWSCloudWatchScrapeJobDataSourceTFModel(ctx, data.StackID.ValueString(), jobResp)
 		resp.Diagnostics.Append(diags...)
 		if resp.Diagnostics.HasError() {
 			return
diff --git a/internal/resources/cloudprovider/models.go b/internal/resources/cloudprovider/models_aws_cloudwatch.go
similarity index 74%
rename from internal/resources/cloudprovider/models.go
rename to internal/resources/cloudprovider/models_aws_cloudwatch.go
index 3882fc73..6cadb644 100644
--- a/internal/resources/cloudprovider/models.go
+++ b/internal/resources/cloudprovider/models_aws_cloudwatch.go
@@ -12,7 +12,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
 )
 
-type awsCWScrapeJobTFResourceModel struct {
+type awsCloudWatchScrapeJobTFResourceModel struct {
 	ID                    types.String `tfsdk:"id"`
 	StackID               types.String `tfsdk:"stack_id"`
 	Name                  types.String `tfsdk:"name"`
@@ -29,7 +29,7 @@ type awsCWScrapeJobTFResourceModel struct {
 	StaticLabels     types.Map  `tfsdk:"static_labels"`
 }
 
-type awsCWScrapeJobTFDataSourceModel struct {
+type awsCloudWatchScrapeJobTFDataSourceModel struct {
 	ID                        types.String `tfsdk:"id"`
 	StackID                   types.String `tfsdk:"stack_id"`
 	Name                      types.String `tfsdk:"name"`
@@ -48,7 +48,7 @@ type awsCWScrapeJobTFDataSourceModel struct {
 	StaticLabels     types.Map  `tfsdk:"static_labels"`
 }
 
-type awsCWScrapeJobServiceTFModel struct {
+type awsCloudWatchScrapeJobServiceTFModel struct {
 	Name                        types.String `tfsdk:"name"`
 	Metrics                     types.List   `tfsdk:"metric"`
 	ScrapeIntervalSeconds       types.Int64  `tfsdk:"scrape_interval_seconds"`
@@ -56,18 +56,18 @@ type awsCWScrapeJobServiceTFModel struct {
 	TagsToAddToMetrics          types.Set    `tfsdk:"tags_to_add_to_metrics"`
 }
 
-func (m awsCWScrapeJobServiceTFModel) attrTypes() map[string]attr.Type {
+func (m awsCloudWatchScrapeJobServiceTFModel) attrTypes() map[string]attr.Type {
 	return map[string]attr.Type{
 		"name": types.StringType,
 		"metric": types.ListType{
 			ElemType: types.ObjectType{
-				AttrTypes: awsCWScrapeJobMetricTFModel{}.attrTypes(),
+				AttrTypes: awsCloudWatchScrapeJobMetricTFModel{}.attrTypes(),
 			},
 		},
 		"scrape_interval_seconds": types.Int64Type,
 		"resource_discovery_tag_filter": types.ListType{
 			ElemType: types.ObjectType{
-				AttrTypes: awsCWScrapeJobTagFilterTFModel{}.attrTypes(),
+				AttrTypes: awsCloudWatchScrapeJobTagFilterTFModel{}.attrTypes(),
 			},
 		},
 		"tags_to_add_to_metrics": types.SetType{
@@ -76,30 +76,30 @@ func (m awsCWScrapeJobServiceTFModel) attrTypes() map[string]attr.Type {
 	}
 }
 
-type awsCWScrapeJobCustomNamespaceTFModel struct {
+type awsCloudWatchScrapeJobCustomNamespaceTFModel struct {
 	Name                  types.String `tfsdk:"name"`
 	Metrics               types.List   `tfsdk:"metric"`
 	ScrapeIntervalSeconds types.Int64  `tfsdk:"scrape_interval_seconds"`
 }
 
-func (m awsCWScrapeJobCustomNamespaceTFModel) attrTypes() map[string]attr.Type {
+func (m awsCloudWatchScrapeJobCustomNamespaceTFModel) attrTypes() map[string]attr.Type {
 	return map[string]attr.Type{
 		"name": types.StringType,
 		"metric": types.ListType{
 			ElemType: types.ObjectType{
-				AttrTypes: awsCWScrapeJobMetricTFModel{}.attrTypes(),
+				AttrTypes: awsCloudWatchScrapeJobMetricTFModel{}.attrTypes(),
 			},
 		},
 		"scrape_interval_seconds": types.Int64Type,
 	}
 }
 
-type awsCWScrapeJobMetricTFModel struct {
+type awsCloudWatchScrapeJobMetricTFModel struct {
 	Name       types.String `tfsdk:"name"`
 	Statistics types.Set    `tfsdk:"statistics"`
 }
 
-func (m awsCWScrapeJobMetricTFModel) attrTypes() map[string]attr.Type {
+func (m awsCloudWatchScrapeJobMetricTFModel) attrTypes() map[string]attr.Type {
 	return map[string]attr.Type{
 		"name": types.StringType,
 		"statistics": types.SetType{
@@ -108,30 +108,30 @@ func (m awsCWScrapeJobMetricTFModel) attrTypes() map[string]attr.Type {
 	}
 }
 
-type awsCWScrapeJobTagFilterTFModel struct {
+type awsCloudWatchScrapeJobTagFilterTFModel struct {
 	Key   types.String `tfsdk:"key"`
 	Value types.String `tfsdk:"value"`
 }
 
-func (m awsCWScrapeJobTagFilterTFModel) attrTypes() map[string]attr.Type {
+func (m awsCloudWatchScrapeJobTagFilterTFModel) attrTypes() map[string]attr.Type {
 	return map[string]attr.Type{
 		"key":   types.StringType,
 		"value": types.StringType,
 	}
 }
 
-type awsCWScrapeJobNoDuplicateServiceNamesValidator struct{}
+type awsCloudWatchScrapeJobNoDuplicateServiceNamesValidator struct{}
 
-func (v awsCWScrapeJobNoDuplicateServiceNamesValidator) Description(ctx context.Context) string {
+func (v awsCloudWatchScrapeJobNoDuplicateServiceNamesValidator) Description(ctx context.Context) string {
 	return "No duplicate service names are allowed."
 }
 
-func (v awsCWScrapeJobNoDuplicateServiceNamesValidator) MarkdownDescription(ctx context.Context) string {
+func (v awsCloudWatchScrapeJobNoDuplicateServiceNamesValidator) MarkdownDescription(ctx context.Context) string {
 	return "No duplicate service names are allowed."
 }
 
-func (v awsCWScrapeJobNoDuplicateServiceNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
-	var services []awsCWScrapeJobServiceTFModel
+func (v awsCloudWatchScrapeJobNoDuplicateServiceNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+	var services []awsCloudWatchScrapeJobServiceTFModel
 	diags := req.ConfigValue.ElementsAs(ctx, &services, true)
 	resp.Diagnostics.Append(diags...)
 	if diags.HasError() {
@@ -147,18 +147,18 @@ func (v awsCWScrapeJobNoDuplicateServiceNamesValidator) ValidateList(ctx context
 	}
 }
 
-type awsCWScrapeJobNoDuplicateCustomNamespaceNamesValidator struct{}
+type awsCloudWatchScrapeJobNoDuplicateCustomNamespaceNamesValidator struct{}
 
-func (v awsCWScrapeJobNoDuplicateCustomNamespaceNamesValidator) Description(ctx context.Context) string {
+func (v awsCloudWatchScrapeJobNoDuplicateCustomNamespaceNamesValidator) Description(ctx context.Context) string {
 	return "No duplicate custom namespace names are allowed."
 }
 
-func (v awsCWScrapeJobNoDuplicateCustomNamespaceNamesValidator) MarkdownDescription(ctx context.Context) string {
+func (v awsCloudWatchScrapeJobNoDuplicateCustomNamespaceNamesValidator) MarkdownDescription(ctx context.Context) string {
 	return "No duplicate custom namespace names are allowed."
 }
 
-func (v awsCWScrapeJobNoDuplicateCustomNamespaceNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
-	var customNamespaces []awsCWScrapeJobCustomNamespaceTFModel
+func (v awsCloudWatchScrapeJobNoDuplicateCustomNamespaceNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+	var customNamespaces []awsCloudWatchScrapeJobCustomNamespaceTFModel
 	diags := req.ConfigValue.ElementsAs(ctx, &customNamespaces, true)
 	resp.Diagnostics.Append(diags...)
 	if diags.HasError() {
@@ -174,19 +174,19 @@ func (v awsCWScrapeJobNoDuplicateCustomNamespaceNamesValidator) ValidateList(ctx
 	}
 }
 
-type awsCWScrapeJobNoDuplicateMetricNamesValidator struct{}
+type awsCloudWatchScrapeJobNoDuplicateMetricNamesValidator struct{}
 
-func (v awsCWScrapeJobNoDuplicateMetricNamesValidator) Description(ctx context.Context) string {
+func (v awsCloudWatchScrapeJobNoDuplicateMetricNamesValidator) Description(ctx context.Context) string {
 	return "Metric names must be unique (case-insensitive) within the same service or custom namespace."
 }
 
-func (v awsCWScrapeJobNoDuplicateMetricNamesValidator) MarkdownDescription(ctx context.Context) string {
+func (v awsCloudWatchScrapeJobNoDuplicateMetricNamesValidator) MarkdownDescription(ctx context.Context) string {
 	return "Metric names must be unique (case-insensitive) within the same service or custom namespace."
 }
 
-func (v awsCWScrapeJobNoDuplicateMetricNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+func (v awsCloudWatchScrapeJobNoDuplicateMetricNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
 	seen := map[string]struct{}{}
-	elems := make([]awsCWScrapeJobMetricTFModel, len(req.ConfigValue.Elements()))
+	elems := make([]awsCloudWatchScrapeJobMetricTFModel, len(req.ConfigValue.Elements()))
 	diags := req.ConfigValue.ElementsAs(ctx, &elems, true)
 	resp.Diagnostics.Append(diags...)
 	if diags.HasError() {
@@ -201,8 +201,8 @@ func (v awsCWScrapeJobNoDuplicateMetricNamesValidator) ValidateList(ctx context.
 	}
 }
 
-// toClientModel converts a awsCWScrapeJobTFModel instance to a cloudproviderapi.AWSCloudWatchScrapeJobRequest instance.
-func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (cloudproviderapi.AWSCloudWatchScrapeJobRequest, diag.Diagnostics) {
+// toClientModel converts a awsCloudWatchScrapeJobTFModel instance to a cloudproviderapi.AWSCloudWatchScrapeJobRequest instance.
+func (tfData awsCloudWatchScrapeJobTFResourceModel) toClientModel(ctx context.Context) (cloudproviderapi.AWSCloudWatchScrapeJobRequest, diag.Diagnostics) {
 	conversionDiags := diag.Diagnostics{}
 	converted := cloudproviderapi.AWSCloudWatchScrapeJobRequest{
 		Name:                 tfData.Name.ValueString(),
@@ -217,7 +217,7 @@ func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (
 		return cloudproviderapi.AWSCloudWatchScrapeJobRequest{}, conversionDiags
 	}
 
-	var services []awsCWScrapeJobServiceTFModel
+	var services []awsCloudWatchScrapeJobServiceTFModel
 	diags = tfData.Services.ElementsAs(ctx, &services, false)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
@@ -236,7 +236,7 @@ func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (
 			return cloudproviderapi.AWSCloudWatchScrapeJobRequest{}, conversionDiags
 		}
 
-		var metrics []awsCWScrapeJobMetricTFModel
+		var metrics []awsCloudWatchScrapeJobMetricTFModel
 		diags = service.Metrics.ElementsAs(ctx, &metrics, false)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
@@ -255,7 +255,7 @@ func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (
 			}
 		}
 
-		var tagFilters []awsCWScrapeJobTagFilterTFModel
+		var tagFilters []awsCloudWatchScrapeJobTagFilterTFModel
 		diags = service.ResourceDiscoveryTagFilters.ElementsAs(ctx, &tagFilters, false)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
@@ -270,7 +270,7 @@ func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (
 		}
 	}
 
-	var customNamepsaces []awsCWScrapeJobCustomNamespaceTFModel
+	var customNamepsaces []awsCloudWatchScrapeJobCustomNamespaceTFModel
 	diags = tfData.CustomNamespaces.ElementsAs(ctx, &customNamepsaces, false)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
@@ -283,7 +283,7 @@ func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (
 			ScrapeIntervalSeconds: customNamespace.ScrapeIntervalSeconds.ValueInt64(),
 		}
 
-		var metrics []awsCWScrapeJobMetricTFModel
+		var metrics []awsCloudWatchScrapeJobMetricTFModel
 		diags = customNamespace.Metrics.ElementsAs(ctx, &metrics, false)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
@@ -312,10 +312,10 @@ func (tfData awsCWScrapeJobTFResourceModel) toClientModel(ctx context.Context) (
 	return converted, conversionDiags
 }
 
-// generateCloudWatchScrapeJobTFResourceModel generates a new awsCWScrapeJobTFResourceModel based on the provided cloudproviderapi.AWSCloudWatchScrapeJobResponse
-func generateCloudWatchScrapeJobTFResourceModel(ctx context.Context, stackID string, scrapeJobData cloudproviderapi.AWSCloudWatchScrapeJobResponse) (awsCWScrapeJobTFResourceModel, diag.Diagnostics) {
+// generateCloudWatchScrapeJobTFResourceModel generates a new awsCloudWatchScrapeJobTFResourceModel based on the provided cloudproviderapi.AWSCloudWatchScrapeJobResponse
+func generateCloudWatchScrapeJobTFResourceModel(ctx context.Context, stackID string, scrapeJobData cloudproviderapi.AWSCloudWatchScrapeJobResponse) (awsCloudWatchScrapeJobTFResourceModel, diag.Diagnostics) {
 	conversionDiags := diag.Diagnostics{}
-	converted := awsCWScrapeJobTFResourceModel{
+	converted := awsCloudWatchScrapeJobTFResourceModel{
 		ID:                   types.StringValue(resourceAWSCloudWatchScrapeJobTerraformID.Make(stackID, scrapeJobData.Name)),
 		StackID:              types.StringValue(stackID),
 		Name:                 types.StringValue(scrapeJobData.Name),
@@ -330,23 +330,23 @@ func generateCloudWatchScrapeJobTFResourceModel(ctx context.Context, stackID str
 		regions, diags := types.SetValueFrom(ctx, basetypes.StringType{}, scrapeJobData.Regions)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
-			return awsCWScrapeJobTFResourceModel{}, conversionDiags
+			return awsCloudWatchScrapeJobTFResourceModel{}, conversionDiags
 		}
 		regionsSubsetOverride = regions
 	}
 	converted.RegionsSubsetOverride = regionsSubsetOverride
 
-	services, diags := convertServicesClientToTFModel(ctx, scrapeJobData.Services)
+	services, diags := convertAWSCloudWatchServicesClientToTFModel(ctx, scrapeJobData.Services)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
-		return awsCWScrapeJobTFResourceModel{}, conversionDiags
+		return awsCloudWatchScrapeJobTFResourceModel{}, conversionDiags
 	}
 	converted.Services = services
 
-	customNamespaces, diags := convertCustomNamespacesClientToTFModel(ctx, scrapeJobData.CustomNamespaces)
+	customNamespaces, diags := convertAWSCloudWatchCustomNamespacesClientToTFModel(ctx, scrapeJobData.CustomNamespaces)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
-		return awsCWScrapeJobTFResourceModel{}, conversionDiags
+		return awsCloudWatchScrapeJobTFResourceModel{}, conversionDiags
 	}
 	converted.CustomNamespaces = customNamespaces
 
@@ -355,7 +355,7 @@ func generateCloudWatchScrapeJobTFResourceModel(ctx context.Context, stackID str
 		staticLabelsMap, diags = types.MapValueFrom(ctx, basetypes.StringType{}, scrapeJobData.StaticLabels)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
-			return awsCWScrapeJobTFResourceModel{}, conversionDiags
+			return awsCloudWatchScrapeJobTFResourceModel{}, conversionDiags
 		}
 	}
 	converted.StaticLabels = staticLabelsMap
@@ -363,10 +363,10 @@ func generateCloudWatchScrapeJobTFResourceModel(ctx context.Context, stackID str
 	return converted, conversionDiags
 }
 
-// generateCloudWatchScrapeJobTFDataSourceModel generates a new awsCWScrapeJobTFDataSourceModel based on the provided cloudproviderapi.AWSCloudWatchScrapeJobResponse
-func generateCloudWatchScrapeJobDataSourceTFModel(ctx context.Context, stackID string, scrapeJobData cloudproviderapi.AWSCloudWatchScrapeJobResponse) (awsCWScrapeJobTFDataSourceModel, diag.Diagnostics) {
+// generateCloudWatchScrapeJobTFDataSourceModel generates a new awsCloudWatchScrapeJobTFDataSourceModel based on the provided cloudproviderapi.AWSCloudWatchScrapeJobResponse
+func generateAWSCloudWatchScrapeJobDataSourceTFModel(ctx context.Context, stackID string, scrapeJobData cloudproviderapi.AWSCloudWatchScrapeJobResponse) (awsCloudWatchScrapeJobTFDataSourceModel, diag.Diagnostics) {
 	conversionDiags := diag.Diagnostics{}
-	converted := awsCWScrapeJobTFDataSourceModel{
+	converted := awsCloudWatchScrapeJobTFDataSourceModel{
 		ID:                        types.StringValue(resourceAWSCloudWatchScrapeJobTerraformID.Make(stackID, scrapeJobData.Name)),
 		StackID:                   types.StringValue(stackID),
 		Name:                      types.StringValue(scrapeJobData.Name),
@@ -381,21 +381,21 @@ func generateCloudWatchScrapeJobDataSourceTFModel(ctx context.Context, stackID s
 	regions, diags := types.SetValueFrom(ctx, basetypes.StringType{}, scrapeJobData.Regions)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
-		return awsCWScrapeJobTFDataSourceModel{}, conversionDiags
+		return awsCloudWatchScrapeJobTFDataSourceModel{}, conversionDiags
 	}
 	converted.Regions = regions
 
-	services, diags := convertServicesClientToTFModel(ctx, scrapeJobData.Services)
+	services, diags := convertAWSCloudWatchServicesClientToTFModel(ctx, scrapeJobData.Services)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
-		return awsCWScrapeJobTFDataSourceModel{}, conversionDiags
+		return awsCloudWatchScrapeJobTFDataSourceModel{}, conversionDiags
 	}
 	converted.Services = services
 
-	customNamespaces, diags := convertCustomNamespacesClientToTFModel(ctx, scrapeJobData.CustomNamespaces)
+	customNamespaces, diags := convertAWSCloudWatchCustomNamespacesClientToTFModel(ctx, scrapeJobData.CustomNamespaces)
 	conversionDiags.Append(diags...)
 	if conversionDiags.HasError() {
-		return awsCWScrapeJobTFDataSourceModel{}, conversionDiags
+		return awsCloudWatchScrapeJobTFDataSourceModel{}, conversionDiags
 	}
 	converted.CustomNamespaces = customNamespaces
 
@@ -404,7 +404,7 @@ func generateCloudWatchScrapeJobDataSourceTFModel(ctx context.Context, stackID s
 		staticLabelsMap, diags = types.MapValueFrom(ctx, basetypes.StringType{}, scrapeJobData.StaticLabels)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
-			return awsCWScrapeJobTFDataSourceModel{}, conversionDiags
+			return awsCloudWatchScrapeJobTFDataSourceModel{}, conversionDiags
 		}
 	}
 	converted.StaticLabels = staticLabelsMap
@@ -412,20 +412,20 @@ func generateCloudWatchScrapeJobDataSourceTFModel(ctx context.Context, stackID s
 	return converted, conversionDiags
 }
 
-func convertServicesClientToTFModel(ctx context.Context, services []cloudproviderapi.AWSCloudWatchService) (types.List, diag.Diagnostics) {
+func convertAWSCloudWatchServicesClientToTFModel(ctx context.Context, services []cloudproviderapi.AWSCloudWatchService) (types.List, diag.Diagnostics) {
 	conversionDiags := diag.Diagnostics{}
-	servicesTF := make([]awsCWScrapeJobServiceTFModel, len(services))
-	servicesListObjType := types.ObjectType{AttrTypes: awsCWScrapeJobServiceTFModel{}.attrTypes()}
+	servicesTF := make([]awsCloudWatchScrapeJobServiceTFModel, len(services))
+	servicesListObjType := types.ObjectType{AttrTypes: awsCloudWatchScrapeJobServiceTFModel{}.attrTypes()}
 
 	for i, service := range services {
-		serviceTF := awsCWScrapeJobServiceTFModel{
+		serviceTF := awsCloudWatchScrapeJobServiceTFModel{
 			Name:                  types.StringValue(service.Name),
 			ScrapeIntervalSeconds: types.Int64Value(service.ScrapeIntervalSeconds),
 		}
 
-		metricsTF := make([]awsCWScrapeJobMetricTFModel, len(service.Metrics))
+		metricsTF := make([]awsCloudWatchScrapeJobMetricTFModel, len(service.Metrics))
 		for j, metric := range service.Metrics {
-			metricsTF[j] = awsCWScrapeJobMetricTFModel{
+			metricsTF[j] = awsCloudWatchScrapeJobMetricTFModel{
 				Name: types.StringValue(metric.Name),
 			}
 			statistics, diags := types.SetValueFrom(ctx, basetypes.StringType{}, services[i].Metrics[j].Statistics)
@@ -435,21 +435,21 @@ func convertServicesClientToTFModel(ctx context.Context, services []cloudprovide
 			}
 			metricsTF[j].Statistics = statistics
 		}
-		metricsTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsCWScrapeJobMetricTFModel{}.attrTypes()}, metricsTF)
+		metricsTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsCloudWatchScrapeJobMetricTFModel{}.attrTypes()}, metricsTF)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
 			return types.ListNull(servicesListObjType), conversionDiags
 		}
 		serviceTF.Metrics = metricsTFList
 
-		tagFiltersTF := make([]awsCWScrapeJobTagFilterTFModel, len(service.ResourceDiscoveryTagFilters))
+		tagFiltersTF := make([]awsCloudWatchScrapeJobTagFilterTFModel, len(service.ResourceDiscoveryTagFilters))
 		for j, tagFilter := range service.ResourceDiscoveryTagFilters {
-			tagFiltersTF[j] = awsCWScrapeJobTagFilterTFModel{
+			tagFiltersTF[j] = awsCloudWatchScrapeJobTagFilterTFModel{
 				Key:   types.StringValue(tagFilter.Key),
 				Value: types.StringValue(tagFilter.Value),
 			}
 		}
-		tagFiltersTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsCWScrapeJobTagFilterTFModel{}.attrTypes()}, tagFiltersTF)
+		tagFiltersTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsCloudWatchScrapeJobTagFilterTFModel{}.attrTypes()}, tagFiltersTF)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
 			return types.ListNull(servicesListObjType), conversionDiags
@@ -478,20 +478,20 @@ func convertServicesClientToTFModel(ctx context.Context, services []cloudprovide
 	return servicesTFList, conversionDiags
 }
 
-func convertCustomNamespacesClientToTFModel(ctx context.Context, customNamespaces []cloudproviderapi.AWSCloudWatchCustomNamespace) (types.List, diag.Diagnostics) {
+func convertAWSCloudWatchCustomNamespacesClientToTFModel(ctx context.Context, customNamespaces []cloudproviderapi.AWSCloudWatchCustomNamespace) (types.List, diag.Diagnostics) {
 	conversionDiags := diag.Diagnostics{}
-	customNamespacesTF := make([]awsCWScrapeJobCustomNamespaceTFModel, len(customNamespaces))
-	customNamspacesListObjType := types.ObjectType{AttrTypes: awsCWScrapeJobCustomNamespaceTFModel{}.attrTypes()}
+	customNamespacesTF := make([]awsCloudWatchScrapeJobCustomNamespaceTFModel, len(customNamespaces))
+	customNamspacesListObjType := types.ObjectType{AttrTypes: awsCloudWatchScrapeJobCustomNamespaceTFModel{}.attrTypes()}
 
 	for i, customNamespace := range customNamespaces {
-		customNamespaceTF := awsCWScrapeJobCustomNamespaceTFModel{
+		customNamespaceTF := awsCloudWatchScrapeJobCustomNamespaceTFModel{
 			Name:                  types.StringValue(customNamespace.Name),
 			ScrapeIntervalSeconds: types.Int64Value(customNamespace.ScrapeIntervalSeconds),
 		}
 
-		metricsTF := make([]awsCWScrapeJobMetricTFModel, len(customNamespace.Metrics))
+		metricsTF := make([]awsCloudWatchScrapeJobMetricTFModel, len(customNamespace.Metrics))
 		for j, metric := range customNamespace.Metrics {
-			metricsTF[j] = awsCWScrapeJobMetricTFModel{
+			metricsTF[j] = awsCloudWatchScrapeJobMetricTFModel{
 				Name: types.StringValue(metric.Name),
 			}
 			statistics, diags := types.SetValueFrom(ctx, basetypes.StringType{}, customNamespaces[i].Metrics[j].Statistics)
@@ -501,7 +501,7 @@ func convertCustomNamespacesClientToTFModel(ctx context.Context, customNamespace
 			}
 			metricsTF[j].Statistics = statistics
 		}
-		metricsTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsCWScrapeJobMetricTFModel{}.attrTypes()}, metricsTF)
+		metricsTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsCloudWatchScrapeJobMetricTFModel{}.attrTypes()}, metricsTF)
 		conversionDiags.Append(diags...)
 		if conversionDiags.HasError() {
 			return types.ListNull(customNamspacesListObjType), conversionDiags
diff --git a/internal/resources/cloudprovider/models_aws_resource_metadata.go b/internal/resources/cloudprovider/models_aws_resource_metadata.go
new file mode 100644
index 00000000..53956eba
--- /dev/null
+++ b/internal/resources/cloudprovider/models_aws_resource_metadata.go
@@ -0,0 +1,217 @@
+package cloudprovider
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+type awsResourceMetadataScrapeJobTFResourceModel struct {
+	ID                    types.String `tfsdk:"id"`
+	StackID               types.String `tfsdk:"stack_id"`
+	Name                  types.String `tfsdk:"name"`
+	Enabled               types.Bool   `tfsdk:"enabled"`
+	AWSAccountResourceID  types.String `tfsdk:"aws_account_resource_id"`
+	RegionsSubsetOverride types.Set    `tfsdk:"regions_subset_override"`
+	DisabledReason        types.String `tfsdk:"disabled_reason"`
+	// TODO(tristan): if the grafana provider is updated to use the Terraform v6 plugin protocol,
+	// we can consider adding additional support to use Set Nested Attributes, instead of Blocks.
+	// See https://developer.hashicorp.com/terraform/plugin/framework/handling-data/attributes#nested-attribute-types
+	Services     types.List `tfsdk:"service"`
+	StaticLabels types.Map  `tfsdk:"static_labels"`
+}
+
+type awsResourceMetadataScrapeJobServiceTFModel struct {
+	Name                        types.String `tfsdk:"name"`
+	ScrapeIntervalSeconds       types.Int64  `tfsdk:"scrape_interval_seconds"`
+	ResourceDiscoveryTagFilters types.List   `tfsdk:"resource_discovery_tag_filter"`
+}
+
+func (m awsResourceMetadataScrapeJobServiceTFModel) attrTypes() map[string]attr.Type {
+	return map[string]attr.Type{
+		"name":                    types.StringType,
+		"scrape_interval_seconds": types.Int64Type,
+		"resource_discovery_tag_filter": types.ListType{
+			ElemType: types.ObjectType{
+				AttrTypes: awsResourceMetadataScrapeJobTagFilterTFModel{}.attrTypes(),
+			},
+		},
+	}
+}
+
+type awsResourceMetadataScrapeJobTagFilterTFModel struct {
+	Key   types.String `tfsdk:"key"`
+	Value types.String `tfsdk:"value"`
+}
+
+func (m awsResourceMetadataScrapeJobTagFilterTFModel) attrTypes() map[string]attr.Type {
+	return map[string]attr.Type{
+		"key":   types.StringType,
+		"value": types.StringType,
+	}
+}
+
+type awsResourceMetadataScrapeJobNoDuplicateServiceNamesValidator struct{}
+
+func (v awsResourceMetadataScrapeJobNoDuplicateServiceNamesValidator) Description(ctx context.Context) string {
+	return "No duplicate service names are allowed."
+}
+
+func (v awsResourceMetadataScrapeJobNoDuplicateServiceNamesValidator) MarkdownDescription(ctx context.Context) string {
+	return "No duplicate service names are allowed."
+}
+
+func (v awsResourceMetadataScrapeJobNoDuplicateServiceNamesValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) {
+	var services []awsResourceMetadataScrapeJobServiceTFModel
+	diags := req.ConfigValue.ElementsAs(ctx, &services, true)
+	resp.Diagnostics.Append(diags...)
+	if diags.HasError() {
+		return
+	}
+	seen := map[string]struct{}{}
+	for _, service := range services {
+		name := service.Name.ValueString()
+		if _, ok := seen[name]; ok {
+			resp.Diagnostics.AddError("Duplicate service name", fmt.Sprintf("Service name %q is duplicated.", name))
+		}
+		seen[name] = struct{}{}
+	}
+}
+
+// toClientModel converts a awsResourceMetadataScrapeJobTFModel instance to a cloudproviderapi.AWSResourceMetadataScrapeJobRequest instance.
+func (tfData awsResourceMetadataScrapeJobTFResourceModel) toClientModel(ctx context.Context) (cloudproviderapi.AWSResourceMetadataScrapeJobRequest, diag.Diagnostics) {
+	conversionDiags := diag.Diagnostics{}
+	converted := cloudproviderapi.AWSResourceMetadataScrapeJobRequest{
+		Name:                 tfData.Name.ValueString(),
+		Enabled:              tfData.Enabled.ValueBool(),
+		AWSAccountResourceID: tfData.AWSAccountResourceID.ValueString(),
+	}
+
+	diags := tfData.RegionsSubsetOverride.ElementsAs(ctx, &converted.RegionsSubsetOverride, false)
+	conversionDiags.Append(diags...)
+	if conversionDiags.HasError() {
+		return cloudproviderapi.AWSResourceMetadataScrapeJobRequest{}, conversionDiags
+	}
+
+	var services []awsResourceMetadataScrapeJobServiceTFModel
+	diags = tfData.Services.ElementsAs(ctx, &services, false)
+	conversionDiags.Append(diags...)
+	if conversionDiags.HasError() {
+		return cloudproviderapi.AWSResourceMetadataScrapeJobRequest{}, conversionDiags
+	}
+	converted.Services = make([]cloudproviderapi.AWSResourceMetadataService, len(services))
+	for i, service := range services {
+		converted.Services[i] = cloudproviderapi.AWSResourceMetadataService{
+			Name:                  service.Name.ValueString(),
+			ScrapeIntervalSeconds: service.ScrapeIntervalSeconds.ValueInt64(),
+		}
+
+		var tagFilters []awsResourceMetadataScrapeJobTagFilterTFModel
+		diags = service.ResourceDiscoveryTagFilters.ElementsAs(ctx, &tagFilters, false)
+		conversionDiags.Append(diags...)
+		if conversionDiags.HasError() {
+			return cloudproviderapi.AWSResourceMetadataScrapeJobRequest{}, conversionDiags
+		}
+		converted.Services[i].ResourceDiscoveryTagFilters = make([]cloudproviderapi.AWSResourceMetadataTagFilter, len(tagFilters))
+		for j, tagFilter := range tagFilters {
+			converted.Services[i].ResourceDiscoveryTagFilters[j] = cloudproviderapi.AWSResourceMetadataTagFilter{
+				Key:   tagFilter.Key.ValueString(),
+				Value: tagFilter.Value.ValueString(),
+			}
+		}
+	}
+
+	diags = tfData.StaticLabels.ElementsAs(ctx, &converted.StaticLabels, false)
+	conversionDiags.Append(diags...)
+	if conversionDiags.HasError() {
+		return cloudproviderapi.AWSResourceMetadataScrapeJobRequest{}, conversionDiags
+	}
+
+	return converted, conversionDiags
+}
+
+// generateAWSResourceMetadataScrapeJobTFResourceModel generates a new awsResourceMetadataScrapeJobTFResourceModel based on the provided cloudproviderapi.AWSResourceMetadataScrapeJobResponse
+func generateAWSResourceMetadataScrapeJobTFResourceModel(ctx context.Context, stackID string, scrapeJobData cloudproviderapi.AWSResourceMetadataScrapeJobResponse) (awsResourceMetadataScrapeJobTFResourceModel, diag.Diagnostics) {
+	conversionDiags := diag.Diagnostics{}
+	converted := awsResourceMetadataScrapeJobTFResourceModel{
+		ID:                   types.StringValue(resourceAWSResourceMetadataScrapeJobTerraformID.Make(stackID, scrapeJobData.Name)),
+		StackID:              types.StringValue(stackID),
+		Name:                 types.StringValue(scrapeJobData.Name),
+		Enabled:              types.BoolValue(scrapeJobData.Enabled),
+		AWSAccountResourceID: types.StringValue(scrapeJobData.AWSAccountResourceID),
+		DisabledReason:       types.StringValue(scrapeJobData.DisabledReason),
+	}
+
+	regionsSubsetOverride := types.SetValueMust(basetypes.StringType{}, []attr.Value{})
+	if scrapeJobData.RegionsSubsetOverrideUsed {
+		regions, diags := types.SetValueFrom(ctx, basetypes.StringType{}, scrapeJobData.Regions)
+		conversionDiags.Append(diags...)
+		if conversionDiags.HasError() {
+			return awsResourceMetadataScrapeJobTFResourceModel{}, conversionDiags
+		}
+		regionsSubsetOverride = regions
+	}
+	converted.RegionsSubsetOverride = regionsSubsetOverride
+
+	services, diags := convertAWSResourceMetadataServicesClientToTFModel(ctx, scrapeJobData.Services)
+	conversionDiags.Append(diags...)
+	if conversionDiags.HasError() {
+		return awsResourceMetadataScrapeJobTFResourceModel{}, conversionDiags
+	}
+	converted.Services = services
+
+	staticLabelsMap := types.MapValueMust(types.StringType, map[string]attr.Value{})
+	if scrapeJobData.StaticLabels != nil {
+		staticLabelsMap, diags = types.MapValueFrom(ctx, basetypes.StringType{}, scrapeJobData.StaticLabels)
+		conversionDiags.Append(diags...)
+		if conversionDiags.HasError() {
+			return awsResourceMetadataScrapeJobTFResourceModel{}, conversionDiags
+		}
+	}
+	converted.StaticLabels = staticLabelsMap
+
+	return converted, conversionDiags
+}
+
+func convertAWSResourceMetadataServicesClientToTFModel(ctx context.Context, services []cloudproviderapi.AWSResourceMetadataService) (types.List, diag.Diagnostics) {
+	conversionDiags := diag.Diagnostics{}
+	servicesTF := make([]awsResourceMetadataScrapeJobServiceTFModel, len(services))
+	servicesListObjType := types.ObjectType{AttrTypes: awsResourceMetadataScrapeJobServiceTFModel{}.attrTypes()}
+
+	for i, service := range services {
+		serviceTF := awsResourceMetadataScrapeJobServiceTFModel{
+			Name:                  types.StringValue(service.Name),
+			ScrapeIntervalSeconds: types.Int64Value(service.ScrapeIntervalSeconds),
+		}
+
+		tagFiltersTF := make([]awsResourceMetadataScrapeJobTagFilterTFModel, len(service.ResourceDiscoveryTagFilters))
+		for j, tagFilter := range service.ResourceDiscoveryTagFilters {
+			tagFiltersTF[j] = awsResourceMetadataScrapeJobTagFilterTFModel{
+				Key:   types.StringValue(tagFilter.Key),
+				Value: types.StringValue(tagFilter.Value),
+			}
+		}
+		tagFiltersTFList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: awsResourceMetadataScrapeJobTagFilterTFModel{}.attrTypes()}, tagFiltersTF)
+		conversionDiags.Append(diags...)
+		if conversionDiags.HasError() {
+			return types.ListNull(servicesListObjType), conversionDiags
+		}
+		serviceTF.ResourceDiscoveryTagFilters = tagFiltersTFList
+
+		servicesTF[i] = serviceTF
+	}
+
+	servicesTFList, diags := types.ListValueFrom(ctx, servicesListObjType, servicesTF)
+	conversionDiags.Append(diags...)
+	if conversionDiags.HasError() {
+		return types.ListNull(servicesListObjType), conversionDiags
+	}
+
+	return servicesTFList, conversionDiags
+}
diff --git a/internal/resources/cloudprovider/resource_aws_cloudwatch_scrape_job.go b/internal/resources/cloudprovider/resource_aws_cloudwatch_scrape_job.go
index e3aea618..4fffe8fa 100644
--- a/internal/resources/cloudprovider/resource_aws_cloudwatch_scrape_job.go
+++ b/internal/resources/cloudprovider/resource_aws_cloudwatch_scrape_job.go
@@ -80,14 +80,14 @@ func (r resourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req resource
 				},
 			},
 			"name": schema.StringAttribute{
-				Description: "The name of the CloudWatch Scrape Job. Part of the Terraform Resource ID.",
+				Description: "The name of the AWS CloudWatch Scrape Job. Part of the Terraform Resource ID.",
 				Required:    true,
 				PlanModifiers: []planmodifier.String{
 					stringplanmodifier.RequiresReplace(),
 				},
 			},
 			"enabled": schema.BoolAttribute{
-				Description: "Whether the CloudWatch Scrape Job is enabled or not.",
+				Description: "Whether the AWS CloudWatch Scrape Job is enabled or not.",
 				Optional:    true,
 				Computed:    true,
 				Default:     booldefault.StaticBool(true),
@@ -110,7 +110,7 @@ func (r resourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req resource
 				Default:     booldefault.StaticBool(true),
 			},
 			"disabled_reason": schema.StringAttribute{
-				Description: "When the CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.",
+				Description: "When the AWS CloudWatch Scrape Job is disabled, this will show the reason that it is in that state.",
 				Computed:    true,
 			},
 			"static_labels": schema.MapAttribute{
@@ -123,10 +123,10 @@ func (r resourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req resource
 		},
 		Blocks: map[string]schema.Block{
 			"service": schema.ListNestedBlock{
-				Description: "One or more configuration blocks to configure AWS services for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
+				Description: "One or more configuration blocks to configure AWS services for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
 				Validators: []validator.List{
 					listvalidator.SizeAtLeast(1),
-					awsCWScrapeJobNoDuplicateServiceNamesValidator{},
+					awsCloudWatchScrapeJobNoDuplicateServiceNamesValidator{},
 				},
 				NestedObject: schema.NestedBlockObject{
 					Attributes: map[string]schema.Attribute{
@@ -153,7 +153,7 @@ func (r resourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req resource
 							Description: "One or more configuration blocks to configure metrics and their statistics to scrape. Please note that AWS metric names must be supplied, and not their PromQL counterparts. Each block must represent a distinct metric name. When accessing this as an attribute reference, it is a list of objects.",
 							Validators: []validator.List{
 								listvalidator.SizeAtLeast(1),
-								awsCWScrapeJobNoDuplicateMetricNamesValidator{},
+								awsCloudWatchScrapeJobNoDuplicateMetricNamesValidator{},
 							},
 							NestedObject: schema.NestedBlockObject{
 								Attributes: map[string]schema.Attribute{
@@ -191,9 +191,9 @@ func (r resourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req resource
 				},
 			},
 			"custom_namespace": schema.ListNestedBlock{
-				Description: "Zero or more configuration blocks to configure custom namespaces for the CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
+				Description: "Zero or more configuration blocks to configure custom namespaces for the AWS CloudWatch Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
 				Validators: []validator.List{
-					awsCWScrapeJobNoDuplicateCustomNamespaceNamesValidator{},
+					awsCloudWatchScrapeJobNoDuplicateCustomNamespaceNamesValidator{},
 				},
 				NestedObject: schema.NestedBlockObject{
 					Attributes: map[string]schema.Attribute{
@@ -213,7 +213,7 @@ func (r resourceAWSCloudWatchScrapeJob) Schema(ctx context.Context, req resource
 							Description: "One or more configuration blocks to configure metrics and their statistics to scrape. Each block must represent a distinct metric name. When accessing this as an attribute reference, it is a list of objects.",
 							Validators: []validator.List{
 								listvalidator.SizeAtLeast(1),
-								awsCWScrapeJobNoDuplicateMetricNamesValidator{},
+								awsCloudWatchScrapeJobNoDuplicateMetricNamesValidator{},
 							},
 							NestedObject: schema.NestedBlockObject{
 								Attributes: map[string]schema.Attribute{
@@ -268,7 +268,7 @@ func (r resourceAWSCloudWatchScrapeJob) ImportState(ctx context.Context, req res
 }
 
 func (r resourceAWSCloudWatchScrapeJob) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
-	var data awsCWScrapeJobTFResourceModel
+	var data awsCloudWatchScrapeJobTFResourceModel
 	diags := req.Plan.Get(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
@@ -297,7 +297,7 @@ func (r resourceAWSCloudWatchScrapeJob) Create(ctx context.Context, req resource
 }
 
 func (r resourceAWSCloudWatchScrapeJob) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
-	var data awsCWScrapeJobTFResourceModel
+	var data awsCloudWatchScrapeJobTFResourceModel
 	diags := req.State.Get(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
@@ -325,14 +325,14 @@ func (r resourceAWSCloudWatchScrapeJob) Read(ctx context.Context, req resource.R
 
 func (r resourceAWSCloudWatchScrapeJob) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
 	// This must be a pointer because ModifyPlan is called even on resource creation, when no state exists yet.
-	var stateData *awsCWScrapeJobTFResourceModel
+	var stateData *awsCloudWatchScrapeJobTFResourceModel
 	diags := req.State.Get(ctx, &stateData)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
 		return
 	}
 
-	var planData *awsCWScrapeJobTFResourceModel
+	var planData *awsCloudWatchScrapeJobTFResourceModel
 	diags = req.Plan.Get(ctx, &planData)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
@@ -357,7 +357,7 @@ func (r resourceAWSCloudWatchScrapeJob) ModifyPlan(ctx context.Context, req reso
 }
 
 func (r resourceAWSCloudWatchScrapeJob) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
-	var planData awsCWScrapeJobTFResourceModel
+	var planData awsCloudWatchScrapeJobTFResourceModel
 	diags := req.Plan.Get(ctx, &planData)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
@@ -386,7 +386,7 @@ func (r resourceAWSCloudWatchScrapeJob) Update(ctx context.Context, req resource
 }
 
 func (r resourceAWSCloudWatchScrapeJob) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
-	var data awsCWScrapeJobTFResourceModel
+	var data awsCloudWatchScrapeJobTFResourceModel
 	diags := req.State.Get(ctx, &data)
 	resp.Diagnostics.Append(diags...)
 	if resp.Diagnostics.HasError() {
diff --git a/internal/resources/cloudprovider/resource_aws_resource_metadata_scrape_job.go b/internal/resources/cloudprovider/resource_aws_resource_metadata_scrape_job.go
new file mode 100644
index 00000000..8a472678
--- /dev/null
+++ b/internal/resources/cloudprovider/resource_aws_resource_metadata_scrape_job.go
@@ -0,0 +1,325 @@
+package cloudprovider
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
+	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+)
+
+var (
+	resourceAWSResourceMetadataScrapeJobTerraformName = "grafana_cloud_provider_aws_resource_metadata_scrape_job"
+	resourceAWSResourceMetadataScrapeJobTerraformID   = common.NewResourceID(common.StringIDField("stack_id"), common.StringIDField("name"))
+)
+
+type resourceAWSResourceMetadataScrapeJob struct {
+	client *cloudproviderapi.Client
+}
+
+func makeResourceAWSResourceMetadataScrapeJob() *common.Resource {
+	return common.NewResource(
+		common.CategoryCloudProvider,
+		"grafana_cloud_provider_aws_resource_metadata_scrape_job",
+		resourceAWSResourceMetadataScrapeJobTerraformID,
+		&resourceAWSResourceMetadataScrapeJob{},
+	)
+}
+
+func (r *resourceAWSResourceMetadataScrapeJob) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	// Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once
+	if req.ProviderData == nil || r.client != nil {
+		return
+	}
+
+	client, err := withClientForResource(req, resp)
+	if err != nil {
+		return
+	}
+
+	r.client = client
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceAWSResourceMetadataScrapeJobTerraformName
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: "The Terraform Resource ID. This has the format \"{{ stack_id }}:{{ name }}\".",
+				Computed:    true,
+				PlanModifiers: []planmodifier.String{
+					// See https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification#usestateforunknown
+					// for details on how this works.
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"stack_id": schema.StringAttribute{
+				Description: "The Stack ID of the Grafana Cloud instance. Part of the Terraform Resource ID.",
+				Required:    true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"name": schema.StringAttribute{
+				Description: "The name of the AWS Resource Metadata Scrape Job. Part of the Terraform Resource ID.",
+				Required:    true,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"enabled": schema.BoolAttribute{
+				Description: "Whether the AWS Resource Metadata Scrape Job is enabled or not.",
+				Optional:    true,
+				Computed:    true,
+				Default:     booldefault.StaticBool(true),
+			},
+			"aws_account_resource_id": schema.StringAttribute{
+				Description: "The ID assigned by the Grafana Cloud Provider API to an AWS Account resource that should be associated with this Resource Metadata Scrape Job. This can be provided by the `resource_id` attribute of the `grafana_cloud_provider_aws_account` resource.",
+				Required:    true,
+			},
+			"regions_subset_override": schema.SetAttribute{
+				Description: "A subset of the regions that are configured in the associated AWS Account resource to apply to this scrape job. If not set or empty, all of the Account resource's regions are scraped.",
+				Optional:    true,
+				Computed:    true,
+				ElementType: types.StringType,
+				Default:     setdefault.StaticValue(types.SetValueMust(types.StringType, []attr.Value{})),
+			},
+			"disabled_reason": schema.StringAttribute{
+				Description: "When the AWS Resource Metadata Scrape Job is disabled, this will show the reason that it is in that state.",
+				Computed:    true,
+			},
+			"static_labels": schema.MapAttribute{
+				Description: "A set of static labels to add to all metrics exported by this scrape job.",
+				Optional:    true,
+				Computed:    true,
+				ElementType: basetypes.StringType{},
+				Default:     mapdefault.StaticValue(types.MapValueMust(types.StringType, map[string]attr.Value{})),
+			},
+		},
+		Blocks: map[string]schema.Block{
+			"service": schema.ListNestedBlock{
+				Description: "One or more configuration blocks to configure AWS services for the Resource Metadata Scrape Job to scrape. Each block must have a distinct `name` attribute. When accessing this as an attribute reference, it is a list of objects.",
+				Validators: []validator.List{
+					listvalidator.SizeAtLeast(1),
+					awsResourceMetadataScrapeJobNoDuplicateServiceNamesValidator{},
+				},
+				NestedObject: schema.NestedBlockObject{
+					Attributes: map[string]schema.Attribute{
+						"name": schema.StringAttribute{
+							Description: "The name of the service to scrape. See https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/aws/cloudwatch-metrics/services/ for supported services.",
+							Required:    true,
+						},
+						"scrape_interval_seconds": schema.Int64Attribute{
+							Description: "The interval in seconds to scrape the service. See https://grafana.com/docs/grafana-cloud/monitor-infrastructure/monitor-cloud-provider/aws/cloudwatch-metrics/services/ for supported scrape intervals.",
+							Optional:    true,
+							Computed:    true,
+							Default:     int64default.StaticInt64(300),
+						},
+					},
+					Blocks: map[string]schema.Block{
+						"resource_discovery_tag_filter": schema.ListNestedBlock{
+							Description: "One or more configuration blocks to configure tag filters applied to discovery of resource entities in the associated AWS account. When accessing this as an attribute reference, it is a list of objects.",
+							NestedObject: schema.NestedBlockObject{
+								Attributes: map[string]schema.Attribute{
+									"key": schema.StringAttribute{
+										Description: "The key of the tag filter.",
+										Required:    true,
+									},
+									"value": schema.StringAttribute{
+										Description: "The value of the tag filter.",
+										Required:    true,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	parts := strings.SplitN(req.ID, ":", 2)
+	if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
+		resp.Diagnostics.AddError("Invalid ID", fmt.Sprintf("Invalid ID: %s", req.ID))
+		return
+	}
+	stackID := parts[0]
+	jobName := parts[1]
+
+	jobResp, err := r.client.GetAWSResourceMetadataScrapeJob(
+		ctx,
+		stackID,
+		jobName,
+	)
+	if err != nil {
+		resp.Diagnostics.AddError("Failed to get AWS Resource Metadata scrape job", err.Error())
+		return
+	}
+
+	jobTF, diags := generateAWSResourceMetadataScrapeJobTFResourceModel(ctx, stackID, jobResp)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	resp.State.Set(ctx, jobTF)
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var data awsResourceMetadataScrapeJobTFResourceModel
+	diags := req.Plan.Get(ctx, &data)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	jobReq, diags := data.toClientModel(ctx)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	jobResp, err := r.client.CreateAWSResourceMetadataScrapeJob(ctx, data.StackID.ValueString(), jobReq)
+	if err != nil {
+		resp.Diagnostics.AddError("Failed to create AWS Resource Metadata scrape job", err.Error())
+		return
+	}
+
+	jobTF, diags := generateAWSResourceMetadataScrapeJobTFResourceModel(ctx, data.StackID.ValueString(), jobResp)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	resp.State.Set(ctx, &jobTF)
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var data awsResourceMetadataScrapeJobTFResourceModel
+	diags := req.State.Get(ctx, &data)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	jobResp, err := r.client.GetAWSResourceMetadataScrapeJob(
+		ctx,
+		data.StackID.ValueString(),
+		data.Name.ValueString(),
+	)
+	if err != nil {
+		resp.Diagnostics.AddError("Failed to get AWS Resource Metadata scrape job", err.Error())
+		return
+	}
+
+	jobTF, diags := generateAWSResourceMetadataScrapeJobTFResourceModel(ctx, data.StackID.ValueString(), jobResp)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	resp.State.Set(ctx, jobTF)
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+	// This must be a pointer because ModifyPlan is called even on resource creation, when no state exists yet.
+	var stateData *awsResourceMetadataScrapeJobTFResourceModel
+	diags := req.State.Get(ctx, &stateData)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	var planData *awsResourceMetadataScrapeJobTFResourceModel
+	diags = req.Plan.Get(ctx, &planData)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// resource is being deleted
+	if planData == nil {
+		return
+	}
+
+	// This helps reduce the occurrences of Unknown states for the disabled_reason attribute
+	// by tying it to how the enabled attribute will change.
+	switch {
+	case (stateData == nil || stateData.Enabled.ValueBool()) && !planData.Enabled.ValueBool():
+		resp.Plan.SetAttribute(ctx, path.Root("disabled_reason"), basetypes.NewStringUnknown())
+	case (stateData == nil || !stateData.Enabled.ValueBool()) && planData.Enabled.ValueBool():
+		resp.Plan.SetAttribute(ctx, path.Root("disabled_reason"), basetypes.NewStringValue(""))
+	default:
+		resp.Plan.SetAttribute(ctx, path.Root("disabled_reason"), basetypes.NewStringValue(stateData.DisabledReason.ValueString()))
+	}
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	var planData awsResourceMetadataScrapeJobTFResourceModel
+	diags := req.Plan.Get(ctx, &planData)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	jobReq, diags := planData.toClientModel(ctx)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	jobResp, err := r.client.UpdateAWSResourceMetadataScrapeJob(ctx, planData.StackID.ValueString(), planData.Name.ValueString(), jobReq)
+	if err != nil {
+		resp.Diagnostics.AddError("Failed to update AWS Resource Metadata scrape job", err.Error())
+		return
+	}
+
+	jobTF, diags := generateAWSResourceMetadataScrapeJobTFResourceModel(ctx, planData.StackID.ValueString(), jobResp)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	resp.State.Set(ctx, jobTF)
+}
+
+func (r resourceAWSResourceMetadataScrapeJob) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var data awsResourceMetadataScrapeJobTFResourceModel
+	diags := req.State.Get(ctx, &data)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	err := r.client.DeleteAWSResourceMetadataScrapeJob(
+		ctx,
+		data.StackID.ValueString(),
+		data.Name.ValueString(),
+	)
+	if err != nil {
+		resp.Diagnostics.AddError("Failed to delete AWS Resource Metadata scrape job", err.Error())
+		return
+	}
+
+	resp.State.Set(ctx, nil)
+}
diff --git a/internal/resources/cloudprovider/resources.go b/internal/resources/cloudprovider/resources.go
index 1f8895fb..133469b0 100644
--- a/internal/resources/cloudprovider/resources.go
+++ b/internal/resources/cloudprovider/resources.go
@@ -19,6 +19,7 @@ var DataSources = []*common.DataSource{
 var Resources = []*common.Resource{
 	makeResourceAWSAccount(),
 	makeResourceAWSCloudWatchScrapeJob(),
+	makeResourceAWSResourceMetadataScrapeJob(),
 	makeResourceAzureCredential(),
 }
 
diff --git a/internal/resources/frontendo11y/resource_frontend_o11y_app.go b/internal/resources/frontendo11y/resource_frontend_o11y_app.go
index e01e6305..0c6c40ec 100644
--- a/internal/resources/frontendo11y/resource_frontend_o11y_app.go
+++ b/internal/resources/frontendo11y/resource_frontend_o11y_app.go
@@ -5,6 +5,8 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"strconv"
+	"strings"
 
 	"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
 	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -25,6 +27,9 @@ import (
 var (
 	resourceFrontendO11yAppName        = "grafana_frontend_o11y_app"
 	resourceFrontendO11yAppTerraformID = common.NewResourceID(common.StringIDField("stack_id"), common.StringIDField("name"))
+
+	// Check interface
+	_ resource.ResourceWithImportState = (*resourceFrontendO11yApp)(nil)
 )
 
 type resourceFrontendO11yApp struct {
@@ -135,6 +140,22 @@ func (r *resourceFrontendO11yApp) getStackCluster(ctx context.Context, stackID s
 	return stack.ClusterSlug, nil
 }
 
+func (r *resourceFrontendO11yApp) getStack(ctx context.Context, stackID string) (*gcom.FormattedApiInstance, error) {
+	stack, res, err := r.gcomClient.InstancesAPI.GetInstance(ctx, stackID).Execute()
+	if err != nil {
+		return nil, err
+	}
+
+	if res.StatusCode >= 500 {
+		return nil, errors.New("server error")
+	}
+
+	if res.StatusCode == http.StatusNotFound {
+		return nil, fmt.Errorf("stack %q not found", stackID)
+	}
+	return stack, nil
+}
+
 func (r *resourceFrontendO11yApp) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
 	var dataTF FrontendO11yAppTFModel
 	diags := req.Plan.Get(ctx, &dataTF)
@@ -163,6 +184,42 @@ func (r *resourceFrontendO11yApp) Create(ctx context.Context, req resource.Creat
 	resp.State.Set(ctx, appTFState)
 }
 
+func (r *resourceFrontendO11yApp) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	reqParts := strings.Split(req.ID, ":")
+	if len(reqParts) != 2 {
+		resp.Diagnostics.AddError("incorrect ID format", "Resource ID should be in the format of 'stackID:appID'")
+		return
+	}
+	stackSlug := reqParts[0]
+	appID := reqParts[1]
+	i64AppID, err := strconv.ParseInt(appID, 10, 64)
+	if err != nil {
+		resp.Diagnostics.AddError("invalid app ID", err.Error())
+		return
+	}
+
+	stack, err := r.getStack(ctx, stackSlug)
+	if err != nil {
+		resp.Diagnostics.AddError("failed to get Grafana Cloud Stack information", err.Error())
+		return
+	}
+	appClientModel, err := r.client.GetApp(
+		ctx,
+		apiURLForCluster(stack.ClusterSlug, r.client.Host()),
+		int64(stack.Id),
+		i64AppID,
+	)
+
+	if err != nil {
+		resp.Diagnostics.AddError("failed to get frontend o11y app", err.Error())
+		return
+	}
+
+	clientTFData, diags := convertClientModelToTFModel(int64(stack.Id), appClientModel)
+	resp.Diagnostics.Append(diags...)
+	resp.State.Set(ctx, clientTFData)
+}
+
 func (r *resourceFrontendO11yApp) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
 	var dataTF FrontendO11yAppTFModel
 	diags := req.State.Get(ctx, &dataTF)
@@ -221,6 +278,7 @@ func (r *resourceFrontendO11yApp) Update(ctx context.Context, req resource.Updat
 	}
 
 	appTFState, diags := convertClientModelToTFModel(dataTF.StackID.ValueInt64(), appClientModel)
+	appTFState.CollectorEndpoint = dataTF.CollectorEndpoint
 	resp.Diagnostics.Append(diags...)
 	resp.State.Set(ctx, appTFState)
 }
diff --git a/internal/resources/k6/common.go b/internal/resources/k6/common.go
new file mode 100644
index 00000000..6f67a5dd
--- /dev/null
+++ b/internal/resources/k6/common.go
@@ -0,0 +1,82 @@
+package k6
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+type basePluginFrameworkResource struct {
+	client *k6.APIClient
+	config *k6providerapi.K6APIConfig
+}
+
+func (r *basePluginFrameworkResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	// Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once
+	if req.ProviderData == nil || r.client != nil {
+		return
+	}
+
+	client, ok := req.ProviderData.(*common.Client)
+	if !ok {
+		resp.Diagnostics.AddError(
+			"Unexpected Resource Configure Type",
+			fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+		)
+
+		return
+	}
+
+	if client.K6APIClient == nil || client.K6APIConfig == nil {
+		resp.Diagnostics.AddError(
+			"The Grafana Provider is missing a configuration for the k6 Cloud API.",
+			"Please ensure that k6_access_token and stack_id are set in the provider configuration.",
+		)
+
+		return
+	}
+
+	r.client = client.K6APIClient
+	r.config = client.K6APIConfig
+}
+
+type basePluginFrameworkDataSource struct {
+	client *k6.APIClient
+	config *k6providerapi.K6APIConfig
+}
+
+func (d *basePluginFrameworkDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	// Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once
+	if req.ProviderData == nil || d.client != nil {
+		return
+	}
+
+	client, ok := req.ProviderData.(*common.Client)
+	if !ok {
+		resp.Diagnostics.AddError(
+			"Unexpected Data Source Configure Type",
+			fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+		)
+
+		return
+	}
+
+	if client.K6APIClient == nil || client.K6APIConfig == nil {
+		resp.Diagnostics.AddError(
+			"The Grafana Provider is missing a configuration for the k6 Cloud API.",
+			"Please ensure that k6_access_token and stack_id are set in the provider configuration.",
+		)
+
+		return
+	}
+
+	d.client = client.K6APIClient
+	d.config = client.K6APIConfig
+}
diff --git a/internal/resources/k6/common_lister.go b/internal/resources/k6/common_lister.go
new file mode 100644
index 00000000..a4ef1c64
--- /dev/null
+++ b/internal/resources/k6/common_lister.go
@@ -0,0 +1,21 @@
+package k6
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+// k6ListerFunction is a helper function that wraps a lister function be used more easily in k6 resources.
+func k6ListerFunction(listerFunc func(ctx context.Context, client *k6.APIClient, config *k6providerapi.K6APIConfig) ([]string, error)) common.ResourceListIDsFunc {
+	return func(ctx context.Context, client *common.Client, data any) ([]string, error) {
+		if client.K6APIClient == nil || client.K6APIConfig == nil {
+			return nil, fmt.Errorf("client not configured for the k6 Cloud API")
+		}
+		return listerFunc(ctx, client.K6APIClient, client.K6APIConfig)
+	}
+}
diff --git a/internal/resources/k6/data_source_k6_project.go b/internal/resources/k6/data_source_k6_project.go
new file mode 100644
index 00000000..9104de60
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project.go
@@ -0,0 +1,124 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*projectDataSource)(nil)
+)
+
+var (
+	dataSourceProjectName = "grafana_k6_project"
+)
+
+func dataSourceProject() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceProjectName,
+		&projectDataSource{},
+	)
+}
+
+// projectDataSourceModel maps the data source schema data.
+type projectDataSourceModel struct {
+	ID               types.Int32  `tfsdk:"id"`
+	Name             types.String `tfsdk:"name"`
+	IsDefault        types.Bool   `tfsdk:"is_default"`
+	GrafanaFolderUID types.String `tfsdk:"grafana_folder_uid"`
+	Created          types.String `tfsdk:"created"`
+	Updated          types.String `tfsdk:"updated"`
+}
+
+// projectDataSource is the data source implementation.
+type projectDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *projectDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceProjectName
+}
+
+// Schema defines the schema for the data source.
+func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves a k6 project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the project.",
+				Required:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project.",
+				Computed:    true,
+			},
+			"is_default": schema.BoolAttribute{
+				Description: "Whether this project is the default for running tests when no explicit project identifier is provided.",
+				Computed:    true,
+			},
+			"grafana_folder_uid": schema.StringAttribute{
+				Description: "The Grafana folder uid.",
+				Computed:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the project was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the project was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state projectDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.ProjectsAPI.ProjectsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	p, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project",
+			"Could not read k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	state.Name = types.StringValue(p.GetName())
+	state.IsDefault = types.BoolValue(p.GetIsDefault())
+	state.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	state.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
+
+func handleGrafanaFolderUID(grafanaFolderUID k6.NullableString) types.String {
+	if !grafanaFolderUID.IsSet() {
+		return types.StringNull()
+	}
+	return types.StringValue(*grafanaFolderUID.Get())
+}
diff --git a/internal/resources/k6/data_source_k6_project_limits.go b/internal/resources/k6/data_source_k6_project_limits.go
new file mode 100644
index 00000000..b2515ddb
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project_limits.go
@@ -0,0 +1,117 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*projectLimitsDataSource)(nil)
+)
+
+var (
+	dataSourceProjectLimitsName = "grafana_k6_project_limits"
+)
+
+func dataSourceProjectLimits() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceProjectLimitsName,
+		&projectLimitsDataSource{},
+	)
+}
+
+// projectLimitsDataSourceModel maps the data source schema data.
+type projectLimitsDataSourceModel struct {
+	ID                  types.Int32 `tfsdk:"id"`
+	ProjectID           types.Int32 `tfsdk:"project_id"`
+	VuhMaxPerMonth      types.Int32 `tfsdk:"vuh_max_per_month"`
+	VuMaxPerTest        types.Int32 `tfsdk:"vu_max_per_test"`
+	VuBrowserMaxPerTest types.Int32 `tfsdk:"vu_browser_max_per_test"`
+	DurationMaxPerTest  types.Int32 `tfsdk:"duration_max_per_test"`
+}
+
+// projectLimitsDataSource is the data source implementation.
+type projectLimitsDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *projectLimitsDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceProjectLimitsName
+}
+
+// Schema defines the schema for the data source.
+func (d *projectLimitsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves a k6 project limits.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "The identifier of the project limits. This is set to the same as the project_id.",
+				Computed:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project to get limits for.",
+				Required:    true,
+			},
+			"vuh_max_per_month": schema.Int32Attribute{
+				Description: "Maximum amount of virtual user hours (VU/h) used per one calendar month.",
+				Computed:    true,
+			},
+			"vu_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent virtual users (VUs) used in one test.",
+				Computed:    true,
+			},
+			"vu_browser_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent browser virtual users (VUs) used in one test.",
+				Computed:    true,
+			},
+			"duration_max_per_test": schema.Int32Attribute{
+				Description: "Maximum duration of a test in seconds.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *projectLimitsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state projectLimitsDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.ProjectsAPI.ProjectsLimitsRetrieve(ctx, state.ProjectID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	limits, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project limits",
+			"Could not read k6 project limits with project id "+strconv.Itoa(int(state.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	state.ID = types.Int32Value(limits.GetProjectId())
+	state.ProjectID = types.Int32Value(limits.GetProjectId())
+	state.VuhMaxPerMonth = types.Int32Value(limits.GetVuhMaxPerMonth())
+	state.VuMaxPerTest = types.Int32Value(limits.GetVuMaxPerTest())
+	state.VuBrowserMaxPerTest = types.Int32Value(limits.GetVuBrowserMaxPerTest())
+	state.DurationMaxPerTest = types.Int32Value(limits.GetDurationMaxPerTest())
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/resources/k6/data_source_k6_projects.go b/internal/resources/k6/data_source_k6_projects.go
new file mode 100644
index 00000000..563193a0
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_projects.go
@@ -0,0 +1,131 @@
+package k6
+
+import (
+	"context"
+	"sort"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*projectsDataSource)(nil)
+)
+
+var (
+	dataSourceProjectsName = "grafana_k6_projects"
+)
+
+func dataSourceProjects() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceProjectsName,
+		&projectsDataSource{},
+	)
+}
+
+// projectsDataSourceModel maps the data source schema data.
+type projectsDataSourceModel struct {
+	ID       types.String             `tfsdk:"id"`
+	Name     types.String             `tfsdk:"name"`
+	Projects []projectDataSourceModel `tfsdk:"projects"`
+}
+
+// projectsDataSource is the data source implementation.
+type projectsDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *projectsDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceProjectsName
+}
+
+// Schema defines the schema for the data source.
+func (d *projectsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves all k6 projects with the given name.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project. This is set to the same as name.",
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project.",
+				Optional:    true,
+			},
+			"projects": schema.ListAttribute{
+				Computed: true,
+				ElementType: types.ObjectType{
+					AttrTypes: map[string]attr.Type{
+						"id":                 types.Int32Type,
+						"name":               types.StringType,
+						"is_default":         types.BoolType,
+						"grafana_folder_uid": types.StringType,
+						"created":            types.StringType,
+						"updated":            types.StringType,
+					},
+				},
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *projectsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state projectsDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the name
+	state.ID = state.Name
+
+	// Retrieve projects
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.ProjectsAPI.ProjectsList(ctx).
+		Name(state.Name.ValueString()).
+		XStackId(d.config.StackID)
+
+	pjs, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 projects",
+			"Could not read k6 projects with name '"+state.Name.ValueString()+"': "+err.Error(),
+		)
+		return
+	}
+
+	// Process the results and populate the state with the retrieved projects
+	var projectStates []projectDataSourceModel
+	sort.Slice(pjs.Value, func(i, j int) bool {
+		return pjs.Value[i].GetCreated().Before(pjs.Value[j].GetCreated())
+	})
+	for _, pj := range pjs.Value {
+		pjState := projectDataSourceModel{
+			ID:               types.Int32Value(pj.GetId()),
+			Name:             types.StringValue(pj.GetName()),
+			IsDefault:        types.BoolValue(pj.GetIsDefault()),
+			GrafanaFolderUID: handleGrafanaFolderUID(pj.GrafanaFolderUid),
+			Created:          types.StringValue(pj.GetCreated().Format(time.RFC3339Nano)),
+			Updated:          types.StringValue(pj.GetUpdated().Format(time.RFC3339Nano)),
+		}
+
+		projectStates = append(projectStates, pjState)
+	}
+
+	state.Projects = projectStates
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/resources/k6/resource_project.go b/internal/resources/k6/resource_project.go
new file mode 100644
index 00000000..1ab239a4
--- /dev/null
+++ b/internal/resources/k6/resource_project.go
@@ -0,0 +1,297 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.ResourceWithConfigure   = (*projectResource)(nil)
+	_ resource.ResourceWithImportState = (*projectResource)(nil)
+)
+
+var (
+	resourceProjectName = "grafana_k6_project"
+	resourceProjectID   = common.NewResourceID(common.IntIDField("id"))
+)
+
+func resourceProject() *common.Resource {
+	return common.NewResource(
+		common.CategoryK6,
+		resourceProjectName,
+		resourceProjectID,
+		&projectResource{},
+	).WithLister(k6ListerFunction(listProjects))
+}
+
+// projectResourceModel maps the resource schema data.
+type projectResourceModel struct {
+	ID               types.Int32  `tfsdk:"id"`
+	Name             types.String `tfsdk:"name"`
+	IsDefault        types.Bool   `tfsdk:"is_default"`
+	GrafanaFolderUID types.String `tfsdk:"grafana_folder_uid"`
+	Created          types.String `tfsdk:"created"`
+	Updated          types.String `tfsdk:"updated"`
+}
+
+// projectResource is the resource implementation.
+type projectResource struct {
+	basePluginFrameworkResource
+}
+
+// Metadata returns the resource type name.
+func (r *projectResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceProjectName
+}
+
+// Schema defines the schema for the resource.
+func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages a k6 project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the project.",
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the project.",
+				Required:    true,
+			},
+			"is_default": schema.BoolAttribute{
+				Description: "Use this project as default for running tests when no explicit project identifier is provided.",
+				Computed:    true,
+			},
+			"grafana_folder_uid": schema.StringAttribute{
+				Description: "The Grafana folder uid.",
+				Computed:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the project was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the project was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Create creates the resource and sets the Terraform state on success.
+func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	// Retrieve values from plan
+	var plan projectResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Generate API request body from plan
+	toCreate := k6.NewCreateProjectApiModel(plan.Name.ValueString())
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsCreate(ctx).
+		CreateProjectApiModel(toCreate).
+		XStackId(r.config.StackID)
+
+	// Create new project
+	p, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error creating k6 project",
+			"Could not create k6 project, unexpected error: "+err.Error(),
+		)
+		return
+	}
+
+	// Map response body to schema and populate Computed attribute values
+	plan.ID = types.Int32Value(p.GetId())
+	plan.Name = types.StringValue(p.GetName())
+	plan.IsDefault = types.BoolValue(p.GetIsDefault())
+	plan.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	plan.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Read retrieves the resource information.
+func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	// Get current state
+	var state projectResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	p, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project",
+			"Could not read k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	state.ID = types.Int32Value(p.GetId())
+	state.Name = types.StringValue(p.GetName())
+	state.IsDefault = types.BoolValue(p.GetIsDefault())
+	state.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	state.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set refreshed state
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	// Retrieve values from plan
+	var plan projectResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Get current state to retrieve the ID
+	var state projectResourceModel
+	diags = req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchProjectApiModel(plan.Name.ValueString())
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	updateReq := r.client.ProjectsAPI.ProjectsPartialUpdate(ctx, state.ID.ValueInt32()).
+		PatchProjectApiModel(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update the project
+	_, err := updateReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 project",
+			"Could not update k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Update resource state with updated items and timestamp
+	fetchReq := r.client.ProjectsAPI.ProjectsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	p, _, err := fetchReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project",
+			"Could not read k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	plan.ID = types.Int32Value(p.GetId())
+	plan.Name = types.StringValue(p.GetName())
+	plan.IsDefault = types.BoolValue(p.GetIsDefault())
+	plan.GrafanaFolderUID = handleGrafanaFolderUID(p.GrafanaFolderUid)
+	plan.Created = types.StringValue(p.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(p.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	// Retrieve values from state
+	var state projectResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Delete existing project
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	deleteReq := r.client.ProjectsAPI.ProjectsDestroy(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	_, err := deleteReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error deleting k6 project",
+			"Could not delete k6 project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+	}
+}
+
+func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	id, err := strconv.ParseInt(req.ID, 10, 32)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing k6 project",
+			"Could not parse k6 project id "+req.ID+": "+err.Error(),
+		)
+		return
+	}
+
+	resp.State.SetAttribute(ctx, path.Root("id"), types.Int32Value(int32(id)))
+
+	readReq := resource.ReadRequest{State: resp.State}
+	readResp := resource.ReadResponse{State: resp.State}
+
+	r.Read(ctx, readReq, &readResp)
+	resp.Diagnostics.Append(readResp.Diagnostics...)
+}
+
+// listProjects retrieves the list ids of all the existing projects.
+func listProjects(ctx context.Context, client *k6.APIClient, config *k6providerapi.K6APIConfig) ([]string, error) {
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, config.Token)
+	resp, _, err := client.ProjectsAPI.ProjectsList(ctx).
+		XStackId(config.StackID).
+		Execute()
+	if err != nil {
+		return nil, err
+	}
+
+	var ids []string
+	for _, p := range resp.Value {
+		ids = append(ids, strconv.Itoa(int(p.GetId())))
+	}
+	return ids, nil
+}
diff --git a/internal/resources/k6/resource_project_limits.go b/internal/resources/k6/resource_project_limits.go
new file mode 100644
index 00000000..2d614c23
--- /dev/null
+++ b/internal/resources/k6/resource_project_limits.go
@@ -0,0 +1,275 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.ResourceWithConfigure   = (*projectLimitsResource)(nil)
+	_ resource.ResourceWithImportState = (*projectLimitsResource)(nil)
+)
+
+var (
+	resourceProjectLimitsName = "grafana_k6_project_limits"
+	resourceProjectLimitsID   = common.NewResourceID(common.IntIDField("project_id"))
+)
+
+func resourceProjectLimits() *common.Resource {
+	return common.NewResource(
+		common.CategoryK6,
+		resourceProjectLimitsName,
+		resourceProjectLimitsID,
+		&projectLimitsResource{},
+	)
+}
+
+// projectLimitsResourceModel maps the resource schema data.
+type projectLimitsResourceModel struct {
+	ID                  types.Int32 `tfsdk:"id"`
+	ProjectID           types.Int32 `tfsdk:"project_id"`
+	VuhMaxPerMonth      types.Int32 `tfsdk:"vuh_max_per_month"`
+	VuMaxPerTest        types.Int32 `tfsdk:"vu_max_per_test"`
+	VuBrowserMaxPerTest types.Int32 `tfsdk:"vu_browser_max_per_test"`
+	DurationMaxPerTest  types.Int32 `tfsdk:"duration_max_per_test"`
+}
+
+// projectLimitsResource is the resource implementation.
+type projectLimitsResource struct {
+	basePluginFrameworkResource
+}
+
+// Metadata returns the resource type name.
+func (r *projectLimitsResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceProjectLimitsName
+}
+
+// Schema defines the schema for the resource.
+func (r *projectLimitsResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages limits for a k6 project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "The identifier of the project limits. This is the same as the project_id.",
+				Computed:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project to manage limits for.",
+				Required:    true,
+			},
+			"vuh_max_per_month": schema.Int32Attribute{
+				Description: "Maximum amount of virtual user hours (VU/h) used per one calendar month.",
+				Optional:    true,
+			},
+			"vu_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent virtual users (VUs) used in one test.",
+				Optional:    true,
+			},
+			"vu_browser_max_per_test": schema.Int32Attribute{
+				Description: "Maximum number of concurrent browser virtual users (VUs) used in one test.",
+				Optional:    true,
+			},
+			"duration_max_per_test": schema.Int32Attribute{
+				Description: "Maximum duration of a test in seconds.",
+				Optional:    true,
+			},
+		},
+	}
+}
+
+// Create creates the resource and sets the Terraform state on success.
+func (r *projectLimitsResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	// Retrieve values from plan
+	var plan projectLimitsResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	plan.ID = plan.ProjectID
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchProjectLimitsRequest()
+	if !plan.VuhMaxPerMonth.IsNull() {
+		toUpdate.SetVuhMaxPerMonth(plan.VuhMaxPerMonth.ValueInt32())
+	}
+	if !plan.VuMaxPerTest.IsNull() {
+		toUpdate.SetVuMaxPerTest(plan.VuMaxPerTest.ValueInt32())
+	}
+	if !plan.VuBrowserMaxPerTest.IsNull() {
+		toUpdate.SetVuBrowserMaxPerTest(plan.VuBrowserMaxPerTest.ValueInt32())
+	}
+	if !plan.DurationMaxPerTest.IsNull() {
+		toUpdate.SetDurationMaxPerTest(plan.DurationMaxPerTest.ValueInt32())
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsPartialUpdate(ctx, plan.ProjectID.ValueInt32()).
+		PatchProjectLimitsRequest(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update project limits
+	_, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 project limits",
+			"Could not update k6 project limits, unexpected error: "+err.Error(),
+		)
+		return
+	}
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Read retrieves the resource information.
+func (r *projectLimitsResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	// Get current state
+	var state projectLimitsResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	state.ID = state.ProjectID
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsRetrieve(ctx, state.ProjectID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	limits, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 project limits",
+			"Could not read k6 project limits for project with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	state.VuhMaxPerMonth = types.Int32Value(limits.GetVuhMaxPerMonth())
+	state.VuMaxPerTest = types.Int32Value(limits.GetVuMaxPerTest())
+	state.VuBrowserMaxPerTest = types.Int32Value(limits.GetVuBrowserMaxPerTest())
+	state.DurationMaxPerTest = types.Int32Value(limits.GetDurationMaxPerTest())
+
+	// Set refreshed state
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *projectLimitsResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	// Retrieve values from plan
+	var plan projectLimitsResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	plan.ID = plan.ProjectID
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchProjectLimitsRequest()
+	if !plan.VuhMaxPerMonth.IsNull() {
+		toUpdate.SetVuhMaxPerMonth(plan.VuhMaxPerMonth.ValueInt32())
+	}
+	if !plan.VuMaxPerTest.IsNull() {
+		toUpdate.SetVuMaxPerTest(plan.VuMaxPerTest.ValueInt32())
+	}
+	if !plan.VuBrowserMaxPerTest.IsNull() {
+		toUpdate.SetVuBrowserMaxPerTest(plan.VuBrowserMaxPerTest.ValueInt32())
+	}
+	if !plan.DurationMaxPerTest.IsNull() {
+		toUpdate.SetDurationMaxPerTest(plan.DurationMaxPerTest.ValueInt32())
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsPartialUpdate(ctx, plan.ProjectID.ValueInt32()).
+		PatchProjectLimitsRequest(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update project limits
+	_, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 project limits",
+			"Could not update k6 project limits for project with id "+strconv.Itoa(int(plan.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *projectLimitsResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	// Retrieve values from state
+	var state projectLimitsResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Reset limits to default values
+	toReset := k6.NewPatchProjectLimitsRequestWithDefaults()
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.ProjectsAPI.ProjectsLimitsPartialUpdate(ctx, state.ProjectID.ValueInt32()).
+		PatchProjectLimitsRequest(toReset).
+		XStackId(r.config.StackID)
+
+	_, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error resetting k6 project limits",
+			"Could not reset k6 project limits for project with id "+strconv.Itoa(int(state.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+	}
+}
+
+func (r *projectLimitsResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	id, err := strconv.ParseInt(req.ID, 10, 32)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing k6 project limits",
+			"Could not parse k6 project id "+req.ID+": "+err.Error(),
+		)
+		return
+	}
+
+	resp.State.SetAttribute(ctx, path.Root("project_id"), types.Int32Value(int32(id)))
+
+	readReq := resource.ReadRequest{State: resp.State}
+	readResp := resource.ReadResponse{State: resp.State}
+
+	r.Read(ctx, readReq, &readResp)
+	resp.Diagnostics.Append(readResp.Diagnostics...)
+}
diff --git a/internal/resources/k6/resources.go b/internal/resources/k6/resources.go
new file mode 100644
index 00000000..32bf6905
--- /dev/null
+++ b/internal/resources/k6/resources.go
@@ -0,0 +1,92 @@
+package k6
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+var Resources = addValidationToResources(
+	resourceProject(),
+	resourceProjectLimits(),
+	resourceLoadTest(),
+)
+
+var DataSources = addValidationToDataSources(
+	dataSourceProject(),
+	dataSourceProjects(),
+	dataSourceProjectLimits(),
+	dataSourceLoadTest(),
+	dataSourceLoadTests(),
+)
+
+func addValidationToResources(resources ...*common.Resource) []*common.Resource {
+	for _, r := range resources {
+		addValidationToSchema(r.Schema)
+	}
+	return resources
+}
+
+func addValidationToDataSources(dataSources ...*common.DataSource) []*common.DataSource {
+	for _, d := range dataSources {
+		addValidationToSchema(d.Schema)
+	}
+	return dataSources
+}
+
+func addValidationToSchema(r *schema.Resource) {
+	if r == nil {
+		return
+	}
+	createFn := r.CreateContext
+	readFn := r.ReadContext
+	updateFn := r.UpdateContext
+	deleteFn := r.DeleteContext
+
+	if createFn != nil {
+		r.CreateContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return createFn(ctx, d, m)
+		}
+	}
+
+	if readFn != nil {
+		r.ReadContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return readFn(ctx, d, m)
+		}
+	}
+
+	if updateFn != nil {
+		r.UpdateContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return updateFn(ctx, d, m)
+		}
+	}
+
+	if deleteFn != nil {
+		r.DeleteContext = func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+			if err := k6ClientResourceValidation(d, m); err != nil {
+				return diag.FromErr(err)
+			}
+			return deleteFn(ctx, d, m)
+		}
+	}
+}
+
+func k6ClientResourceValidation(_ *schema.ResourceData, m interface{}) error {
+	if m.(*common.Client).K6APIClient == nil || m.(*common.Client).K6APIConfig == nil {
+		return fmt.Errorf("the k6 Cloud API client is required for this resource. Set the k6_access_token and stack_id provider attributes")
+	}
+	return nil
+}
diff --git a/internal/resources/syntheticmonitoring/resource_check_alert.go b/internal/resources/syntheticmonitoring/resource_check_alert.go
new file mode 100644
index 00000000..d7126860
--- /dev/null
+++ b/internal/resources/syntheticmonitoring/resource_check_alert.go
@@ -0,0 +1,196 @@
+package syntheticmonitoring
+
+import (
+	"context"
+	"strconv"
+
+	smapi "github.com/grafana/synthetic-monitoring-api-go-client"
+	"github.com/grafana/synthetic-monitoring-api-go-client/model"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
+)
+
+var resourceCheckAlertID = common.NewResourceID(common.IntIDField("check_id"))
+
+const (
+	// Alert names
+	AlertNameProbeFailedExecutionsTooHigh        = "ProbeFailedExecutionsTooHigh"
+	AlertNameTLSTargetCertificateCloseToExpiring = "TLSTargetCertificateCloseToExpiring"
+)
+
+func resourceCheckAlerts() *common.Resource {
+	return common.NewLegacySDKResource(
+		common.CategorySyntheticMonitoring,
+		"grafana_synthetic_monitoring_check_alerts",
+		resourceCheckAlertID,
+		&schema.Resource{
+			Description: `
+Manages alerts for a check in Grafana Synthetic Monitoring.
+
+* [Official documentation](https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/configure-alerts/configure-per-check-alerts/)`,
+
+			CreateContext: withClient[schema.CreateContextFunc](resourceCheckAlertCreate),
+			ReadContext:   withClient[schema.ReadContextFunc](resourceCheckAlertRead),
+			UpdateContext: withClient[schema.UpdateContextFunc](resourceCheckAlertUpdate),
+			DeleteContext: withClient[schema.DeleteContextFunc](resourceCheckAlertDelete),
+
+			Importer: &schema.ResourceImporter{
+				StateContext: schema.ImportStatePassthroughContext,
+			},
+
+			Schema: map[string]*schema.Schema{
+				"check_id": {
+					Description: "The ID of the check to manage alerts for.",
+					Type:        schema.TypeInt,
+					Required:    true,
+					ForceNew:    true,
+				},
+				"alerts": {
+					Description: "List of alerts for the check.",
+					Type:        schema.TypeSet,
+					Required:    true,
+					ConfigMode:  schema.SchemaConfigModeAttr,
+					Elem: &schema.Resource{
+						Schema: map[string]*schema.Schema{
+							"name": {
+								Description: "Name of the alert. Required.",
+								Type:        schema.TypeString,
+								Required:    true,
+								ValidateFunc: validation.StringInSlice([]string{
+									AlertNameProbeFailedExecutionsTooHigh,
+									AlertNameTLSTargetCertificateCloseToExpiring,
+								}, false),
+							},
+							"threshold": {
+								Description: "Threshold value for the alert.",
+								Type:        schema.TypeFloat,
+								Required:    true,
+							},
+							"period": {
+								Description: "Period for the alert. Required and must be one of: `5m`, `10m`, `15m`, `20m`, `30m`, `1h`.",
+								Type:        schema.TypeString,
+								Required:    false,
+								Optional:    true,
+								Default:     "",
+								ValidateFunc: validation.StringInSlice([]string{
+									"", "5m", "10m", "15m", "20m", "30m", "1h",
+								}, false),
+							},
+						},
+					},
+				},
+			},
+		},
+	)
+}
+
+func resourceCheckAlertCreate(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics {
+	checkID := int64(d.Get("check_id").(int))
+
+	alerts, err := makeCheckAlerts(d)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	_, err = c.UpdateCheckAlerts(ctx, checkID, alerts)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	d.SetId(strconv.FormatInt(checkID, 10))
+	return resourceCheckAlertRead(ctx, d, c)
+}
+
+func resourceCheckAlertRead(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics {
+	checkID, err := strconv.ParseInt(d.Id(), 10, 64)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	d.Set("check_id", checkID)
+
+	alerts, err := c.GetCheckAlerts(ctx, checkID)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	// Transform alerts into schema format
+	alertsList := make([]map[string]interface{}, len(alerts))
+	for i, alert := range alerts {
+		alertMap := map[string]interface{}{
+			"name":      alert.Name,
+			"threshold": alert.Threshold,
+		}
+		if alert.Period != "" {
+			alertMap["period"] = alert.Period
+		}
+		alertsList[i] = alertMap
+	}
+
+	if err := d.Set("alerts", alertsList); err != nil {
+		return diag.FromErr(err)
+	}
+
+	return nil
+}
+
+func resourceCheckAlertUpdate(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics {
+	checkID, err := strconv.ParseInt(d.Id(), 10, 64)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	alerts, err := makeCheckAlerts(d)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	_, err = c.UpdateCheckAlerts(ctx, checkID, alerts)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	return resourceCheckAlertRead(ctx, d, c)
+}
+
+func resourceCheckAlertDelete(ctx context.Context, d *schema.ResourceData, c *smapi.Client) diag.Diagnostics {
+	checkID, err := strconv.ParseInt(d.Id(), 10, 64)
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	// Delete all alerts by setting an empty list
+	_, err = c.UpdateCheckAlerts(ctx, checkID, []model.CheckAlert{})
+	if err != nil {
+		return diag.FromErr(err)
+	}
+
+	return nil
+}
+
+func makeCheckAlerts(d *schema.ResourceData) ([]model.CheckAlert, error) {
+	alertsSet := d.Get("alerts").(*schema.Set)
+	alertsList := alertsSet.List()
+	alerts := make([]model.CheckAlert, len(alertsList))
+
+	for i, alertMap := range alertsList {
+		alertData := alertMap.(map[string]interface{})
+		name := alertData["name"].(string)
+		period, hasPeriod := alertData["period"].(string)
+
+		alert := model.CheckAlert{
+			Name:      name,
+			Threshold: alertData["threshold"].(float64),
+		}
+
+		if hasPeriod {
+			alert.Period = period
+		}
+
+		alerts[i] = alert
+	}
+
+	return alerts, nil
+}
diff --git a/internal/resources/syntheticmonitoring/resources.go b/internal/resources/syntheticmonitoring/resources.go
index b8e1dd0d..222be7ec 100644
--- a/internal/resources/syntheticmonitoring/resources.go
+++ b/internal/resources/syntheticmonitoring/resources.go
@@ -29,4 +29,5 @@ var DataSources = []*common.DataSource{
 var Resources = []*common.Resource{
 	resourceCheck(),
 	resourceProbe(),
+	resourceCheckAlerts(),
 }
diff --git a/pkg/generate/postprocessing/replace_references.go b/pkg/generate/postprocessing/replace_references.go
index 13c558da..984054e4 100644
--- a/pkg/generate/postprocessing/replace_references.go
+++ b/pkg/generate/postprocessing/replace_references.go
@@ -25,6 +25,7 @@ var knownReferences = []string{
 	"grafana_cloud_private_data_source_connect_network_token.pdc_network_id=grafana_cloud_private_data_source_connect_network.pdc_network_id",
 	"grafana_cloud_private_data_source_connect_network_token.region=grafana_cloud_private_data_source_connect_network.region",
 	"grafana_cloud_provider_aws_cloudwatch_scrape_job.aws_account_resource_id=grafana_cloud_provider_aws_account.resource_id",
+	"grafana_cloud_provider_aws_resource_metadata_scrape_job.aws_account_resource_id=grafana_cloud_provider_aws_account.resource_id",
 	"grafana_cloud_stack_service_account.stack_slug=grafana_cloud_stack.slug",
 	"grafana_cloud_stack_service_account_token.auth=grafana_cloud_stack_service_account_token.key",
 	"grafana_cloud_stack_service_account_token.service_account_id=grafana_cloud_stack_service_account.id",
@@ -70,6 +71,12 @@ var knownReferences = []string{
 	"grafana_folder_permission_item.team=grafana_team.id",
 	"grafana_folder_permission_item.user=grafana_service_account.id",
 	"grafana_folder_permission_item.user=grafana_user.id",
+	"grafana_k6_installation.cloud_access_policy_token=grafana_cloud_access_policy_token.token",
+	"grafana_k6_installation.grafana_sa_token=grafana_cloud_stack_service_account_token.key",
+	"grafana_k6_installation.k6_access_token=grafana_k6_installation.k6_access_token",
+	"grafana_k6_installation.stack_id=grafana_cloud_stack.id",
+	"grafana_k6_load_test.project_id=grafana_k6_project.id",
+	"grafana_k6_project_limits.project_id=grafana_k6_project.id",
 	"grafana_library_panel.folder_uid=grafana_folder.uid",
 	"grafana_library_panel.org_id=grafana_organization.id",
 	"grafana_machine_learning_alert.job_id=grafana_machine_learning_job.id",
@@ -125,6 +132,7 @@ var knownReferences = []string{
 	"grafana_service_account_permission_item.user=grafana_user.id",
 	"grafana_service_account_token.service_account_id=grafana_service_account.id",
 	"grafana_slo.folder_uid=grafana_folder.uid",
+	"grafana_synthetic_monitoring_check_alerts.check_id=grafana_synthetic_monitoring_check.id",
 	"grafana_synthetic_monitoring_installation.metrics_publisher_key=grafana_cloud_access_policy_token.token",
 	"grafana_synthetic_monitoring_installation.sm_access_token=grafana_synthetic_monitoring_installation.sm_access_token",
 	"grafana_synthetic_monitoring_installation.sm_url=grafana_synthetic_monitoring_installation.stack_sm_api_url",
diff --git a/pkg/provider/configure_clients.go b/pkg/provider/configure_clients.go
index 6a4a5ed8..e86905eb 100644
--- a/pkg/provider/configure_clients.go
+++ b/pkg/provider/configure_clients.go
@@ -17,9 +17,11 @@ import (
 	"github.com/grafana/grafana-app-sdk/resource"
 	"github.com/grafana/grafana-com-public-clients/go/gcom"
 	goapi "github.com/grafana/grafana-openapi-client-go/client"
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
 	"github.com/grafana/machine-learning-go-client/mlapi"
 	"github.com/grafana/slo-openapi-client/go/slo"
 	SMAPI "github.com/grafana/synthetic-monitoring-api-go-client"
+
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/rest"
 
@@ -33,6 +35,7 @@ import (
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/connectionsapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/fleetmanagementapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/common/frontendo11yapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/grafana"
 )
 
@@ -92,6 +95,12 @@ func CreateClients(providerConfig ProviderConfig) (*common.Client, error) {
 		}
 	}
 
+	if !providerConfig.K6AccessToken.IsNull() && !providerConfig.StackID.IsNull() {
+		if err := createK6Client(c, providerConfig); err != nil {
+			return nil, err
+		}
+	}
+
 	grafana.StoreDashboardSHA256 = providerConfig.StoreDashboardSha256.ValueBool()
 
 	return c, nil
@@ -357,6 +366,37 @@ func createConnectionsClient(client *common.Client, providerConfig ProviderConfi
 	return nil
 }
 
+func createK6Client(client *common.Client, providerConfig ProviderConfig) error {
+	k6Cfg := k6.NewConfiguration()
+	if !providerConfig.K6URL.IsNull() {
+		k6Cfg.Servers = []k6.ServerConfiguration{
+			{URL: providerConfig.K6URL.ValueString()},
+		}
+	}
+
+	k6Cfg.HTTPClient = getRetryClient(providerConfig)
+
+	httpHeaders, err := getHTTPHeadersMap(providerConfig)
+	if err != nil {
+		return err
+	}
+	for k, v := range httpHeaders {
+		k6Cfg.DefaultHeader[k] = v
+	}
+
+	var stackID int32
+	if stackID, err = common.ToInt32(providerConfig.StackID.ValueInt64()); err != nil {
+		return fmt.Errorf("could not convert stack_id to int32: %s", err.Error())
+	}
+
+	client.K6APIClient = k6.NewAPIClient(k6Cfg)
+	client.K6APIConfig = &k6providerapi.K6APIConfig{
+		Token:   providerConfig.K6AccessToken.ValueString(),
+		StackID: stackID,
+	}
+	return nil
+}
+
 func createFleetManagementClient(client *common.Client, providerConfig ProviderConfig) error {
 	providerHeaders, err := getHTTPHeadersMap(providerConfig)
 	if err != nil {
diff --git a/pkg/provider/framework_provider.go b/pkg/provider/framework_provider.go
index 9a093878..06faef10 100644
--- a/pkg/provider/framework_provider.go
+++ b/pkg/provider/framework_provider.go
@@ -53,6 +53,9 @@ type ProviderConfig struct {
 	FleetManagementURL         types.String `tfsdk:"fleet_management_url"`
 	FrontendO11yAPIAccessToken types.String `tfsdk:"frontend_o11y_api_access_token"`
 
+	K6URL         types.String `tfsdk:"k6_url"`
+	K6AccessToken types.String `tfsdk:"k6_access_token"`
+
 	UserAgent types.String `tfsdk:"-"`
 	Version   types.String `tfsdk:"-"`
 }
@@ -91,6 +94,9 @@ func (c *ProviderConfig) SetDefaults() error {
 		c.StackID = v
 	}
 
+	c.K6URL = envDefaultFuncString(c.K6URL, "GRAFANA_K6_URL")
+	c.K6AccessToken = envDefaultFuncString(c.K6AccessToken, "GRAFANA_K6_ACCESS_TOKEN")
+
 	if c.StoreDashboardSha256, err = envDefaultFuncBool(c.StoreDashboardSha256, "GRAFANA_STORE_DASHBOARD_SHA256", false); err != nil {
 		return fmt.Errorf("failed to parse GRAFANA_STORE_DASHBOARD_SHA256: %w", err)
 	}
@@ -261,6 +267,15 @@ func (p *frameworkProvider) Schema(_ context.Context, _ provider.SchemaRequest,
 				Sensitive:           true,
 				MarkdownDescription: "A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.",
 			},
+			"k6_url": schema.StringAttribute{
+				Optional:            true,
+				MarkdownDescription: "The k6 Cloud API url. May alternatively be set via the `GRAFANA_K6_URL` environment variable.",
+			},
+			"k6_access_token": schema.StringAttribute{
+				Optional:            true,
+				Sensitive:           true,
+				MarkdownDescription: "The k6 Cloud API token. May alternatively be set via the `GRAFANA_K6_ACCESS_TOKEN` environment variable.",
+			},
 		},
 	}
 }
diff --git a/pkg/provider/legacy_provider.go b/pkg/provider/legacy_provider.go
index 6bee0165..181221a4 100644
--- a/pkg/provider/legacy_provider.go
+++ b/pkg/provider/legacy_provider.go
@@ -194,6 +194,19 @@ func Provider(version string) *schema.Provider {
 				Sensitive:   true,
 				Description: "A Grafana Frontend Observability API access token. May alternatively be set via the `GRAFANA_FRONTEND_O11Y_API_ACCESS_TOKEN` environment variable.",
 			},
+
+			"k6_url": {
+				Type:         schema.TypeString,
+				Optional:     true,
+				Description:  "The k6 Cloud API url. May alternatively be set via the `GRAFANA_K6_URL` environment variable.",
+				ValidateFunc: validation.IsURLWithHTTPorHTTPS,
+			},
+			"k6_access_token": {
+				Type:        schema.TypeString,
+				Optional:    true,
+				Sensitive:   true,
+				Description: "The k6 Cloud API token. May alternatively be set via the `GRAFANA_K6_ACCESS_TOKEN` environment variable.",
+			},
 		},
 
 		ResourcesMap:   legacySDKResources(),
@@ -271,6 +284,8 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema
 			FleetManagementAuth:        stringValueOrNull(d, "fleet_management_auth"),
 			FleetManagementURL:         stringValueOrNull(d, "fleet_management_url"),
 			FrontendO11yAPIAccessToken: stringValueOrNull(d, "frontend_o11y_api_access_token"),
+			K6URL:                      stringValueOrNull(d, "k6_url"),
+			K6AccessToken:              stringValueOrNull(d, "k6_access_token"),
 			StoreDashboardSha256:       boolValueOrNull(d, "store_dashboard_sha256"),
 			HTTPHeaders:                headers,
 			Retries:                    int64ValueOrNull(d, "retries"),
diff --git a/pkg/provider/resources.go b/pkg/provider/resources.go
index 586e6a31..a3613210 100644
--- a/pkg/provider/resources.go
+++ b/pkg/provider/resources.go
@@ -11,6 +11,7 @@ import (
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/fleetmanagement"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/frontendo11y"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/grafana"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/k6"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/machinelearning"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/oncall"
 	"github.com/grafana/terraform-provider-grafana/v3/internal/resources/slo"
@@ -27,6 +28,7 @@ func DataSources() []*common.DataSource {
 	resources = append(resources, machinelearning.DataSources...)
 	resources = append(resources, oncall.DataSources...)
 	resources = append(resources, slo.DataSources...)
+	resources = append(resources, k6.DataSources...)
 	resources = append(resources, syntheticmonitoring.DataSources...)
 	resources = append(resources, cloudprovider.DataSources...)
 	resources = append(resources, connections.DataSources...)
@@ -66,6 +68,7 @@ func Resources() []*common.Resource {
 	resources = append(resources, machinelearning.Resources...)
 	resources = append(resources, oncall.Resources...)
 	resources = append(resources, slo.Resources...)
+	resources = append(resources, k6.Resources...)
 	resources = append(resources, syntheticmonitoring.Resources...)
 	resources = append(resources, cloudprovider.Resources...)
 	resources = append(resources, connections.Resources...)
diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl
index df795945..21120c30 100644
--- a/templates/index.md.tmpl
+++ b/templates/index.md.tmpl
@@ -55,6 +55,29 @@ provider "grafana" {
 }
 ```
 
+#### Import existing Frontend Observability apps into Terraform
+
+To manage an existing Frontend Observability app with Terraform, you can import it into your Terraform state.
+
+##### Before you begin
+
+You will need the following:
+
+- **Stack slug** – This is the first part of your Grafana Cloud URL. For example, in `https://mystack.grafana.net`, the stack slug is `mystack`.
+
+- **App ID** – Navigate to the app in your Grafana Cloud instance and copy the ID from the URL. It will look like this: `https://<stack-slug>.grafana.net/a/grafana-kowalski-app/apps/<app-id>`
+
+
+##### Import command
+
+Run the following command to import the app into your Terraform state:
+
+```bash
+terraform import '<stack-slug>:<app-id>' my-app
+```
+
+Replace <stack-slug> and <app-id> with the appropriate values for your environment.
+
 {{ .SchemaMarkdown | trimspace }}
 
 ### Managing Cloud Provider



Test patch:
diff --git a/examples/data-sources/grafana_k6_load_test/data-source.tf b/examples/data-sources/grafana_k6_load_test/data-source.tf
new file mode 100644
index 00000000..d9d1dc27
--- /dev/null
+++ b/examples/data-sources/grafana_k6_load_test/data-source.tf
@@ -0,0 +1,17 @@
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
+
+data "grafana_k6_load_test" "from_id" {
+  id = grafana_k6_load_test.test_load_test.id
+}
diff --git a/examples/data-sources/grafana_k6_load_tests/data-source.tf b/examples/data-sources/grafana_k6_load_tests/data-source.tf
new file mode 100644
index 00000000..c659dd34
--- /dev/null
+++ b/examples/data-sources/grafana_k6_load_tests/data-source.tf
@@ -0,0 +1,49 @@
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_project.load_test_project,
+  ]
+}
+
+resource "grafana_k6_load_test" "test_load_test_2" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test (2)"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+  ]
+}
+
+data "grafana_k6_load_tests" "from_project_id" {
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test,
+    grafana_k6_load_test.test_load_test_2
+  ]
+}
+
+data "grafana_k6_load_tests" "filter_by_name" {
+  name       = "Terraform Test Load Test (2)"
+  project_id = grafana_k6_project.load_test_project.id
+
+  depends_on = [
+    grafana_k6_load_test.test_load_test_2,
+  ]
+}
\ No newline at end of file
diff --git a/examples/resources/grafana_k6_load_test/import.sh b/examples/resources/grafana_k6_load_test/import.sh
new file mode 100644
index 00000000..4c8c2bcd
--- /dev/null
+++ b/examples/resources/grafana_k6_load_test/import.sh
@@ -0,0 +1 @@
+terraform import grafana_k6_load_test.name "{{ id }}"
diff --git a/examples/resources/grafana_k6_load_test/resource.tf b/examples/resources/grafana_k6_load_test/resource.tf
new file mode 100644
index 00000000..749c8406
--- /dev/null
+++ b/examples/resources/grafana_k6_load_test/resource.tf
@@ -0,0 +1,13 @@
+resource "grafana_k6_project" "load_test_project" {
+  name = "Terraform Load Test Project"
+}
+
+resource "grafana_k6_load_test" "test_load_test" {
+  project_id = grafana_k6_project.load_test_project.id
+  name       = "Terraform Test Load Test"
+  script     = <<-EOT
+    export default function() {
+      console.log('Hello from k6!');
+    }
+  EOT
+}
\ No newline at end of file
diff --git a/internal/resources/cloud/resource_k6_installation_test.go b/internal/resources/cloud/resource_k6_installation_test.go
new file mode 100644
index 00000000..554b1238
--- /dev/null
+++ b/internal/resources/cloud/resource_k6_installation_test.go
@@ -0,0 +1,71 @@
+package cloud_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/grafana-com-public-clients/go/gcom"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccK6Installation(t *testing.T) {
+	testutils.CheckCloudAPITestsEnabled(t)
+
+	var stack gcom.FormattedApiInstance
+	stackPrefix := "tfk6installtest"
+	testAccDeleteExistingStacks(t, stackPrefix)
+	stackSlug := GetRandomStackName(stackPrefix)
+
+	accessPolicyPrefix := "testk6install-"
+	testAccDeleteExistingAccessPolicies(t, "us", accessPolicyPrefix)
+	accessPolicyName := accessPolicyPrefix + acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy:             testAccStackCheckDestroy(&stack),
+		Steps: []resource.TestStep{
+			{
+				Config: testAccK6Installation(stackSlug, accessPolicyName),
+				Check: resource.ComposeTestCheckFunc(
+					testAccStackCheckExists("grafana_cloud_stack.test", &stack),
+					resource.TestCheckResourceAttrSet("grafana_k6_installation.test", "k6_access_token"),
+					resource.TestCheckResourceAttrSet("grafana_k6_installation.test", "k6_organization"),
+				),
+			},
+		},
+	})
+}
+
+func testAccK6InstallationBase(stackSlug, accessPolicyName string) string {
+	return testAccStackConfigBasic(stackSlug, stackSlug, "description") +
+		testAccCloudAccessPolicyTokenConfigBasic(accessPolicyName, accessPolicyName, "eu", []string{"stacks:read", "stacks:write", "subscriptions:read", "orgs:read"}, "")
+}
+
+func testAccK6Installation(stackSlug, apiKeyName string) string {
+	return testAccK6InstallationBase(stackSlug, apiKeyName) +
+		`
+	resource "grafana_cloud_stack_service_account" "tfk6installtest_sa" {
+		stack_slug = grafana_cloud_stack.test.slug
+		name        = "tfk6installtest-sa"
+		role        = "Admin"
+		is_disabled = false 
+	}
+
+	resource "grafana_cloud_stack_service_account_token" "tfk6installtest_sa_token" {
+		stack_slug = grafana_cloud_stack.test.slug
+		service_account_id = grafana_cloud_stack_service_account.tfk6installtest_sa.id
+		name       = "tfk6installtest-sa-token"
+	}
+	` +
+		`
+	resource "grafana_k6_installation" "test" {
+		cloud_access_policy_token = grafana_cloud_access_policy_token.test.token
+		stack_id                  = grafana_cloud_stack.test.id
+		grafana_sa_token          = grafana_cloud_stack_service_account_token.tfk6installtest_sa_token.key
+		grafana_user              = "admin"
+	}
+	`
+}
diff --git a/internal/resources/cloudprovider/resource_aws_resource_metadata_scrape_job_test.go b/internal/resources/cloudprovider/resource_aws_resource_metadata_scrape_job_test.go
new file mode 100644
index 00000000..f733c6ed
--- /dev/null
+++ b/internal/resources/cloudprovider/resource_aws_resource_metadata_scrape_job_test.go
@@ -0,0 +1,245 @@
+package cloudprovider_test
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/cloudproviderapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+	"github.com/hashicorp/hcl/v2/hclwrite"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+	"github.com/zclconf/go-cty/cty"
+)
+
+var testAWSResourceMetadataScrapeJobData = cloudproviderapi.AWSResourceMetadataScrapeJobRequest{
+	RegionsSubsetOverride: []string{"us-east-1", "us-east-2"},
+	Services: []cloudproviderapi.AWSResourceMetadataService{
+		{
+			Name:                  "AWS/EC2",
+			ScrapeIntervalSeconds: 300,
+			ResourceDiscoveryTagFilters: []cloudproviderapi.AWSResourceMetadataTagFilter{
+				{
+					Key:   "k8s.io/cluster-autoscaler/enabled",
+					Value: "true",
+				},
+			},
+		},
+	},
+}
+
+func TestAccResourceAWSResourceMetadataScrapeJob(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	testCfg := makeTestConfig(t)
+
+	jobName := "test-job" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum)
+
+	var gotJob cloudproviderapi.AWSResourceMetadataScrapeJobResponse
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: AWSResourceMetadataScrapeJobResourceData(testCfg.stackID,
+					jobName,
+					false,
+					testCfg.accountID,
+					testAWSResourceMetadataScrapeJobData,
+				),
+				Check: resource.ComposeTestCheckFunc(
+					checkAWSResourceMetadataScrapeJobResourceExists("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", testCfg.stackID, &gotJob),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "stack_id", testCfg.stackID),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "name", jobName),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "enabled", "false"),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "disabled_reason", "disabled_by_user"),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "aws_account_resource_id", testCfg.accountID),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "regions_subset_override.#", "2"),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "regions_subset_override.0", testAWSResourceMetadataScrapeJobData.RegionsSubsetOverride[0]),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "regions_subset_override.1", testAWSResourceMetadataScrapeJobData.RegionsSubsetOverride[1]),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.#", fmt.Sprintf("%d", len(testAWSResourceMetadataScrapeJobData.Services))),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.name", testAWSResourceMetadataScrapeJobData.Services[0].Name),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.scrape_interval_seconds", fmt.Sprintf("%d", testAWSResourceMetadataScrapeJobData.Services[0].ScrapeIntervalSeconds)),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.resource_discovery_tag_filter.#", fmt.Sprintf("%d", len(testAWSResourceMetadataScrapeJobData.Services[0].ResourceDiscoveryTagFilters))),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.resource_discovery_tag_filter.0.key", testAWSResourceMetadataScrapeJobData.Services[0].ResourceDiscoveryTagFilters[0].Key),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.resource_discovery_tag_filter.0.value", testAWSResourceMetadataScrapeJobData.Services[0].ResourceDiscoveryTagFilters[0].Value),
+				),
+			},
+			// update to remove regions_subset_override so that the account's regions are used instead
+			{
+				Config: AWSResourceMetadataScrapeJobResourceData(testCfg.stackID,
+					jobName,
+					false,
+					testCfg.accountID,
+					func() cloudproviderapi.AWSResourceMetadataScrapeJobRequest {
+						req := testAWSResourceMetadataScrapeJobData
+						req.RegionsSubsetOverride = nil
+						return req
+					}(),
+				),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "regions_subset_override.#", "0"),
+				),
+			},
+			// update to enable the job
+			{
+				Config: AWSResourceMetadataScrapeJobResourceData(testCfg.stackID,
+					jobName,
+					true,
+					testCfg.accountID,
+					testAWSResourceMetadataScrapeJobData,
+				),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "stack_id", testCfg.stackID),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "name", jobName),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "enabled", "true"),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "disabled_reason", ""),
+				),
+			},
+			// update to disable the job again
+			{
+				Config: AWSResourceMetadataScrapeJobResourceData(testCfg.stackID,
+					jobName,
+					false,
+					testCfg.accountID,
+					testAWSResourceMetadataScrapeJobData,
+				),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "stack_id", testCfg.stackID),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "name", jobName),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "enabled", "false"),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "disabled_reason", "disabled_by_user"),
+				),
+			},
+			// update to unset optional tags_to_add_for_metrics field in service block
+			{
+				Config: AWSResourceMetadataScrapeJobResourceData(testCfg.stackID,
+					jobName,
+					false,
+					testCfg.accountID,
+					func() cloudproviderapi.AWSResourceMetadataScrapeJobRequest {
+						req := testAWSResourceMetadataScrapeJobData
+						req.Services[0].ResourceDiscoveryTagFilters = nil
+						return req
+					}(),
+				),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.#", fmt.Sprintf("%d", len(testAWSResourceMetadataScrapeJobData.Services))),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.name", testAWSResourceMetadataScrapeJobData.Services[0].Name),
+					// expect this to be stored in the state as an empty list, not null
+					resource.TestCheckResourceAttrSet("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.resource_discovery_tag_filter.#"),
+					resource.TestCheckResourceAttr("grafana_cloud_provider_aws_resource_metadata_scrape_job.test", "service.0.resource_discovery_tag_filter.#", "0"),
+				),
+			},
+			{
+				ResourceName:      "grafana_cloud_provider_aws_resource_metadata_scrape_job.test",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+		CheckDestroy: checkAWSResourceMetadataScrapeJobResourceDestroy(testCfg.stackID, &gotJob),
+	})
+}
+
+func checkAWSResourceMetadataScrapeJobResourceExists(rn string, stackID string, job *cloudproviderapi.AWSResourceMetadataScrapeJobResponse) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("resource not found: %s\n %#v", rn, s.RootModule().Resources)
+		}
+
+		if rs.Primary.ID == "" {
+			return fmt.Errorf("resource id not set")
+		}
+
+		parts := strings.SplitN(rs.Primary.ID, ":", 2)
+		if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
+			return fmt.Errorf("Invalid ID: %s", rs.Primary.ID)
+		}
+		gotStackID := parts[0]
+		jobName := parts[1]
+
+		if gotStackID == "" {
+			return fmt.Errorf("stack id not set")
+		}
+
+		if gotStackID != stackID {
+			return fmt.Errorf("stack id mismatch, expected %s, but %s was in the TF state", stackID, gotStackID)
+		}
+
+		if jobName == "" {
+			return fmt.Errorf("jobName not set")
+		}
+
+		client := testutils.Provider.Meta().(*common.Client).CloudProviderAPI
+		gotJob, err := client.GetAWSResourceMetadataScrapeJob(context.Background(), stackID, jobName)
+		if err != nil {
+			return fmt.Errorf("error getting account: %s", err)
+		}
+
+		*job = gotJob
+
+		return nil
+	}
+}
+
+func checkAWSResourceMetadataScrapeJobResourceDestroy(stackID string, job *cloudproviderapi.AWSResourceMetadataScrapeJobResponse) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		if job.Name == "" {
+			return fmt.Errorf("checking deletion of empty job name")
+		}
+
+		client := testutils.Provider.Meta().(*common.Client).CloudProviderAPI
+		_, err := client.GetAWSResourceMetadataScrapeJob(context.Background(), stackID, job.Name)
+		if err == nil {
+			return fmt.Errorf("job still exists")
+		} else if !common.IsNotFoundError(err) {
+			return fmt.Errorf("unexpected error retrieving job: %s", err)
+		}
+
+		return nil
+	}
+}
+
+func AWSResourceMetadataScrapeJobResourceData(stackID string, jobName string, enabled bool, awsAccountResourceID string, req cloudproviderapi.AWSResourceMetadataScrapeJobRequest) string {
+	req.Name = jobName
+	req.Enabled = enabled
+	req.AWSAccountResourceID = awsAccountResourceID
+
+	hclFile := hclwrite.NewFile()
+	jobRes := hclFile.Body().AppendNewBlock("resource", []string{"grafana_cloud_provider_aws_resource_metadata_scrape_job", "test"})
+
+	jobBody := jobRes.Body()
+	jobBody.SetAttributeValue("name", cty.StringVal(req.Name))
+	jobBody.SetAttributeValue("enabled", cty.BoolVal(req.Enabled))
+	jobBody.SetAttributeValue("aws_account_resource_id", cty.StringVal(req.AWSAccountResourceID))
+	jobBody.SetAttributeValue("stack_id", cty.StringVal(stackID))
+
+	if len(req.RegionsSubsetOverride) > 0 {
+		vals := make([]cty.Value, len(req.RegionsSubsetOverride))
+		for i, v := range req.RegionsSubsetOverride {
+			vals[i] = cty.StringVal(v)
+		}
+		jobBody.SetAttributeValue("regions_subset_override", cty.ListVal(vals))
+	}
+
+	for _, svc := range req.Services {
+		svcBody := jobBody.AppendNewBlock("service", nil).Body()
+		svcBody.SetAttributeValue("name", cty.StringVal(svc.Name))
+		svcBody.SetAttributeValue("scrape_interval_seconds", cty.NumberIntVal(svc.ScrapeIntervalSeconds))
+		for _, tagFilter := range svc.ResourceDiscoveryTagFilters {
+			tagBody := svcBody.AppendNewBlock("resource_discovery_tag_filter", nil).Body()
+			tagBody.SetAttributeValue("key", cty.StringVal(tagFilter.Key))
+			tagBody.SetAttributeValue("value", cty.StringVal(tagFilter.Value))
+		}
+	}
+
+	var b bytes.Buffer
+	hclFile.WriteTo(&b)
+	return b.String()
+}
diff --git a/internal/resources/examples_test.go b/internal/resources/examples_test.go
index 5411775b..a0c5aa5d 100644
--- a/internal/resources/examples_test.go
+++ b/internal/resources/examples_test.go
@@ -103,6 +103,13 @@ func TestAccExamples(t *testing.T) {
 				testutils.CheckCloudInstanceTestsEnabled(t)
 			},
 		},
+		{
+			category: "k6",
+			testCheck: func(t *testing.T, filename string) {
+				t.Skip() // TODO: Make all examples work
+				testutils.CheckCloudInstanceTestsEnabled(t)
+			},
+		},
 		{
 			category: "Cloud Provider",
 			testCheck: func(t *testing.T, filename string) {
diff --git a/internal/resources/k6/common_check_exists_test.go b/internal/resources/k6/common_check_exists_test.go
new file mode 100644
index 00000000..0b23edc8
--- /dev/null
+++ b/internal/resources/k6/common_check_exists_test.go
@@ -0,0 +1,129 @@
+package k6_test
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+// Helpers that check if a resource exists or doesn't. To define a new one, use the newCheckExistsHelper function.
+// A function that gets a resource by their Terraform id is required.
+var (
+	projectCheckExists = newCheckExistsHelper(
+		func(p *k6.ProjectApiModel) int32 { return p.GetId() },
+		func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*k6.ProjectApiModel, error) {
+			ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+			m, _, err := client.ProjectsAPI.ProjectsRetrieve(ctx, id).
+				XStackId(config.StackID).
+				Execute()
+			return payloadOrError(m, err)
+		},
+	)
+	projectLimitsCheckExists = newCheckExistsHelper(
+		func(p *k6.ProjectLimitsApiModel) int32 { return p.GetProjectId() },
+		func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*k6.ProjectLimitsApiModel, error) {
+			ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+			m, _, err := client.ProjectsAPI.ProjectsLimitsRetrieve(ctx, id).
+				XStackId(config.StackID).
+				Execute()
+			return payloadOrError(m, err)
+		},
+	)
+	loadTestCheckExists = newCheckExistsHelper(
+		func(lt *k6.LoadTestApiModel) int32 { return lt.GetId() },
+		func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*k6.LoadTestApiModel, error) {
+			ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+			m, _, err := client.LoadTestsAPI.LoadTestsRetrieve(ctx, id).
+				XStackId(config.StackID).
+				Execute()
+			return payloadOrError(m, err)
+		},
+	)
+)
+
+type checkExistsGetResourceFunc[T interface{}] func(client *k6.APIClient, config *k6providerapi.K6APIConfig, id int32) (*T, error)
+type checkExistsGetIDFunc[T interface{}] func(*T) int32
+
+type checkExistsHelper[T interface{}] struct {
+	getIDFunc       func(*T) int32
+	getResourceFunc checkExistsGetResourceFunc[T]
+}
+
+// newCheckExistsHelper creates a test helper that checks if a resource exists or not.
+// The getIDFunc function should return the id of the resource.
+// The getResourceFunc function should return the resource from the given id.
+func newCheckExistsHelper[T interface{}](getIDFunc checkExistsGetIDFunc[T], getResourceFunc checkExistsGetResourceFunc[T]) checkExistsHelper[T] {
+	return checkExistsHelper[T]{getIDFunc: getIDFunc, getResourceFunc: getResourceFunc}
+}
+
+// exists checks that the resource exists.
+func (h *checkExistsHelper[T]) exists(rn string, v *T) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("resource not found: %s\n %#v", rn, s.RootModule().Resources)
+		}
+		if rs.Primary.ID == "" {
+			return fmt.Errorf("resource id not set")
+		}
+
+		var resourceID int32
+		if intResourceID, err := strconv.Atoi(rs.Primary.ID); err != nil {
+			return fmt.Errorf("could not convert resource id to integer: %s", err.Error())
+		} else if resourceID, err = common.ToInt32(intResourceID); err != nil {
+			return fmt.Errorf("could not convert resource id to int32: %s", err.Error())
+		}
+
+		obj, err := h.getResourceFunc(
+			testutils.Provider.Meta().(*common.Client).K6APIClient,
+			testutils.Provider.Meta().(*common.Client).K6APIConfig,
+			resourceID,
+		)
+		if err != nil {
+			return fmt.Errorf("error getting resource %s with id %q: %s", rn, rs.Primary.ID, err)
+		}
+
+		// Sanity check: The "destroyed" function should fail here because the resource exists
+		if err := h.destroyed(obj)(s); err == nil {
+			return fmt.Errorf("the destroyed check passed but shouldn't for resource %s with id %q. This is a bug in the test", rn, rs.Primary.ID)
+		}
+
+		*v = *obj
+		return nil
+	}
+}
+
+// destroyed checks that the resource doesn't exist.
+func (h *checkExistsHelper[T]) destroyed(v *T) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		resourceID := h.getIDFunc(v)
+		_, err := h.getResourceFunc(
+			testutils.Provider.Meta().(*common.Client).K6APIClient,
+			testutils.Provider.Meta().(*common.Client).K6APIConfig,
+			resourceID,
+		)
+		if err == nil {
+			return fmt.Errorf("%T %d still exists", v, resourceID)
+		} else if !common.IsNotFoundError(err) {
+			return fmt.Errorf("error checking if resource with id %d was destroyed: %s", resourceID, err)
+		}
+		return nil
+	}
+}
+
+// payloadOrError returns the error if not nil, or the payload otherwise.
+func payloadOrError[T interface{}](t *T, err error) (*T, error) {
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
diff --git a/internal/resources/k6/data_source_k6_loadtest.go b/internal/resources/k6/data_source_k6_loadtest.go
new file mode 100644
index 00000000..6333827e
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtest.go
@@ -0,0 +1,145 @@
+package k6
+
+import (
+	"context"
+	"strconv"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*loadTestDataSource)(nil)
+)
+
+var (
+	dataSourceLoadTestName = "grafana_k6_load_test"
+)
+
+func dataSourceLoadTest() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceLoadTestName,
+		&loadTestDataSource{},
+	)
+}
+
+// loadTestDataSourceModel maps the data source schema data.
+type loadTestDataSourceModel struct {
+	ID                types.Int32  `tfsdk:"id"`
+	ProjectID         types.Int32  `tfsdk:"project_id"`
+	Name              types.String `tfsdk:"name"`
+	Script            types.String `tfsdk:"script"`
+	BaselineTestRunID types.Int32  `tfsdk:"baseline_test_run_id"`
+	Created           types.String `tfsdk:"created"`
+	Updated           types.String `tfsdk:"updated"`
+}
+
+// loadTestDataSource is the data source implementation.
+type loadTestDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *loadTestDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceLoadTestName
+}
+
+// Schema defines the schema for the data source.
+func (d *loadTestDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves a k6 load test.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the load test.",
+				Required:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project this load test belongs to.",
+				Computed:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the load test.",
+				Computed:    true,
+			},
+			"script": schema.StringAttribute{
+				Description: "The k6 test script content.",
+				Computed:    true,
+			},
+			"baseline_test_run_id": schema.Int32Attribute{
+				Description: "Identifier of a baseline test run used for results comparison.",
+				Computed:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the load test was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the load test was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *loadTestDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state loadTestDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Retrieve the load test attributes
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.LoadTestsAPI.LoadTestsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	lt, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test",
+			"Could not read k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Retrieve the load test script content
+	scriptReq := d.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	script, _, err := scriptReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test script",
+			"Could not read k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	state.Name = types.StringValue(lt.GetName())
+	state.ProjectID = types.Int32Value(lt.GetProjectId())
+	state.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	state.Script = types.StringValue(script)
+	state.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
+
+func handleBaselineTestRunID(baselineTestRunID int32) types.Int32 {
+	if baselineTestRunID == 0 {
+		// If the API returned 0, set it as null
+		return types.Int32Null()
+	}
+	return types.Int32Value(baselineTestRunID)
+}
diff --git a/internal/resources/k6/data_source_k6_loadtest_test.go b/internal/resources/k6/data_source_k6_loadtest_test.go
new file mode 100644
index 00000000..bedba089
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtest_test.go
@@ -0,0 +1,29 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6LoadTest_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_load_test/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_test.from_id", "name", "Terraform Test Load Test"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_test.from_id", "updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_loadtests.go b/internal/resources/k6/data_source_k6_loadtests.go
new file mode 100644
index 00000000..ffc6722b
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtests.go
@@ -0,0 +1,161 @@
+package k6
+
+import (
+	"context"
+	"slices"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ datasource.DataSourceWithConfigure = (*loadTestsDataSource)(nil)
+)
+
+var (
+	dataSourceLoadTestsName = "grafana_k6_load_tests"
+)
+
+func dataSourceLoadTests() *common.DataSource {
+	return common.NewDataSource(
+		common.CategoryK6,
+		dataSourceLoadTestsName,
+		&loadTestsDataSource{},
+	)
+}
+
+// loadTestsDataSourceModel maps the data source schema data.
+type loadTestsDataSourceModel struct {
+	ID        types.Int32               `tfsdk:"id"`
+	ProjectID types.Int32               `tfsdk:"project_id"`
+	Name      types.String              `tfsdk:"name"`
+	LoadTests []loadTestDataSourceModel `tfsdk:"load_tests"`
+}
+
+// loadTestsDataSource is the data source implementation.
+type loadTestsDataSource struct {
+	basePluginFrameworkDataSource
+}
+
+// Metadata returns the data source type name.
+func (d *loadTestsDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = dataSourceLoadTestsName
+}
+
+// Schema defines the schema for the data source.
+func (d *loadTestsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Retrieves all k6 load tests that belong to a project.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "The identifier of the project the load tests belong to. " +
+					"This is set to the same as the project_id.",
+				Computed: true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project the load tests belong to.",
+				Required:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the load test.",
+				Optional:    true,
+			},
+			"load_tests": schema.ListAttribute{
+				Computed: true,
+				ElementType: types.ObjectType{
+					AttrTypes: map[string]attr.Type{
+						"id":                   types.Int32Type,
+						"name":                 types.StringType,
+						"project_id":           types.Int32Type,
+						"script":               types.StringType,
+						"baseline_test_run_id": types.Int32Type,
+						"created":              types.StringType,
+						"updated":              types.StringType,
+					},
+				},
+			},
+		},
+	}
+}
+
+// Read refreshes the Terraform state with the latest data.
+func (d *loadTestsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	var state loadTestsDataSourceModel
+	diags := req.Config.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Set the ID to match the project_id
+	state.ID = state.ProjectID
+
+	// Retrieve the project's load tests
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, d.config.Token)
+	k6Req := d.client.LoadTestsAPI.ProjectsLoadTestsRetrieve(ctx, state.ProjectID.ValueInt32()).
+		XStackId(d.config.StackID)
+
+	lts, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load tests",
+			"Could not read k6 load tests with project id "+strconv.Itoa(int(state.ProjectID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Filter the results by name, if provided
+	if !state.Name.IsNull() {
+		lts.Value = slices.DeleteFunc(lts.Value, func(lt k6.LoadTestApiModel) bool {
+			return !strings.EqualFold(lt.GetName(), state.Name.ValueString())
+		})
+	}
+
+	// Process the results and populate the state with the retrieved load tests
+	var loadTests []loadTestDataSourceModel
+	sort.Slice(lts.Value, func(i, j int) bool {
+		return lts.Value[i].GetCreated().Before(lts.Value[j].GetCreated())
+	})
+	for _, lt := range lts.Value {
+		// Retrieve the load test script content
+		scriptReq := d.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, lt.GetId()).
+			XStackId(d.config.StackID)
+
+		script, _, err := scriptReq.Execute()
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Error reading k6 load test script",
+				"Could not read k6 load test script with id"+strconv.Itoa(int(lt.GetId()))+": "+err.Error(),
+			)
+			return
+		}
+
+		ltState := loadTestDataSourceModel{
+			ID:                types.Int32Value(lt.GetId()),
+			Name:              types.StringValue(lt.GetName()),
+			ProjectID:         types.Int32Value(lt.GetProjectId()),
+			BaselineTestRunID: handleBaselineTestRunID(lt.GetBaselineTestRunId()),
+			Script:            types.StringValue(script),
+			Created:           types.StringValue(lt.GetCreated().Format(time.RFC3339Nano)),
+			Updated:           types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano)),
+		}
+
+		loadTests = append(loadTests, ltState)
+	}
+
+	state.LoadTests = loadTests
+
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+}
diff --git a/internal/resources/k6/data_source_k6_loadtests_test.go b/internal/resources/k6/data_source_k6_loadtests_test.go
new file mode 100644
index 00000000..bd708da3
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_loadtests_test.go
@@ -0,0 +1,59 @@
+package k6_test
+
+import (
+	"fmt"
+	"strconv"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6LoadTests_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var project k6.ProjectApiModel
+
+	checkProjectIDMatch := func(value string) error {
+		if value != strconv.Itoa(int(project.GetId())) {
+			return fmt.Errorf("project_id does not match the expected value: %s", value)
+		}
+		return nil
+	}
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_load_tests/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.load_test_project", &project),
+					// from_project_id.0
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_tests.from_project_id", "load_tests.0.name", "Terraform Test Load Test"),
+					resource.TestCheckResourceAttrWith("data.grafana_k6_load_tests.from_project_id", "load_tests.0.project_id", checkProjectIDMatch),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.0.updated"),
+					// from_project_id.1
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_tests.from_project_id", "load_tests.1.name", "Terraform Test Load Test (2)"),
+					resource.TestCheckResourceAttrWith("data.grafana_k6_load_tests.from_project_id", "load_tests.1.project_id", checkProjectIDMatch),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.from_project_id", "load_tests.1.updated"),
+					// filter_by_name.0
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.name", "Terraform Test Load Test (2)"),
+					resource.TestCheckResourceAttrWith("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.project_id", checkProjectIDMatch),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.script"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_load_tests.filter_by_name", "load_tests.0.updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_project_limits_test.go b/internal/resources/k6/data_source_k6_project_limits_test.go
new file mode 100644
index 00000000..551fd5dc
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project_limits_test.go
@@ -0,0 +1,29 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6ProjectLimits_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_project_limits/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					// project_id
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "vuh_max_per_month", "10000"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "vu_max_per_test", "10000"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "vu_browser_max_per_test", "1000"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project_limits.from_project_id", "duration_max_per_test", "3600"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_project_test.go b/internal/resources/k6/data_source_k6_project_test.go
new file mode 100644
index 00000000..fc85243f
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_project_test.go
@@ -0,0 +1,30 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6Project_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_project/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_project.from_id", "name", "Terraform Test Project"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "is_default"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "grafana_folder_uid"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_project.from_id", "updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/data_source_k6_projects_test.go b/internal/resources/k6/data_source_k6_projects_test.go
new file mode 100644
index 00000000..0ab63d9f
--- /dev/null
+++ b/internal/resources/k6/data_source_k6_projects_test.go
@@ -0,0 +1,47 @@
+package k6_test
+
+import (
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccDataSourceK6Projects_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var (
+		project  k6.ProjectApiModel
+		project2 k6.ProjectApiModel
+	)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "data-sources/grafana_k6_projects/data-source.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.project", &project),
+					projectCheckExists.exists("grafana_k6_project.project_2", &project2),
+					// from_name.0
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_projects.from_name", "projects.0.name", "Terraform Test Project"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.is_default"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.grafana_folder_uid"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.0.updated"),
+					// from_name.1
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.id"),
+					resource.TestCheckResourceAttr("data.grafana_k6_projects.from_name", "projects.1.name", "Terraform Test Project"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.is_default"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.grafana_folder_uid"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.created"),
+					resource.TestCheckResourceAttrSet("data.grafana_k6_projects.from_name", "projects.1.updated"),
+				),
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/resource_loadtest.go b/internal/resources/k6/resource_loadtest.go
new file mode 100644
index 00000000..dc78ebdc
--- /dev/null
+++ b/internal/resources/k6/resource_loadtest.go
@@ -0,0 +1,353 @@
+package k6
+
+import (
+	"context"
+	"io"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common/k6providerapi"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+	_ resource.ResourceWithConfigure   = (*loadTestResource)(nil)
+	_ resource.ResourceWithImportState = (*loadTestResource)(nil)
+)
+
+var (
+	resourceLoadTestName = "grafana_k6_load_test"
+	resourceLoadTestID   = common.NewResourceID(common.IntIDField("id"))
+)
+
+func resourceLoadTest() *common.Resource {
+	return common.NewResource(
+		common.CategoryK6,
+		resourceLoadTestName,
+		resourceLoadTestID,
+		&loadTestResource{},
+	).WithLister(k6ListerFunction(listLoadTests))
+}
+
+// loadTestResourceModel maps the resource schema data.
+type loadTestResourceModel struct {
+	ID                types.Int32  `tfsdk:"id"`
+	ProjectID         types.Int32  `tfsdk:"project_id"`
+	Name              types.String `tfsdk:"name"`
+	Script            types.String `tfsdk:"script"`
+	BaselineTestRunID types.Int32  `tfsdk:"baseline_test_run_id"`
+	Created           types.String `tfsdk:"created"`
+	Updated           types.String `tfsdk:"updated"`
+}
+
+// loadTestResource is the resource implementation.
+type loadTestResource struct {
+	basePluginFrameworkResource
+}
+
+// Metadata returns the resource type name.
+func (r *loadTestResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = resourceLoadTestName
+}
+
+// Schema defines the schema for the resource.
+func (r *loadTestResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "Manages a k6 load test.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.Int32Attribute{
+				Description: "Numeric identifier of the load test.",
+				Computed:    true,
+			},
+			"project_id": schema.Int32Attribute{
+				Description: "The identifier of the project this load test belongs to.",
+				Required:    true,
+			},
+			"name": schema.StringAttribute{
+				Description: "Human-friendly identifier of the load test.",
+				Required:    true,
+			},
+			"script": schema.StringAttribute{
+				Description: "The k6 test script content. Can be provided inline or via the `file()` function.",
+				Required:    true,
+			},
+			"baseline_test_run_id": schema.Int32Attribute{
+				Description: "Identifier of a baseline test run used for results comparison.",
+				Optional:    true,
+			},
+			"created": schema.StringAttribute{
+				Description: "The date when the load test was created.",
+				Computed:    true,
+			},
+			"updated": schema.StringAttribute{
+				Description: "The date when the load test was last updated.",
+				Computed:    true,
+			},
+		},
+	}
+}
+
+// Create creates the resource and sets the Terraform state on success.
+func (r *loadTestResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	// Retrieve values from plan
+	var plan loadTestResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.LoadTestsAPI.ProjectsLoadTestsCreate(ctx, plan.ProjectID.ValueInt32()).
+		Name(plan.Name.ValueString()).
+		Script(io.NopCloser(strings.NewReader(plan.Script.ValueString()))).
+		XStackId(r.config.StackID)
+
+	// Create new load test
+	lt, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error creating load test",
+			"Could not create load test, unexpected error: "+err.Error(),
+		)
+		return
+	}
+
+	// Map response body to schema and populate Computed attribute values
+	plan.ID = types.Int32Value(lt.GetId())
+	plan.Name = types.StringValue(lt.GetName())
+	plan.ProjectID = types.Int32Value(lt.GetProjectId())
+	plan.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	plan.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set state to fully populated data
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Read retrieves the resource information.
+func (r *loadTestResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	// Get current state
+	var state loadTestResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Retrieve the load test attributes
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	k6Req := r.client.LoadTestsAPI.LoadTestsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	lt, _, err := k6Req.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test",
+			"Could not read k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Retrieve the load test script content
+	scriptReq := r.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	script, _, err := scriptReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test script",
+			"Could not read k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	state.ID = types.Int32Value(lt.GetId())
+	state.Name = types.StringValue(lt.GetName())
+	state.ProjectID = types.Int32Value(lt.GetProjectId())
+	state.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	state.Script = types.StringValue(script)
+	state.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	state.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	// Set refreshed state
+	diags = resp.State.Set(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Update updates the resource and sets the updated Terraform state on success.
+func (r *loadTestResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	// Retrieve values from plan
+	var plan loadTestResourceModel
+	diags := req.Plan.Get(ctx, &plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Get current state to retrieve the ID
+	var state loadTestResourceModel
+	diags = req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Generate API request body from plan
+	toUpdate := k6.NewPatchLoadTestApiModel()
+	toUpdate.SetName(plan.Name.ValueString())
+	if plan.BaselineTestRunID.IsNull() {
+		toUpdate.SetBaselineTestRunIdNil()
+	} else {
+		toUpdate.SetBaselineTestRunId(plan.BaselineTestRunID.ValueInt32())
+	}
+
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	updateReq := r.client.LoadTestsAPI.LoadTestsPartialUpdate(ctx, state.ID.ValueInt32()).
+		PatchLoadTestApiModel(toUpdate).
+		XStackId(r.config.StackID)
+
+	// Update the load test
+	_, err := updateReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error updating k6 load test",
+			"Could not update k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Update the script if it has changed
+	if !plan.Script.Equal(state.Script) {
+		scriptReq := r.client.LoadTestsAPI.LoadTestsScriptUpdate(ctx, state.ID.ValueInt32()).
+			Body(io.NopCloser(strings.NewReader(plan.Script.ValueString()))).
+			XStackId(r.config.StackID)
+
+		_, err = scriptReq.Execute()
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Error updating k6 load test script",
+				"Could not update k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+			)
+			return
+		}
+	}
+
+	// Update resource state with updated items and timestamp
+	fetchReq := r.client.LoadTestsAPI.LoadTestsRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	lt, _, err := fetchReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test",
+			"Could not read k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Retrieve the updated script content
+	scriptReq := r.client.LoadTestsAPI.LoadTestsScriptRetrieve(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	script, _, err := scriptReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error reading k6 load test script",
+			"Could not read k6 load test script with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+		return
+	}
+
+	// Overwrite items with refreshed state
+	plan.ID = types.Int32Value(lt.GetId())
+	plan.Name = types.StringValue(lt.GetName())
+	plan.ProjectID = types.Int32Value(lt.GetProjectId())
+	plan.BaselineTestRunID = handleBaselineTestRunID(lt.GetBaselineTestRunId())
+	plan.Script = types.StringValue(script)
+	plan.Created = types.StringValue(lt.GetCreated().Format(time.RFC3339Nano))
+	plan.Updated = types.StringValue(lt.GetUpdated().Format(time.RFC3339Nano))
+
+	diags = resp.State.Set(ctx, plan)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+}
+
+// Delete deletes the resource and removes the Terraform state on success.
+func (r *loadTestResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	// Retrieve values from state
+	var state loadTestResourceModel
+	diags := req.State.Get(ctx, &state)
+	resp.Diagnostics.Append(diags...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	// Delete existing load test
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, r.config.Token)
+	deleteReq := r.client.LoadTestsAPI.LoadTestsDestroy(ctx, state.ID.ValueInt32()).
+		XStackId(r.config.StackID)
+
+	_, err := deleteReq.Execute()
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error deleting k6 load test",
+			"Could not delete k6 load test with id "+strconv.Itoa(int(state.ID.ValueInt32()))+": "+err.Error(),
+		)
+	}
+}
+
+func (r *loadTestResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+	id, err := strconv.ParseInt(req.ID, 10, 32)
+	if err != nil {
+		resp.Diagnostics.AddError(
+			"Error importing k6 load test",
+			"Could not parse k6 load test id "+req.ID+": "+err.Error(),
+		)
+		return
+	}
+
+	resp.State.SetAttribute(ctx, path.Root("id"), types.Int32Value(int32(id)))
+
+	readReq := resource.ReadRequest{State: resp.State}
+	readResp := resource.ReadResponse{State: resp.State}
+
+	r.Read(ctx, readReq, &readResp)
+	resp.Diagnostics.Append(readResp.Diagnostics...)
+}
+
+// listLoadTests retrieves the list ids of all the existing load tests.
+func listLoadTests(ctx context.Context, client *k6.APIClient, config *k6providerapi.K6APIConfig) ([]string, error) {
+	ctx = context.WithValue(ctx, k6.ContextAccessToken, config.Token)
+	resp, _, err := client.LoadTestsAPI.LoadTestsList(ctx).
+		XStackId(config.StackID).
+		Execute()
+	if err != nil {
+		return nil, err
+	}
+
+	var ids []string
+	for _, lt := range resp.Value {
+		ids = append(ids, strconv.Itoa(int(lt.GetId())))
+	}
+	return ids, nil
+}
diff --git a/internal/resources/k6/resource_loadtest_test.go b/internal/resources/k6/resource_loadtest_test.go
new file mode 100644
index 00000000..58e508cf
--- /dev/null
+++ b/internal/resources/k6/resource_loadtest_test.go
@@ -0,0 +1,111 @@
+package k6_test
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccLoadTest_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var (
+		project  k6.ProjectApiModel
+		loadTest k6.LoadTestApiModel
+	)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy: resource.ComposeTestCheckFunc(
+			loadTestCheckExists.destroyed(&loadTest),
+			projectCheckExists.destroyed(&project),
+		),
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "resources/grafana_k6_load_test/resource.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.load_test_project", &project),
+					loadTestCheckExists.exists("grafana_k6_load_test.test_load_test", &loadTest),
+					resource.TestMatchResourceAttr("grafana_k6_load_test.test_load_test", "id", defaultIDRegexp),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "name", "Terraform Test Load Test"),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "script", "export default function() {\n  console.log('Hello from k6!');\n}\n"),
+					testutils.CheckLister("grafana_k6_load_test.test_load_test"),
+				),
+			},
+			{
+				ResourceName:      "grafana_k6_load_test.test_load_test",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+			// Change the name and script of a load test. This shouldn't recreate the load test.
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_k6_load_test/resource.tf", map[string]string{
+					"Terraform Test Load Test":      "Terraform Test Load Test Updated",
+					"console.log('Hello from k6!')": "console.log('Hello from updated k6!')",
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					testAccLoadTestWasntRecreated("grafana_k6_load_test.test_load_test", &loadTest),
+					testAccLoadTestUnchangedAttr("grafana_k6_load_test.test_load_test", "id", func() string { return strconv.Itoa(int(loadTest.GetId())) }),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "name", "Terraform Test Load Test Updated"),
+					resource.TestCheckResourceAttr("grafana_k6_load_test.test_load_test", "script", "export default function() {\n  console.log('Hello from updated k6!');\n}\n"),
+					testAccLoadTestUnchangedAttr("grafana_k6_load_test.test_load_test", "created", func() string { return loadTest.GetCreated().Truncate(time.Microsecond).Format(time.RFC3339Nano) }),
+				),
+			},
+		},
+	})
+}
+
+func testAccLoadTestUnchangedAttr(resName, attrName string, oldValueGetter func() string) resource.TestCheckFunc {
+	return resource.TestCheckResourceAttrWith(resName, attrName, func(newVal string) error {
+		if oldValue := oldValueGetter(); oldValue != newVal {
+			return fmt.Errorf("%s has changed: %s -> %s", attrName, oldValue, newVal)
+		}
+		return nil
+	})
+}
+
+func testAccLoadTestWasntRecreated(rn string, oldLoadTest *k6.LoadTestApiModel) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		newLoadTestResource, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("load test not found: %s", rn)
+		}
+		if newLoadTestResource.Primary.ID == "" {
+			return fmt.Errorf("load test id not set")
+		}
+		var newLoadTestID int32
+		if loadTestID, err := strconv.Atoi(newLoadTestResource.Primary.ID); err != nil {
+			return fmt.Errorf("could not convert load test id to integer: %s", err.Error())
+		} else if newLoadTestID, err = common.ToInt32(loadTestID); err != nil {
+			return fmt.Errorf("could not convert load test id to int32: %s", err.Error())
+		}
+
+		client := testutils.Provider.Meta().(*common.Client).K6APIClient
+		config := testutils.Provider.Meta().(*common.Client).K6APIConfig
+
+		ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+		newLoadTest, _, err := client.LoadTestsAPI.LoadTestsRetrieve(ctx, newLoadTestID).
+			XStackId(config.StackID).
+			Execute()
+		if err != nil {
+			return fmt.Errorf("error getting load test: %s", err)
+		}
+		if newLoadTest.Created != oldLoadTest.Created {
+			return fmt.Errorf("load test creation date has changed: %s -> %s", oldLoadTest.Created, newLoadTest.Created)
+		}
+		if !oldLoadTest.GetUpdated().Before(newLoadTest.GetUpdated()) {
+			return fmt.Errorf("load test update date hasn't changed: %s -> %s", oldLoadTest.Updated, newLoadTest.Updated)
+		}
+		return nil
+	}
+}
diff --git a/internal/resources/k6/resource_project_limits_test.go b/internal/resources/k6/resource_project_limits_test.go
new file mode 100644
index 00000000..df71b8ec
--- /dev/null
+++ b/internal/resources/k6/resource_project_limits_test.go
@@ -0,0 +1,62 @@
+package k6_test
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+func TestAccProjectLimits_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var project k6.ProjectApiModel
+	var projectLimits k6.ProjectLimitsApiModel
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy: resource.ComposeTestCheckFunc(
+			projectCheckExists.destroyed(&project),
+			projectLimitsCheckExists.destroyed(&projectLimits),
+		),
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "resources/grafana_k6_project_limits/resource.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.test_project_limits", &project),
+					projectLimitsCheckExists.exists("grafana_k6_project_limits.test_limits", &projectLimits),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vuh_max_per_month", "1000"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_max_per_test", "100"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_browser_max_per_test", "10"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "duration_max_per_test", "3600"),
+				),
+			},
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_k6_project_limits/resource.tf", map[string]string{
+					"vuh_max_per_month       = 1000": "vuh_max_per_month       = 2000",
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.test_project_limits", &project),
+					projectLimitsCheckExists.exists("grafana_k6_project_limits.test_limits", &projectLimits),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vuh_max_per_month", "2000"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_max_per_test", "100"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "vu_browser_max_per_test", "10"),
+					resource.TestCheckResourceAttr("grafana_k6_project_limits.test_limits", "duration_max_per_test", "3600"),
+				),
+			},
+			{
+				ResourceName:      "grafana_k6_project_limits.test_limits",
+				ImportState:       true,
+				ImportStateVerify: true,
+				ImportStateIdFunc: func(s *terraform.State) (string, error) {
+					return strconv.Itoa(int(project.GetId())), nil
+				},
+			},
+		},
+	})
+}
diff --git a/internal/resources/k6/resource_project_test.go b/internal/resources/k6/resource_project_test.go
new file mode 100644
index 00000000..f6ad52ad
--- /dev/null
+++ b/internal/resources/k6/resource_project_test.go
@@ -0,0 +1,110 @@
+package k6_test
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strconv"
+	"testing"
+	"time"
+
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+
+	"github.com/grafana/k6-cloud-openapi-client-go/k6"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/common"
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+)
+
+var defaultIDRegexp = regexp.MustCompile(`^\d{5,8}$`)
+
+func TestAccProject_basic(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	var project k6.ProjectApiModel
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		CheckDestroy: resource.ComposeTestCheckFunc(
+			projectCheckExists.destroyed(&project),
+		),
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExample(t, "resources/grafana_k6_project/resource.tf"),
+				Check: resource.ComposeTestCheckFunc(
+					projectCheckExists.exists("grafana_k6_project.test_project", &project),
+					resource.TestMatchResourceAttr("grafana_k6_project.test_project", "id", defaultIDRegexp),
+					resource.TestCheckResourceAttr("grafana_k6_project.test_project", "name", "Terraform Test Project"),
+					resource.TestMatchResourceAttr("grafana_k6_project.test_project", "is_default", regexp.MustCompile(`^(true|false)$`)),
+					resource.TestCheckResourceAttrSet("grafana_k6_project.test_project", "grafana_folder_uid"),
+					testutils.CheckLister("grafana_k6_project.test_project"),
+				),
+			},
+			{
+				ResourceName:            "grafana_k6_project.test_project",
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"grafana_folder_uid"},
+			},
+			// Change the title of a project. This shouldn't recreate the project.
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_k6_project/resource.tf", map[string]string{
+					"Terraform Test Project": "Terraform Test Project Updated",
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					testAccProjectWasntRecreated("grafana_k6_project.test_project", &project),
+					testAccProjectUnchangedAttr("grafana_k6_project.test_project", "id", func() string { return strconv.Itoa(int(project.GetId())) }),
+					resource.TestCheckResourceAttr("grafana_k6_project.test_project", "name", "Terraform Test Project Updated"),
+					testAccProjectUnchangedAttr("grafana_k6_project.test_project", "grafana_folder_uid", project.GetGrafanaFolderUid),
+					testAccProjectUnchangedAttr("grafana_k6_project.test_project", "created", func() string { return project.GetCreated().Truncate(time.Microsecond).Format(time.RFC3339Nano) }),
+				),
+			},
+		},
+	})
+}
+
+func testAccProjectUnchangedAttr(resName, attrName string, oldValueGetter func() string) resource.TestCheckFunc {
+	return resource.TestCheckResourceAttrWith(resName, attrName, func(newVal string) error {
+		if oldValue := oldValueGetter(); oldValue != newVal {
+			return fmt.Errorf("%s has changed: %s -> %s", attrName, oldValue, newVal)
+		}
+		return nil
+	})
+}
+
+func testAccProjectWasntRecreated(rn string, oldProject *k6.ProjectApiModel) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		newProjectResource, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("project not found: %s", rn)
+		}
+		if newProjectResource.Primary.ID == "" {
+			return fmt.Errorf("project id not set")
+		}
+		var newProjectID int32
+		if projectID, err := strconv.Atoi(newProjectResource.Primary.ID); err != nil {
+			return fmt.Errorf("could not convert project id to integer: %s", err.Error())
+		} else if newProjectID, err = common.ToInt32(projectID); err != nil {
+			return fmt.Errorf("could not convert project id to int32: %s", err.Error())
+		}
+
+		client := testutils.Provider.Meta().(*common.Client).K6APIClient
+		config := testutils.Provider.Meta().(*common.Client).K6APIConfig
+
+		ctx := context.WithValue(context.Background(), k6.ContextAccessToken, config.Token)
+		newProject, _, err := client.ProjectsAPI.ProjectsRetrieve(ctx, newProjectID).
+			XStackId(config.StackID).
+			Execute()
+		if err != nil {
+			return fmt.Errorf("error getting project: %s", err)
+		}
+		if newProject.Created != oldProject.Created {
+			return fmt.Errorf("project creation date has changed: %s -> %s", oldProject.Created, newProject.Created)
+		}
+		if !oldProject.GetUpdated().Before(newProject.GetUpdated()) {
+			return fmt.Errorf("project update date hasn't changed: %s -> %s", oldProject.Updated, newProject.Updated)
+		}
+		return nil
+	}
+}
diff --git a/internal/resources/syntheticmonitoring/resource_check_alert_test.go b/internal/resources/syntheticmonitoring/resource_check_alert_test.go
new file mode 100644
index 00000000..e23312b2
--- /dev/null
+++ b/internal/resources/syntheticmonitoring/resource_check_alert_test.go
@@ -0,0 +1,99 @@
+package syntheticmonitoring_test
+
+import (
+	"regexp"
+	"strconv"
+	"testing"
+
+	"github.com/grafana/terraform-provider-grafana/v3/internal/testutils"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+)
+
+func TestAccResourceCheckAlerts(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	// Create a random job name to avoid conflicts
+	jobName := acctest.RandomWithPrefix("check-alert")
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_synthetic_monitoring_check_alerts/resource.tf", map[string]string{
+					`"Check Alert Test"`: strconv.Quote(jobName),
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("grafana_synthetic_monitoring_check_alerts.main", "id"),
+					resource.TestCheckResourceAttrSet("grafana_synthetic_monitoring_check_alerts.main", "check_id"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.0.name", "ProbeFailedExecutionsTooHigh"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.0.threshold", "1"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.0.period", "15m"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.1.name", "TLSTargetCertificateCloseToExpiring"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.1.threshold", "14"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.1.period", ""),
+				),
+			},
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_synthetic_monitoring_check_alerts/resource_update.tf", map[string]string{
+					`"Check Alert Test Updated"`: strconv.Quote(jobName),
+				}),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttrSet("grafana_synthetic_monitoring_check_alerts.main", "id"),
+					resource.TestCheckResourceAttrSet("grafana_synthetic_monitoring_check_alerts.main", "check_id"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.0.name", "ProbeFailedExecutionsTooHigh"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.0.threshold", "2"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.0.period", "10m"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.1.name", "TLSTargetCertificateCloseToExpiring"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.1.threshold", "7"),
+					resource.TestCheckResourceAttr("grafana_synthetic_monitoring_check_alerts.main", "alerts.1.period", ""),
+				),
+			},
+		},
+	})
+}
+
+func TestAccResourceCheckAlert_InvalidAlertName(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config:      testAccResourceCheckAlert_InvalidAlertName,
+				PlanOnly:    true,
+				ExpectError: regexp.MustCompile(`expected alerts\.0\.name to be one of \["ProbeFailedExecutionsTooHigh" "TLSTargetCertificateCloseToExpiring"\], got InvalidAlertName`),
+			},
+		},
+	})
+}
+
+func TestAccResourceCheckAlert_Import(t *testing.T) {
+	testutils.CheckCloudInstanceTestsEnabled(t)
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testutils.TestAccExampleWithReplace(t, "resources/grafana_synthetic_monitoring_check_alerts/resource.tf", map[string]string{
+					`"Check Alert Test"`: strconv.Quote(acctest.RandomWithPrefix("check-alert")),
+				}),
+			},
+			{
+				ResourceName:      "grafana_synthetic_monitoring_check_alerts.main",
+				ImportState:       true,
+				ImportStateVerify: true,
+			},
+		},
+	})
+}
+
+const testAccResourceCheckAlert_InvalidAlertName = `
+resource "grafana_synthetic_monitoring_check_alerts" "main" {
+	check_id = 1
+	alerts = [{
+		name = "InvalidAlertName"
+		threshold = 0.5
+		period = ""
+	}]
+}`
diff --git a/internal/testutils/provider.go b/internal/testutils/provider.go
index 4faeca65..1111241c 100644
--- a/internal/testutils/provider.go
+++ b/internal/testutils/provider.go
@@ -188,6 +188,7 @@ func CheckCloudInstanceTestsEnabled(t *testing.T) {
 	CheckEnvVarsSet(t,
 		"GRAFANA_URL",
 		"GRAFANA_AUTH",
+		"GRAFANA_K6_ACCESS_TOKEN",
 		"GRAFANA_SM_ACCESS_TOKEN",
 		"GRAFANA_ONCALL_ACCESS_TOKEN",
 		"GRAFANA_CLOUD_PROVIDER_URL",