diff --git a/terraform/azure/README.md b/terraform/azure/README.md
new file mode 100644
index 0000000..c5caef2
--- /dev/null
+++ b/terraform/azure/README.md
@@ -0,0 +1,402 @@
+# Azure Blob Storage Terraform Module
+
+Production-ready Terraform module for creating secure, well-configured Azure Storage Accounts with Blob Storage containers, versioning, encryption, and lifecycle management.
+
+## Features
+
+- **Security First**: HTTPS-only traffic and TLS 1.2 minimum by default
+- **Infrastructure Encryption**: Additional encryption layer enabled by default
+- **Public Access Protection**: Public blob access blocked by default
+- **Versioning**: Enabled by default to protect against accidental deletion
+- **Soft Delete**: Configurable retention for deleted blobs and containers
+- **Lifecycle Management**: Flexible lifecycle rules for cost optimization
+- **Network Security**: Optional network rules for IP/VNet restrictions
+- **Customer-Managed Keys**: Support for Azure Key Vault encryption keys
+- **Compliance Ready**: Follows Azure security best practices
+- **Fully Tested**: Includes test configurations and passes security scans
+
+## Usage
+
+### Basic Example
+
+```hcl
+module "blob_storage" {
+ source = "github.com/your-org/infra-modules//terraform/azure?ref=v1.0.0"
+
+ storage_account_name = "mystorageaccount123"
+ resource_group_name = "my-resource-group"
+ location = "eastus"
+ container_name = "my-container"
+
+ tags = {
+ Environment = "production"
+ Application = "my-app"
+ ManagedBy = "terraform"
+ }
+}
+```
+
+### Advanced Example with Customer-Managed Keys
+
+```hcl
+# Create a Key Vault and key first
+resource "azurerm_key_vault_key" "storage_key" {
+ name = "storage-encryption-key"
+ key_vault_id = azurerm_key_vault.example.id
+ key_type = "RSA"
+ key_size = 2048
+
+ key_opts = [
+ "decrypt",
+ "encrypt",
+ "sign",
+ "unwrapKey",
+ "verify",
+ "wrapKey",
+ ]
+}
+
+module "secure_storage" {
+ source = "github.com/your-org/infra-modules//terraform/azure?ref=v1.0.0"
+
+ storage_account_name = "securestorage123"
+ resource_group_name = "my-resource-group"
+ location = "eastus"
+ container_name = "secure-data"
+
+ # Use customer-managed encryption key
+ customer_managed_key_vault_key_id = azurerm_key_vault_key.storage_key.id
+
+ # Enhanced security settings
+ enable_https_traffic_only = true
+ min_tls_version = "TLS1_2"
+ infrastructure_encryption_enabled = true
+
+ # Enable versioning and soft delete
+ versioning_enabled = true
+ blob_soft_delete_retention_days = 30
+ container_soft_delete_retention_days = 30
+
+ tags = {
+ Environment = "production"
+ Compliance = "required"
+ DataClass = "sensitive"
+ }
+}
+```
+
+### Example with Network Restrictions
+
+```hcl
+module "restricted_storage" {
+ source = "github.com/your-org/infra-modules//terraform/azure?ref=v1.0.0"
+
+ storage_account_name = "restrictedstorage123"
+ resource_group_name = "my-resource-group"
+ location = "eastus"
+ container_name = "restricted-data"
+
+ # Enable network rules
+ network_rules_enabled = true
+ network_rules_default_action = "Deny"
+ network_rules_bypass = ["AzureServices", "Logging", "Metrics"]
+
+ # Allow specific IPs and VNets
+ network_rules_ip_rules = [
+ "203.0.113.0/24",
+ "198.51.100.42"
+ ]
+ network_rules_subnet_ids = [
+ azurerm_subnet.trusted.id
+ ]
+
+ tags = {
+ Environment = "production"
+ Security = "restricted"
+ }
+}
+```
+
+### Example with Lifecycle Management
+
+```hcl
+module "archive_storage" {
+ source = "github.com/your-org/infra-modules//terraform/azure?ref=v1.0.0"
+
+ storage_account_name = "archivestorage123"
+ resource_group_name = "my-resource-group"
+ location = "eastus"
+ container_name = "archive-data"
+
+ # Enable last access time tracking for lifecycle policies
+ last_access_time_enabled = true
+
+ # Define lifecycle rules for cost optimization
+ lifecycle_rules = [
+ {
+ name = "archive-old-logs"
+ enabled = true
+ prefix_match = ["logs/"]
+ blob_types = ["blockBlob"]
+
+ base_blob_actions = {
+ # Move to cool storage after 30 days
+ tier_to_cool_after_days = 30
+
+ # Move to archive after 90 days
+ tier_to_archive_after_days = 90
+
+ # Delete after 2 years
+ delete_after_days = 730
+ }
+
+ # Manage snapshots
+ snapshot_actions = {
+ tier_to_archive_after_days = 30
+ delete_after_days = 90
+ }
+
+ # Manage old versions
+ version_actions = {
+ tier_to_archive_after_days = 30
+ delete_after_days = 90
+ }
+ },
+ {
+ name = "cleanup-temp-files"
+ enabled = true
+ prefix_match = ["temp/"]
+ blob_types = ["blockBlob"]
+
+ base_blob_actions = {
+ # Delete temp files after 7 days
+ delete_after_days = 7
+ }
+ },
+ {
+ name = "archive-based-on-access"
+ enabled = true
+ prefix_match = ["documents/"]
+ blob_types = ["blockBlob"]
+
+ base_blob_actions = {
+ # Move to cool after 60 days of no access
+ tier_to_cool_after_last_access_days = 60
+
+ # Move to archive after 180 days of no access
+ tier_to_archive_after_last_access_days = 180
+ }
+ }
+ ]
+
+ tags = {
+ Environment = "production"
+ Purpose = "archival"
+ }
+}
+```
+
+### Example with Geo-Replication
+
+```hcl
+module "geo_replicated_storage" {
+ source = "github.com/your-org/infra-modules//terraform/azure?ref=v1.0.0"
+
+ storage_account_name = "geostorage123"
+ resource_group_name = "my-resource-group"
+ location = "eastus"
+ container_name = "replicated-data"
+
+ # Enable geo-redundant replication
+ replication_type = "RAGRS" # Read-Access Geo-Redundant Storage
+
+ # Standard tier for geo-replication
+ account_tier = "Standard"
+
+ tags = {
+ Environment = "production"
+ HighAvailability = "true"
+ }
+}
+```
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [azurerm](#requirement\_azurerm) | ~> 3.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [azurerm](#provider\_azurerm) | ~> 3.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [azurerm_storage_account.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) | resource |
+| [azurerm_storage_container.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container) | resource |
+| [azurerm_storage_management_policy.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_management_policy) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [container\_name](#input\_container\_name) | Name of the blob container. Must be between 3-63 characters, lowercase letters, numbers, and hyphens only. | `string` | n/a | yes |
+| [location](#input\_location) | Azure region where the storage account will be created (e.g., 'eastus', 'westeurope'). | `string` | n/a | yes |
+| [resource\_group\_name](#input\_resource\_group\_name) | Name of the resource group where the storage account will be created. | `string` | n/a | yes |
+| [storage\_account\_name](#input\_storage\_account\_name) | Name of the Azure Storage Account. Must be globally unique, between 3-24 characters, and contain only lowercase letters and numbers. | `string` | n/a | yes |
+| [account\_kind](#input\_account\_kind) | Storage account kind. StorageV2 is recommended for most scenarios. | `string` | `"StorageV2"` | no |
+| [account\_tier](#input\_account\_tier) | Storage account tier. Standard for general-purpose v2, Premium for high-performance scenarios. | `string` | `"Standard"` | no |
+| [allow\_public\_access](#input\_allow\_public\_access) | Allow public access to blobs. Recommended to keep disabled (false) for security. | `bool` | `false` | no |
+| [blob\_soft\_delete\_retention\_days](#input\_blob\_soft\_delete\_retention\_days) | Number of days to retain deleted blobs. Set to 0 to disable soft delete. Recommended: 7-30 days. | `number` | `7` | no |
+| [change\_feed\_enabled](#input\_change\_feed\_enabled) | Enable change feed to track create, update, and delete changes to blobs. Useful for auditing and event-driven architectures. | `bool` | `false` | no |
+| [container\_access\_type](#input\_container\_access\_type) | Access type for the container. Options: 'private' (no public access), 'blob' (public read for blobs only), 'container' (public read for container and blobs). | `string` | `"private"` | no |
+| [container\_metadata](#input\_container\_metadata) | Metadata to assign to the container as key-value pairs. | `map(string)` | `{}` | no |
+| [container\_soft\_delete\_retention\_days](#input\_container\_soft\_delete\_retention\_days) | Number of days to retain deleted containers. Set to 0 to disable soft delete. Recommended: 7-30 days. | `number` | `7` | no |
+| [customer\_managed\_key\_user\_assigned\_identity\_id](#input\_customer\_managed\_key\_user\_assigned\_identity\_id) | User-assigned managed identity ID for accessing the customer-managed key. If not specified, system-assigned identity will be used. | `string` | `null` | no |
+| [customer\_managed\_key\_vault\_key\_id](#input\_customer\_managed\_key\_vault\_key\_id) | Key Vault Key ID for customer-managed encryption keys. If not specified, Microsoft-managed keys will be used. | `string` | `null` | no |
+| [enable\_https\_traffic\_only](#input\_enable\_https\_traffic\_only) | Enforce HTTPS-only traffic to the storage account. Recommended to keep enabled for security. | `bool` | `true` | no |
+| [infrastructure\_encryption\_enabled](#input\_infrastructure\_encryption\_enabled) | Enable infrastructure encryption for additional security layer. Recommended for sensitive data. | `bool` | `true` | no |
+| [last\_access\_time\_enabled](#input\_last\_access\_time\_enabled) | Enable last access time tracking for lifecycle management policies based on access patterns. | `bool` | `false` | no |
+| [lifecycle\_rules](#input\_lifecycle\_rules) | List of lifecycle management rules for optimizing storage costs.
Each rule can include:
- name: Unique name for the rule
- enabled: Whether the rule is active
- prefix\_match: List of blob prefixes to match (optional)
- blob\_types: Types of blobs to apply the rule to (e.g., ["blockBlob"])
- base\_blob\_actions: Actions for current versions
- tier\_to\_cool\_after\_days: Days to tier to cool storage
- tier\_to\_cool\_after\_last\_access\_days: Days since last access to tier to cool
- tier\_to\_archive\_after\_days: Days to tier to archive storage
- tier\_to\_archive\_after\_last\_access\_days: Days since last access to tier to archive
- delete\_after\_days: Days to delete blobs
- delete\_after\_last\_access\_days: Days since last access to delete
- snapshot\_actions: Actions for snapshots
- version\_actions: Actions for older versions |
list(object({
name = string
enabled = bool
prefix_match = optional(list(string), [])
blob_types = list(string)
base_blob_actions = optional(object({
tier_to_cool_after_days = optional(number)
tier_to_cool_after_last_access_days = optional(number)
tier_to_archive_after_days = optional(number)
tier_to_archive_after_last_access_days = optional(number)
delete_after_days = optional(number)
delete_after_last_access_days = optional(number)
}))
snapshot_actions = optional(object({
tier_to_cool_after_days = optional(number)
tier_to_archive_after_days = optional(number)
delete_after_days = optional(number)
}))
version_actions = optional(object({
tier_to_cool_after_days = optional(number)
tier_to_archive_after_days = optional(number)
delete_after_days = optional(number)
}))
})) | `[]` | no |
+| [min\_tls\_version](#input\_min\_tls\_version) | Minimum TLS version for requests to the storage account. | `string` | `"TLS1_2"` | no |
+| [network\_rules\_bypass](#input\_network\_rules\_bypass) | Services that can bypass network rules. Options: 'AzureServices', 'Logging', 'Metrics', 'None'. | `list(string)` | [| no | +| [network\_rules\_default\_action](#input\_network\_rules\_default\_action) | Default action for network rules. 'Deny' blocks all traffic except allowed IPs/VNets. 'Allow' permits all traffic. | `string` | `"Deny"` | no | +| [network\_rules\_enabled](#input\_network\_rules\_enabled) | Enable network rules to restrict access to the storage account. | `bool` | `false` | no | +| [network\_rules\_ip\_rules](#input\_network\_rules\_ip\_rules) | List of public IP addresses or CIDR ranges that can access the storage account. | `list(string)` | `[]` | no | +| [network\_rules\_subnet\_ids](#input\_network\_rules\_subnet\_ids) | List of virtual network subnet IDs that can access the storage account. | `list(string)` | `[]` | no | +| [replication\_type](#input\_replication\_type) | Storage account replication type. Options: LRS (Locally Redundant), GRS (Geo-Redundant), RAGRS (Read-Access Geo-Redundant), ZRS (Zone-Redundant), GZRS (Geo-Zone-Redundant), RAGZRS (Read-Access Geo-Zone-Redundant). | `string` | `"LRS"` | no | +| [tags](#input\_tags) | A map of tags to add to all resources. Use this to add consistent tagging across your infrastructure. | `map(string)` | `{}` | no | +| [versioning\_enabled](#input\_versioning\_enabled) | Enable blob versioning to protect against accidental deletion and provide object history. Recommended for production. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [account\_kind](#output\_account\_kind) | The kind of the storage account. | +| [account\_tier](#output\_account\_tier) | The tier of the storage account (Standard or Premium). | +| [container\_id](#output\_container\_id) | The ID of the blob container. | +| [container\_name](#output\_container\_name) | The name of the blob container. | +| [https\_traffic\_only](#output\_https\_traffic\_only) | Whether HTTPS-only traffic is enforced. | +| [identity](#output\_identity) | The managed identity configuration of the storage account. | +| [infrastructure\_encryption\_enabled](#output\_infrastructure\_encryption\_enabled) | Whether infrastructure encryption is enabled for additional security. | +| [location](#output\_location) | The Azure region where the storage account is deployed. | +| [min\_tls\_version](#output\_min\_tls\_version) | The minimum TLS version required for requests. | +| [primary\_access\_key](#output\_primary\_access\_key) | The primary access key for the storage account. Use this for authentication when accessing the storage account. | +| [primary\_blob\_connection\_string](#output\_primary\_blob\_connection\_string) | The primary blob service connection string. Use this for blob-specific SDK connections. | +| [primary\_blob\_endpoint](#output\_primary\_blob\_endpoint) | The primary blob service endpoint. Use this for accessing blobs in the storage account. | +| [primary\_blob\_host](#output\_primary\_blob\_host) | The hostname for the primary blob service endpoint. | +| [primary\_connection\_string](#output\_primary\_connection\_string) | The primary connection string for the storage account. Use this for SDK and tool connections. | +| [replication\_type](#output\_replication\_type) | The replication type of the storage account. | +| [resource\_group\_name](#output\_resource\_group\_name) | The name of the resource group containing the storage account. | +| [secondary\_access\_key](#output\_secondary\_access\_key) | The secondary access key for the storage account. Use this for key rotation scenarios. | +| [secondary\_blob\_connection\_string](#output\_secondary\_blob\_connection\_string) | The secondary blob service connection string. Use this for blob-specific failover scenarios. | +| [secondary\_blob\_endpoint](#output\_secondary\_blob\_endpoint) | The secondary blob service endpoint. Available when geo-replication is enabled. | +| [secondary\_blob\_host](#output\_secondary\_blob\_host) | The hostname for the secondary blob service endpoint. Available when geo-replication is enabled. | +| [secondary\_connection\_string](#output\_secondary\_connection\_string) | The secondary connection string for the storage account. Use this for failover scenarios. | +| [storage\_account\_id](#output\_storage\_account\_id) | The ID of the storage account. Use this for resource references and configurations. | +| [storage\_account\_name](#output\_storage\_account\_name) | The name of the storage account. Use this for accessing the storage account. | +| [tags](#output\_tags) | All tags applied to the storage account, including default and custom tags. | +| [versioning\_enabled](#output\_versioning\_enabled) | Whether blob versioning is enabled. Important for compliance and data protection verification. | + + +## Security Considerations + +### Default Security Posture + +This module implements Azure security best practices by default: + +1. **HTTPS-Only Traffic**: All connections must use HTTPS +2. **TLS 1.2 Minimum**: Modern TLS version enforced +3. **Infrastructure Encryption**: Additional encryption layer enabled +4. **Public Access**: Public blob access blocked by default +5. **Versioning**: Enabled by default to protect against accidental deletion +6. **Soft Delete**: 7-day retention for deleted blobs and containers + +### Encryption Options + +**Microsoft-Managed Keys (Default)** +- Automatic key management by Azure +- No additional configuration required +- Suitable for most scenarios + +**Customer-Managed Keys (CMK)** +- Full control over encryption keys +- Key rotation and lifecycle management +- Audit trail in Azure Key Vault +- Requires Azure Key Vault setup + +```hcl +customer_managed_key_vault_key_id = azurerm_key_vault_key.example.id +``` + +### Network Security + +Restrict access using network rules: + +```hcl +network_rules_enabled = true +network_rules_default_action = "Deny" +network_rules_ip_rules = ["203.0.113.0/24"] +network_rules_subnet_ids = [azurerm_subnet.trusted.id] +``` + +Benefits: +- Prevent unauthorized access from the internet +- Allow only trusted networks and IPs +- Maintain access for Azure services (optional) + +### Replication Options + +- **LRS** (Locally Redundant): 3 copies in one datacenter (lowest cost) +- **ZRS** (Zone-Redundant): 3 copies across availability zones +- **GRS** (Geo-Redundant): 6 copies across two regions +- **RAGRS** (Read-Access Geo-Redundant): GRS with read access to secondary region +- **GZRS** (Geo-Zone-Redundant): ZRS + geo-replication +- **RAGZRS** (Read-Access Geo-Zone-Redundant): GZRS with read access to secondary + +### Lifecycle Best Practices + +Use lifecycle rules to: +1. Reduce storage costs by tiering to Cool or Archive storage +2. Meet compliance requirements for data retention +3. Automatically delete old data +4. Manage versioned blobs and snapshots efficiently + +## Comparison with AWS S3 + +| Feature | Azure Blob Storage | AWS S3 | +|---------|-------------------|---------| +| **Storage Tiers** | Hot, Cool, Archive | Standard, IA, Glacier | +| **Versioning** | Yes | Yes | +| **Encryption** | Microsoft/Customer-managed | SSE-S3, SSE-KMS | +| **Lifecycle** | Tiering and deletion | Tiering and expiration | +| **Replication** | LRS, ZRS, GRS, RAGRS | None (use S3 replication) | +| **Access Tiers** | Hot, Cool, Archive | Standard, IA, Glacier | + +## Testing + +Run the basic test: + +```bash +cd tests/basic +terraform init -backend=false +terraform plan +``` + +## Support and Contributions + +For issues, questions, or contributions, please see the repository guidelines. + +## License + +See [LICENSE](../../LICENSE) file for details. diff --git a/terraform/azure/main.tf b/terraform/azure/main.tf new file mode 100644 index 0000000..a5aafaf --- /dev/null +++ b/terraform/azure/main.tf @@ -0,0 +1,169 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0" + } + } +} + +# Storage Account +# Creates the main Azure Storage Account for blob storage +resource "azurerm_storage_account" "this" { + name = var.storage_account_name + resource_group_name = var.resource_group_name + location = var.location + account_tier = var.account_tier + account_replication_type = var.replication_type + account_kind = var.account_kind + + # Security: Enable HTTPS-only traffic by default + enable_https_traffic_only = var.enable_https_traffic_only + + # Security: Minimum TLS version + min_tls_version = var.min_tls_version + + # Security: Disable public blob access by default + allow_nested_items_to_be_public = var.allow_public_access + + # Enable infrastructure encryption for additional security + infrastructure_encryption_enabled = var.infrastructure_encryption_enabled + + # Versioning for blob storage + blob_properties { + versioning_enabled = var.versioning_enabled + + # Blob soft delete + dynamic "delete_retention_policy" { + for_each = var.blob_soft_delete_retention_days > 0 ? [1] : [] + content { + days = var.blob_soft_delete_retention_days + } + } + + # Container soft delete + dynamic "container_delete_retention_policy" { + for_each = var.container_soft_delete_retention_days > 0 ? [1] : [] + content { + days = var.container_soft_delete_retention_days + } + } + + # Change feed for tracking changes + change_feed_enabled = var.change_feed_enabled + + # Last access time tracking for lifecycle management + last_access_time_enabled = var.last_access_time_enabled + } + + # Network rules for security + dynamic "network_rules" { + for_each = var.network_rules_enabled ? [1] : [] + content { + default_action = var.network_rules_default_action + bypass = var.network_rules_bypass + ip_rules = var.network_rules_ip_rules + virtual_network_subnet_ids = var.network_rules_subnet_ids + } + } + + # Customer-managed key encryption + dynamic "customer_managed_key" { + for_each = var.customer_managed_key_vault_key_id != null ? [1] : [] + content { + key_vault_key_id = var.customer_managed_key_vault_key_id + user_assigned_identity_id = var.customer_managed_key_user_assigned_identity_id + } + } + + # Identity for customer-managed keys + dynamic "identity" { + for_each = var.customer_managed_key_vault_key_id != null ? [1] : [] + content { + type = var.customer_managed_key_user_assigned_identity_id != null ? "UserAssigned" : "SystemAssigned" + identity_ids = var.customer_managed_key_user_assigned_identity_id != null ? [var.customer_managed_key_user_assigned_identity_id] : null + } + } + + tags = merge( + var.tags, + { + Name = var.storage_account_name + } + ) +} + +# Blob Container +# Creates the blob container within the storage account +resource "azurerm_storage_container" "this" { + name = var.container_name + storage_account_name = azurerm_storage_account.this.name + container_access_type = var.container_access_type + + # Container metadata + metadata = var.container_metadata +} + +# Lifecycle Management Policy +# Manages blob lifecycle for cost optimization +resource "azurerm_storage_management_policy" "this" { + count = length(var.lifecycle_rules) > 0 ? 1 : 0 + + storage_account_id = azurerm_storage_account.this.id + + dynamic "rule" { + for_each = var.lifecycle_rules + + content { + name = rule.value.name + enabled = rule.value.enabled + + filters { + prefix_match = rule.value.prefix_match + blob_types = rule.value.blob_types + } + + actions { + # Base blob actions + # Note: Azure resource property names are verbose but match the official Azure API + # to ensure compatibility and clarity with Azure documentation + dynamic "base_blob" { + for_each = rule.value.base_blob_actions != null ? [rule.value.base_blob_actions] : [] + + content { + tier_to_cool_after_days_since_modification_greater_than = base_blob.value.tier_to_cool_after_days + tier_to_cool_after_days_since_last_access_time_greater_than = base_blob.value.tier_to_cool_after_last_access_days + tier_to_archive_after_days_since_modification_greater_than = base_blob.value.tier_to_archive_after_days + tier_to_archive_after_days_since_last_access_time_greater_than = base_blob.value.tier_to_archive_after_last_access_days + delete_after_days_since_modification_greater_than = base_blob.value.delete_after_days + delete_after_days_since_last_access_time_greater_than = base_blob.value.delete_after_last_access_days + } + } + + # Snapshot actions + dynamic "snapshot" { + for_each = rule.value.snapshot_actions != null ? [rule.value.snapshot_actions] : [] + + content { + change_tier_to_archive_after_days_since_creation = snapshot.value.tier_to_archive_after_days + change_tier_to_cool_after_days_since_creation = snapshot.value.tier_to_cool_after_days + delete_after_days_since_creation_greater_than = snapshot.value.delete_after_days + } + } + + # Version actions (for versioned blobs) + dynamic "version" { + for_each = rule.value.version_actions != null ? [rule.value.version_actions] : [] + + content { + change_tier_to_archive_after_days_since_creation = version.value.tier_to_archive_after_days + change_tier_to_cool_after_days_since_creation = version.value.tier_to_cool_after_days + delete_after_days_since_creation = version.value.delete_after_days + } + } + } + } + } +} diff --git a/terraform/azure/outputs.tf b/terraform/azure/outputs.tf new file mode 100644 index 0000000..e098576 --- /dev/null +++ b/terraform/azure/outputs.tf @@ -0,0 +1,166 @@ +# ----------------------------------------------------------------------------- +# Storage Account Identification Outputs +# ----------------------------------------------------------------------------- + +output "storage_account_id" { + description = "The ID of the storage account. Use this for resource references and configurations." + value = azurerm_storage_account.this.id +} + +output "storage_account_name" { + description = "The name of the storage account. Use this for accessing the storage account." + value = azurerm_storage_account.this.name +} + +# ----------------------------------------------------------------------------- +# Container Identification Outputs +# ----------------------------------------------------------------------------- + +output "container_id" { + description = "The ID of the blob container." + value = azurerm_storage_container.this.id +} + +output "container_name" { + description = "The name of the blob container." + value = azurerm_storage_container.this.name +} + +# ----------------------------------------------------------------------------- +# Endpoint Outputs +# ----------------------------------------------------------------------------- + +output "primary_blob_endpoint" { + description = "The primary blob service endpoint. Use this for accessing blobs in the storage account." + value = azurerm_storage_account.this.primary_blob_endpoint +} + +output "primary_blob_host" { + description = "The hostname for the primary blob service endpoint." + value = azurerm_storage_account.this.primary_blob_host +} + +output "secondary_blob_endpoint" { + description = "The secondary blob service endpoint. Available when geo-replication is enabled." + value = azurerm_storage_account.this.secondary_blob_endpoint +} + +output "secondary_blob_host" { + description = "The hostname for the secondary blob service endpoint. Available when geo-replication is enabled." + value = azurerm_storage_account.this.secondary_blob_host +} + +# ----------------------------------------------------------------------------- +# Access Key Outputs (Marked Sensitive) +# ----------------------------------------------------------------------------- + +output "primary_access_key" { + description = "The primary access key for the storage account. Use this for authentication when accessing the storage account." + value = azurerm_storage_account.this.primary_access_key + sensitive = true +} + +output "secondary_access_key" { + description = "The secondary access key for the storage account. Use this for key rotation scenarios." + value = azurerm_storage_account.this.secondary_access_key + sensitive = true +} + +# ----------------------------------------------------------------------------- +# Connection String Outputs (Marked Sensitive) +# ----------------------------------------------------------------------------- + +output "primary_connection_string" { + description = "The primary connection string for the storage account. Use this for SDK and tool connections." + value = azurerm_storage_account.this.primary_connection_string + sensitive = true +} + +output "secondary_connection_string" { + description = "The secondary connection string for the storage account. Use this for failover scenarios." + value = azurerm_storage_account.this.secondary_connection_string + sensitive = true +} + +output "primary_blob_connection_string" { + description = "The primary blob service connection string. Use this for blob-specific SDK connections." + value = azurerm_storage_account.this.primary_blob_connection_string + sensitive = true +} + +output "secondary_blob_connection_string" { + description = "The secondary blob service connection string. Use this for blob-specific failover scenarios." + value = azurerm_storage_account.this.secondary_blob_connection_string + sensitive = true +} + +# ----------------------------------------------------------------------------- +# Configuration Outputs +# ----------------------------------------------------------------------------- + +output "location" { + description = "The Azure region where the storage account is deployed." + value = azurerm_storage_account.this.location +} + +output "account_tier" { + description = "The tier of the storage account (Standard or Premium)." + value = azurerm_storage_account.this.account_tier +} + +output "replication_type" { + description = "The replication type of the storage account." + value = azurerm_storage_account.this.account_replication_type +} + +output "account_kind" { + description = "The kind of the storage account." + value = azurerm_storage_account.this.account_kind +} + +# ----------------------------------------------------------------------------- +# Security Outputs +# ----------------------------------------------------------------------------- + +output "versioning_enabled" { + description = "Whether blob versioning is enabled. Important for compliance and data protection verification." + value = var.versioning_enabled +} + +output "https_traffic_only" { + description = "Whether HTTPS-only traffic is enforced." + value = var.enable_https_traffic_only +} + +output "min_tls_version" { + description = "The minimum TLS version required for requests." + value = azurerm_storage_account.this.min_tls_version +} + +output "infrastructure_encryption_enabled" { + description = "Whether infrastructure encryption is enabled for additional security." + value = azurerm_storage_account.this.infrastructure_encryption_enabled +} + +# ----------------------------------------------------------------------------- +# Identity Outputs +# ----------------------------------------------------------------------------- + +output "identity" { + description = "The managed identity configuration of the storage account." + value = try(azurerm_storage_account.this.identity[0], null) +} + +# ----------------------------------------------------------------------------- +# Reference Outputs +# ----------------------------------------------------------------------------- + +output "resource_group_name" { + description = "The name of the resource group containing the storage account." + value = azurerm_storage_account.this.resource_group_name +} + +output "tags" { + description = "All tags applied to the storage account, including default and custom tags." + value = azurerm_storage_account.this.tags +} diff --git a/terraform/azure/tests/basic/main.tf b/terraform/azure/tests/basic/main.tf new file mode 100644 index 0000000..1302251 --- /dev/null +++ b/terraform/azure/tests/basic/main.tf @@ -0,0 +1,255 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0" + } + } +} + +# Azure provider configuration for testing +# Note: Unlike AWS, Azure requires valid credentials to run terraform plan +# Use Azure CLI authentication: az login +# Or set environment variables: ARM_SUBSCRIPTION_ID, ARM_TENANT_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET +provider "azurerm" { + features {} + skip_provider_registration = true +} + +# Mock resource group for testing +resource "azurerm_resource_group" "test" { + name = "test-rg" + location = "eastus" +} + +# ----------------------------------------------------------------------------- +# Test 1: Basic storage account with default security settings +# ----------------------------------------------------------------------------- + +module "basic_storage" { + source = "../../" + + storage_account_name = "basictest123" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + container_name = "basic-container" + + tags = { + Environment = "test" + ManagedBy = "terraform" + Purpose = "basic-testing" + } +} + +# ----------------------------------------------------------------------------- +# Test 2: Storage account with customer-managed encryption +# ----------------------------------------------------------------------------- + +module "cmk_storage" { + source = "../../" + + storage_account_name = "cmktest123" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + container_name = "cmk-container" + + # Mock Key Vault key ID for testing + customer_managed_key_vault_key_id = "https://test-keyvault.vault.azure.net/keys/test-key/12345678901234567890123456789012" + + tags = { + Environment = "test" + ManagedBy = "terraform" + Purpose = "cmk-testing" + } +} + +# ----------------------------------------------------------------------------- +# Test 3: Storage account with lifecycle rules +# ----------------------------------------------------------------------------- + +module "lifecycle_storage" { + source = "../../" + + storage_account_name = "lifecycletest123" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + container_name = "lifecycle-container" + + # Enable last access time tracking + last_access_time_enabled = true + + lifecycle_rules = [ + { + name = "archive-logs" + enabled = true + prefix_match = ["logs/"] + blob_types = ["blockBlob"] + + base_blob_actions = { + tier_to_cool_after_days = 30 + tier_to_archive_after_days = 90 + delete_after_days = 365 + } + + snapshot_actions = { + tier_to_archive_after_days = 30 + delete_after_days = 90 + } + + version_actions = { + tier_to_archive_after_days = 30 + delete_after_days = 90 + } + }, + { + name = "cleanup-temp" + enabled = true + prefix_match = ["temp/"] + blob_types = ["blockBlob"] + + base_blob_actions = { + delete_after_days = 7 + } + }, + { + name = "archive-by-access" + enabled = true + prefix_match = ["documents/"] + blob_types = ["blockBlob"] + + base_blob_actions = { + tier_to_cool_after_last_access_days = 60 + tier_to_archive_after_last_access_days = 180 + } + } + ] + + tags = { + Environment = "test" + ManagedBy = "terraform" + Purpose = "lifecycle-testing" + } +} + +# ----------------------------------------------------------------------------- +# Test 4: Storage account with network restrictions +# ----------------------------------------------------------------------------- + +module "restricted_storage" { + source = "../../" + + storage_account_name = "restrictedtest123" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + container_name = "restricted-container" + + # Enable network rules + network_rules_enabled = true + network_rules_default_action = "Deny" + network_rules_bypass = ["AzureServices", "Logging"] + + # Mock IP rules for testing + network_rules_ip_rules = [ + "203.0.113.0/24", + "198.51.100.42" + ] + + tags = { + Environment = "test" + ManagedBy = "terraform" + Purpose = "network-restriction-testing" + } +} + +# ----------------------------------------------------------------------------- +# Test 5: Storage account with geo-replication +# ----------------------------------------------------------------------------- + +module "geo_storage" { + source = "../../" + + storage_account_name = "geotest123" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + container_name = "geo-container" + + # Enable geo-redundant replication + replication_type = "RAGRS" + + tags = { + Environment = "test" + ManagedBy = "terraform" + Purpose = "geo-replication-testing" + } +} + +# ----------------------------------------------------------------------------- +# Test 6: Storage account with versioning disabled and no soft delete +# ----------------------------------------------------------------------------- + +module "minimal_storage" { + source = "../../" + + storage_account_name = "minimaltest123" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + container_name = "minimal-container" + + # Disable optional features + versioning_enabled = false + blob_soft_delete_retention_days = 0 + container_soft_delete_retention_days = 0 + infrastructure_encryption_enabled = false + + tags = { + Environment = "test" + ManagedBy = "terraform" + Purpose = "minimal-config-testing" + } +} + +# ----------------------------------------------------------------------------- +# Test Outputs +# ----------------------------------------------------------------------------- + +output "basic_storage_account_id" { + description = "ID of the basic test storage account" + value = module.basic_storage.storage_account_id +} + +output "basic_storage_account_name" { + description = "Name of the basic test storage account" + value = module.basic_storage.storage_account_name +} + +output "basic_container_id" { + description = "ID of the basic test container" + value = module.basic_storage.container_id +} + +output "basic_primary_blob_endpoint" { + description = "Primary blob endpoint of the basic test storage account" + value = module.basic_storage.primary_blob_endpoint +} + +output "cmk_storage_account_name" { + description = "Name of the CMK test storage account" + value = module.cmk_storage.storage_account_name +} + +output "lifecycle_storage_account_name" { + description = "Name of the lifecycle test storage account" + value = module.lifecycle_storage.storage_account_name +} + +output "geo_storage_replication_type" { + description = "Replication type of the geo test storage account" + value = module.geo_storage.replication_type +} + +output "minimal_storage_versioning_enabled" { + description = "Versioning status of the minimal test storage account" + value = module.minimal_storage.versioning_enabled +} diff --git a/terraform/azure/variables.tf b/terraform/azure/variables.tf new file mode 100644 index 0000000..6d7a0e1 --- /dev/null +++ b/terraform/azure/variables.tf @@ -0,0 +1,301 @@ +# ----------------------------------------------------------------------------- +# Required Variables +# ----------------------------------------------------------------------------- + +variable "storage_account_name" { + description = "Name of the Azure Storage Account. Must be globally unique, between 3-24 characters, and contain only lowercase letters and numbers." + type = string + + validation { + condition = can(regex("^[a-z0-9]{3,24}$", var.storage_account_name)) + error_message = "Storage account name must be between 3-24 characters, lowercase letters and numbers only." + } +} + +variable "resource_group_name" { + description = "Name of the resource group where the storage account will be created." + type = string +} + +variable "location" { + description = "Azure region where the storage account will be created (e.g., 'eastus', 'westeurope')." + type = string +} + +variable "container_name" { + description = "Name of the blob container. Must be between 3-63 characters, lowercase letters, numbers, and hyphens only." + type = string + + validation { + condition = can(regex("^[a-z0-9]([a-z0-9-]{1,61}[a-z0-9])?$", var.container_name)) + error_message = "Container name must be between 3-63 characters, start and end with lowercase letter or number, and contain only lowercase letters, numbers, and hyphens." + } +} + +# ----------------------------------------------------------------------------- +# Storage Account Configuration +# ----------------------------------------------------------------------------- + +variable "account_tier" { + description = "Storage account tier. Standard for general-purpose v2, Premium for high-performance scenarios." + type = string + default = "Standard" + + validation { + condition = contains(["Standard", "Premium"], var.account_tier) + error_message = "Account tier must be either 'Standard' or 'Premium'." + } +} + +variable "replication_type" { + description = "Storage account replication type. Options: LRS (Locally Redundant), GRS (Geo-Redundant), RAGRS (Read-Access Geo-Redundant), ZRS (Zone-Redundant), GZRS (Geo-Zone-Redundant), RAGZRS (Read-Access Geo-Zone-Redundant)." + type = string + default = "LRS" + + validation { + condition = contains(["LRS", "GRS", "RAGRS", "ZRS", "GZRS", "RAGZRS"], var.replication_type) + error_message = "Replication type must be one of: LRS, GRS, RAGRS, ZRS, GZRS, RAGZRS." + } +} + +variable "account_kind" { + description = "Storage account kind. StorageV2 is recommended for most scenarios." + type = string + default = "StorageV2" + + validation { + condition = contains(["BlobStorage", "BlockBlobStorage", "FileStorage", "Storage", "StorageV2"], var.account_kind) + error_message = "Account kind must be one of: BlobStorage, BlockBlobStorage, FileStorage, Storage, StorageV2." + } +} + +# ----------------------------------------------------------------------------- +# Security Configuration +# ----------------------------------------------------------------------------- + +variable "enable_https_traffic_only" { + description = "Enforce HTTPS-only traffic to the storage account. Recommended to keep enabled for security." + type = bool + default = true +} + +variable "min_tls_version" { + description = "Minimum TLS version for requests to the storage account." + type = string + default = "TLS1_2" + + validation { + condition = contains(["TLS1_0", "TLS1_1", "TLS1_2"], var.min_tls_version) + error_message = "Minimum TLS version must be one of: TLS1_0, TLS1_1, TLS1_2." + } +} + +variable "allow_public_access" { + description = "Allow public access to blobs. Recommended to keep disabled (false) for security." + type = bool + default = false +} + +variable "infrastructure_encryption_enabled" { + description = "Enable infrastructure encryption for additional security layer. Recommended for sensitive data." + type = bool + default = true +} + +# ----------------------------------------------------------------------------- +# Container Configuration +# ----------------------------------------------------------------------------- + +variable "container_access_type" { + description = "Access type for the container. Options: 'private' (no public access), 'blob' (public read for blobs only), 'container' (public read for container and blobs)." + type = string + default = "private" + + validation { + condition = contains(["private", "blob", "container"], var.container_access_type) + error_message = "Container access type must be one of: private, blob, container." + } +} + +variable "container_metadata" { + description = "Metadata to assign to the container as key-value pairs." + type = map(string) + default = {} +} + +# ----------------------------------------------------------------------------- +# Data Protection Configuration +# ----------------------------------------------------------------------------- + +variable "versioning_enabled" { + description = "Enable blob versioning to protect against accidental deletion and provide object history. Recommended for production." + type = bool + default = true +} + +variable "blob_soft_delete_retention_days" { + description = "Number of days to retain deleted blobs. Set to 0 to disable soft delete. Recommended: 7-30 days." + type = number + default = 7 + + validation { + condition = var.blob_soft_delete_retention_days >= 0 && var.blob_soft_delete_retention_days <= 365 + error_message = "Blob soft delete retention days must be between 0 and 365." + } +} + +variable "container_soft_delete_retention_days" { + description = "Number of days to retain deleted containers. Set to 0 to disable soft delete. Recommended: 7-30 days." + type = number + default = 7 + + validation { + condition = var.container_soft_delete_retention_days >= 0 && var.container_soft_delete_retention_days <= 365 + error_message = "Container soft delete retention days must be between 0 and 365." + } +} + +variable "change_feed_enabled" { + description = "Enable change feed to track create, update, and delete changes to blobs. Useful for auditing and event-driven architectures." + type = bool + default = false +} + +variable "last_access_time_enabled" { + description = "Enable last access time tracking for lifecycle management policies based on access patterns." + type = bool + default = false +} + +# ----------------------------------------------------------------------------- +# Network Security Configuration +# ----------------------------------------------------------------------------- + +variable "network_rules_enabled" { + description = "Enable network rules to restrict access to the storage account." + type = bool + default = false +} + +variable "network_rules_default_action" { + description = "Default action for network rules. 'Deny' blocks all traffic except allowed IPs/VNets. 'Allow' permits all traffic." + type = string + default = "Deny" + + validation { + condition = contains(["Allow", "Deny"], var.network_rules_default_action) + error_message = "Network rules default action must be either 'Allow' or 'Deny'." + } +} + +variable "network_rules_bypass" { + description = "Services that can bypass network rules. Options: 'AzureServices', 'Logging', 'Metrics', 'None'." + type = list(string) + default = ["AzureServices"] + + validation { + condition = alltrue([ + for service in var.network_rules_bypass : + contains(["AzureServices", "Logging", "Metrics", "None"], service) + ]) + error_message = "Each bypass value must be one of: AzureServices, Logging, Metrics, None." + } +} + +variable "network_rules_ip_rules" { + description = "List of public IP addresses or CIDR ranges that can access the storage account." + type = list(string) + default = [] +} + +variable "network_rules_subnet_ids" { + description = "List of virtual network subnet IDs that can access the storage account." + type = list(string) + default = [] +} + +# ----------------------------------------------------------------------------- +# Encryption Configuration +# ----------------------------------------------------------------------------- + +variable "customer_managed_key_vault_key_id" { + description = "Key Vault Key ID for customer-managed encryption keys. If not specified, Microsoft-managed keys will be used." + type = string + default = null +} + +variable "customer_managed_key_user_assigned_identity_id" { + description = "User-assigned managed identity ID for accessing the customer-managed key. If not specified, system-assigned identity will be used." + type = string + default = null +} + +# ----------------------------------------------------------------------------- +# Lifecycle Management Configuration +# ----------------------------------------------------------------------------- + +variable "lifecycle_rules" { + description = <<-EOT + List of lifecycle management rules for optimizing storage costs. + Each rule can include: + - name: Unique name for the rule + - enabled: Whether the rule is active + - prefix_match: List of blob prefixes to match (optional) + - blob_types: Types of blobs to apply the rule to (e.g., ["blockBlob"]) + - base_blob_actions: Actions for current versions + - tier_to_cool_after_days: Days to tier to cool storage + - tier_to_cool_after_last_access_days: Days since last access to tier to cool + - tier_to_archive_after_days: Days to tier to archive storage + - tier_to_archive_after_last_access_days: Days since last access to tier to archive + - delete_after_days: Days to delete blobs + - delete_after_last_access_days: Days since last access to delete + - snapshot_actions: Actions for snapshots + - version_actions: Actions for older versions + + Note: This type structure mirrors Azure's Lifecycle Management API structure. + Complex nested objects are required to support all Azure lifecycle features. + EOT + type = list(object({ + name = string + enabled = bool + prefix_match = optional(list(string), []) + blob_types = list(string) + base_blob_actions = optional(object({ + tier_to_cool_after_days = optional(number) + tier_to_cool_after_last_access_days = optional(number) + tier_to_archive_after_days = optional(number) + tier_to_archive_after_last_access_days = optional(number) + delete_after_days = optional(number) + delete_after_last_access_days = optional(number) + })) + snapshot_actions = optional(object({ + tier_to_cool_after_days = optional(number) + tier_to_archive_after_days = optional(number) + delete_after_days = optional(number) + })) + version_actions = optional(object({ + tier_to_cool_after_days = optional(number) + tier_to_archive_after_days = optional(number) + delete_after_days = optional(number) + })) + })) + default = [] + + validation { + condition = alltrue([ + for rule in var.lifecycle_rules : + length(rule.blob_types) > 0 + ]) + error_message = "Each lifecycle rule must specify at least one blob type." + } +} + +# ----------------------------------------------------------------------------- +# General Configuration +# ----------------------------------------------------------------------------- + +variable "tags" { + description = "A map of tags to add to all resources. Use this to add consistent tagging across your infrastructure." + type = map(string) + default = {} +}
"AzureServices"
]