AWS Service Catalog: Self-Service Infrastructure for Enterprise Teams (2026)

AWS Service Catalog Self-Service Infrastructure

Large enterprises face a brutal tension: development teams need to move fast, but the platform team must enforce security, compliance, and cost controls. Without guardrails, developers provision whatever they want — untagged resources, public S3 buckets, oversized instances, no encryption. AWS Service Catalog solves this by letting platform engineers pre-approve a catalogue of infrastructure products that developers can self-provision through a clean console or API, with all governance constraints baked in invisibly. No more waiting on tickets; no more policy drift.

1. Service Catalog vs AWS Proton vs CDK vs CloudFormation Direct

Before committing to AWS Service Catalog, it is worth understanding where it sits in the AWS provisioning ecosystem. Four tools overlap in this space, and choosing the wrong one means retrofitting later.

Tool Best For Governance Model End-User Skills Needed Multi-Account
Service Catalog Self-service provisioning for non-platform teams; enforcing approved products across org Portfolios, constraints, TagOptions, launch IAM roles None — GUI/CLI, no IaC knowledge Yes (portfolio sharing via Organizations)
AWS Proton Platform teams delivering full service templates (environment + service) to container/serverless app developers Template versioning, CodeBuild/CodePipeline sync Low — deploy and update via console/CLI Yes (environment account connections)
AWS CDK Developer teams who write infrastructure in TypeScript/Python and want full programmatic control Construct libraries, custom aspects, policy-as-code High — developers own IaC code Via CDK Pipelines
CloudFormation Direct Operations and platform teams deploying via pipeline; StackSets for baseline compliance StackSets, Service-Managed permissions, drift detection Medium — YAML template authors Yes (StackSets)
When to Choose Service Catalog: Your platform team has CloudFormation expertise but your end-users (app teams, data scientists, QA engineers) do not. You need to enforce tags, restrict instance types, require encryption, and prevent cost sprawl — without issuing full IAM permissions to end-users. Service Catalog is the answer.

AWS Proton is designed around the concept of an environment + microservice pair and integrates deeply with container registries and CI pipelines. If your primary concern is application deployment patterns (ECS services, Lambda functions, serverless APIs), Proton is the better fit. If your concern is general infrastructure — databases, VPCs, EC2 fleets, EKS clusters, S3 buckets — Service Catalog is the right tool. The two are not mutually exclusive: large enterprises often use Proton for application-layer products and Service Catalog for infrastructure primitives.

CloudFormation StackSets and Service Catalog also complement each other. StackSets handle baseline resources that must exist in every account (CloudTrail, Config rules, VPC flow logs) — these are not optional and not user-initiated. Service Catalog handles on-demand, user-initiated provisioning. Think of StackSets as the mandatory foundation and Service Catalog as the approved menu.

2. Core Concepts: Portfolios, Products, Constraints, and TagOptions

AWS Service Catalog is built on a small set of primitives. Understanding them deeply before you start building saves significant refactoring time later.

Portfolio — a container that groups related products and controls who can access them. A portfolio is shared with IAM principals (users, groups, roles) or entire AWS accounts. You might have a "Data Platform" portfolio containing approved EMR clusters, RDS instances, and Redshift clusters, shared only with the data engineering group. Portfolios are the unit of access control in Service Catalog.

Product — a CloudFormation template (or Terraform configuration via an external connector) packaged for self-service use. Each product has versions, a description, a support contact, and a distributor name. Products belong to one or more portfolios. When you update a CloudFormation template and publish a new version, existing provisioned products are not automatically updated — end-users receive an "update available" notification and can choose when to apply it.

Provisioned Product — the live instance created when an end-user launches a product. It maps 1:1 to a CloudFormation stack. End-users can update, terminate, or view outputs of their provisioned products. Platform teams can view all provisioned products in the account and intervene if needed.

Constraints — rules attached to a product within a portfolio. There are four types:

  • Launch constraint — specifies the IAM role used to provision the product (the end-user's own permissions are bypassed)
  • Notification constraint — specifies an SNS topic that receives all CloudFormation stack events for provisioned products
  • Template constraint — restricts which CloudFormation parameter values an end-user can select (e.g., only allow t3.medium and t3.large)
  • StackSet constraint — deploys the product as a StackSet across multiple accounts and regions

TagOptions — key-value pairs that can be marked required or optional, applied to provisioned products. TagOptions are defined at the portfolio or product level and are enforced at provisioning time. They are the primary mechanism for preventing untagged resources that blow up your cost allocation reports.

Key Distinction: Constraints and TagOptions are enforced server-side by the Service Catalog service itself. An end-user cannot bypass them by calling CloudFormation directly — they would need the launch IAM role ARN (which they do not have) to do so. This is what makes Service Catalog "approved infrastructure" rather than just "template sharing."

3. Creating a Portfolio and Product — CloudFormation + CLI + Terraform

Let us build a real example: a "Golden VPC" product that provisions a three-tier VPC with public, private, and data subnets, flow logs enabled, and no option to disable encryption.

First, create the CloudFormation template that will become the product. Store it in S3 — Service Catalog references templates by S3 URL:

# golden-vpc.yaml — CloudFormation template for the Service Catalog product
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Approved Golden VPC — 3-tier with flow logs'

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, staging, prod]
    Description: Target environment (controls CIDR range)

  VpcCidr:
    Type: String
    Default: '10.0.0.0/16'
    AllowedPattern: '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
    Description: VPC CIDR block

Mappings:
  EnvConfig:
    dev:
      FlowLogRetentionDays: 7
    staging:
      FlowLogRetentionDays: 30
    prod:
      FlowLogRetentionDays: 90

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub 'golden-vpc-${Environment}'
        - Key: ManagedBy
          Value: ServiceCatalog

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [0, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Tier
          Value: public

  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [2, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Tier
          Value: private

  DataSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [4, !Cidr [!Ref VpcCidr, 6, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - Key: Tier
          Value: data

  FlowLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: !FindInMap [EnvConfig, !Ref Environment, FlowLogRetentionDays]

  FlowLogRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: vpc-flow-logs.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: VpcFlowLogsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt FlowLogGroup.Arn

  VpcFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      ResourceId: !Ref VPC
      ResourceType: VPC
      TrafficType: ALL
      LogGroupName: !Ref FlowLogGroup
      DeliverLogsPermissionArn: !GetAtt FlowLogRole.Arn

Outputs:
  VpcId:
    Value: !Ref VPC
    Export:
      Name: !Sub '${AWS::StackName}-VpcId'
  PublicSubnetId:
    Value: !Ref PublicSubnetA
    Export:
      Name: !Sub '${AWS::StackName}-PublicSubnetA'
  PrivateSubnetId:
    Value: !Ref PrivateSubnetA
    Export:
      Name: !Sub '${AWS::StackName}-PrivateSubnetA'

Upload the template to S3 and create the portfolio and product via CLI:

# Upload template to S3
aws s3 cp golden-vpc.yaml s3://my-service-catalog-templates/golden-vpc/v1/golden-vpc.yaml

# Create a portfolio
PORTFOLIO_ID=$(aws servicecatalog create-portfolio \
  --display-name "Platform Infrastructure" \
  --description "Approved infrastructure products for all teams" \
  --provider-name "Platform Engineering" \
  --query 'PortfolioDetail.Id' --output text)

echo "Portfolio ID: $PORTFOLIO_ID"

# Create the product
PRODUCT_RESULT=$(aws servicecatalog create-product \
  --name "Golden VPC" \
  --description "Standard 3-tier VPC with flow logs, approved subnets, and tagging" \
  --owner "Platform Engineering" \
  --type CLOUD_FORMATION_TEMPLATE \
  --provisioning-artifact-parameters '{
    "Name": "v1.0",
    "Description": "Initial release with 3-tier layout and VPC flow logs",
    "Type": "CLOUD_FORMATION_TEMPLATE",
    "Info": {
      "LoadTemplateFromURL": "https://my-service-catalog-templates.s3.amazonaws.com/golden-vpc/v1/golden-vpc.yaml"
    }
  }')

PRODUCT_ID=$(echo $PRODUCT_RESULT | jq -r '.ProductViewDetail.ProductViewSummary.ProductId')
echo "Product ID: $PRODUCT_ID"

# Associate the product with the portfolio
aws servicecatalog associate-product-with-portfolio \
  --product-id $PRODUCT_ID \
  --portfolio-id $PORTFOLIO_ID

# Grant access to the data engineering IAM group
aws servicecatalog associate-principal-with-portfolio \
  --portfolio-id $PORTFOLIO_ID \
  --principal-arn arn:aws:iam::123456789012:group/DataEngineering \
  --principal-type IAM

The same portfolio and product can be managed with Terraform using the AWS provider:

# terraform/service_catalog.tf

resource "aws_servicecatalog_portfolio" "platform" {
  name          = "Platform Infrastructure"
  description   = "Approved infrastructure products for all teams"
  provider_name = "Platform Engineering"
}

resource "aws_s3_object" "golden_vpc_template" {
  bucket = "my-service-catalog-templates"
  key    = "golden-vpc/v1/golden-vpc.yaml"
  source = "${path.module}/templates/golden-vpc.yaml"
  etag   = filemd5("${path.module}/templates/golden-vpc.yaml")
}

resource "aws_servicecatalog_product" "golden_vpc" {
  name        = "Golden VPC"
  description = "Standard 3-tier VPC with flow logs"
  owner       = "Platform Engineering"
  type        = "CLOUD_FORMATION_TEMPLATE"

  provisioning_artifact_parameters {
    name         = "v1.0"
    description  = "Initial release"
    type         = "CLOUD_FORMATION_TEMPLATE"
    template_url = "https://${aws_s3_object.golden_vpc_template.bucket}.s3.amazonaws.com/${aws_s3_object.golden_vpc_template.key}"
  }

  depends_on = [aws_s3_object.golden_vpc_template]
}

resource "aws_servicecatalog_product_portfolio_association" "golden_vpc" {
  portfolio_id = aws_servicecatalog_portfolio.platform.id
  product_id   = aws_servicecatalog_product.golden_vpc.id
}

resource "aws_servicecatalog_principal_portfolio_association" "data_eng" {
  portfolio_id   = aws_servicecatalog_portfolio.platform.id
  principal_arn  = "arn:aws:iam::123456789012:group/DataEngineering"
  principal_type = "IAM"
}

4. Launch Constraints: Least-Privilege IAM for Provisioning

By default, when an end-user provisions a Service Catalog product, the provisioning happens using their own IAM permissions. This defeats the governance purpose — you would need to grant end-users broad CloudFormation and EC2 permissions. Launch constraints solve this elegantly.

A launch constraint specifies an IAM role that Service Catalog assumes on behalf of the end-user to provision the product. The end-user only needs the servicecatalog:ProvisionProduct permission. The launch role needs the specific permissions required by the CloudFormation template — and nothing else.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ServiceCatalogLaunchVPC",
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVpc",
        "ec2:DeleteVpc",
        "ec2:ModifyVpcAttribute",
        "ec2:CreateSubnet",
        "ec2:DeleteSubnet",
        "ec2:CreateFlowLogs",
        "ec2:DeleteFlowLogs",
        "ec2:CreateTags",
        "ec2:DescribeVpcs",
        "ec2:DescribeSubnets",
        "ec2:DescribeAvailabilityZones",
        "logs:CreateLogGroup",
        "logs:DeleteLogGroup",
        "logs:PutRetentionPolicy",
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:PutRolePolicy",
        "iam:DeleteRolePolicy",
        "iam:PassRole",
        "cloudformation:CreateStack",
        "cloudformation:DeleteStack",
        "cloudformation:UpdateStack",
        "cloudformation:DescribeStacks",
        "cloudformation:DescribeStackEvents",
        "cloudformation:GetTemplate",
        "s3:GetObject"
      ],
      "Resource": "*"
    }
  ]
}

The trust policy on the launch role must allow both Service Catalog and CloudFormation to assume it:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "servicecatalog.amazonaws.com",
          "cloudformation.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Create and associate the launch constraint via CLI:

# Create the launch IAM role (attach the policy above to this role)
aws iam create-role \
  --role-name SC-Launch-GoldenVPC \
  --assume-role-policy-document file://trust-policy.json

aws iam put-role-policy \
  --role-name SC-Launch-GoldenVPC \
  --policy-name GoldenVPCLaunchPolicy \
  --policy-document file://launch-policy.json

# Create the launch constraint linking the role to the product/portfolio
aws servicecatalog create-constraint \
  --portfolio-id $PORTFOLIO_ID \
  --product-id $PRODUCT_ID \
  --type LAUNCH \
  --parameters '{
    "RoleArn": "arn:aws:iam::123456789012:role/SC-Launch-GoldenVPC"
  }' \
  --description "Launch role for Golden VPC provisioning"
Security Best Practice: Create one launch role per product (not one role for all products). This ensures that if a product's CloudFormation template is updated with new resource types, you only expand the permissions on that specific role. It also makes IAM audits trivial — the role name directly identifies which product it serves.

With a launch constraint in place, an end-user in the DataEngineering group needs only this minimal policy to provision Service Catalog products:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "servicecatalog:ListPortfolios",
        "servicecatalog:SearchProducts",
        "servicecatalog:DescribeProduct",
        "servicecatalog:DescribeProductView",
        "servicecatalog:DescribeProvisioningParameters",
        "servicecatalog:ProvisionProduct",
        "servicecatalog:ListProvisionedProductPlans",
        "servicecatalog:DescribeProvisionedProduct",
        "servicecatalog:UpdateProvisionedProduct",
        "servicecatalog:TerminateProvisionedProduct",
        "servicecatalog:ListLaunchPaths",
        "cloudformation:DescribeStacks",
        "cloudformation:DescribeStackEvents"
      ],
      "Resource": "*"
    }
  ]
}

5. Notification Constraints and Approval Workflows

A notification constraint connects provisioned product events to an SNS topic. Every CloudFormation stack event (CREATE_IN_PROGRESS, CREATE_COMPLETE, UPDATE_COMPLETE, DELETE_FAILED, etc.) is published to the topic. This enables several important patterns: centralised audit logging, real-time alerting to Slack or PagerDuty, and approval workflows.

# Create an SNS topic for provisioning notifications
aws sns create-topic --name sc-provisioning-events

# Subscribe a Lambda function or email to the topic
aws sns subscribe \
  --topic-arn arn:aws:sns:us-east-1:123456789012:sc-provisioning-events \
  --protocol email \
  --notification-endpoint platform-team@techoral.com

# Create the notification constraint
aws servicecatalog create-constraint \
  --portfolio-id $PORTFOLIO_ID \
  --product-id $PRODUCT_ID \
  --type NOTIFICATION \
  --parameters '{
    "NotificationArns": [
      "arn:aws:sns:us-east-1:123456789012:sc-provisioning-events"
    ]
  }' \
  --description "Route all provisioning events to platform team SNS topic"

For approval workflows, the pattern is to subscribe a Lambda function to the SNS topic. The Lambda inspects the event, and if it meets certain criteria (e.g., a production environment product being provisioned, or a large instance type being requested), it can send an approval request to a Step Functions workflow or post to a Slack channel with approve/deny buttons:

# approval_handler.py — Lambda function subscribed to the SNS topic
import json
import boto3
import os

def handler(event, context):
    sns_message = json.loads(event['Records'][0]['Sns']['Message'])

    stack_name = sns_message.get('StackName', '')
    resource_status = sns_message.get('ResourceStatus', '')
    resource_type = sns_message.get('ResourceType', '')
    logical_id = sns_message.get('LogicalResourceId', '')

    # Detect large instance provisioning
    if resource_type == 'AWS::EC2::Instance' and resource_status == 'CREATE_IN_PROGRESS':
        check_instance_size_and_notify(sns_message)

    # Alert on any DELETE_FAILED — this needs human attention
    if 'DELETE_FAILED' in resource_status:
        send_platform_alert(
            f"DELETE_FAILED in stack {stack_name}: {logical_id} ({resource_type})"
        )

    return {'statusCode': 200}

def send_platform_alert(message):
    slack_client = boto3.client('lambda')
    # Call Slack notification Lambda
    slack_client.invoke(
        FunctionName=os.environ['SLACK_NOTIFIER_FUNCTION'],
        InvocationType='Event',
        Payload=json.dumps({'message': message, 'channel': '#platform-alerts'})
    )
Template Constraints for Parameter Guardrails: Use template constraints to restrict what parameters end-users can enter. For example, restrict instance types to an approved list: aws servicecatalog create-constraint --type TEMPLATE --parameters '{"Rules": {"AllowedInstanceTypes": {"Assertions": [{"Assert": {"Fn::Contains": [["t3.medium","t3.large","m6i.large"], {"Ref": "InstanceType"}]}, "AssertDescription": "Only approved instance types are allowed"}]}}}'. Template constraints are evaluated before provisioning begins, giving users immediate feedback rather than a CloudFormation error mid-deployment.

6. TagOptions: Enforcing Mandatory Tags at Provisioning Time

Untagged resources are a FinOps nightmare. Without cost allocation tags, you cannot answer "which team owns this $3,000/month RDS cluster?" TagOptions in Service Catalog make tags mandatory at provisioning time — end-users cannot complete the provisioning flow without providing required tag values.

TagOptions are managed separately from portfolios and products, then associated with either. A TagOption is a key with a list of allowed values. Marking a TagOption as active and associating it with a portfolio means every product in that portfolio requires the tag.

# Create TagOption for CostCenter
COST_TAG_ID=$(aws servicecatalog create-tag-option \
  --key "CostCenter" \
  --value "PLATFORM" \
  --query 'TagOptionDetail.Id' --output text)

# Create more allowed values for the same key
aws servicecatalog create-tag-option --key "CostCenter" --value "DATA"
aws servicecatalog create-tag-option --key "CostCenter" --value "PRODUCT"
aws servicecatalog create-tag-option --key "CostCenter" --value "SECURITY"

# Create TagOption for Environment
aws servicecatalog create-tag-option --key "Environment" --value "dev"
aws servicecatalog create-tag-option --key "Environment" --value "staging"
aws servicecatalog create-tag-option --key "Environment" --value "prod"

# Create TagOption for Team (free-form, single allowed value acts as a prompt)
aws servicecatalog create-tag-option --key "Team" --value "required"

# Associate CostCenter TagOption with the portfolio (applies to all products)
aws servicecatalog associate-tag-option-with-resource \
  --resource-id $PORTFOLIO_ID \
  --tag-option-id $COST_TAG_ID

When TagOptions are associated at the portfolio level, all products within that portfolio inherit them. For product-specific tags (e.g., a DatabaseEngine tag only relevant to RDS products), associate the TagOption directly with the product ID instead of the portfolio ID.

In Terraform:

resource "aws_servicecatalog_tag_option" "cost_center_platform" {
  key   = "CostCenter"
  value = "PLATFORM"
}

resource "aws_servicecatalog_tag_option" "cost_center_data" {
  key   = "CostCenter"
  value = "DATA"
}

resource "aws_servicecatalog_tag_option" "environment_prod" {
  key   = "Environment"
  value = "prod"
}

resource "aws_servicecatalog_tag_option_resource_association" "portfolio_cost_center" {
  tag_option_id = aws_servicecatalog_tag_option.cost_center_platform.id
  resource_id   = aws_servicecatalog_portfolio.platform.id
}
TagOption Strategy: Keep TagOption values in a Terraform module or CloudFormation StackSet so they are version-controlled and consistent across all accounts. When Finance adds a new cost centre code, update the module, apply it, and all portfolios in all accounts pick up the new allowed value automatically. Never manage TagOptions manually in the console — drift is inevitable.

TagOptions work in conjunction with AWS Cost Allocation Tags. Activate your Service Catalog tags (CostCenter, Environment, Team) as cost allocation tags in the Billing console, wait 24 hours, and then filter your Cost Explorer reports by these dimensions to get per-team, per-environment breakdowns that are impossible without consistent tagging.

7. Cross-Account Portfolio Sharing via Organizations

In a multi-account AWS organization, maintaining a separate copy of your approved products in every account is unsustainable. Service Catalog supports portfolio sharing: a portfolio defined in a management (or delegated admin) account can be shared to spoke accounts, either individually or to entire OUs in your Organizations hierarchy.

When a portfolio is shared, the receiving account sees the products in the portfolio and can optionally add their own access controls. The original portfolio's constraints and TagOptions are inherited by default (and can be enforced so spoke accounts cannot override them).

# From the management account — share portfolio to an entire OU
aws servicecatalog create-portfolio-share \
  --portfolio-id $PORTFOLIO_ID \
  --organization-node '{
    "Type": "ORGANIZATIONAL_UNIT",
    "Value": "ou-root-abc12345"
  }' \
  --share-tag-options true \
  --share-principals false

# Or share to a specific account
aws servicecatalog create-portfolio-share \
  --portfolio-id $PORTFOLIO_ID \
  --account-id 987654321098 \
  --share-tag-options true

# In the spoke account — import the shared portfolio
aws servicecatalog accept-portfolio-share \
  --portfolio-id $PORTFOLIO_ID

# Associate IAM principals in the spoke account with the imported portfolio
aws servicecatalog associate-principal-with-portfolio \
  --portfolio-id $PORTFOLIO_ID \
  --principal-arn arn:aws:iam::987654321098:group/Developers \
  --principal-type IAM

For Organizations-managed sharing, the management account must first enable Service Catalog integration with Organizations:

# Enable Organizations integration (run once from management account)
aws servicecatalog enable-aws-organizations-access

# Verify the integration status
aws servicecatalog describe-portfolio-shares \
  --portfolio-id $PORTFOLIO_ID \
  --type ORGANIZATIONAL_UNIT

# List all portfolios shared from management account
aws servicecatalog list-portfolio-access --portfolio-id $PORTFOLIO_ID
Delegated Administrator: You do not have to manage portfolios from the management account. Register a dedicated "Service Catalog admin" account as a delegated administrator: aws organizations register-delegated-administrator --account-id ACCOUNT_ID --service-principal servicecatalog.amazonaws.com. This follows the AWS best practice of keeping the management account for billing and root-level operations only.

When sharing portfolios with --share-principals true, the principal associations you define in the management account are also applied in spoke accounts automatically. This is useful for large organizations where you want the platform team to centrally control which groups get access to which products — no spoke-account action required after sharing.

8. Service Catalog AppRegistry

AppRegistry (now part of AWS Service Management Connector and AWS Systems Manager Application Manager) provides a way to logically group AWS resources and CloudFormation stacks into an application. This answers the question: "of the thousands of resources in this account, which ones belong to the Payments application?"

AppRegistry uses two concepts: applications and attribute groups. An application is a named container. Attribute groups are JSON schemas of metadata attached to the application (e.g., team name, business unit, compliance tier, SLA level). Resources — either individual ARNs or CloudFormation stacks — are associated with applications.

# Create an application in AppRegistry
APP_ARN=$(aws servicecatalog-appregistry create-application \
  --name "payments-service" \
  --description "All infrastructure for the Payments microservice" \
  --tags '{"BusinessUnit": "FinTech", "CriticalityTier": "P1"}' \
  --query 'application.arn' --output text)

echo "Application ARN: $APP_ARN"

# Create an attribute group with metadata schema
ATTR_GROUP_ARN=$(aws servicecatalog-appregistry create-attribute-group \
  --name "payments-metadata" \
  --description "Operational metadata for the Payments application" \
  --attributes '{
    "team": "payments-engineering",
    "slack_channel": "#payments-oncall",
    "runbook_url": "https://wiki.techoral.com/payments/runbook",
    "compliance_tier": "PCI-DSS",
    "sla_rto_hours": "4",
    "sla_rpo_hours": "1"
  }' \
  --query 'attributeGroup.arn' --output text)

# Associate the attribute group with the application
aws servicecatalog-appregistry associate-attribute-group \
  --application $APP_ARN \
  --attribute-group $ATTR_GROUP_ARN

# Associate a CloudFormation stack with the application
aws servicecatalog-appregistry associate-resource \
  --application $APP_ARN \
  --resource-type CFN_STACK \
  --resource arn:aws:cloudformation:us-east-1:123456789012:stack/payments-rds/abc123

# Associate a standalone resource (e.g., an S3 bucket not in a CF stack)
aws servicecatalog-appregistry associate-resource \
  --application $APP_ARN \
  --resource-type RESOURCE_TAG_VALUE \
  --resource arn:aws:s3:::payments-event-archive

Once resources are associated with an application, you can query the application's resource list to get a complete inventory, use it as a filter in AWS Cost Explorer (the appregistry tag is automatically applied), and view it in the Systems Manager Application Manager console as a unified operations dashboard.

# List all resources associated with the application
aws servicecatalog-appregistry list-associated-resources \
  --application payments-service

# Get the full application detail including all attribute groups
aws servicecatalog-appregistry get-application \
  --application payments-service

# Sync the application tag to all associated resources automatically
aws servicecatalog-appregistry put-configuration \
  --configuration '{
    "application": {
      "tagKey": "aws:servicecatalog:applicationArn",
      "applyToStackType": "ALL"
    }
  }'

9. End-User Self-Service: Console, CLI, and Provisioning Parameters

From the end-user's perspective, Service Catalog is a clean product catalogue. In the AWS console under Service Catalog → Products, they see only the products their portfolio access grants them. Each product shows a description, version, support contact, and a "Launch product" button. The launch flow prompts for a name, version selection, parameter values (filtered by any template constraints), and TagOption values.

Via CLI, end-users provision, update, and terminate products without needing to know anything about the underlying CloudFormation:

# End-user: list available products (only sees products in their portfolios)
aws servicecatalog search-products \
  --filters '{"FullTextSearch": ["VPC"]}' \
  --query 'ProductViewSummaries[*].{Name:Name,ProductId:ProductId,ShortDescription:ShortDescription}'

# Get provisioning parameters for a specific product
aws servicecatalog describe-provisioning-parameters \
  --product-id $PRODUCT_ID \
  --provisioning-artifact-id $ARTIFACT_ID \
  --path-id $PATH_ID

# Provision the product (launch)
aws servicecatalog provision-product \
  --product-id $PRODUCT_ID \
  --provisioning-artifact-id $ARTIFACT_ID \
  --provisioned-product-name "data-team-vpc-dev" \
  --provisioning-parameters '[
    {"Key": "Environment", "Value": "dev"},
    {"Key": "VpcCidr", "Value": "10.10.0.0/16"}
  ]' \
  --tags '[
    {"Key": "CostCenter", "Value": "DATA"},
    {"Key": "Team", "Value": "data-engineering"}
  ]'

# Check provisioning status
aws servicecatalog describe-provisioned-product \
  --name "data-team-vpc-dev"

# Update the provisioned product (e.g., change parameters after new version released)
aws servicecatalog update-provisioned-product \
  --provisioned-product-name "data-team-vpc-dev" \
  --product-id $PRODUCT_ID \
  --provisioning-artifact-id $NEW_ARTIFACT_ID \
  --provisioning-parameters '[
    {"Key": "Environment", "Value": "dev"},
    {"Key": "VpcCidr", "Value": "10.10.0.0/16"}
  ]'

# Terminate (destroy) the provisioned product
aws servicecatalog terminate-provisioned-product \
  --provisioned-product-name "data-team-vpc-dev"

End-users can also view outputs from their provisioned products (CloudFormation stack outputs are surfaced directly in the Service Catalog console), which is how they get VPC IDs, subnet IDs, and other values they need for subsequent deployments.

Plans (Provisioned Product Plans): Service Catalog supports a "plan" workflow where end-users request a provisioned product but it does not deploy until approved. Create a plan with aws servicecatalog create-provisioned-product-plan, review the plan (which shows what CloudFormation will create), then execute or reject it. This is especially useful for production environment products that require a second set of eyes before provisioning.

10. Governance, Compliance, and Audit

Service Catalog's governance value multiplies when integrated with AWS Config, CloudTrail, and AWS Budgets. Together they create a closed loop: only approved products can be provisioned (Service Catalog), configurations are continuously validated (Config), spending is capped (Budgets), and every action is audited (CloudTrail).

AWS Config Integration

Every provisioned product creates a CloudFormation stack. AWS Config records all configuration changes to the resources in that stack. If an end-user or automation modifies a resource outside of Service Catalog (e.g., opens a security group rule manually), Config records the drift. You can create Config rules that specifically target Service Catalog provisioned resources using the aws:servicecatalog:portfolioArn and aws:servicecatalog:productArn tags automatically applied by Service Catalog:

# AWS Config rule: detect VPCs without flow logs enabled (affects all SC provisioned VPCs)
aws configservice put-config-rule \
  --config-rule '{
    "ConfigRuleName": "vpc-flow-logs-enabled",
    "Source": {
      "Owner": "AWS",
      "SourceIdentifier": "VPC_FLOW_LOGS_ENABLED"
    },
    "Scope": {
      "ComplianceResourceTypes": ["AWS::EC2::VPC"]
    }
  }'

# Config rule: require tags on all EC2 resources
aws configservice put-config-rule \
  --config-rule '{
    "ConfigRuleName": "required-tags-ec2",
    "Source": {
      "Owner": "AWS",
      "SourceIdentifier": "REQUIRED_TAGS"
    },
    "InputParameters": "{\"tag1Key\": \"CostCenter\", \"tag2Key\": \"Environment\", \"tag3Key\": \"Team\"}",
    "Scope": {
      "ComplianceResourceTypes": ["AWS::EC2::Instance", "AWS::RDS::DBInstance"]
    }
  }'

# Enable drift detection on all SC provisioned products stacks
aws cloudformation detect-stack-drift --stack-name sc-$(date +%Y%m%d)-data-team-vpc-dev

Budget Guardrails

Attach an AWS Budget to each portfolio's tag dimension. When provisioned products breach a cost threshold, automated actions can stop EC2 instances or apply a restrictive SCP:

{
  "BudgetName": "DataTeam-ServiceCatalog-Monthly",
  "BudgetLimit": {
    "Amount": "5000",
    "Unit": "USD"
  },
  "BudgetType": "COST",
  "TimeUnit": "MONTHLY",
  "CostFilters": {
    "TagKeyValue": ["CostCenter$DATA"]
  },
  "NotificationsWithSubscribers": [
    {
      "Notification": {
        "NotificationType": "ACTUAL",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80,
        "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
        {"SubscriptionType": "EMAIL", "Address": "platform-team@techoral.com"}
      ]
    }
  ]
}

CloudTrail Audit

All Service Catalog API calls are recorded in CloudTrail under the servicecatalog.amazonaws.com event source. Key events to monitor: ProvisionProduct, TerminateProvisionedProduct, UpdateProvisionedProduct, and CreatePortfolioShare. Set up CloudWatch Metric Filters on your CloudTrail log group to alert on sensitive actions:

# CloudWatch metric filter: alert on any TerminateProvisionedProduct in production
aws logs put-metric-filter \
  --log-group-name "aws-cloudtrail-logs" \
  --filter-name "SCTerminateProd" \
  --filter-pattern '{ ($.eventSource = "servicecatalog.amazonaws.com") && ($.eventName = "TerminateProvisionedProduct") && ($.requestParameters.provisionedProductName = "*prod*") }' \
  --metric-transformations '[{
    "metricName": "SCProdTerminations",
    "metricNamespace": "ServiceCatalog/Governance",
    "metricValue": "1",
    "defaultValue": 0
  }]'

aws cloudwatch put-metric-alarm \
  --alarm-name "SCProdTerminationAlert" \
  --metric-name "SCProdTerminations" \
  --namespace "ServiceCatalog/Governance" \
  --statistic Sum \
  --period 60 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:platform-critical-alerts
Complete Governance Checklist:
  • Launch constraints on all products — no end-user needs broad IAM permissions
  • TagOptions (CostCenter, Environment, Team) associated at portfolio level — enforced at provisioning time
  • Template constraints preventing insecure or expensive parameter values
  • Notification constraints routing all events to a central SNS topic
  • AWS Config rules validating that provisioned resources remain compliant post-deployment
  • CloudTrail alerts on sensitive Service Catalog API calls
  • AWS Budgets with tag filters per team/cost centre
  • AppRegistry associating all provisioned products with named applications for operational visibility

Frequently Asked Questions

Can I use Terraform templates with Service Catalog?

Yes, via the AWS Service Catalog Terraform open-source engine (a Lambda-based solution) or the native Terraform Cloud connector. Terraform products are referenced as product type TERRAFORM_OPEN_SOURCE or TERRAFORM_CLOUD. The workflow is identical to CloudFormation products from the end-user perspective.

What happens to a provisioned product if the portfolio share is removed?

Existing provisioned products are not deleted. They continue to run. However, end-users lose the ability to update or re-provision from the removed portfolio. The underlying CloudFormation stacks remain until explicitly deleted. Always communicate portfolio removals to end-users before revoking access.

How do I handle product version updates across hundreds of provisioned products?

Use the Service Catalog bulk update CLI command or write a script that lists all provisioned products for a given product ID, checks the current version, and calls update-provisioned-product for each one that is not on the latest version. For critical security patches, consider making the old version inactive (which triggers "update required" UI warnings) to accelerate adoption.

Is Service Catalog free?

Service Catalog itself has no additional charge — you pay only for the underlying AWS resources provisioned. AppRegistry is also free. The cost comes from the resources your end-users provision. This makes TagOptions and Budgets integration essential: without them, a self-service portal is a cost liability waiting to happen.

Key Concepts
  • Portfolio — groups products + access
  • Product — versioned CloudFormation template
  • Launch Constraint — provisioning IAM role
  • TagOptions — enforced mandatory tags
  • AppRegistry — application resource grouping