> ## Documentation Index
> Fetch the complete documentation index at: https://arizeai-433a7140-ehutt-trail-benchmark-new-tasks.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS with CloudFormation

> Phoenix can be deployed on AWS Fargate using CloudFormation

## Prerequisites

**AWS Account & CLI**

* An [AWS account](https://www.googleadservices.com/pagead/aclk?sa=L\&ai=DChsSEwiAtLCbkOqMAxUSAa0GHXCbNXsYACICCAEQABoCcHY\&ae=2\&aspm=1\&co=1\&ase=5\&gclid=Cj0KCQjw2ZfABhDBARIsAHFTxGwg0XwS9htaZw1EV3FQkuQkeRBffwh7i_zMVLieS6vZVVN6_0C_aO4aAr_lEALw_wcB\&ei=RsAGaKGZO52_0PEPjr3TgQw\&ohost=www.google.com\&cid=CAESVuD2SCOo1kgykltK7QlneZO0kLjLVm-DLo7K-rZ_XoWbVM-U6idlYAS9y_mWcJ4NbmQ708KWp8jyoVeqtfRilZiTt4Y6dLdSq2xpvHvSjMlfCp9Rrvm1\&sig=AOD64_0SOO-BnErIbCFIx6UqvzYtcdt8Uw\&q\&sqi=2\&adurl\&ved=2ahUKEwih1KubkOqMAxWdHzQIHY7eNMAQ0Qx6BAgJEAE).

* [AWS CLI v2 installed](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html#getting-started-install-instructions) and configured (`aws configure`) with credentials that can deploy CloudFormation stacks.

**Export your AWS Account and Region for future use**

Before you run any of the CloudFormation or ECR commands, export two env‑vars so you don’t have to repeat your account/region everywhere:

```bash theme={null}
# grab your AWS account ID and default region from your CLI creds
export ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
export REGION=$(aws configure get region)
```

## IAM Permissions

Before you can deploy any of the CloudFormation stacks below, you’ll want a single, least‑privilege managed policy that your CI/CD user or role can assume.

**Save the following policy document as** `permissions.json`:

```json permissions.json expandable theme={null}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:CreateStack",
        "cloudformation:UpdateStack",
        "cloudformation:DeleteStack",
        "cloudformation:DescribeStacks",
        "cloudformation:ListStacks",
        "cloudformation:ValidateTemplate"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:AttachRolePolicy",
        "iam:DetachRolePolicy",
        "iam:PutRolePolicy",
        "iam:DeleteRolePolicy",
        "iam:PassRole"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeVpcs",
        "ec2:DescribeSubnets",
        "ec2:DescribeRouteTables",
        "ec2:DescribeSecurityGroups",
        "ec2:CreateSecurityGroup",
        "ec2:DeleteSecurityGroup",
        "ec2:AuthorizeSecurityGroupIngress",
        "ec2:RevokeSecurityGroupIngress",
        "ec2:AuthorizeSecurityGroupEgress",
        "ec2:RevokeSecurityGroupEgress"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeleteCluster",
        "ecs:DescribeClusters",
        "ecs:RegisterTaskDefinition",
        "ecs:DeregisterTaskDefinition",
        "ecs:DescribeTaskDefinition",
        "ecs:CreateService",
        "ecs:UpdateService",
        "ecs:DeleteService",
        "ecs:DescribeServices",
        "ecs:RunTask",
        "ecs:StopTask",
        "ecs:DescribeTasks"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:CreateRepository",
        "ecr:DeleteRepository",
        "ecr:DescribeRepositories",
        "ecr:GetAuthorizationToken",
        "ecr:BatchGetImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:CreateSecret",
        "secretsmanager:DeleteSecret",
        "secretsmanager:DescribeSecret",
        "secretsmanager:GetSecretValue",
        "secretsmanager:PutSecretValue",
        "secretsmanager:TagResource"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:CreateLoadBalancer",
        "elasticloadbalancing:DeleteLoadBalancer",
        "elasticloadbalancing:DescribeLoadBalancers",
        "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:DeleteTargetGroup",
        "elasticloadbalancing:DescribeTargetGroups",
        "elasticloadbalancing:RegisterTargets",
        "elasticloadbalancing:DeregisterTargets",
        "elasticloadbalancing:CreateListener",
        "elasticloadbalancing:DeleteListener",
        "elasticloadbalancing:ModifyListener"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:DeleteLogGroup",
        "logs:DescribeLogGroups",
        "logs:CreateLogStream",
        "logs:DescribeLogStreams",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}
```

#### Create the IAM policy

```bash theme={null}
aws iam create-policy \
  --policy-name PhoenixDeployPermissions \
  --policy-document file://permissions.json
```

This will return the new policy’s ARN, e.g.:

```bash theme={null}
arn:aws:iam::123456789012:policy/PhoenixDeployPermissions
```

#### Attach it to your deploy principal

**a) If you use an IAM User:**

```bash theme={null}
aws iam attach-user-policy \
  --user-name <YOUR_DEPLOY_USER> \
  --policy-arn <PASTE ARN HERE>
```

**b) If you use an IAM Role (e.g. CodeBuild or CI/CD):**

```bash theme={null}
aws iam attach-role-policy \
  --role-name <YOUR_DEPLOY_ROLE> \
  --policy-arn <PASTE ARN HERE>
```

***

Once attached, that user or role will have exactly the rights needed to stand up all of the VPCs, ECS/Fargate clusters, ECR repos, Secrets Manager secrets, ALBs, CloudWatch Logs, IAM roles, and CloudFormation stacks in this guide

## Set up ECR (Elastic Container Registry) Repositories for Phoenix and your application

Before deploying Phoenix and your application into ECS/Fargate, you need to host your container images in Amazon Elastic Container Registry (ECR). Here’s a quick rundown:

#### 1. Create ECR repositories

Run these once:

```bash theme={null}
# Repository for the Phoenix UI image
aws ecr create-repository \
  --repository-name phoenix \
  --image-scanning-configuration scanOnPush=true \
  --region $REGION

# Repository for your application (agent, backend, frontend, etc.)
aws ecr create-repository \
  --repository-name my-app \
  --image-scanning-configuration scanOnPush=true \
  --region $REGION
```

Each command returns the repo URI:

```bash theme={null}
$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/phoenix
$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/my-app
```

***

#### 2. Authenticate Docker to ECR

```bash theme={null}
aws ecr get-login-password --region $REGION \
  | docker login \
    --username AWS \
    --password-stdin $ACCOUNT.dkr.ecr.$REGION.amazonaws.com
```

This writes your Docker credentials so you can push and pull.

***

### 3. Push Phoenix and your apps (backend, frontend, db)

If you’ve built locally:

```bash theme={null}
# Tag your local Phoenix image
docker pull arizephoenix/phoenix
docker tag arizephoenix/phoenix:latest \
  $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/phoenix:latest

# Push to ECR
docker push $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/phoenix:latest

# Repeat for your app
docker tag my-app:latest \
  $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/my-app:latest
docker push $ACCOUNT.dkr.ecr.$REGION.amazonaws.com/my-app:latest
```

## Create VPCs and Subnets

**Save the following CloudFormation template as** `phoenix-network.yml`:

```yaml phoenix-network.yml expandable theme={null}
# phoenix-network.yml – minimal 2-AZ VPC with public + private subnets + NAT
AWSTemplateFormatVersion: '2010-09-09'
Description: Two-AZ VPC (10.0.0.0/16) with public & private subnets

Parameters:
  AZ1: {Type: AWS::EC2::AvailabilityZone::Name}
  AZ2: {Type: AWS::EC2::AvailabilityZone::Name}

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: [{Key: Name, Value: phoenix-vpc}]

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: {VpcId: !Ref VPC}

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # ---------- Public subnets ----------
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ1
      CidrBlock: 10.0.0.0/24
      MapPublicIpOnLaunch: true
      Tags: [{Key: Name, Value: public-az1}]

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ2
      CidrBlock: 10.0.1.0/24
      MapPublicIpOnLaunch: true
      Tags: [{Key: Name, Value: public-az2}]

  AssocPub1:                       # attach route table
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1
      RouteTableId: !Ref PublicRouteTable

  AssocPub2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet2
      RouteTableId: !Ref PublicRouteTable

  # ---------- NAT for private subnets ----------
  NatEIP:
    Type: AWS::EC2::EIP
    Properties: {Domain: vpc}

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEIP.AllocationId
      SubnetId: !Ref PublicSubnet1

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: {VpcId: !Ref VPC}

  PrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  # ---------- Private subnets ----------
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ1
      CidrBlock: 10.0.10.0/24
      Tags: [{Key: Name, Value: private-az1}]

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Ref AZ2
      CidrBlock: 10.0.11.0/24
      Tags: [{Key: Name, Value: private-az2}]

  AssocPriv1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1
      RouteTableId: !Ref PrivateRouteTable

  AssocPriv2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet2
      RouteTableId: !Ref PrivateRouteTable

Outputs:
  VpcId:               {Value: !Ref VPC,               Export: {Name: Phoenix-VpcId}}
  PublicSubnetIds:     {Value: !Join [',', [!Ref PublicSubnet1,  !Ref PublicSubnet2]],
                        Export: {Name: Phoenix-PublicSubnets}}
  PrivateSubnetIds:    {Value: !Join [',', [!Ref PrivateSubnet1, !Ref PrivateSubnet2]],
                        Export: {Name: Phoenix-PrivateSubnets}}
```

This template:

1. **Creates a VPC** (10.0.0.0/16 for example) with DNS support

2. **Provisions two public subnets** (one per AZ) for your Internet‑facing components (ALBs, NAT gateway)

3. **Provisions two private subnets** (one per AZ) for your ECS tasks (Phoenix, agents, backend services)

4. **Deploys a NAT Gateway** in the first public subnet so private tasks can reach out (for secrets, Docker registries, external APIs)

5. **Exports** the VPC ID and the comma‑separated Public/Private Subnet IDs for easy consumption by downstream stacks

#### Deploy with:

```bash theme={null}
aws cloudformation deploy \
  --template-file phoenix-network.yml \
  --stack-name phoenix-network \
  --parameter-overrides \
      AZ1=us-west-2a \
      AZ2=us-west-2b \
  --capabilities CAPABILITY_NAMED_IAM
```

Make sure to change AZ1 and AZ2 to match your region.

**You can now access your VPC and public + private subnet IDs at AWS -> CloudFormation -> Stacks -> phoenix-network -> Outputs.**

#### Extending for multiple components

If you plan to run **multiple services** (e.g. a separate frontend, backend, worker pool), consider:

* **Additional subnets** You might carve out extra AZ‑distributed subnets (e.g. “app‑subnets”, “db‑subnets”) with their own route tables and security rules.

* **Security groups per tier**

  * **ALB SG**: allows inbound HTTP(S) from 0.0.0.0/0

  * **App SG**: allows inbound from the ALB SG on your HTTP port

  * **DB SG**: allows inbound only from your App SG on your database port

## Deploy Phoenix (with Authentication)

**Save the following CloudFormation template as** `phoenix-auth.yml`:

```yaml phoenix-auth.yml expandable theme={null}
AWSTemplateFormatVersion: '2010-09-09'
Description: Bootstrap Phoenix (auth enabled) on Fargate behind an ALB. Phase 1 - no agent; goal is to log in and create a System API key.

###############################################################################
# 1. Parameters – only what we need
###############################################################################
Parameters:
  PhoenixImageUri:
    Type: String
    Description: Phoenix image (e.g. 123456789012.dkr.ecr.us-west-2.amazonaws.com/phoenix:latest)

  VpcId:
    Type: AWS::EC2::VPC::Id

  PublicSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Two or more public subnets for the ALB

  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>
    Description: Two or more private subnets for the tasks

###############################################################################
# 2. Resources
###############################################################################
Resources:

  # ────────────────────────────────────────────────────────────────────────────
  # Networking (security groups)
  # ────────────────────────────────────────────────────────────────────────────
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: Allow HTTP from anywhere to ALB
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0      # tighten later if you wish

  TaskSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: Allow ALB tasks on port 6006
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 6006
          ToPort: 6006
          SourceSecurityGroupId: !Ref ALBSecurityGroup
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0

  # ────────────────────────────────────────────────────────────────────────────
  # Auth signing key (PHOENIX_SECRET)
  # ────────────────────────────────────────────────────────────────────────────
  PhoenixJwtSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: phoenix-jwt-secret
      Description: Signing key for Phoenix auth (in "secret" JSON field)
      GenerateSecretString:
        SecretStringTemplate: "{}"
        GenerateStringKey: secret
        PasswordLength: 32
        ExcludePunctuation: false

  # ────────────────────────────────────────────────────────────────────────────
  # IAM roles
  # ────────────────────────────────────────────────────────────────────────────
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: ReadJwtSecret
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource: !Ref PhoenixJwtSecret

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal: { Service: ecs-tasks.amazonaws.com }
            Action: sts:AssumeRole

  # ────────────────────────────────────────────────────────────────────────────
  # Logging
  # ────────────────────────────────────────────────────────────────────────────
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7

  # ────────────────────────────────────────────────────────────────────────────
  # ECS cluster, task definition, service
  # ────────────────────────────────────────────────────────────────────────────
  Cluster:
    Type: AWS::ECS::Cluster

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      RequiresCompatibilities: [FARGATE]
      Cpu: 1024
      Memory: 4096
      NetworkMode: awsvpc
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      TaskRoleArn:       !GetAtt TaskRole.Arn
      RuntimePlatform:
        CpuArchitecture: ARM64
        OperatingSystemFamily: LINUX
      ContainerDefinitions:
        - Name: phoenix
          Image: !Ref PhoenixImageUri
          PortMappings:
            - ContainerPort: 6006
            - ContainerPort: 4317
          Environment:
            - Name: PHOENIX_ENABLE_AUTH
              Value: "true"
          Secrets:
            - Name: PHOENIX_SECRET
              ValueFrom: !Sub "${PhoenixJwtSecret}:secret::"
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:         !Ref LogGroup
              awslogs-region:        !Ref AWS::Region
              awslogs-stream-prefix: phoenix
          MemoryReservation: 2048
          Memory:            3584

  # ────────────────────────────────────────────────────────────────────────────
  # Load balancer
  # ────────────────────────────────────────────────────────────────────────────
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      Subnets: !Ref PublicSubnetIds
      SecurityGroups: [ !Ref ALBSecurityGroup ]

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VpcId
      Port: 6006
      Protocol: HTTP
      TargetType: ip
      HealthCheckPath: /
      Matcher: { HttpCode: 200 }

  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

  Service:
    Type: AWS::ECS::Service
    DependsOn: Listener
    Properties:
      Cluster: !Ref Cluster
      DesiredCount: 1
      LaunchType: FARGATE
      TaskDefinition: !Ref TaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          Subnets: !Ref PrivateSubnetIds
          SecurityGroups: [ !Ref TaskSecurityGroup ]
      LoadBalancers:
        - TargetGroupArn: !Ref TargetGroup
          ContainerName: phoenix
          ContainerPort: 6006
      HealthCheckGracePeriodSeconds: 300

###############################################################################
# 3. Outputs
###############################################################################
Outputs:
  PhoenixURL:
    Description: "Open this URL → log in as admin@localhost / admin"
    Value: !Sub "http://${ALB.DNSName}"
```

You are now ready to deploy Phoenix to AWS.

**Deploy the** `phoenix-auth.yml` **stack**
Consume the network stack’s outputs (`VPC‑ID`, `PublicSubnetIds`, `PrivateSubnetIds`) along with your new ECR URI:

```bash expandable theme={null}
aws cloudformation deploy \
  --template-file phoenix-auth.yml \
  --stack-name phoenix-auth \
  --capabilities CAPABILITY_NAMED_IAM \
  --parameter-overrides \
    PhoenixImageUri=$ACCOUNT.dkr.ecr.$REGION.amazonaws.com/phoenix:latest \
    VpcId=$(aws cloudformation describe-stacks \
            --stack-name phoenix-network \
            --query 'Stacks[0].Outputs[?OutputKey==`VpcId`].OutputValue' \
            --output text) \
    PublicSubnetIds=$(aws cloudformation describe-stacks \
            --stack-name phoenix-network \
            --query 'Stacks[0].Outputs[?OutputKey==`PublicSubnetIds`].OutputValue' \
            --output text) \
    PrivateSubnetIds=$(aws cloudformation describe-stacks \
            --stack-name phoenix-network \
            --query 'Stacks[0].Outputs[?OutputKey==`PrivateSubnetIds`].OutputValue' \
            --output text)
```

**Fetch your Phoenix URL**

```bash theme={null}
PHOENIX_URL=$(aws cloudformation describe-stacks \
  --stack-name phoenix-auth \
  --query 'Stacks[0].Outputs[?OutputKey==`PhoenixURL`].OutputValue' \
  --output text)
```

**Log in and create your System API key**

* In your browser, go to `$PHOENIX_URL`.
* Sign in as `admin@localhost` / `admin`.
* Set a new admin password.
* Go to **Settings → API Keys** and **Create System Key**.
* **Copy the new key** (you’ll need it in the next step).

**IMPORTANT**: **Security‑group ingress** is wide open (`0.0.0.0/0` on port 80). You should tighten it if you know your CIDR or are behind a corporate proxy.

## Store Phoenix API Key + other API Keys

Once you have your Phoenix System API key, bundle it (and any other service keys) into AWS Secrets Manager:

```bash theme={null}
# Store Phoenix API key
aws secretsmanager create-secret \
  --name phoenix-system-api-key \
  --description "Phoenix System API Key for OTLP traces" \
  --secret-string '{"PHOENIX_API_KEY":"<PASTE SYSTEM KEY HERE>"}'

# (Optional) Store other service keys similarly:
aws secretsmanager create-secret \
  --name openai-api-key \
  --description "OpenAI API key for LLM calls" \
  --secret-string '{"OPENAI_API_KEY":"<PASTE OPENAI KEY HERE>"}'
```

You can now reference these secrets in your downstream CloudFormation templates (e.g. in your application stacks) via the `PhoenixArn`, `OpenAIArn`, etc., parameters.

## Deploying your App that ships Spans to Phoenix

**Save the following CloudFormation template as** `app.yml`:

```yaml app.yml expandable theme={null}
AWSTemplateFormatVersion: '2010-09-09'
Description: Agent-only Fargate task that sends traces to an existing Phoenix deployment (phoenix-auth stack).

###############################################################################
# Parameters
###############################################################################
Parameters:

  AgentImageUri:
    Type: String
    Description: ECR image URI for the agent container

  PhoenixHost:
    Type: String
    Description: DNS name (or https://host:port) of the running Phoenix UI, e.g. phoenix-auth-ALB-abc123.us-west-2.elb.amazonaws.com

  VpcId:
    Type: AWS::EC2::VPC::Id

  PrivateSubnetIds:
    Type: List<AWS::EC2::Subnet::Id>

  # ‑‑‑ Secrets stored in AWS Secrets Manager ‑‑‑
  OpenAIArn:
    Type: String
    Description: ARN of secret holding {"OPENAI_API_KEY" "..."}
  PhoenixArn:
    Type: String
    Description: ARN of **system** key secret holding {"PHOENIX_API_KEY" "..."}

  #### ---------- ADD OTHER ARNS FOR OTHER SECRETS HERE ----------

###############################################################################
# Networking
###############################################################################
Resources:

  TaskSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VpcId
      GroupDescription: Allow outbound Internet for Agent
      SecurityGroupEgress:
        - IpProtocol: -1   # all
          FromPort: 0
          ToPort: 0
          CidrIp: 0.0.0.0/0        # NAT‑GW / IGW handles actual routing

###############################################################################
# Logs
###############################################################################
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7

###############################################################################
# IAM
###############################################################################
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Policies:
        - PolicyName: ReadSecrets
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Sid: AllowAgentToReadSecrets
                Effect: Allow
                Action: secretsmanager:GetSecretValue
                Resource:
                  - !Ref OpenAIArn
                  - !Ref PhoenixArn
                  #### ---------- ADD OTHER ARNs FOR OTHER SECRETS HERE ----------

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole

###############################################################################
# ECS cluster & task
###############################################################################
  Cluster:
    Type: AWS::ECS::Cluster

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: app
      RequiresCompatibilities: [ FARGATE ]
      RuntimePlatform:
        CpuArchitecture: ARM64
        OperatingSystemFamily: LINUX
      Cpu: "512"
      Memory: "1024"
      NetworkMode: awsvpc
      ExecutionRoleArn: !GetAtt TaskExecutionRole.Arn
      TaskRoleArn:      !GetAtt TaskRole.Arn
      ContainerDefinitions:
        - Name: agent
          Image: !Ref AgentImageUri
          Essential: true
          MemoryReservation: 256
          Memory: 512
          Environment:
            - Name: PHOENIX_COLLECTOR_ENDPOINT
              Value: !Sub "http://${PhoenixHost}"
          Secrets:
            - Name: PHOENIX_API_KEY
              ValueFrom: !Sub "${PhoenixArn}:PHOENIX_API_KEY::"

            - Name: OPENAI_API_KEY
              ValueFrom: !Sub "${OpenAIArn}:OPENAI_API_KEY::"

          #### ---------- ADD OTHER SECRETS HERE ----------

          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:  !Ref LogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: agent

###############################################################################
# Service
###############################################################################
  Service:
    Type: AWS::ECS::Service
    Properties:
      Cluster:         !Ref Cluster
      DesiredCount:    1
      LaunchType:      FARGATE
      TaskDefinition:  !Ref TaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED
          SecurityGroups: [ !Ref TaskSecurityGroup ]
          Subnets: !Ref PrivateSubnetIds

###############################################################################
# Outputs
###############################################################################
Outputs:
  AgentTaskDefinition:
    Description: Task definition ARN for the Agent-only task
    Value: !Ref TaskDefinition
```

This CloudFormation stack (`app.yml`) launches:

* An ECS **Cluster**
* An **IAM Task Execution Role** & **Task Role** (to pull images, read secrets)
* A **Security Group** that allows outbound Internet access
* A **Log Group** for container logs
* An ECS **Task Definition** (family: `app`)
* An ECS **Service** (Fargate) running one copy of the “app” container

The app container will:

1. Read your OTLP endpoint and Phoenix API key from Secrets Manager
2. Read your other API keys from Secrets Manager
3. Emit spans to your Phoenix deployment

**IMPORTANT: This template assumes only one secret is being used (OpenAI API Key). Make sure to add ARNs for additional secrets wherever the template is marked** `ADD OTHER ARNS FOR OTHER SECRETS HERE` **(once under** `Parameters`**, once under the** `ReadSecrets` **policy) and expose those secrets to the container where it is marked** `ADD OTHER SECRETS HERE`**.**

**Template parameters**

| Parameter | Description | Example |
| :- | :- | :- |
| **AgentImageUri** | ECR URI (with tag) of your agent container | `123456789012.dkr.ecr.us-west-2.amazonaws.com/agent:latest` |
| **PhoenixHost** | DNS name (no scheme) of your Phoenix UI load‑balancer | `phoenix-auth-ALB-abc123.us-west-2.elb.amazonaws.com` |
| **VpcId** | Your VPC ID | `vpc-0abc123def456ghi7` |
| **PrivateSubnetIds** | Comma‑separated private subnets for your ECS tasks | `subnet-ccc333,subnet-ddd444` |
| **OpenAIArn** | ARN of Secrets Manager secret holding `{"OPENAI_API_KEY": "…"}` | `arn:aws:secretsmanager:us-west-2:920904165384:secret:openai-key` |
| **PhoenixArn** | ARN of secret holding `{"PHOENIX_API_KEY": "…"}` | `arn:aws:secretsmanager:us-west-2:920904165384:secret:phoenix-key` |

**Example deploy command (add additional ARNs for more secrets):**

```bash theme={null}
aws cloudformation deploy \
  --template-file app.yml \
  --stack-name app \
  --parameter-overrides \
      AgentImageUri=123456789012.dkr.ecr.us-west-2.amazonaws.com/agent:latest \
      PhoenixHost=phoenix-auth-ALB-abc123.us-west-2.elb.amazonaws.com \
      VpcId=vpc-0abc123def456ghi7 \
      PrivateSubnetIds=subnet-ccc333,subnet-ddd444 \
      OpenAIArn=arn:aws:secretsmanager:us-west-2:920904165384:secret:openai-key \
      PhoenixArn=arn:aws:secretsmanager:us-west-2:920904165384:secret:phoenix-key \
  --capabilities CAPABILITY_NAMED_IAM
```

Once complete, ECS will spin up your agent task, and you can verify in the console:

```bash theme={null}
# See the running task
aws ecs list-tasks --cluster agent-only --output table

# Check logs for any “Connectivity to http://<PhoenixHost>” output
aws logs tail \
  --log-group-name /aws/ecs/agent \
  --since 5m
```

**Now your traces should flow from the agent into your Phoenix project!**

## (Optional) Extending for Additional Components (Frontend, DB, etc.)

Once you have Phoenix + your agent/LLM task up and running, you can easily add more services to the same VPC:

1. **Create new ECR repos** for your frontend, backend, DB‑migrations job, worker service, etc.

2. **Add new TaskDefinitions / Services** to your `app.yml` (or split each into its own CFN stack):

   ```yaml expandable theme={null}
   Parameters:
     FrontendImageUri: …
     BackendImageUri: …
     DbMigrationImageUri: …

   Resources:
     # reuse same VpcId, SubnetIds, SecurityGroup
     FrontendTaskDef:
       Type: AWS::ECS::TaskDefinition
       Properties:
         Family: frontend
         ContainerDefinitions:
           - Name: frontend
             Image: !Ref FrontendImageUri
             PortMappings: [{ContainerPort: 80}]
             # …env & secrets…
     FrontendService:
       Type: AWS::ECS::Service
       Properties:
         Cluster: !Ref Cluster
         DesiredCount: 2
         TaskDefinition: !Ref FrontendTaskDef
         LoadBalancers:  # attach to same (or new) ALB
   ```

3. **Subnet segmentation** If you want stricter isolation, you can carve out **“app‑subnets”** vs **“db‑subnets”**, each with its own route table and SG rules. Just update `phoenix-network.yml` to output those extra subnet IDs.

4. **Security groups per tier**

   * ALB SG → allows 0.0.0.0/0 on 80/443

   * App SG → allows inbound from ALB SG on 80

   * DB SG → allows inbound from App SG on 5432 (Postgres) or 3306 (MySQL)

With these patterns you can grow from one simple “agent-only” service into a multi‑tier architecture, all launched and managed via CloudFormation.
