AWS Service Catalog: Self-Service Infrastructure for Enterprise Teams (2026)
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.
Table of Contents
- Service Catalog vs Proton vs CDK vs CloudFormation Direct
- Core Concepts: Portfolios, Products, Constraints
- Creating Portfolios and Products
- Launch Constraints and Least-Privilege IAM
- Notification Constraints and Approval Workflows
- TagOptions: Enforcing Mandatory Tags
- Cross-Account Portfolio Sharing via Organizations
- Service Catalog AppRegistry
- End-User Self-Service Experience
- Governance, Compliance, and Audit
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) |
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.
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"
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'})
)
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
}
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
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.
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
- 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.
AWS Articles
Key Concepts
- Portfolio — groups products + access
- Product — versioned CloudFormation template
- Launch Constraint — provisioning IAM role
- TagOptions — enforced mandatory tags
- AppRegistry — application resource grouping