Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 110 additions & 5 deletions docs/resources/app.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ description: |-

Provides a Heroku App resource. This can be used to create and manage applications on Heroku.

The Heroku platform supports two generations:
- **Cedar** (default): Traditional platform with support for buildpacks, stack configuration, and internal routing
- **Fir**: Next-generation platform with Cloud Native Buildpacks (CNB), enhanced security, and modern containerization

-> **Always reference apps by ID (UUID) in Terraform configuration**
Starting with v5.0 of this provider, all HCL app references are by ID. Read more details in [Upgrading](guides/upgrading.html).

## Example Usage

### Cedar Generation (Default)
```hcl-terraform
resource "heroku_app" "default" {
name = "my-cool-app"
resource "heroku_app" "cedar_app" {
name = "my-cedar-app"
region = "us"
config_vars = {
Expand All @@ -27,6 +32,37 @@ resource "heroku_app" "default" {
buildpacks = [
"heroku/go"
]
stack = "heroku-22"
}
```

### Fir Generation (via Fir Space)
```hcl-terraform
# Create a Fir generation space first
resource "heroku_space" "fir_space" {
name = "my-fir-space"
organization = "my-org"
region = "virginia"
generation = "fir"
}
# Apps deployed to Fir spaces automatically use Fir generation
resource "heroku_app" "fir_app" {
name = "my-fir-app"
region = "virginia"
space = heroku_space.fir_space.name
organization {
name = "my-org"
}
config_vars = {
FOOBAR = "baz"
}
# Note: buildpacks and stack are not supported for Fir generation
# Use project.toml in your application code instead
}
```

Expand All @@ -52,9 +88,12 @@ The following arguments are supported:
* `name` - (Required) The name of the application. In Heroku, this is also the
unique ID, so it must be unique and have a minimum of 3 characters.
* `region` - (Required) The region that the app should be deployed in.
* `stack` - (Optional) The application stack is what platform to run the application in.
* `generation` - (Computed) Generation of the app platform. Automatically determined based on the space the app is deployed to. Apps in Fir generation spaces will be `fir`, all other apps will be `cedar`.
- `cedar`: Traditional platform supporting buildpacks, stack configuration, and internal routing
- `fir`: Next-generation platform with Cloud Native Buildpacks (CNB). Does not support `buildpacks`, `stack`, or `internal_routing` fields
* `stack` - (Optional) The application stack is what platform to run the application in. **Note**: Not supported for `fir` generation apps.
* `buildpacks` - (Optional) Buildpack names or URLs for the application.
Buildpacks configured externally won't be altered if this is not present.
Buildpacks configured externally won't be altered if this is not present. **Note**: Not supported for `fir` generation apps. Use project.toml for Cloud Native Buildpacks configuration instead.
* `config_vars`<sup>[1](#deleting-vars)</sup> - (Optional) Configuration variables for the application.
The config variables in this map are not the final set of configuration
variables, but rather variables you want present. That is, other
Expand All @@ -68,7 +107,7 @@ The following arguments are supported:
* `space` - (Optional) The name of a private space to create the app in.
* `internal_routing` - (Optional) If true, the application will be routable
only internally in a private space. This option is only available for apps
that also specify `space`.
that also specify `space`. **Note**: Not supported for `fir` generation apps.
* `organization` - (Optional) A block that can be specified once to define
Heroku Team settings for this app. The fields for this block are
documented below.
Expand Down Expand Up @@ -96,6 +135,7 @@ The following attributes are exported:

* `id` - The ID (UUID) of the app.
* `name` - The name of the app.
* `generation` - Generation of the app platform (cedar or fir). Automatically determined from the space the app is deployed to.
* `stack` - The application stack is what platform to run the application in.
* `space` - The private space the app should run in.
* `internal_routing` - Whether internal routing is enabled the private space app.
Expand All @@ -113,6 +153,71 @@ The following attributes are exported:
attribute `set_app_all_config_vars_in_state` is `false`.
* `uuid` - The unique UUID of the Heroku app. **NOTE:** Use this for `null_resource` triggers.

## Cloud Native Buildpacks (Fir Generation)

When apps are deployed to Fir generation spaces, they automatically use Cloud Native Buildpacks (CNB) instead of traditional Heroku buildpacks. This requires different configuration approaches:

### project.toml Configuration

Instead of specifying `buildpacks` in Terraform, create a `project.toml` file in your application root:

```toml
[build]
[[build.buildpacks]]
id = "heroku/nodejs"

[[build.buildpacks]]
id = "heroku/procfile"

[build.env]
BP_NODE_VERSION = "18.*"
```

### Migration from Cedar to Fir

When migrating from Cedar to Fir generation:

1. **Create Fir space**: Create a new space with `generation = "fir"`
2. **Remove incompatible fields**: Remove `buildpacks`, `stack`, and `internal_routing` from your Terraform configuration
3. **Add project.toml**: Create a `project.toml` file in your application code with buildpack configuration
4. **Update app space**: Change your app's `space` to use the Fir space
5. **Redeploy**: Deploy your application with the new configuration

```hcl-terraform
# Before (Cedar)
resource "heroku_space" "cedar_space" {
name = "my-space"
organization = "my-org"
region = "virginia"
}
resource "heroku_app" "example" {
name = "my-app"
region = "virginia"
space = heroku_space.cedar_space.name
buildpacks = ["heroku/nodejs"]
stack = "heroku-22"
}
# After (Fir)
resource "heroku_space" "fir_space" {
name = "my-space-fir"
organization = "my-org"
region = "virginia"
generation = "fir"
}
resource "heroku_app" "example" {
name = "my-app"
region = "virginia"
space = heroku_space.fir_space.name
# buildpacks and stack removed - configured via project.toml
# generation is automatically "fir" from the space
}
```

## Import

Apps can be imported using an existing app's `UUID` or name.
Expand Down
18 changes: 18 additions & 0 deletions heroku/heroku_supported_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ var featureMatrix = map[string]map[string]map[string]bool{
"inbound_ruleset": true, // Cedar supports inbound rulesets
"peering_connection": true, // Cedar supports IPv4 peering
},
"app": {
"buildpacks": true, // Cedar supports traditional buildpacks
"stack": true, // Cedar supports stack configuration
"internal_routing": true, // Cedar supports internal routing
"cloud_native_buildpacks": false, // Cedar doesn't use CNB by default
},
"drain": {
"app_log_drains": true, // Cedar supports traditional log drains
},
},
"fir": {
"space": {
Expand All @@ -34,6 +43,15 @@ var featureMatrix = map[string]map[string]map[string]bool{
"inbound_ruleset": false, // Inbound rulesets not supported
"peering_connection": false, // IPv4 peering not supported
},
"app": {
"buildpacks": false, // Fir doesn't support traditional buildpacks
"stack": false, // Fir doesn't use traditional stack config
"internal_routing": false, // Fir doesn't support internal routing
"cloud_native_buildpacks": true, // Fir uses CNB exclusively
},
"drain": {
"app_log_drains": false, // Fir apps don't support traditional log drains
},
},
}

Expand Down
54 changes: 52 additions & 2 deletions heroku/heroku_supported_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,63 @@ func TestIsFeatureSupported(t *testing.T) {
},

// Unknown resource type tests
// App feature tests
{
name: "Cedar app features should be unsupported (not implemented yet)",
name: "Cedar app buildpacks should be supported",
generation: "cedar",
resourceType: "app",
feature: "some_feature",
feature: "buildpacks",
expected: true,
},
{
name: "Cedar app stack should be supported",
generation: "cedar",
resourceType: "app",
feature: "stack",
expected: true,
},
{
name: "Cedar app internal_routing should be supported",
generation: "cedar",
resourceType: "app",
feature: "internal_routing",
expected: true,
},
{
name: "Cedar app cloud_native_buildpacks should be unsupported",
generation: "cedar",
resourceType: "app",
feature: "cloud_native_buildpacks",
expected: false,
},
{
name: "Fir app buildpacks should be unsupported",
generation: "fir",
resourceType: "app",
feature: "buildpacks",
expected: false,
},
{
name: "Fir app stack should be unsupported",
generation: "fir",
resourceType: "app",
feature: "stack",
expected: false,
},
{
name: "Fir app internal_routing should be unsupported",
generation: "fir",
resourceType: "app",
feature: "internal_routing",
expected: false,
},
{
name: "Fir app cloud_native_buildpacks should be supported",
generation: "fir",
resourceType: "app",
feature: "cloud_native_buildpacks",
expected: true,
},
{
name: "Fir build features should be unsupported (not implemented yet)",
generation: "fir",
Expand Down
59 changes: 51 additions & 8 deletions heroku/resource_heroku_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,17 @@ type application struct {
Vars map[string]string // Represents all vars on a heroku app.
Buildpacks []string // The application's buildpack names or URLs
IsTeamApp bool // Is the application a team (organization) app
Generation string // The generation of the app platform (cedar/fir)
}

func resourceHerokuApp() *schema.Resource {
return &schema.Resource{
Create: switchHerokuAppCreate,
Read: resourceHerokuAppRead,
Update: resourceHerokuAppUpdate,
Delete: resourceHerokuAppDelete,
Exists: resourceHerokuAppExists,
Create: switchHerokuAppCreate,
Read: resourceHerokuAppRead,
Update: resourceHerokuAppUpdate,
Delete: resourceHerokuAppDelete,
Exists: resourceHerokuAppExists,
CustomizeDiff: resourceHerokuAppCustomizeDiff,

Importer: &schema.ResourceImporter{
State: resourceHerokuAppImport,
Expand All @@ -77,6 +79,12 @@ func resourceHerokuApp() *schema.Resource {
ForceNew: true,
},

"generation": {
Type: schema.TypeString,
Computed: true,
Description: "Generation of the app platform. Determined by the space the app is deployed to.",
},

"stack": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -441,6 +449,9 @@ func resourceHerokuAppRead(d *schema.ResourceData, meta interface{}) error {
return buildpacksErr
}

// Set generation based on app characteristics
d.Set("generation", app.Generation)

if app.IsTeamApp {
orgErr := setTeamDetails(d, app)
if orgErr != nil {
Expand Down Expand Up @@ -630,6 +641,14 @@ func (a *application) Update() error {
a.App.Space = app.Space.Name
}

// Determine generation from app's generation field (available in AppInfo response)
if app.Generation.Name != "" {
a.Generation = app.Generation.Name
} else {
// Default to cedar if generation is not specified
a.Generation = "cedar"
}

// If app is a team/org app, define additional values.
if app.Organization != nil && app.Team != nil {
// Set to true to control additional state actions downstream
Expand All @@ -648,9 +667,17 @@ func (a *application) Update() error {

var errs []error
var err error
a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client)
if err != nil {
errs = append(errs, err)

// Only retrieve buildpacks for apps that support traditional buildpacks
if IsFeatureSupported(a.Generation, "app", "buildpacks") {
a.Buildpacks, err = retrieveBuildpacks(a.Id, a.Client)
if err != nil {
errs = append(errs, err)
}
} else {
// CNB apps don't have traditional buildpacks
log.Printf("[DEBUG] App %s uses generation %s which doesn't support traditional buildpacks", a.Id, a.Generation)
a.Buildpacks = []string{}
}

a.Vars, err = retrieveConfigVars(a.Id, a.Client)
Expand Down Expand Up @@ -685,6 +712,16 @@ func retrieveBuildpacks(id string, client *heroku.Service) ([]string, error) {
return buildpacks, nil
}

// isCNBError checks if an error is related to Cloud Native Buildpacks
func isCNBError(err error) bool {
if err == nil {
return false
}
errorMessage := err.Error()
return strings.Contains(errorMessage, "Cloud Native Buildpacks") ||
strings.Contains(errorMessage, "project.toml")
}

func retrieveAcm(id string, client *heroku.Service) (bool, error) {
result, err := client.AppInfo(context.TODO(), id)
if err != nil {
Expand Down Expand Up @@ -1034,3 +1071,9 @@ func resourceHerokuAppStateUpgradeV0(ctx context.Context, rawState map[string]in

return rawState, nil
}

func resourceHerokuAppCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error {
// Note: Generation is now computed based on the space, not user-configurable.
// Validation will happen during the apply phase when we can determine the actual generation.
return nil
}
Loading