--- title: "ECS Deployment" slug: "ecs-deployment" updated: 2026-08-30T14:49:16Z published: 2026-08-30T14:49:16Z canonical: "docs.plainid.io/ecs-deployment" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.plainid.io/llms.txt > Use this file to discover all available pages before exploring further. # ECS Deployment 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](/docs/ecs-deployment#prerequisites) - [Networking model](/docs/ecs-deployment#networking-model) - [What the pack creates](/docs/ecs-deployment#what-the-pack-creates) - [Installing the pack](/docs/ecs-deployment#installing-the-pack) - [Configuring services](/docs/ecs-deployment#configuring-services) - [Secrets](/docs/ecs-deployment#secrets) - [Redis modes](/docs/ecs-deployment#redis-modes) - [Container images](/docs/ecs-deployment#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** Obtaining an Terraform Pack Contact [PlainID Support](https://plainid.atlassian.net/servicedesk/customer/portals) to obtain a Terraform Pack. ## 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://...` or `https://...`). - 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 example `runtime.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, `awsvpc` networking) - 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.internal` by 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](/v1/docs/plainid-docker-hub-authentication) article for details on authenticating with the PlainID private Docker Hub repository. ## Installing the pack ### Basic installation 1. **Get the pack and navigate to its directory.** ``` cd deploy/terraform/plainid-paa-ecs ``` 2. **Create your variables file from the provided example.** ``` cp terraform.tfvars.example terraform.tfvars ``` `terraform.tfvars.example` lists 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 of `terraform.tfvars` falls back to its default value, so you only need to include what you want to change. 3. **Edit `terraform.tfvars` with your environment details.** At minimum, set: See [Configuring services](/docs/ecs-deployment#configuring-services), [Secrets](/docs/ecs-deployment#secrets), and [Redis modes](/docs/ecs-deployment#redis-modes) for the full set of options and how each one behaves. - `vpc_id` and your subnet IDs - `ecs_cluster_arn` (an existing cluster) or `create_ecs_cluster = true` (have the pack create one) - `redis_password` or `redis_password_ssm_parameter_arn` - `agent_secret_key` or `agent_secret_key_ssm_parameter_arn` - Your PAA identity: `tenant_id`, `paa_id`, and `remote_api` (from Platform **Tenant Settings**) 4. **Initialize Terraform.** This downloads the AWS provider and sets up the working directory. ``` terraform init ``` 5. **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 ``` 6. **Apply.** This creates the resources in AWS. Terraform shows the plan again and asks you to confirm before proceeding. ``` terraform apply ``` A first-time apply typically takes a few minutes while ECS provisions tasks, registers them in Cloud Map, and runs initial health checks. 7. **Confirm the services are healthy.** In the AWS Console, check that each ECS service under your cluster shows its tasks as `RUNNING` and 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 to `external` mode 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 1. **Update the version.** In `terraform.tfvars`, change `paa_version` to upgrade every service at once, or set `<service>.image_tag` on an individual service to upgrade it independently of the others. 2. **Review the plan.** Run `terraform plan` to confirm only the intended service(s) will change. 3. **Apply the change.** ``` terraform apply ``` Each 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. 4. **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_arn` to 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 ignores `ecs_cluster_arn` and instead provisions a new cluster registered with both the `FARGATE` and `FARGATE_SPOT` capacity providers (`FARGATE` is the default strategy). Optionally set `container_insights = true` to 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: 1. Set `<service>.autoscaling.enabled = true`. 2. Set the minimum and maximum task count bounds, for example `min_capacity` and `max_capacity`. 3. 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 // --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: ``` /" 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](https://plainid.atlassian.net/servicedesk/customer/portals).