Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This documentation describes a list of rules available by enabling this ruleset.

|Rule|Enabled by default|
| --- | --- |
|[azurerm_app_service_app_insights_hidden_link](rules/azurerm_app_service_app_insights_hidden_link.md)|✔|
|[azurerm_linux_virtual_machine_invalid_name](rules/azurerm_linux_virtual_machine_invalid_name.md)|✔|
|[azurerm_linux_virtual_machine_invalid_size](rules/azurerm_linux_virtual_machine_invalid_size.md)|✔|
|[azurerm_linux_virtual_machine_scale_set_invalid_sku](rules/azurerm_linux_virtual_machine_scale_set_invalid_sku.md)|✔|
Expand Down
127 changes: 127 additions & 0 deletions docs/rules/azurerm_app_service_app_insights_hidden_link.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# azurerm_app_service_app_insights_hidden_link

Disallow missing lifecycle ignore_changes for Application Insights hidden-link tags.

This rule applies to all Azure App Service resource types (Web Apps and Function Apps):
- `azurerm_linux_web_app`
- `azurerm_linux_web_app_slot`
- `azurerm_windows_web_app`
- `azurerm_windows_web_app_slot`
- `azurerm_linux_function_app`
- `azurerm_linux_function_app_slot`
- `azurerm_windows_function_app`
- `azurerm_windows_function_app_slot`

## Configuration

```hcl
rule "azurerm_app_service_app_insights_hidden_link" {
enabled = true
}
```

## Example

### Terraform Configuration

```hcl
# Non-compliant: Linux Web App with Application Insights configured without ignore_changes
resource "azurerm_linux_web_app" "example" {
app_settings = {
"APPLICATIONINSIGHTS_CONNECTION_STRING" = "example"
}
}

# Non-compliant: Windows Web App Slot with Application Insights configured without ignore_changes
resource "azurerm_windows_web_app_slot" "example" {
name = "example-slot"
app_service_id = azurerm_windows_web_app.example.id

app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = "example-key"
}
}

# Non-compliant: Windows Function App with Application Insights configured without ignore_changes
resource "azurerm_windows_function_app" "example" {
app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = "example-key"
}
}

# Non-compliant: Linux Function App with Application Insights in site_config without ignore_changes
resource "azurerm_linux_function_app" "example" {
site_config {
application_insights_connection_string = "example-connection"
}
}

# Compliant: Linux Function App with proper ignore_changes
resource "azurerm_linux_function_app" "example" {
app_settings = {
"APPLICATIONINSIGHTS_CONNECTION_STRING" = "example"
}

lifecycle {
ignore_changes = [
tags["hidden-link: /app-insights-conn-string"],
tags["hidden-link: /app-insights-instrumentation-key"],
tags["hidden-link: /app-insights-resource-id"],
]
}
}

# Compliant: Windows Web App Slot with proper ignore_changes
resource "azurerm_windows_web_app_slot" "example" {
app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = "example"
}

lifecycle {
ignore_changes = [
tags["hidden-link: /app-insights-conn-string"],
tags["hidden-link: /app-insights-instrumentation-key"],
tags["hidden-link: /app-insights-resource-id"],
]
}
}

# Compliant: Windows Function App with Application Insights in site_config and proper ignore_changes
resource "azurerm_windows_function_app" "example" {
site_config {
application_insights_key = "example-key"
}

lifecycle {
ignore_changes = [
"tags["hidden-link: /app-insights-conn-string"]",
"tags["hidden-link: /app-insights-instrumentation-key"]",
"tags["hidden-link: /app-insights-resource-id"]",
]
}
}
```

## Why

When Application Insights is configured for Azure App Service resources (Web Apps, Web App Slots, Function Apps, or Function App Slots for both Linux and Windows), Azure automatically adds hidden-link tags to the resource. These tags can change during deployments and may cause unnecessary Terraform diffs. Using `lifecycle { ignore_changes }` for these specific tags prevents Terraform from attempting to manage these Azure-managed tags.

Application Insights can be configured in two ways:
1. **app_settings**: Using `APPLICATIONINSIGHTS_CONNECTION_STRING` or `APPINSIGHTS_INSTRUMENTATIONKEY` keys
2. **site_config**: Using `application_insights_connection_string` or `application_insights_key` attributes (only applicable to Function Apps)

This rule checks for the presence of Application Insights configuration in both locations and ensures that the corresponding hidden-link tags are properly ignored.

## How to Fix

Add a `lifecycle` block with `ignore_changes` containing the Application Insights hidden-link tags:

```hcl
lifecycle {
ignore_changes = [
"tags["hidden-link: /app-insights-conn-string"]",
"tags["hidden-link: /app-insights-instrumentation-key"]",
"tags["hidden-link: /app-insights-resource-id"]",
]
}
```
229 changes: 229 additions & 0 deletions rules/azurerm_app_service_app_insights_hidden_link.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package rules

import (
"strings"

"github.com/hashicorp/hcl/v2"
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/logger"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/terraform-linters/tflint-ruleset-azurerm/project"
"github.com/zclconf/go-cty/cty"
)

// AzurermAppServiceAppInsightsHiddenLinkRule checks whether lifecycle ignore_changes includes hidden-link tags when Application Insights is configured
type AzurermAppServiceAppInsightsHiddenLinkRule struct {
tflint.DefaultRule
}

const (
appServiceIgnoreChangesAttrName = "ignore_changes"
appServiceAppSettingsAttrName = "app_settings"
appServiceSiteConfigAttrName = "site_config"
appServiceAppInsightsConnectionKey = "APPLICATIONINSIGHTS_CONNECTION_STRING"
appServiceAppInsightsInstrumentKey = "APPINSIGHTS_INSTRUMENTATIONKEY"
appServiceSiteConfigAppInsightsConnectionKey = "application_insights_connection_string"
appServiceSiteConfigAppInsightsKey = "application_insights_key"
)

var appServiceRequiredHiddenLinkTags = []string{
"hidden-link: /app-insights-conn-string",
"hidden-link: /app-insights-instrumentation-key",
"hidden-link: /app-insights-resource-id",
}

var appServiceResourceTypes = []string{
"azurerm_linux_web_app",
"azurerm_linux_web_app_slot",
"azurerm_windows_web_app",
"azurerm_windows_web_app_slot",
"azurerm_linux_function_app",
"azurerm_linux_function_app_slot",
"azurerm_windows_function_app",
"azurerm_windows_function_app_slot",
}

// NewAzurermAppServiceAppInsightsHiddenLinkRule returns new rule for checking Application Insights hidden-link configuration
func NewAzurermAppServiceAppInsightsHiddenLinkRule() *AzurermAppServiceAppInsightsHiddenLinkRule {
return &AzurermAppServiceAppInsightsHiddenLinkRule{}
}

// Name returns the rule name
func (r *AzurermAppServiceAppInsightsHiddenLinkRule) Name() string {
return "azurerm_app_service_app_insights_hidden_link"
}

// Enabled returns whether the rule is enabled by default
func (r *AzurermAppServiceAppInsightsHiddenLinkRule) Enabled() bool {
return true
}

// Severity returns the rule severity
func (r *AzurermAppServiceAppInsightsHiddenLinkRule) Severity() tflint.Severity {
return tflint.WARNING
}

// Link returns the rule reference link
func (r *AzurermAppServiceAppInsightsHiddenLinkRule) Link() string {
return project.ReferenceLink(r.Name())
}

// checkResourceType checks a specific resource type for Application Insights hidden-link configuration
func (r *AzurermAppServiceAppInsightsHiddenLinkRule) checkResourceType(runner tflint.Runner, resourceType string) error {
resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{Name: appServiceAppSettingsAttrName},
},
Blocks: []hclext.BlockSchema{
{
Type: "lifecycle",
Body: &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{Name: appServiceIgnoreChangesAttrName},
},
},
},
{
Type: "site_config",
Body: &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{Name: appServiceSiteConfigAppInsightsConnectionKey},
{Name: appServiceSiteConfigAppInsightsKey},
},
},
},
},
}, nil)

if err != nil {
return err
}

for _, resource := range resources.Blocks {
logger.Debug("checking", "resource type", resource.Labels[0], "resource name", resource.Labels[1])

// Check if Application Insights is configured
hasAppInsights := false

// Check in app_settings
if appSettingsAttr, exists := resource.Body.Attributes[appServiceAppSettingsAttrName]; exists {
err := runner.EvaluateExpr(appSettingsAttr.Expr, func(val map[string]string) error {
for key := range val {
if strings.EqualFold(key, appServiceAppInsightsConnectionKey) || strings.EqualFold(key, appServiceAppInsightsInstrumentKey) {
hasAppInsights = true
break
}
}
return nil
}, nil)

if err != nil {
return err
}
}

// Check in site_config block
if !hasAppInsights {
for _, block := range resource.Body.Blocks {
if block.Type == "site_config" {
// Check for application_insights_connection_string
if attr, exists := block.Body.Attributes[appServiceSiteConfigAppInsightsConnectionKey]; exists {
err := runner.EvaluateExpr(attr.Expr, func(val string) error {
if val != "" {
hasAppInsights = true
}
return nil
}, nil)
if err != nil {
return err
}
}

// Check for application_insights_key
if !hasAppInsights {
if attr, exists := block.Body.Attributes[appServiceSiteConfigAppInsightsKey]; exists {
err := runner.EvaluateExpr(attr.Expr, func(val string) error {
if val != "" {
hasAppInsights = true
}
return nil
}, nil)
if err != nil {
return err
}
}
}
break
}
}
}

// If Application Insights is not configured, skip this resource
if !hasAppInsights {
logger.Debug("no Application Insights configuration found", "resource type", resource.Labels[0], "resource name", resource.Labels[1])
continue
}

// Check if lifecycle block exists and contains proper ignore_changes
hasProperIgnoreChanges := false

for _, block := range resource.Body.Blocks {
if block.Type == "lifecycle" {
if ignoreChangesAttr, ok := block.Body.Attributes[appServiceIgnoreChangesAttrName]; ok {
// Parse the ignore_changes expression to find ignored tag keys
foundTags := 0
tagsIgnored := false
if exprs, diags := hcl.ExprList(ignoreChangesAttr.Expr); diags == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to consider the case where ignore_changes = all. We can use hcl.ExprAsKeyword.

for _, expr := range exprs {
if traversal, diags := hcl.AbsTraversalForExpr(expr); diags == nil {
if len(traversal) == 1 {
if root, ok := traversal[0].(hcl.TraverseRoot); ok && root.Name == "tags" {
tagsIgnored = true
break
}
} else if len(traversal) == 2 {
if root, ok := traversal[0].(hcl.TraverseRoot); ok && root.Name == "tags" {
if index, ok := traversal[1].(hcl.TraverseIndex); ok && index.Key.Type() == cty.String {
tagKey := index.Key.AsString()
for _, requiredTag := range appServiceRequiredHiddenLinkTags {
if tagKey == requiredTag {
foundTags++
break
}
}
}
}
}
}
}
}
if tagsIgnored || foundTags == len(appServiceRequiredHiddenLinkTags) {
hasProperIgnoreChanges = true
}
break
}
}
}

// Emit issue if Application Insights is configured but hidden-link tags are not properly ignored
if !hasProperIgnoreChanges {
issue := "When Application Insights is configured, lifecycle { ignore_changes } should include all hidden-link tags: tags[\"hidden-link: /app-insights-conn-string\"], tags[\"hidden-link: /app-insights-instrumentation-key\"], tags[\"hidden-link: /app-insights-resource-id\"]"
if err := runner.EmitIssue(r, issue, resource.DefRange); err != nil {
return err
}
}
}

return nil
}

// Check checks whether hidden-link tags are ignored when Application Insights is configured
func (r *AzurermAppServiceAppInsightsHiddenLinkRule) Check(runner tflint.Runner) error {
for _, resourceType := range appServiceResourceTypes {
if err := r.checkResourceType(runner, resourceType); err != nil {
return err
}
}

return nil
}
Loading