Note that this is a preproduction feature that is available only to preproduction Tenants.
This guide provides complete instructions for deploying the PlainID Policy Authorization Agent (PAA) to the AWS Elastic Container Service (ECS) on Fargate using Terraform. It walks you through prerequisites, configuration, installation, and advanced customization.
Deploying the PAA through this Terraform pack gives you a full set of PAA services (agent, runtime, pip-operator, idp-webhook, secrets-mgmt) running on ECS-native primitives.
This deployment guide covers:
- Prerequisites
- Networking model
- What the pack creates
- Installing the pack
- Configuring services
- Secrets
- Redis modes
- Container images
Prerequisites
Before you begin, ensure you have the following:
- Terraform version 1.9 or later
- AWS provider version 5.40 or later
- An AWS account with sufficient permissions to create ECS services, IAM roles, Cloud Map resources, CloudWatch Logs groups, and Application Auto Scaling resources
- An existing VPC and subnets
- Task subnets with outbound network reachability to ECR, the SSM API, the ECS API, and the CloudWatch Logs API (a NAT gateway or VPC endpoints), plus reachability to the PlainID tunnel endpoint (
remote_api) - Your PlainID Tenant ID, PAA ID, and Agent Secret Key, available in Platform Tenant Settings
Networking model
PAA traffic is outbound and in-VPC only. This pack doesn't provision a load balancer, target groups, listener rules, or a TLS certificate.
- The agent service opens an outbound tunnel to PlainID over
remote_api(wss://...orhttps://...). - Callers inside your VPC, both other PAA services and your own applications, reach runtime, pip-operator, and secrets-mgmt over AWS Cloud Map private DNS (
<service>.<service_discovery_namespace>, for exampleruntime.plainid-paa.internal:8010). - The pack disables the idp-webhook by default. If you need inbound webhooks from external identity providers, add your own load balancer or API Gateway in front of it and point it at its Cloud Map address.
If you need inbound access to runtime from outside your VPC, for example fronting it with a load balancer, add that yourself. The pack doesn't create it.
What the pack creates
For each service, the pack creates:
- An ECS task definition (Fargate,
awsvpcnetworking) - An ECS service with rolling deployment and automatic rollback if a deployment fails
- A Cloud Map DNS entry for service-to-service communication (for example,
agent.plainid-paa.internal) - An Application Auto Scaling target with a CPU-based scaling policy
- A security group allowing traffic from your VPC on the container port
It also creates, once for the whole deployment:
- An AWS Cloud Map private DNS namespace (
plainid-paa.internalby default) - An IAM task-execution role with SSM Parameter Store read access
- An IAM task role (empty by default; you can extend it if your services need additional AWS permissions)
- A CloudWatch Logs group per service
Optionally, depending on your configuration, the pack can also create an ECS Fargate cluster, SSM parameters for your secrets, and a Redis instance. Each of these is described in the sections below.
Service ports
| Service | Ports | Notes |
|---|---|---|
| agent | 8080 (HTTP), 8081 (management/health) | Opens the outbound tunnel to PlainID |
| runtime | 8010 (HTTP and health) | Connects to pip-operator and the agent; serves authorization decisions to your applications |
| pip-operator | 8080 (HTTP), 8089 (management), 31350 (JDBC), 31351 (Postgres) | Connects to the agent over WebSocket |
| idp-webhook | 8080 (HTTP), 9090 (management) | Disabled by default |
| secrets-mgmt | 8080 (HTTP), 8081 (management) | Connects to Redis only if secrets_mgmt.redis_integration = true |
Docker Images
Docker images are listed in the images.txt file included with the Helm Chart package.
Refer to the Working with PlainID DockerHub article for details on authenticating with the PlainID private Docker Hub repository.
Installing the pack
Basic installation
-
Get the pack and navigate to its directory.
cd deploy/terraform/plainid-paa-ecs -
Create your variables file from the provided example.
cp terraform.tfvars.example terraform.tfvarsterraform.tfvars.examplelists every available setting with comments. Copying it gives you a starting point you can trim down to just what you need. Any setting you leave out ofterraform.tfvarsfalls back to its default value, so you only need to include what you want to change. -
Edit
terraform.tfvarswith your environment details. At minimum, set:vpc_idand your subnet IDsecs_cluster_arn(an existing cluster) orcreate_ecs_cluster = true(have the pack create one)redis_passwordorredis_password_ssm_parameter_arnagent_secret_keyoragent_secret_key_ssm_parameter_arn- Your PAA identity:
tenant_id,paa_id, andremote_api(from Platform Tenant Settings)
See Configuring services, Secrets, and Redis modes for the full set of options and how each one behaves.
-
Initialize Terraform. This downloads the AWS provider and sets up the working directory.
terraform init -
Review the plan. This shows every resource Terraform will create, change, or destroy, without making any changes yet. Read through it to confirm it matches what you expect, especially the VPC, subnets, and cluster.
terraform plan -
Apply. This creates the resources in AWS. Terraform shows the plan again and asks you to confirm before proceeding.
terraform applyA first-time apply typically takes a few minutes while ECS provisions tasks, registers them in Cloud Map, and runs initial health checks.
-
Confirm the services are healthy. In the AWS Console, check that each ECS service under your cluster shows its tasks as
RUNNINGand passing health checks. You can also check the CloudWatch Logs group for each service if a task fails to start.
Once applied, clients inside your VPC can reach each service at:
agent.plainid-paa.internal:8080
runtime.plainid-paa.internal:8010
pip-operator.plainid-paa.internal:8080
secrets-mgmt.plainid-paa.internal:8080
Adjust the namespace suffix to match your service_discovery_namespace value if you changed it from the default.
Uninstalling
To remove the deployment and all its AWS resources:
terraform destroy
Terraform shows every resource it will remove and asks for confirmation before proceeding. Keep in mind:
- If running
redis_mode = "ecs"(the default), destroying the deployment also removes that Redis task and any data it holds, since it has no persistent storage. If you need to keep that data, back it up first or move toexternalmode before destroying. - SSM parameters that the pack created for your secrets (in plaintext mode) are removed as well. Parameters you created yourself and referenced by ARN (bring-your-own-ARN mode) aren't managed by Terraform and stay in place.
- This action isn't reversible. Confirm you're pointed at the right AWS account and state file before running it, especially if you manage multiple environments from the same working directory.
Upgrading
-
Update the version. In
terraform.tfvars, changepaa_versionto upgrade every service at once, or set<service>.image_tagon an individual service to upgrade it independently of the others. -
Review the plan. Run
terraform planto confirm only the intended service(s) will change. -
Apply the change.
terraform applyEach affected ECS service performs a rolling deployment: it starts new tasks on the updated version alongside the old ones, waits for them to pass health checks, then shifts traffic over and stops the old tasks. If the new version fails its health checks, the deployment circuit breaker automatically rolls the service back to the previous version, so you don't need to intervene manually.
-
Verify. Check the ECS service events (in the console or via
aws ecs describe-services) to confirm the deployment completed and the new task definition revision is the one running.
Configuring services
Use terraform.tfvars to configure each service. Each top-level key (agent, runtime, pip_operator, idp_webhook, secrets_mgmt) accepts the settings for that service.
ECS cluster
You have two options:
- Use an existing cluster (default). Set
ecs_cluster_arnto the ARN of a cluster you already manage. The pack deploys the PAA services into it alongside anything else running there. - Have the pack create a cluster for you. Set
create_ecs_cluster = true. In this mode, the pack ignoresecs_cluster_arnand instead provisions a new cluster registered with both theFARGATEandFARGATE_SPOTcapacity providers (FARGATEis the default strategy). Optionally setcontainer_insights = trueto enable CloudWatch Container Insights on the new cluster for additional monitoring.
Scaling
Autoscaling is CPU target-tracking, configured per service. For each service you want to scale automatically:
- Set
<service>.autoscaling.enabled = true. - Set the minimum and maximum task count bounds, for example
min_capacityandmax_capacity. - Set the target CPU utilization percentage the scaling policy should track.
Once autoscaling is running, Application Auto Scaling owns the task count for that service, so don't expect desired_count to stay fixed after deployment. If you check the ECS console and see a different task count than what's in terraform.tfvars, that's Auto Scaling responding to load, not configuration drift.
Logging
Each service writes to its own CloudWatch Logs group, named /<name_prefix>/<service> (for example, /plainid-paa/runtime), with the log stream prefixed by the service name. The top-level log_retention_days setting controls retention uniformly across all services; increase it if you need logs to persist longer for auditing or troubleshooting, or decrease it to reduce storage costs. You can view logs live by tailing the group in the CloudWatch console, or with:
aws logs tail /<name_prefix>/<service> --follow
Secrets
You have two options for handling the Redis password and the agent's CLIENT_SECRET_KEY:
| Mode | What you set | Where the value lives |
|---|---|---|
| Plaintext (default) | redis_password / agent_secret_key |
Terraform writes the value to an SSM SecureString parameter for you. The value is marked sensitive so Terraform won't print it in plan or apply output, but it does remain in Terraform state. |
| Bring your own ARN | redis_password_ssm_parameter_arn / agent_secret_key_ssm_parameter_arn |
You create the SSM parameter yourself, outside Terraform. The plaintext value never enters Terraform state. |
Set exactly one option per secret. For high-sensitivity environments, use the bring-your-own-ARN mode.
Each service also has an extra_secret_env setting for additional secret environment variables, mapping an environment variable name to a secret ARN. Use this when a service needs credentials beyond the Redis password and agent secret key covered above, for example a database password or a third-party API token that pip-operator reads at startup.
extra_secret_env accepts either an SSM Parameter Store ARN or a Secrets Manager ARN per entry, so you can mix sources or standardize on whichever your organization already uses.
Example:
pip_operator = {
extra_secret_env = {
PIP_DB_PASSWORD = "arn:aws:ssm:...:parameter/plainid-paa/pip/db-password"
SOME_API_TOKEN = "arn:aws:ssm:...:parameter/plainid-paa/pip/api-token"
}
}
If you keep your secrets in Secrets Manager instead, point the same keys at Secrets Manager ARNs:
pip_operator = {
extra_secret_env = {
PIP_DB_PASSWORD = "arn:aws:secretsmanager:us-east-2:123456789012:plainid-paa/pip/db-password-AbCdEf"
SOME_API_TOKEN = "arn:aws:secretsmanager:us-east-2:123456789012:plainid-paa/pip/api-token-GhIjKl"
}
}
Redis modes
Set redis_mode to choose where Redis runs:
| redis_mode | Behavior |
|---|---|
ecs (default) |
The pack runs Redis as a Fargate task in your ECS cluster. It's registered in Cloud Map as redis.<service_discovery_namespace>. You provide redis_password_ssm_parameter_arn; the Redis container reads its password from SSM. |
external |
You manage Redis yourself, typically with Amazon ElastiCache. Set redis_host to your Redis endpoint and redis_password_ssm_parameter_arn to your SSM parameter. |
The ecs mode runs as a single task with no persistence, which is convenient for getting started since it requires no external dependency. For staging and production environments where you need high availability and durability, use external mode with ElastiCache, which supports multi-AZ failover and encryption in transit.
Container images
Each service's image resolves as:
<image_repository, or "<image_registry>/<default-name>" if not set>:<image_tag, or paa_version if not set>
| Setting | Scope | Default | When to use it |
|---|---|---|---|
image_registry |
Whole deployment | plainid |
You want every service to share one registry and use the default image names. |
<service>.image_repository |
Per service | Not set (uses the default) | A service needs a fully-qualified repository URL, for example your own ECR repository. |
<service>.image_tag |
Per service | Not set (uses paa_version) |
You want to pin one service to a different version than the rest. |
paa_version |
Whole deployment | 5.2622.0 |
Sets the default version for every service. |
Example: per-service ECR repositories
agent = {
image_repository = "123456789012.dkr.ecr.us-east-2.amazonaws.com/plainid-agent"
}
runtime = {
image_repository = "123456789012.dkr.ecr.us-east-2.amazonaws.com/plainid-runtime"
}
pip_operator = {
image_repository = "123456789012.dkr.ecr.us-east-2.amazonaws.com/plainid-pip-operator"
}
Example: one shared ECR registry with default image names
image_registry = "123456789012.dkr.ecr.us-east-2.amazonaws.com"
If you're pulling from a private ECR repository in another AWS account, also set repository_credentials_secret_arn. You don't need this if the ECR repository is in the same account as your task execution role, since that role already has ECR pull permissions.
If you require additional help or support, contact PlainID Support.