> For the complete documentation index, see [llms.txt](https://docs.p0.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.p0.dev/integrations/resource-integrations/custom-application/deploying-a-connector-with-terraform.md).

# Deploying a connector with Terraform

Deploy your custom application connector to AWS Lambda or Google Cloud Run, grant P0 permission to invoke it, and register it with P0, all from Terraform.

This guide deploys a connector you have built with `@p0security/connector-sdk` (from the [connector repository](https://github.com/p0-security/connector)) and registers it as a [Custom application](/integrations/resource-integrations/custom-application.md), using the [P0 Terraform provider](https://registry.terraform.io/providers/p0-security/p0/latest/docs) alongside the AWS or Google Cloud provider.

{% hint style="info" %}
The custom application integration is in preview. Contact P0 if you'd like to build a connector for one of your internal applications.
{% endhint %}

Three things have to be true before a requestor can ask for access through your connector:

1. The connector is deployed and running in your account.
2. P0 is allowed to invoke it, and nothing else is.
3. P0 has the connector's address, which the `p0_app` resource gives it.

Terraform does all three. The last one verifies the first two. Applying `p0_app` makes P0 call your connector, and the apply fails if the call does not succeed.

To deploy the SDK's example with a cloud CLI instead of Terraform, see [Deploying a connector to AWS Lambda](/integrations/resource-integrations/custom-application/deploying-a-connector-to-aws-lambda.md) or [Deploying a connector to Google Cloud Run](/integrations/resource-integrations/custom-application/deploying-a-connector-to-google-cloud-run.md).

## Prerequisites

* A connector built with `@p0security/connector-sdk`. Start from the `aws-lambda` or `gcp-cloudrun` example in the [connector repository](https://github.com/p0-security/connector) and replace its placeholder actions with calls into your own application's API.
* [Terraform](https://developer.hashicorp.com/terraform/install) installed, with the [P0 provider](https://registry.terraform.io/providers/p0-security/p0/latest/docs) configured. See [Manage integrations as code with the P0 Terraform provider](/getting-started/get-started-with-the-p0-terraform-provider.md).
* For AWS Lambda hosting, an installed P0 AWS integration ([AWS](/integrations/resource-integrations/aws.md)) for the account the connector runs in, and permission to create Lambda functions and IAM roles there.
* For Google Cloud Run hosting, an installed P0 Google Cloud integration ([Google Cloud](/integrations/resource-integrations/google-cloud.md)), and permission to deploy Cloud Run services in the project.
* A registry your cloud can pull from, if you deploy a container image.

## Build the connector

Terraform deploys an artifact. It does not build one, so produce that first.

For Cloud Run, build and push a container image. The SDK example ships a Dockerfile:

```bash
docker build --platform linux/amd64 -t <your-image-tag> .
docker push <your-image-tag>
```

{% hint style="warning" %}
Pass `--platform linux/amd64` when you build on Apple silicon. Cloud Run runs `linux/amd64`, and an arm64 image fails to start with an exec format error.
{% endhint %}

For Lambda, bundle your connector into a zip archive whose entrypoint exports the handler returned by `newCustomAppLambdaHandler`. The `aws-lambda` example already wires this up in `src/index.ts`; build and package it with its own script:

```bash
./package.sh
```

This compiles `src/` to `dist/` and writes `connector-lambda.zip`, with `dist/`, a production-only `node_modules`, and `package.json` at the zip root. Because `dist/` stays nested rather than flattened, the Lambda handler is `dist/index.handler`, not `index.handler`.

## AWS Lambda

P0 assumes the role created by your AWS integration and invokes the connector's function. That role needs `lambda:InvokeFunction` on the function and no other permission on it.

### Step 1: Deploy the connector

The connector runs under its own execution role. Grant that role whatever access your application requires, plus CloudWatch Logs.

```terraform
locals {
  account_id     = "123456789012"
  region         = "us-east-1"
  connector_name = "p0-custom-app-connector"
}

resource "aws_iam_role" "connector" {
  name = "${local.connector_name}-execution"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { Service = "lambda.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "connector_logs" {
  role       = aws_iam_role.connector.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_lambda_function" "connector" {
  function_name    = local.connector_name
  role             = aws_iam_role.connector.arn
  filename         = "connector-lambda.zip"
  source_code_hash = filebase64sha256("connector-lambda.zip")
  handler          = "dist/index.handler"
  runtime          = "nodejs22.x"
  timeout          = 60
  memory_size      = 512

  depends_on = [aws_iam_role_policy_attachment.connector_logs]
}
```

### Step 2: Let P0 invoke the connector

Attach the invoke policy to the role P0 assumes in this account. If you installed the AWS integration with Terraform, that role is the one your `p0_aws_iam_write_staged` resource named, as below. If you installed it from the P0 app, use `P0RoleIamManager` instead.

```terraform
resource "aws_iam_role_policy" "invoke_connector" {
  name = "InvokeCustomConnector"
  role = p0_aws_iam_write_staged.example.role.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid      = "InvokeCustomConnector"
      Effect   = "Allow"
      Action   = "lambda:InvokeFunction"
      Resource = aws_lambda_function.connector.arn
    }]
  })
}
```

### Step 3: Register the connector with P0

```terraform
resource "p0_app" "example" {
  id = "internal-admin-tool"

  hosting = {
    type             = "aws"
    account_id       = local.account_id
    connector_name   = aws_lambda_function.connector.function_name
    connector_region = local.region
  }

  depends_on = [aws_iam_role_policy.invoke_connector]
}
```

P0 derives the function's ARN from the account, region, and name you give it, so there is no ARN to paste.

## Google Cloud Run

Three settings decide who may call a Cloud Run connector. The first two take the same service account, the one from your P0 Google Cloud installation:

| Setting              | What it controls                                          |
| -------------------- | --------------------------------------------------------- |
| `roles/run.invoker`  | Who may reach the service at all                          |
| `INVOKER_SA_EMAIL`   | The identity the connector accepts on each request        |
| `CONNECTOR_AUDIENCE` | The service URL the connector accepts identity tokens for |

The connector refuses to start when `INVOKER_SA_EMAIL` or `CONNECTOR_AUDIENCE` is unset, rather than failing request by request in a way that is hard to diagnose from outside.

### Step 1: Deploy the connector

`p0_gcp.example` below is your existing Google Cloud installation. Its `service_account_email` is the identity P0 calls from, and it appears in two of the settings above.

`CONNECTOR_AUDIENCE` must be the service's own URL, which Cloud Run assigns when it creates the service. A resource can't read its own URL, so the audience comes from the `connector_audience` variable. It starts as a placeholder and you set it after the first apply.

```terraform
variable "connector_audience" {
  type    = string
  default = "set-after-first-deploy"
}

locals {
  project = "my-project-id"
  region  = "us-central1"
}

resource "google_project_service" "run" {
  project            = local.project
  service            = "run.googleapis.com"
  disable_on_destroy = false
}

resource "google_service_account" "connector" {
  account_id = "p0-custom-app-connector"
  project    = local.project
}

resource "google_cloud_run_v2_service" "connector" {
  name                = "p0-custom-app-connector"
  project             = local.project
  location            = local.region
  ingress             = "INGRESS_TRAFFIC_ALL"
  deletion_protection = false

  template {
    service_account = google_service_account.connector.email

    containers {
      image = "<your-image-tag>"

      env {
        name  = "INVOKER_SA_EMAIL"
        value = p0_gcp.example.service_account_email
      }

      env {
        name  = "CONNECTOR_AUDIENCE"
        value = var.connector_audience
      }
    }
  }

  depends_on = [google_project_service.run]
}

output "connector_uri" {
  value = google_cloud_run_v2_service.connector.uri
}
```

Create the service on its own first, so it has a URL:

```bash
terraform apply -target=google_cloud_run_v2_service.connector
```

Then set the variable to that URL in `terraform.tfvars`:

```terraform
connector_audience = "<connector_uri output>"
```

You do this once. The URL stays the same for the life of the service, and later applies read the value from `terraform.tfvars`.

The service relies on IAM to gate it rather than on ingress restrictions. P0 calls the connector with a signed identity token, and unauthenticated callers are turned away.

Grant `google_service_account.connector` whatever access your application requires.

### Step 2: Let P0 invoke the connector

```terraform
resource "google_cloud_run_v2_service_iam_member" "invoke_connector" {
  project  = local.project
  location = local.region
  name     = google_cloud_run_v2_service.connector.name
  role     = "roles/run.invoker"
  member   = "serviceAccount:${p0_gcp.example.service_account_email}"
}
```

### Step 3: Register the connector with P0

```terraform
resource "p0_app" "example" {
  id = "internal-admin-tool"

  hosting = {
    type             = "gcp"
    project_id       = local.project
    connector_name   = google_cloud_run_v2_service.connector.name
    connector_region = local.region
  }

  depends_on = [google_cloud_run_v2_service_iam_member.invoke_connector]
}
```

P0 looks up the service's invocation URL from the project, region, and service name, and reports it back on `hosting.connector_service_uri`. You do not paste the Cloud Run URL in.

## Inputs

| Attribute                  | Description                                                                         |
| -------------------------- | ----------------------------------------------------------------------------------- |
| `id`                       | A unique identifier for the application. Any string, such as `internal-admin-tool`. |
| `hosting.type`             | `aws` for Lambda, or `gcp` for Cloud Run                                            |
| `hosting.account_id`       | The AWS account the function is deployed in. AWS hosting only.                      |
| `hosting.project_id`       | The Google Cloud project the service is deployed in. Cloud Run hosting only.        |
| `hosting.connector_name`   | The Lambda function name or Cloud Run service name                                  |
| `hosting.connector_region` | The region the connector is deployed in                                             |

## Outputs

| Attribute                       | Description                                                               |
| ------------------------------- | ------------------------------------------------------------------------- |
| `hosting.connector_service_uri` | The connector's invocation URL, resolved by P0. Cloud Run hosting only.   |
| `label`                         | The label for this installation, defaulting to the application identifier |
| `state`                         | The install progress: `stage`, `configure`, or `installed`                |

## After the apply

Apply, then check the install in the P0 app under **Integrations > Custom application**.

A failed apply on `p0_app` means P0 could not call the connector. The error names what failed, and the usual causes are a connector still deploying, a missing invoke grant, or, on Cloud Run, an `INVOKER_SA_EMAIL` that does not match the service account P0 uses or a `CONNECTOR_AUDIENCE` that does not match the `connector_uri` output. Fix the cause and apply again.

Then request access to one of your policies as a test user, let the grant expire or revoke it by hand, and confirm your application provisioned and deprovisioned the user as you expect.

## Related links

* [Custom application](/integrations/resource-integrations/custom-application.md), for what a connector does and how P0 reaches it
* [Deploying a connector to AWS Lambda](/integrations/resource-integrations/custom-application/deploying-a-connector-to-aws-lambda.md), the same install with the AWS CLI
* [Deploying a connector to Google Cloud Run](/integrations/resource-integrations/custom-application/deploying-a-connector-to-google-cloud-run.md), the same install with the `gcloud` CLI
* [`p0_app`](https://registry.terraform.io/providers/p0-security/p0/latest/docs/resources/app) in the Terraform registry
* [Requesting access](/access-management/access-management/just-in-time-access/requesting-access.md)
