How to Build Self-Service Provisioning with AWS Service Catalog

February 03, 2027 · 16 min read

Solutions Architect Pro · SAP-C02 · part of The Exam Room

The situation

The platform team handles roughly fifteen “spin me up a database” tickets per week. Half are RDS, a third are DynamoDB, the rest are Redis or OpenSearch. Each request goes through the same dance:

  • Product team writes a Slack message.
  • Platform engineer asks three clarifying questions.
  • Engineer opens Terraform, copies a module, adjusts parameters, opens a PR.
  • Review, merge, apply.
  • Two days elapsed for what could be fifteen minutes.

The team wants to replace the dance with a portal. Product fills out a form; the portal provisions the resource with the right guardrails; audit gets a trail of who requested what; platform stops being a bottleneck.

Before we commit to any one tool, we need to walk the pieces, understand what a “self-service portal for AWS resources” needs to do, and design an organisation-wide deployment that scales across twenty accounts.

What actually matters

The core trade in self-service portals is constraint in exchange for speed. The team wants to go fast, so the portal must be fast. Security wants control, so the portal must constrain. The art is making the constrained path obviously easier than going around it, so easy that the terraform-first engineers choose the portal voluntarily because it saves them time.

The first thing to ask is: what’s a “product”? A launchable unit isn’t a generic template; it’s an opinionated one. “A database” is not a product until someone specifies the instance class options, the storage sizes, the engine versions, the subnet group, the backup plan, the encryption key, the parameter group, the security group rules. A product is what an opinionated template becomes once the choices have been narrowed.

The second is: who launches the product, under whose permissions? If the end user (a developer) launches the product with their own IAM permissions, they need IAM to create the underlying resources, which is exactly what we’re trying to centralise. The portal has to be able to run the launch under a privileged role so the developer only needs permission to launch products, not to create the resources directly.

The third is: how do constraints get encoded? Several governance questions land here: which role does the launch, which parameter values are allowed, what tags are automatically applied, what can be changed after launch. Each is a different control surface, and the portal has to support all of them or push the gaps elsewhere.

The fourth is: how does the catalogue reach users in different accounts? Twenty accounts means twenty consumers. Either the portal distributes itself across accounts natively, or we end up maintaining a per-account copy. Native cross-account distribution, ideally through Organizations, is the difference between a portal that scales and a portal that becomes its own bottleneck.

The fifth is: how is this maintained? Templates are versioned artefacts. New versions need to publish through a pipeline; existing provisioned resources need an update path. The source of truth has to be Git, not a console upload.

What we’ll filter on

  1. Constraint enforcement, can the portal prevent the user from picking a bad configuration?
  2. IAM delegation, does launching need user-level IAM on the underlying service?
  3. Cross-account distribution, how do products reach multiple accounts?
  4. Product lifecycle, how are new versions rolled out and old ones deprecated?
  5. Integration with existing IaC, does the team keep writing CloudFormation, or can they use Terraform/CDK?

The self-service landscape

  1. AWS Service Catalog with a shared Portfolio. Portfolio in the platform account, products authored as CloudFormation templates in Git, published to Service Catalog via a CI pipeline. Portfolio shared to the org root (or specific OUs) via Organizations integration. End users in member accounts see the portfolio in their console and launch products. Launch constraints specify a privileged role in the member account that executes the CloudFormation.

  2. AWS Service Catalog with AppRegistry and Portfolios. Same core, with AppRegistry tagging launched products into “applications” for visibility. Useful for large portfolios; optional for a small one.

  3. Terraform Cloud / Terraform Enterprise / Atlantis. Self-service via Terraform modules in a registry, launched via a web UI. Works well if the team is already Terraform-shop. Doesn’t integrate as cleanly with AWS’s account model; the provisioning runs from a CI system rather than inside AWS Organizations.

  4. AWS Proton. A platform-team product for defining environment and service templates, with a pipeline that deploys them. More opinionated than Service Catalog, more rigid; aimed at full application lifecycles rather than “provision an RDS.” Often overkill for the immediate problem.

  5. Custom ServiceNow / Jira Service Desk integration. The “enterprise workflow tool” answer: a form in ServiceNow, a workflow that calls the platform team, who run Terraform. Works, but still has the platform team in the loop, it’s ticket automation, not self-service.

  6. An internal developer portal (Backstage, Cortex). A higher layer that can aggregate Service Catalog, Terraform, documentation, and templates into one developer-facing UI. Wraps other tools rather than replacing them. Answers the “portal experience” question on top of whichever provisioning tool sits underneath.

Side by side

Option Constraints IAM delegation Cross-account Lifecycle IaC fit
Service Catalog + org sharing Launch/template/tag Via launch constraint Share to OU Versioned products CloudFormation
Service Catalog + AppRegistry Same + registry Same Same Same + app tagging CloudFormation
Terraform Cloud In the module Via workspace role Per workspace Module versions Terraform
AWS Proton Environment + service Via Proton role Per account Template versions CloudFormation
ServiceNow + Terraform In the workflow Via platform team Wherever platform has creds Workflow versions Whatever
Backstage + anything Whatever wraps Whatever wraps Whatever wraps Whatever wraps Whatever

For an AWS-native organisation with a CloudFormation-or-CDK codebase, Service Catalog with Organizations sharing is the fit. For a Terraform-shop, Terraform Cloud is usually a better story. We’re an AWS-native shop; Service Catalog it is.

The portfolio shape

Git: platform/service-catalog products/rds/template.yaml parameters, tag constraints products/dynamodb/template.yaml key schema, capacity mode pipeline.yaml (CodePipeline) Platform account (portfolio owner) Portfolio: StandardStatefulResources principals: ProductDeveloperRole RDS MySQL v1, v2, v3 (current) launch constraint: RDSProvisioner DynamoDB v1, v2 (current) launch constraint: DDBProvisioner ElastiCache Redis v1 (current) template constraint: instance-class in [m6g.large, m6g.xlarge] publish Org Shared portfolio shared to: ou-Production/* via AWS Organizations StackSets deploy: - ProductDeveloperRole - RDSProvisioner role - DDBProvisioner role ...in each member account ou-Production (member accounts) acct: payments-prod imported portfolio visible ProductDeveloperRole assumed launches RDS (v3) CFN stack created run by RDSProvisioner role (launch constraint) acct: ledger-prod imported portfolio visible ProductDeveloperRole assumed launches DynamoDB (v2) CFN stack created run by DDBProvisioner role acct: customer-prod imported portfolio visible multiple provisioned products RDS + DDB + Redis New account joins Production OU StackSet deploys roles portfolio appears zero manual steps
Products published from Git into the platform account's portfolio; portfolio shared to the Production OU; each member account launches products under its own privileged provisioner roles.

The picks in depth

The Portfolio and its products. One portfolio, StandardStatefulResources, in the platform account. Three products to start:

  • RDS MySQL, parameters for instance class (dropdown from a curated list), storage size (slider 20-2000GB), database name, and the Environment tag (dropdown from prod, staging, dev). The template creates an RDS instance in a subnet group provided by the platform, with a KMS key in the member account, attached to a security group template that only allows inbound from a named application SG, Performance Insights enabled, automated backups, multi-AZ if Environment=prod.
  • DynamoDB table, parameters for partition key, sort key (optional), capacity mode (on-demand or provisioned with min/max), global secondary indexes (up to 3). Template creates the table with encryption at rest, point-in-time recovery enabled, contributor-insights enabled, and the Backup=daily-prod tag if Environment=prod.
  • ElastiCache Redis, instance class, number of nodes, version. Template creates a replication group in the platform’s subnet group with encryption at rest and in transit, automatic failover for multi-node, daily snapshots.

Each product is a CloudFormation template in Git. A new version is “a new commit that passes CI”; the pipeline publishes it to the portfolio as a new version number. Existing provisioned products can be updated to a new version via the portal, with change-review if the template declares a stack update is disruptive.

Launch constraints. Each product has a launch constraint naming a role in the member account: RDSProvisioner for the RDS product, DDBProvisioner for DynamoDB, etc. When a developer launches an RDS product, Service Catalog runs the CloudFormation under RDSProvisioner, not under the developer’s IAM principal. RDSProvisioner has the narrow permissions needed to create exactly what the RDS template creates, nothing more.

This is the delegation pattern. The developer needs servicecatalog:LaunchProduct and some resource-filtering permissions; they do not need rds:CreateDBInstance. Security-team concerns about “who can launch an RDS” are answered by “whoever the portfolio lets launch the product, via a role that can only do the template.”

Template constraints. Not every parameter should be user-choice. A template constraint is JSON Schema-style: instance_class must be one of a whitelist; storage must be between 20 and 2000; multi_az must be true if Environment is prod. Template constraints are enforced by the portal before the CloudFormation runs, the user sees validation errors, not CloudFormation failures.

Combined with the template’s own logic (conditionals, Fn::If, etc.), template constraints ensure the launched resource matches the security baseline. A developer cannot launch an RDS with public accessibility enabled, because the template doesn’t expose that parameter.

Tag constraints. Every product launches with a baseline tag set: Environment, Owner, CostCentre, Backup, CreatedBy. The CreatedBy tag is populated with aws:username automatically. Tag constraints on the portfolio ensure every launched resource gets these tags, which feeds the backup policies (see the backup post), the cost-allocation reports, and the auditor’s queries.

Organizations-level sharing. In the platform account, AWSOrganizationsPortfolioShare on the portfolio, targeting the Production OU. Every account in Production sees the portfolio in its console without needing to accept a share invitation. Adding an account to the OU grants it access automatically; removing it revokes.

StackSets for the supporting roles. ProductDeveloperRole, RDSProvisioner, DDBProvisioner, CacheProvisioner, these must exist in every member account for launches to work. A StackSet deployed to the Production OU creates them. New accounts joining the OU get the roles automatically (service-managed permissions + automatic deployment on account-added trigger).

The developer experience. A developer in payments-prod opens the AWS console, navigates to Service Catalog, sees the three products. Clicks RDS MySQL, fills in the form, clicks Launch. Service Catalog shows a progress page; under the covers, CloudFormation runs under RDSProvisioner. Two minutes later (a minute of CloudFormation + a minute of RDS provisioning), the developer has a fully tagged, fully encrypted, fully backup-registered RDS. Time from Slack message to usable database: fifteen minutes.

A worked update: product v3

The security team discovers that the RDS product v2 allows storage auto-scaling to 500GB when it should cap at 200GB. Fix:

  1. Developer on the platform team opens a PR in platform/service-catalog updating the template constraint for the storage parameter.
  2. CI validates, runs tests, reviewer approves, merge.
  3. Pipeline publishes the new template as RDS MySQL v3 in the portfolio.
  4. Existing provisioned products still run on v2, they don’t auto-update. Teams that want the new constraint choose “update” in the portal and select v3.
  5. After 60 days, v2 is marked deprecated; new launches must use v3. Six months later, v2 is deleted; the remaining provisioned products are migrated or the teams contacted.

This is versioned, reviewable, rollback-able product management. The portal isn’t a one-shot “deploy a thing”; it’s a lifecycle for the building blocks.

A worked launch trace

A developer in customer-prod launches an RDS MySQL v3:

  1. The developer has assumed ProductDeveloperRole in customer-prod. Her SSO session allows this role.
  2. Navigates to Service Catalog, selects the portfolio, selects RDS MySQL, version 3.
  3. Fills in parameters: DBName=customer_profile, InstanceClass=db.m6g.large, Environment=prod.
  4. Clicks Launch. Service Catalog validates parameters against template constraints. Pass.
  5. Service Catalog assumes RDSProvisioner role in customer-prod (launch constraint). Role has rds:CreateDBInstance, kms:CreateKey, ec2:CreateSecurityGroup and a few others, scoped to the VPC the developer’s account uses.
  6. CloudFormation stack runs under RDSProvisioner. Creates KMS key, creates SG (referencing the application SG by a well-known name), creates RDS. Tags applied per tag constraint.
  7. Stack completes. Service Catalog marks the provisioned product as available. The developer sees the endpoint, uses Secrets Manager (the template created a Secrets Manager secret too) for the password, connects.

CloudTrail in customer-prod records: servicecatalog:ProvisionProduct by the developer’s principal; sts:AssumeRole for RDSProvisioner; rds:CreateDBInstance by RDSProvisioner. The audit question “who created this RDS” has a clear answer: the developer requested it via the portal; the platform-sanctioned role did the actual work.

What’s worth remembering

  1. A product is an opinionated template, not a generic one. The value is in what you constrained, not in what you offered.
  2. Launch constraints separate “who can request” from “what IAM does the work”. Developers need servicecatalog:LaunchProduct, not rds:CreateDBInstance. The provisioner role is the IAM boundary.
  3. Template constraints enforce parameter rules before CloudFormation runs. Cheaper than failing mid-stack; better feedback for the user.
  4. Tag constraints put governance tags on every launched resource. Backup policy, cost allocation, ownership, all derived from tags applied at launch.
  5. Organizations sharing is the distribution channel. One portfolio, shared to an OU, appears in every member account. New accounts inherit; removed accounts lose access.
  6. StackSets deploy the supporting IAM. The portfolio is nothing without the provisioner roles. StackSets to the same OU keeps them in lockstep.
  7. Products are versioned. New versions don’t disrupt existing deployments; teams opt in. Deprecation is a lifecycle, not an event.
  8. The portal has to be easier than the workaround. If using the portal takes longer than filing a Terraform PR, engineers will file Terraform PRs. Measure time-to-database and tune until the portal wins.

Service Catalog isn’t magic, it’s a pattern for publishing constrained IaC to the organisation. Done well, it’s the path everyone takes because it’s the fastest path. Done badly, it’s a second IaC tool nobody uses. The difference is whether the products match real requests, the constraints match real policy, and the distribution reaches the teams that need them.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.