Apoc
How it works Install guide Trust & safety About
My account Contact us
Installation guide

Set up Apoc in about ten minutes

One read-only IAM role in your AWS account. One app in your Slack workspace. Nothing else.

Apoc runs on our side. The only things you deploy are a cross-account IAM role that lets it read your bill, and the Slack app it talks through. This page walks through both, with the role created either from the AWS CLI or with Terraform. You can delete the role at any moment and Apoc loses all access the same second.

Before you start

  • Someone who can install apps in your Slack workspace. If that is not you, the account page mints a 24-hour install link you can hand to them.
  • Two values from your account page: the agent role ARN and the external ID. If you are signed in, this page fills them in for you.

1Register

Registration is by invitation. Contact us with your company name, a work email and the 12-digit ID of the AWS account Apoc should review. We send you an invitation link.

  • Open the link. It shows your company, your slug (the short name that prefixes the resources we run for you) and the AWS account, all fixed by the invitation.
  • Set a password and register. You land on your account page, where you sign in later with that email and password.
  • The account page holds everything the next two steps need: the setup values for the IAM role and the Add to Slack button.

2Create the read-only IAM role

Apoc reads your account through one IAM role that you create yourself. Its trust policy admits exactly one principal, your dedicated Apoc agent role in our account, and only when it presents your external ID. The role name must be exactly the one shown below: the agent is pinned to that name and cannot assume anything else.

Checking whether you are signed in…
Agent role ARN
arn:aws:iam::660409040798:role/apoc-agent-<your-slug>
External ID
<external-id>
Role name
apoc-readonly-<your-slug>
AWS account
<your-aws-account-id>

The role is read-only, nothing else: Cost Explorer and billing reads, AWS's own cost recommendations, and a describe/list/get inventory.

Pick your tool

2.1Verify the account ID

aws sts get-caller-identity --query Account --output text

Expected output: <your-aws-account-id>. Anything else: switch AWS profile.

2.2Save the two files

Same directory, these exact names.

apoc-readonly-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "IdentityAndEKSReads",
      "Effect": "Allow",
      "Action": [
        "sts:GetCallerIdentity",
        "eks:Describe*",
        "eks:List*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CostWatchBillingReads",
      "Effect": "Allow",
      "Action": [
        "ce:GetCostAndUsage",
        "ce:GetCostForecast",
        "ce:GetTags",
        "ce:ListCostAllocationTags",
        "ce:GetDimensionValues",
        "ce:GetCostAndUsageWithResources",
        "ce:GetSavingsPlansUtilization",
        "ce:GetSavingsPlansCoverage",
        "savingsplans:DescribeSavingsPlans",
        "savingsplans:DescribeSavingsPlansOfferingRates",
        "ce:GetCostAndUsageComparisons",
        "ce:GetCostComparisonDrivers",
        "ce:GetUsageForecast",
        "ce:GetCostCategories",
        "ce:ListCostCategoryDefinitions",
        "ce:DescribeCostCategoryDefinition",
        "ce:ListCostAllocationTagBackfillHistory",
        "ce:GetReservationCoverage",
        "ce:GetReservationUtilization",
        "ce:GetReservationPurchaseRecommendation",
        "ce:GetSavingsPlansUtilizationDetails",
        "ce:GetSavingsPlansPurchaseRecommendation",
        "budgets:ViewBudget",
        "freetier:GetFreeTierUsage"
      ],
      "Resource": "*"
    },
    {
      "Sid": "FinOpsReads",
      "Effect": "Allow",
      "Action": [
        "ce:GetAnomalies",
        "ce:GetAnomalyMonitors",
        "ce:GetAnomalySubscriptions",
        "cost-optimization-hub:ListRecommendations",
        "cost-optimization-hub:GetRecommendation",
        "cost-optimization-hub:ListRecommendationSummaries",
        "cost-optimization-hub:GetPreferences",
        "compute-optimizer:GetEnrollmentStatus",
        "compute-optimizer:GetRecommendationSummaries",
        "compute-optimizer:GetEC2InstanceRecommendations",
        "compute-optimizer:GetAutoScalingGroupRecommendations",
        "compute-optimizer:GetEBSVolumeRecommendations",
        "compute-optimizer:GetLambdaFunctionRecommendations",
        "compute-optimizer:GetECSServiceRecommendations",
        "compute-optimizer:GetRDSDatabaseRecommendations",
        "compute-optimizer:GetIdleRecommendations",
        "compute-optimizer:GetEC2RecommendationProjectedMetrics",
        "compute-optimizer:GetECSServiceRecommendationProjectedMetrics",
        "compute-optimizer:GetRDSDatabaseRecommendationProjectedMetrics",
        "compute-optimizer:GetLicenseRecommendations",
        "compute-optimizer:GetRecommendationPreferences",
        "compute-optimizer:GetEffectiveRecommendationPreferences",
        "pricing:GetProducts",
        "pricing:DescribeServices",
        "pricing:GetAttributeValues",
        "bcm-pricing-calculator:GetPreferences",
        "bcm-pricing-calculator:GetWorkloadEstimate",
        "bcm-pricing-calculator:ListWorkloadEstimates",
        "bcm-pricing-calculator:ListWorkloadEstimateUsage"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AdHocReadOnlyInventory",
      "Effect": "Allow",
      "Action": [
        "ec2:Describe*",
        "autoscaling:Describe*",
        "elasticloadbalancing:Describe*",
        "rds:Describe*",
        "rds:ListTagsForResource",
        "s3:ListAllMyBuckets",
        "s3:GetBucketLocation",
        "s3:ListBucket",
        "cloudwatch:Describe*",
        "cloudwatch:GetMetricData",
        "cloudwatch:GetMetricStatistics",
        "cloudwatch:ListMetrics",
        "logs:Describe*",
        "logs:FilterLogEvents",
        "route53:List*",
        "route53:Get*",
        "tag:GetResources",
        "tag:GetTagKeys",
        "tag:GetTagValues",
        "lambda:ListFunctions",
        "lambda:ListProvisionedConcurrencyConfigs",
        "ecs:ListClusters",
        "ecs:ListServices"
      ],
      "Resource": "*"
    }
  ]
}
apoc-trust.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::660409040798:role/apoc-agent-<your-slug>" },
      "Action": "sts:AssumeRole",
      "Condition": { "StringEquals": { "sts:ExternalId": "<external-id>" } }
    }
  ]
}

2.3Create the role

From that directory.

aws iam create-role \
  --role-name apoc-readonly-<your-slug> \
  --description "Apoc cost-review agent: read-only cross-account access" \
  --assume-role-policy-document file://apoc-trust.json \
  --tags Key=App,Value=apoc Key=Component,Value=client-role

aws iam put-role-policy \
  --role-name apoc-readonly-<your-slug> \
  --policy-name apoc-readonly \
  --policy-document file://apoc-readonly-policy.json

2.4Check

aws iam get-role --role-name apoc-readonly-<your-slug> --query Role.Arn --output text

Expected output: arn:aws:iam::<your-aws-account-id>:role/apoc-readonly-<your-slug>.

Done. Nothing to send back: Apoc starts reading within a day and posts its intro card once it is in your Slack.

2.1Verify the account ID

aws sts get-caller-identity --query Account --output text

Expected output: <your-aws-account-id>. Anything else: switch AWS profile. Terraform 1.10 or newer.

2.2Save the two files

An empty directory, these exact names.

main.tf
terraform {
  required_version = ">= 1.10"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.40"
    }
  }
  # No backend: state stays with you. Add your own backend here if you prefer.
}

provider "aws" {
  region = var.region
}

variable "agent_role_arn" {
  description = "Your dedicated Apoc agent role (account page). The only principal allowed to assume this role."
  type        = string
}

variable "external_id" {
  description = "The external id Apoc presents on every assume (account page, next to the agent role ARN)."
  type        = string
}

variable "role_name" {
  description = "Leave empty: derived from agent_role_arn (apoc-agent-<slug> becomes apoc-readonly-<slug>), the exact name Apoc expects."
  type        = string
  default     = ""
}

variable "region" {
  description = "Only anchors the provider; IAM is global."
  type        = string
  default     = "us-east-1"
}

locals {
  role_name = var.role_name != "" ? var.role_name : replace(split("/", var.agent_role_arn)[1], "apoc-agent", "apoc-readonly")
}

resource "aws_iam_role" "readonly" {
  name        = local.role_name
  description = "Apoc cost-review agent: read-only cross-account access"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = var.agent_role_arn }
      Action    = "sts:AssumeRole"
      Condition = { StringEquals = { "sts:ExternalId" = var.external_id } }
    }]
  })

  tags = {
    App       = "apoc"
    Component = "client-role"
  }
}

resource "aws_iam_role_policy" "readonly" {
  name   = "apoc-readonly"
  role   = aws_iam_role.readonly.id
  policy = file("${path.module}/apoc-readonly-policy.json")
}

output "role_arn" {
  value = aws_iam_role.readonly.arn
}

output "role_name" {
  value = aws_iam_role.readonly.name
}

2.3Apply

From that directory.

terraform init
terraform apply -var agent_role_arn=arn:aws:iam::660409040798:role/apoc-agent-<your-slug> -var external_id=<external-id>

The plan: one role, one inline policy. State stays with you, local by default.

2.4Check

terraform output role_arn

Expected output: arn:aws:iam::<your-aws-account-id>:role/apoc-readonly-<your-slug>.

Done. Nothing to send back: Apoc starts reading within a day and posts its intro card once it is in your Slack.

3Install Apoc in your Slack

On your account page, click Add to Slack and approve the app in the workspace you want. Installing an app usually needs a workspace admin or an owner. If that is someone else, click Generate install link instead and send them the link; it is valid for 24 hours and does the same install for your account.

Say hello once

Open the channel where you want the daily line to live and send Apoc a message there, for example @Apoc hello. That first conversation becomes the delivery channel. A couple of minutes later Apoc posts its intro card: this month so far, AWS's forecast, last month, a starting limit on the whole bill, and where your spend goes by service and by tag. From then on it is one line a day.

  • Private channel? Invite the app first with /invite @Apoc, then send your message. Public channels need no invitation: Apoc joins them itself when you point it there.
  • Move it later with /apoc channel #aws-costs from anywhere, or /apoc channel here from the channel itself.
  • Who can talk to Apoc is set on your account page: a list of Slack member IDs, or empty to allow everyone in the workspace.

First commands

Type them in any channel. Slash commands only run when typed, so copy the words, not a code snippet.

  • /apoc daily 08:00 Europe/Prague — when the daily line arrives.
  • /apoc limit total 12000 — a monthly limit on the whole bill; Apoc checks AWS's forecast against it every day.
  • /apoc limit service ec2 5000 — the same for one service.
  • /apoc status — channel, daily time, data date, what is over its limit.
  • /apoc help — every command.

Questions about the bill need no command: ask Apoc in a thread.

4Revoke or uninstall

Delete the role and Apoc loses all access to your account the same second. If you created it with the AWS CLI:

aws iam delete-role-policy --role-name apoc-readonly-<your-slug> --policy-name apoc-readonly
aws iam delete-role --role-name apoc-readonly-<your-slug>

If you created it with Terraform, terraform destroy in the same directory.

To remove the Slack app, open your workspace's app settings, find Apoc and remove it. To close your account, write to support@getapoc.ai.

Apoc
Contact Install guide Terms Privacy Refunds Support
© 2026 MITON AI Spice s.r.o.