Terraform Sentinel 政策即程式碼

Terraform Sentinel Policy as Code Implementation

Sentinel 是 HashiCorp 開發的政策即程式碼(Policy as Code)框架,專為 Terraform Enterprise 和 Terraform Cloud 設計。透過 Sentinel,組織可以定義、部署和強制執行細緻的治理政策,確保基礎設施配置符合安全性、合規性和營運最佳實務。

Sentinel 政策即程式碼概述

什麼是 Sentinel?

Sentinel 是一種嵌入式政策即程式碼框架,允許組織在 Terraform 工作流程中實施精細的、基於邏輯的政策決策。與傳統的人工審核相比,Sentinel 提供自動化、可重複且一致的政策執行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
┌─────────────────────────────────────────────────────────────────┐
│                    Terraform 工作流程整合                          │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐  │
│  │   Code   │───▶│   Plan   │───▶│ Sentinel │───▶│  Apply   │  │
│  │  變更    │    │  產生    │    │  檢查    │    │  執行    │  │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘  │
│                                        │                        │
│                                        ▼                        │
│                                 ┌──────────────┐               │
│                                 │ Pass / Fail  │               │
│                                 └──────────────┘               │
└─────────────────────────────────────────────────────────────────┘

Sentinel 的核心價值

價值說明
合規自動化自動驗證基礎設施是否符合法規和內部政策
安全強制執行防止不安全的配置被部署
成本控制限制昂貴資源的使用和規格
標準化確保資源命名、標籤和配置的一致性
審計追蹤提供政策執行的完整記錄

Sentinel 與其他政策工具比較

特性SentinelOPA/RegoAWS ConfigAzure Policy
整合深度原生整合 Terraform需額外設定AWS 限定Azure 限定
執行時機Plan 階段彈性部署後部署時
學習曲線中等較陡
跨雲支援完整完整
測試框架內建內建有限

Sentinel 語言基礎語法

基本結構

Sentinel 是一種專門設計的程式語言,語法類似於 Python 和 Go。每個 Sentinel 政策包含 imports、規則和主要規則。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 匯入模組
import "tfplan/v2" as tfplan
import "strings"

# 定義輔助函數
get_resources = func(type) {
    return filter tfplan.resource_changes as _, rc {
        rc.type is type and
        rc.mode is "managed" and
        (rc.change.actions contains "create" or rc.change.actions contains "update")
    }
}

# 定義規則
required_tags_rule = rule {
    all get_resources("aws_instance") as _, instance {
        instance.change.after.tags contains "Environment" and
        instance.change.after.tags contains "Owner"
    }
}

# 主要規則(必須)
main = rule {
    required_tags_rule
}

資料類型

Sentinel 支援多種基本資料類型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 字串
name = "production"

# 數字
count = 42
price = 19.99

# 布林值
enabled = true
disabled = false

# 列表(List)
regions = ["ap-northeast-1", "us-west-2", "eu-west-1"]

# 映射(Map)
tags = {
    "Environment": "production",
    "Team": "platform",
    "CostCenter": "12345",
}

# Null
empty_value = null

# Undefined
# 使用 is undefined 來檢查

運算子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 比較運算子
x == y      # 等於
x != y      # 不等於
x < y       # 小於
x <= y      # 小於等於
x > y       # 大於
x >= y      # 大於等於

# 邏輯運算子
a and b     # 且
a or b      # 或
not a       # 非

# 成員運算子
"key" in map            # 鍵存在於映射中
value in list           # 值存在於列表中
map contains "key"      # 映射包含鍵

# 類型檢查運算子
value is "string"       # 類型檢查
value is not undefined  # 非 undefined

控制結構

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# if-else 語句
check_environment = func(env) {
    if env == "production" {
        return true
    } else if env == "staging" {
        return true
    } else {
        return false
    }
}

# for 迴圈
validate_all_tags = func(resources) {
    for resources as resource {
        if resource.tags is undefined {
            return false
        }
    }
    return true
}

# 量詞表達式
# all - 所有元素必須滿足條件
all_have_tags = rule {
    all tfplan.resource_changes as _, rc {
        rc.change.after.tags is not undefined
    }
}

# any - 至少一個元素滿足條件
has_production = rule {
    any tfplan.resource_changes as _, rc {
        rc.change.after.tags["Environment"] == "production"
    }
}

# filter - 過濾符合條件的元素
production_instances = filter tfplan.resource_changes as _, rc {
    rc.type is "aws_instance" and
    rc.change.after.tags["Environment"] == "production"
}

函數定義

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 基本函數
get_instance_type = func(instance) {
    return instance.change.after.instance_type
}

# 帶有多個參數的函數
is_allowed_type = func(instance_type, allowed_types) {
    return instance_type in allowed_types
}

# 使用函數
allowed_types = ["t3.micro", "t3.small", "t3.medium"]

check_instance_types = rule {
    all get_resources("aws_instance") as _, instance {
        is_allowed_type(get_instance_type(instance), allowed_types)
    }
}

政策設定與組織

政策集(Policy Sets)結構

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
sentinel-policies/
├── sentinel.hcl           # 政策設定檔
├── policies/
│   ├── require-tags.sentinel
│   ├── restrict-instance-types.sentinel
│   ├── enforce-encryption.sentinel
│   └── cost-estimation.sentinel
├── modules/
│   ├── tfplan-functions/
│   │   └── tfplan-functions.sentinel
│   └── aws-functions/
│       └── aws-functions.sentinel
└── test/
    ├── require-tags/
    │   ├── pass.hcl
    │   ├── fail.hcl
    │   └── mock-tfplan-pass.sentinel
    └── restrict-instance-types/
        ├── pass.hcl
        └── fail.hcl

sentinel.hcl 設定檔

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# sentinel.hcl - 政策集設定檔

# 定義政策
policy "require-tags" {
    source            = "./policies/require-tags.sentinel"
    enforcement_level = "hard-mandatory"
}

policy "restrict-instance-types" {
    source            = "./policies/restrict-instance-types.sentinel"
    enforcement_level = "soft-mandatory"
}

policy "enforce-encryption" {
    source            = "./policies/enforce-encryption.sentinel"
    enforcement_level = "hard-mandatory"
}

policy "cost-estimation" {
    source            = "./policies/cost-estimation.sentinel"
    enforcement_level = "advisory"
}

# 定義模組
module "tfplan-functions" {
    source = "./modules/tfplan-functions/tfplan-functions.sentinel"
}

module "aws-functions" {
    source = "./modules/aws-functions/aws-functions.sentinel"
}

政策參數化

使用參數讓政策更具彈性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# sentinel.hcl
policy "restrict-instance-types" {
    source            = "./policies/restrict-instance-types.sentinel"
    enforcement_level = "soft-mandatory"

    params = {
        allowed_types = ["t3.micro", "t3.small", "t3.medium", "t3.large"]
        max_count     = 10
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# policies/restrict-instance-types.sentinel
import "tfplan/v2" as tfplan

# 參數宣告
param allowed_types default ["t3.micro", "t3.small"]
param max_count default 5

# 使用參數
validate_instance_type = rule {
    all tfplan.resource_changes as _, rc {
        rc.type is "aws_instance" and
        rc.change.actions contains "create" implies
        rc.change.after.instance_type in allowed_types
    }
}

main = rule {
    validate_instance_type
}

常見政策範例

強制標籤政策

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# policies/require-tags.sentinel
# 確保所有 AWS 資源都具有必要的標籤

import "tfplan/v2" as tfplan
import "strings"

# 必要標籤列表
param mandatory_tags default ["Environment", "Owner", "Project", "CostCenter"]

# 支援標籤的 AWS 資源類型
taggable_resources = [
    "aws_instance",
    "aws_s3_bucket",
    "aws_rds_cluster",
    "aws_lambda_function",
    "aws_ecs_cluster",
    "aws_eks_cluster",
    "aws_vpc",
    "aws_subnet",
    "aws_security_group",
    "aws_lb",
]

# 取得所有新建或更新的可標籤資源
get_taggable_resources = func() {
    return filter tfplan.resource_changes as _, rc {
        rc.type in taggable_resources and
        rc.mode is "managed" and
        (rc.change.actions contains "create" or rc.change.actions contains "update")
    }
}

# 檢查資源是否具有所有必要標籤
has_required_tags = func(resource) {
    tags = resource.change.after.tags else {}

    if tags is null {
        return false
    }

    for mandatory_tags as tag {
        if not (tag in tags) {
            print("Resource", resource.address, "is missing required tag:", tag)
            return false
        }
        if tags[tag] is "" or tags[tag] is null {
            print("Resource", resource.address, "has empty value for tag:", tag)
            return false
        }
    }
    return true
}

# 主要規則
main = rule {
    all get_taggable_resources() as _, resource {
        has_required_tags(resource)
    }
}

限制 EC2 執行個體類型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# policies/restrict-instance-types.sentinel
# 限制允許使用的 EC2 執行個體類型

import "tfplan/v2" as tfplan

# 允許的執行個體類型(依環境區分)
param allowed_types_production default [
    "t3.large",
    "t3.xlarge",
    "m5.large",
    "m5.xlarge",
    "r5.large",
]

param allowed_types_development default [
    "t3.micro",
    "t3.small",
    "t3.medium",
]

# 禁止使用的執行個體類型(過於昂貴)
param forbidden_types default [
    "p3.16xlarge",
    "p4d.24xlarge",
    "x1e.32xlarge",
    "u-24tb1.metal",
]

# 取得所有 EC2 執行個體
get_ec2_instances = func() {
    return filter tfplan.resource_changes as _, rc {
        rc.type is "aws_instance" and
        rc.mode is "managed" and
        (rc.change.actions contains "create" or rc.change.actions contains "update")
    }
}

# 檢查執行個體類型
validate_instance_type = func(instance) {
    instance_type = instance.change.after.instance_type
    tags = instance.change.after.tags else {}
    environment = tags["Environment"] else "development"

    # 禁止使用的類型
    if instance_type in forbidden_types {
        print("Instance", instance.address, "uses forbidden type:", instance_type)
        return false
    }

    # 依環境檢查允許的類型
    if environment == "production" {
        if not (instance_type in allowed_types_production) {
            print("Production instance", instance.address, "uses non-approved type:", instance_type)
            return false
        }
    } else {
        if not (instance_type in allowed_types_development) {
            print("Development instance", instance.address, "uses non-approved type:", instance_type)
            return false
        }
    }

    return true
}

main = rule {
    all get_ec2_instances() as _, instance {
        validate_instance_type(instance)
    }
}

強制加密政策

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# policies/enforce-encryption.sentinel
# 確保所有儲存資源都啟用加密

import "tfplan/v2" as tfplan

# 檢查 S3 儲存桶加密
s3_encryption = rule {
    all filter tfplan.resource_changes as _, rc {
        rc.type is "aws_s3_bucket" and
        rc.change.actions contains "create"
    } as _, bucket {
        # S3 儲存桶需要有對應的加密設定資源
        print("Checking S3 bucket:", bucket.address)
        true  # 需配合 aws_s3_bucket_server_side_encryption_configuration
    }
}

# 檢查 EBS 卷加密
ebs_encryption = rule {
    all filter tfplan.resource_changes as _, rc {
        rc.type is "aws_ebs_volume" and
        rc.change.actions contains "create"
    } as _, volume {
        volume.change.after.encrypted is true else false
    }
}

# 檢查 RDS 執行個體加密
rds_encryption = rule {
    all filter tfplan.resource_changes as _, rc {
        rc.type is "aws_db_instance" and
        rc.change.actions contains "create"
    } as _, db {
        encrypted = db.change.after.storage_encrypted else false
        if not encrypted {
            print("RDS instance", db.address, "is not encrypted")
        }
        encrypted
    }
}

# 檢查 EC2 執行個體根磁碟區加密
ec2_root_encryption = rule {
    all filter tfplan.resource_changes as _, rc {
        rc.type is "aws_instance" and
        rc.change.actions contains "create"
    } as _, instance {
        root_block_device = instance.change.after.root_block_device else []
        all root_block_device as _, device {
            device.encrypted is true else false
        }
    }
}

# 主要規則
main = rule {
    s3_encryption and
    ebs_encryption and
    rds_encryption and
    ec2_root_encryption
}

網路安全政策

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# policies/network-security.sentinel
# 確保安全群組不允許來自 0.0.0.0/0 的危險連接埠存取

import "tfplan/v2" as tfplan
import "strings"

# 禁止對外開放的危險連接埠
param restricted_ports default [22, 3389, 3306, 5432, 27017, 6379]

# 取得安全群組規則
get_security_group_rules = func() {
    return filter tfplan.resource_changes as _, rc {
        rc.type is "aws_security_group_rule" and
        rc.mode is "managed" and
        (rc.change.actions contains "create" or rc.change.actions contains "update")
    }
}

# 取得內嵌規則的安全群組
get_security_groups = func() {
    return filter tfplan.resource_changes as _, rc {
        rc.type is "aws_security_group" and
        rc.mode is "managed" and
        (rc.change.actions contains "create" or rc.change.actions contains "update")
    }
}

# 檢查 CIDR 是否為開放存取
is_open_cidr = func(cidr) {
    return cidr is "0.0.0.0/0" or cidr is "::/0"
}

# 檢查連接埠範圍是否包含受限連接埠
includes_restricted_port = func(from_port, to_port) {
    for restricted_ports as port {
        if from_port <= port and port <= to_port {
            return true
        }
    }
    return false
}

# 驗證安全群組規則
validate_sg_rule = func(rule) {
    # 只檢查入站規則
    if rule.change.after.type is not "ingress" {
        return true
    }

    cidr_blocks = rule.change.after.cidr_blocks else []
    from_port = rule.change.after.from_port else 0
    to_port = rule.change.after.to_port else 0

    for cidr_blocks as cidr {
        if is_open_cidr(cidr) and includes_restricted_port(from_port, to_port) {
            print("Security group rule", rule.address,
                  "allows restricted port range", from_port, "-", to_port,
                  "from", cidr)
            return false
        }
    }
    return true
}

# 驗證內嵌安全群組規則
validate_sg_inline_rules = func(sg) {
    ingress_rules = sg.change.after.ingress else []

    for ingress_rules as rule {
        cidr_blocks = rule.cidr_blocks else []
        from_port = rule.from_port else 0
        to_port = rule.to_port else 0

        for cidr_blocks as cidr {
            if is_open_cidr(cidr) and includes_restricted_port(from_port, to_port) {
                print("Security group", sg.address,
                      "has inline rule allowing restricted port range",
                      from_port, "-", to_port, "from", cidr)
                return false
            }
        }
    }
    return true
}

# 主要規則
main = rule {
    all get_security_group_rules() as _, rule {
        validate_sg_rule(rule)
    } and
    all get_security_groups() as _, sg {
        validate_sg_inline_rules(sg)
    }
}

成本控制政策

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# policies/cost-control.sentinel
# 基於成本估算限制資源部署

import "tfplan/v2" as tfplan
import "tfrun"
import "decimal"

# 參數:每月成本限制(美元)
param monthly_cost_limit default 1000

# 參數:單次變更成本增加限制
param cost_increase_limit default 500

# 取得成本估算資料
cost_estimate = tfrun.cost_estimate

# 規則:檢查預估月成本
monthly_cost_check = rule {
    if cost_estimate is null or cost_estimate is undefined {
        print("Cost estimation is not available")
        true  # 如果沒有成本估算,允許通過
    } else {
        proposed_monthly = decimal.new(cost_estimate.proposed_monthly_cost)
        limit = decimal.new(monthly_cost_limit)

        if proposed_monthly.greater_than(limit) {
            print("Proposed monthly cost $", cost_estimate.proposed_monthly_cost,
                  "exceeds limit $", monthly_cost_limit)
            false
        } else {
            true
        }
    }
}

# 規則:檢查成本變化
cost_increase_check = rule {
    if cost_estimate is null or cost_estimate is undefined {
        true
    } else {
        delta = decimal.new(cost_estimate.delta_monthly_cost)
        limit = decimal.new(cost_increase_limit)

        if delta.greater_than(limit) {
            print("Monthly cost increase $", cost_estimate.delta_monthly_cost,
                  "exceeds limit $", cost_increase_limit)
            false
        } else {
            true
        }
    }
}

# 主要規則
main = rule {
    monthly_cost_check and
    cost_increase_check
}

與 Terraform Cloud/Enterprise 整合

設定政策集

在 Terraform Cloud 中設定 Sentinel 政策集:

  1. 進入 Organization Settings
  2. 選擇 Policy Sets
  3. 建立新的 Policy Set
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 使用 Terraform 管理政策集
resource "tfe_policy_set" "production_policies" {
    name          = "production-governance"
    description   = "Production environment governance policies"
    organization  = "my-organization"
    kind          = "sentinel"

    # VCS 設定
    vcs_repo {
        identifier         = "my-org/sentinel-policies"
        branch             = "main"
        ingress_submodules = false
        oauth_token_id     = tfe_oauth_client.github.oauth_token_id
    }

    # 套用到特定工作區
    workspace_ids = [
        tfe_workspace.production_vpc.id,
        tfe_workspace.production_eks.id,
        tfe_workspace.production_rds.id,
    ]
}

# 參數化政策集
resource "tfe_policy_set_parameter" "cost_limit" {
    key          = "monthly_cost_limit"
    value        = "5000"
    policy_set_id = tfe_policy_set.production_policies.id
}

工作流程整合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────────────────────┐
│                    Terraform Cloud 執行流程                          │
│                                                                      │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐     │
│   │  Queue   │───▶│  Plan    │───▶│ Cost Est │───▶│ Sentinel │     │
│   │          │    │          │    │          │    │  Check   │     │
│   └──────────┘    └──────────┘    └──────────┘    └──────────┘     │
│                                                          │           │
│                          ┌───────────────────────────────┤           │
│                          │                               │           │
│                          ▼                               ▼           │
│                   ┌──────────┐                    ┌──────────┐      │
│                   │  Pass    │                    │  Fail    │      │
│                   │          │                    │          │      │
│                   └────┬─────┘                    └──────────┘      │
│                        │                                             │
│                        ▼                                             │
│                 ┌──────────────┐                                    │
│                 │ Apply Ready  │                                    │
│                 │  (審核等待)   │                                    │
│                 └──────────────┘                                    │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

在 Run 中檢視政策結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 使用 Terraform CLI 觸發 run
terraform plan

# 輸出範例
Running plan in Terraform Cloud...

Sentinel Policies:
  ✓ require-tags          (hard-mandatory)  PASSED
  ✓ enforce-encryption    (hard-mandatory)  PASSED
  ⚠ restrict-instance-types (soft-mandatory) FAILED
      Rule "main" failed
      Instance dev-web-server uses non-approved type: t3.2xlarge

  ℹ cost-control          (advisory)        PASSED

1 policies passed, 1 policy failed (soft-mandatory), 1 advisory

Do you want to override the soft-mandatory policy? (yes/no)

API 整合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 使用 API 取得政策檢查結果
curl \
  --header "Authorization: Bearer $TFE_TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  "https://app.terraform.io/api/v2/runs/run-xxxx/policy-checks"

# 回應範例
{
  "data": [
    {
      "id": "polchk-xxxx",
      "type": "policy-checks",
      "attributes": {
        "result": {
          "passed": 2,
          "total-failed": 1,
          "hard-failed": 0,
          "soft-failed": 1,
          "advisory-failed": 0
        },
        "status": "soft_failed",
        "actions": {
          "is-overridable": true
        }
      }
    }
  ]
}

政策測試與偵錯

設定測試環境

1
2
3
4
5
# 安裝 Sentinel CLI
curl -o sentinel.zip https://releases.hashicorp.com/sentinel/0.24.0/sentinel_0.24.0_linux_amd64.zip
unzip sentinel.zip
sudo mv sentinel /usr/local/bin/
sentinel version

測試檔案結構

1
2
3
4
5
6
test/
└── require-tags/
    ├── pass.hcl           # 測試設定(應通過)
    ├── fail.hcl           # 測試設定(應失敗)
    ├── mock-tfplan-pass.sentinel   # Mock 資料(通過案例)
    └── mock-tfplan-fail.sentinel   # Mock 資料(失敗案例)

建立 Mock 資料

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# test/require-tags/mock-tfplan-pass.sentinel

terraform_version = "1.5.0"

resource_changes = {
    "aws_instance.web": {
        "address": "aws_instance.web",
        "mode": "managed",
        "type": "aws_instance",
        "name": "web",
        "provider_name": "registry.terraform.io/hashicorp/aws",
        "change": {
            "actions": ["create"],
            "before": null,
            "after": {
                "ami": "ami-12345678",
                "instance_type": "t3.medium",
                "tags": {
                    "Environment": "production",
                    "Owner": "platform-team",
                    "Project": "web-app",
                    "CostCenter": "12345",
                },
            },
            "after_unknown": {},
        },
    },
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# test/require-tags/mock-tfplan-fail.sentinel

terraform_version = "1.5.0"

resource_changes = {
    "aws_instance.web": {
        "address": "aws_instance.web",
        "mode": "managed",
        "type": "aws_instance",
        "name": "web",
        "provider_name": "registry.terraform.io/hashicorp/aws",
        "change": {
            "actions": ["create"],
            "before": null,
            "after": {
                "ami": "ami-12345678",
                "instance_type": "t3.medium",
                "tags": {
                    "Name": "web-server",
                    # 缺少必要標籤:Environment, Owner, Project, CostCenter
                },
            },
            "after_unknown": {},
        },
    },
}

測試設定檔

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# test/require-tags/pass.hcl

mock "tfplan/v2" {
    module {
        source = "./mock-tfplan-pass.sentinel"
    }
}

test {
    rules = {
        main = true
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# test/require-tags/fail.hcl

mock "tfplan/v2" {
    module {
        source = "./mock-tfplan-fail.sentinel"
    }
}

test {
    rules = {
        main = false
    }
}

執行測試

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 執行單一政策測試
sentinel test policies/require-tags.sentinel

# 執行所有測試
sentinel test

# 詳細輸出
sentinel test -verbose

# 輸出範例
PASS - require-tags.sentinel
  PASS - test/require-tags/pass.hcl
  PASS - test/require-tags/fail.hcl

偵錯技巧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 使用 print 語句偵錯
validate_tags = func(resource) {
    print("Checking resource:", resource.address)
    print("Resource type:", resource.type)
    print("Tags:", resource.change.after.tags)

    tags = resource.change.after.tags else {}
    print("Processed tags:", tags)

    for mandatory_tags as tag {
        if tag in tags {
            print("  Found tag:", tag, "=", tags[tag])
        } else {
            print("  Missing tag:", tag)
        }
    }

    return true
}

# 使用 trace 追蹤規則評估
main = rule {
    trace(all get_resources() as _, r { validate_tags(r) })
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 使用 trace 輸出執行
sentinel apply -trace policies/require-tags.sentinel

# 輸出範例
Trace output:
  require-tags.sentinel:45:1 - Rule "main"
    require-tags.sentinel:46:5 - all expression
      require-tags.sentinel:46:25 - Rule iteration 1
        Checking resource: aws_instance.web
        Resource type: aws_instance
        Tags: {"Environment": "production", "Owner": "platform-team"}
          Found tag: Environment = production
          Found tag: Owner = platform-team
          Missing tag: Project
          Missing tag: CostCenter
        Result: false
    Result: false
  Result: false

FAIL - require-tags.sentinel

軟性與硬性政策區別

強制層級說明

Sentinel 提供三種政策強制層級:

層級說明可覆寫使用場景
advisory警告性質N/A(不阻擋)最佳實務建議
soft-mandatory軟性強制是(需授權)大多數政策
hard-mandatory硬性強制安全性/合規關鍵

層級定義範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# sentinel.hcl

# Advisory - 僅提供警告,不阻擋執行
policy "naming-convention" {
    source            = "./policies/naming-convention.sentinel"
    enforcement_level = "advisory"
}

# Soft Mandatory - 需要授權才能覆寫
policy "cost-control" {
    source            = "./policies/cost-control.sentinel"
    enforcement_level = "soft-mandatory"
}

# Hard Mandatory - 完全不可覆寫
policy "encryption-required" {
    source            = "./policies/encryption-required.sentinel"
    enforcement_level = "hard-mandatory"
}

policy "no-public-s3" {
    source            = "./policies/no-public-s3.sentinel"
    enforcement_level = "hard-mandatory"
}

覆寫流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
┌────────────────────────────────────────────────────────────────┐
│                     政策失敗處理流程                             │
│                                                                 │
│   ┌─────────────────┐                                          │
│   │ Sentinel Check  │                                          │
│   └────────┬────────┘                                          │
│            │                                                    │
│            ▼                                                    │
│   ┌─────────────────┐                                          │
│   │   Policy Fail   │                                          │
│   └────────┬────────┘                                          │
│            │                                                    │
│     ┌──────┴──────────────────────┐                            │
│     │                             │                             │
│     ▼                             ▼                             │
│  advisory                  soft/hard-mandatory                  │
│     │                             │                             │
│     ▼                        ┌────┴────┐                       │
│ ┌───────────┐                │         │                       │
│ │ Log Only  │          soft-mandatory  hard-mandatory          │
│ │ Continue  │                │         │                       │
│ └───────────┘                ▼         ▼                       │
│                        ┌──────────┐  ┌──────────┐              │
│                        │ Override │  │  Block   │              │
│                        │ Possible │  │ Always   │              │
│                        └────┬─────┘  └──────────┘              │
│                             │                                   │
│                             ▼                                   │
│                      ┌──────────────┐                          │
│                      │ Authorized   │                          │
│                      │ User Override│                          │
│                      └──────────────┘                          │
│                                                                 │
└────────────────────────────────────────────────────────────────┘

權限管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 設定可以覆寫軟性政策的團隊
resource "tfe_team" "policy_overriders" {
    name         = "policy-overriders"
    organization = "my-organization"

    organization_access {
        manage_policies   = true
        manage_workspaces = false
    }
}

resource "tfe_team_access" "overriders_access" {
    team_id      = tfe_team.policy_overriders.id
    workspace_id = tfe_workspace.production.id

    permissions {
        runs              = "apply"
        run_tasks         = false
        sentinel_mocks    = "read"
        state_versions    = "read"
        variables         = "read"
        workspace_locking = false
    }
}

政策層級選擇指南

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
選擇 Hard-Mandatory 當:
- 違反會導致安全漏洞(如公開 S3 儲存桶)
- 違反法規合規要求(如 PCI-DSS、HIPAA)
- 違反會造成嚴重財務損失
- 沒有任何例外情況應該被允許

選擇 Soft-Mandatory 當:
- 需要彈性處理特殊情況
- 政策適用於大多數但非所有情境
- 需要人工審核才能決定是否覆寫
- 團隊正在逐步採用新政策

選擇 Advisory 當:
- 推廣最佳實務而非強制要求
- 政策仍在測試階段
- 提供資訊給團隊參考
- 不希望阻擋任何部署

最佳實務與治理策略

政策開發最佳實務

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1. 使用共用模組避免重複程式碼
# modules/tfplan-functions/tfplan-functions.sentinel

# 取得指定類型的所有資源變更
get_resource_changes = func(resource_type) {
    return filter tfplan.resource_changes as _, rc {
        rc.type is resource_type and
        rc.mode is "managed" and
        (rc.change.actions contains "create" or
         rc.change.actions contains "update")
    }
}

# 取得所有正在建立的資源
get_created_resources = func() {
    return filter tfplan.resource_changes as _, rc {
        rc.mode is "managed" and
        rc.change.actions contains "create"
    }
}

# 安全取得巢狀屬性
get_attribute = func(obj, path, default_value) {
    result = obj
    for strings.split(path, ".") as key {
        if result is null or result is undefined {
            return default_value
        }
        if key in result {
            result = result[key]
        } else {
            return default_value
        }
    }
    return result else default_value
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 2. 使用政策匯入共用模組
# policies/require-encryption.sentinel

import "tfplan/v2" as tfplan
import "tfplan-functions" as plan

# 使用共用函數
ebs_volumes = plan.get_resource_changes("aws_ebs_volume")

main = rule {
    all ebs_volumes as _, volume {
        plan.get_attribute(volume, "change.after.encrypted", false) is true
    }
}

政策組織策略

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sentinel-policies/
├── sentinel.hcl
├── policies/
│   ├── security/                  # 安全相關政策
│   │   ├── encryption.sentinel
│   │   ├── network-security.sentinel
│   │   └── iam-policies.sentinel
│   │
│   ├── compliance/                # 合規相關政策
│   │   ├── tagging.sentinel
│   │   ├── logging.sentinel
│   │   └── data-residency.sentinel
│   │
│   ├── cost/                      # 成本相關政策
│   │   ├── instance-types.sentinel
│   │   └── cost-limits.sentinel
│   │
│   └── operational/               # 營運相關政策
│       ├── naming-convention.sentinel
│       └── high-availability.sentinel
├── modules/
│   ├── tfplan-functions/
│   └── aws-functions/
└── test/
    └── ...

漸進式政策採用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 階段 1:Advisory 模式(觀察)
policy "new-tagging-policy" {
    source            = "./policies/tagging-v2.sentinel"
    enforcement_level = "advisory"
}

# 階段 2:Soft-Mandatory(軟性強制)
# 在團隊熟悉後升級
policy "new-tagging-policy" {
    source            = "./policies/tagging-v2.sentinel"
    enforcement_level = "soft-mandatory"
}

# 階段 3:Hard-Mandatory(硬性強制)
# 確認沒有問題後升級
policy "new-tagging-policy" {
    source            = "./policies/tagging-v2.sentinel"
    enforcement_level = "hard-mandatory"
}

政策版本控制

1
2
3
4
5
6
# 使用 Git 標籤管理政策版本
git tag -a v1.0.0 -m "Initial policy release"
git tag -a v1.1.0 -m "Add cost control policies"
git tag -a v2.0.0 -m "Breaking: Update tagging requirements"

# 政策集可以指定特定分支或標籤
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 在 Terraform Cloud 中指定政策版本
resource "tfe_policy_set" "production_v1" {
    name         = "production-policies-v1"
    organization = "my-organization"

    vcs_repo {
        identifier     = "my-org/sentinel-policies"
        branch         = "v1.x"  # 使用版本分支
        oauth_token_id = tfe_oauth_client.github.oauth_token_id
    }
}

治理框架建議

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
## 政策治理框架

### 政策生命週期
1. 提案 → 審核 → 開發 → 測試 → 部署 → 監控 → 更新/淘汰

### 角色與責任
| 角色 | 責任 |
|------|------|
| 安全團隊 | 定義安全政策需求 |
| 合規團隊 | 定義合規政策需求 |
| 平台團隊 | 開發和維護政策 |
| 應用團隊 | 遵循政策、回報問題 |
| 管理層 | 核准政策覆寫 |

### 審核機制
- 所有政策變更需經過 Code Review
- 重大變更需要安全團隊審核
- 定期(季度)審查政策有效性

### 例外處理
1. 提交例外申請單
2. 安全/合規團隊審核
3. 管理層核准
4. 設定有時間限制的覆寫
5. 定期複審例外

監控與報告

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用 Terraform 建立政策執行報告
data "tfe_policy_set" "all" {
    for_each     = toset(var.policy_set_names)
    name         = each.value
    organization = var.organization
}

# 整合監控系統
resource "aws_cloudwatch_metric_alarm" "policy_failures" {
    alarm_name          = "sentinel-policy-failures"
    comparison_operator = "GreaterThanThreshold"
    evaluation_periods  = "1"
    metric_name         = "PolicyFailures"
    namespace           = "TerraformCloud"
    period              = "3600"
    statistic           = "Sum"
    threshold           = "10"
    alarm_description   = "Alert when policy failures exceed threshold"

    alarm_actions = [aws_sns_topic.alerts.arn]
}

總結

Terraform Sentinel 提供了強大的政策即程式碼能力,讓組織能夠:

  • 自動化合規:在基礎設施變更前自動驗證合規性
  • 強化安全:防止不安全的配置進入生產環境
  • 控制成本:限制資源使用和規格
  • 標準化配置:確保資源配置的一致性
  • 提供審計軌跡:完整記錄政策執行結果

透過本文介紹的概念和範例,您可以開始建立適合組織需求的 Sentinel 政策集,實現基礎設施治理的自動化。

快速開始檢查清單

  1. 識別需要強制執行的政策需求
  2. 設計政策架構和分類
  3. 建立共用模組和函數庫
  4. 撰寫政策並建立測試案例
  5. 以 Advisory 模式部署觀察
  6. 逐步提升強制層級
  7. 建立例外處理流程
  8. 持續監控和改進

參考資料

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy