diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 72c8f8e..4ab9367 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ # AVM core team owns key files -.github/policies/ @Azure/avm-core-team-technical -.github/CODEOWNERS @Azure/avm-core-team-technical +.github/policies/ @Azure/avm-core-team-technical-terraform +.github/CODEOWNERS @Azure/avm-core-team-technical-terraform diff --git a/.github/ISSUE_TEMPLATE/avm_module_issue.yml b/.github/ISSUE_TEMPLATE/avm_module_issue.yml index 8b05245..ec5d7a9 100644 --- a/.github/ISSUE_TEMPLATE/avm_module_issue.yml +++ b/.github/ISSUE_TEMPLATE/avm_module_issue.yml @@ -27,7 +27,6 @@ body: - "" - "Feature Request" - "Bug" - - "Security Bug" - "I'm not sure" validations: required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1346202..534ea7d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -16,11 +16,11 @@ Closes #456 - [ ] Non-module change (e.g. CI/CD, documentation, etc.) - [ ] Azure Verified Module updates: - - [ ] Bugfix containing backwards compatible bug fixes, and I have NOT bumped the MAJOR or MINOR version in `locals.version.tf.json`: + - [ ] Bugfix containing backwards compatible bug fixes - [ ] Someone has opened a bug report issue, and I have included "Closes #{bug_report_issue_number}" in the PR description. - [ ] The bug was found by the module author, and no one has opened an issue to report it yet. - - [ ] Feature update backwards compatible feature updates, and I have bumped the MINOR version in `locals.version.tf.json`. - - [ ] Breaking changes and I have bumped the MAJOR version in `locals.version.tf.json`. + - [ ] Feature update backwards compatible feature updates. + - [ ] Breaking changes. - [ ] Update to documentation # Checklist diff --git a/.github/policies/eventResponder.yml b/.github/policies/eventResponder.yml index 1d5ba40..bad111d 100644 --- a/.github/policies/eventResponder.yml +++ b/.github/policies/eventResponder.yml @@ -17,18 +17,6 @@ configuration: then: - addLabel: label: "Needs: Triage :mag:" - - addReply: - reply: | - > [!IMPORTANT] - > **The "Needs: Triage :mag:" label must be removed once the triage process is complete!** - - - - > [!NOTE] - > This label was added as per [ITA06](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita06). - description: 'ITA09 - When #RR is used in an issue, add the "Needs: Author Feedback :ear:" label' if: @@ -43,10 +31,6 @@ configuration: then: - addLabel: label: "Needs: Author Feedback :ear:" - - addReply: - reply: | - > [!NOTE] - > The "Needs: Author Feedback :ear:" label was added as per [ITA09](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita09). - description: 'ITA10 - When #wontfix is used in an issue, mark it by using the label of "Status: Won''t Fix :broken_heart:"' if: @@ -62,10 +46,6 @@ configuration: - addLabel: label: "Status: Won't Fix :broken_heart:" - closeIssue - - addReply: - reply: | - > [!NOTE] - > The "Status: Won't Fix :broken_heart:" label was added and the issue was closed as per [ITA10](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita10). - description: 'ITA11 - When a reply from anyone to an issue occurs, remove the "Needs: Author Feedback :ear:" label and label with "Needs: Attention :wave:"' if: @@ -82,10 +62,6 @@ configuration: label: "Needs: Author Feedback :ear:" - addLabel: label: "Needs: Attention :wave:" - - addReply: - reply: | - > [!NOTE] - > The "Needs: Author Feedback :ear:" label was removed and the "Needs: Attention :wave:" label was added as per [ITA11](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita11). - description: "ITA12 - Clean email replies on every comment" if: @@ -113,16 +89,10 @@ configuration: label: "Type: New Module Proposal :bulb:" - hasLabel: label: "Type: Question/Feedback :raising_hand:" - - hasLabel: - label: "Type: Security Bug :lock:" - isAssignedToSomeone then: - removeLabel: label: "Needs: Triage :mag:" - - addReply: - reply: | - > [!NOTE] - > The "Needs: Triage :mag:" label was removed as per [ITA15](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita15). - description: 'ITA20 - If the type is feature request, add the "Type: Feature Request :heavy_plus_sign:" label on the issue' if: @@ -140,10 +110,6 @@ configuration: then: - addLabel: label: "Type: Feature Request :heavy_plus_sign:" - - addReply: - reply: | - > [!NOTE] - > The "Type: Feature Request :heavy_plus_sign:" label was added as per [ITA20](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita20). - description: 'ITA21 - If the type is bug, add the "Type: Bug :bug:" label on the issue' if: @@ -161,32 +127,6 @@ configuration: then: - addLabel: label: "Type: Bug :bug:" - - addReply: - reply: | - > [!NOTE] - > The "Type: Bug :bug:" label was added as per [ITA21](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita21). - - - description: 'ITA22 - If the type is security bug, add the "Type: Security Bug :lock:" label on the issue' - if: - - payloadType: Issues - - isAction: - action: Opened - - bodyContains: - pattern: | - ### Issue Type? - - Security Bug - - not: - hasLabel: - label: "Type: Security Bug :lock:" - then: - - addLabel: - label: "Type: Security Bug :lock:" - - addReply: - reply: | - > [!NOTE] - > The "Type: Security Bug :lock:" label was added as per [ITA22](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita22). - - description: 'ITA23 - Remove the "Status: In PR" label from an issue when it''s closed.' if: @@ -198,7 +138,3 @@ configuration: then: - removeLabel: label: "Status: In PR :point_right:" - - addReply: - reply: | - > [!NOTE] - > The "Status: In PR :point_right:" label was removed as per [ITA23](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita23). diff --git a/.github/policies/scheduledSearches.yml b/.github/policies/scheduledSearches.yml index 452cca7..3b4b468 100644 --- a/.github/policies/scheduledSearches.yml +++ b/.github/policies/scheduledSearches.yml @@ -36,9 +36,6 @@ configuration: > [!TIP] > - To prevent further actions to take effect, the "Status: Response Overdue 🚩" label must be removed, once this issue has been responded to. > - To avoid this rule being (re)triggered, the ""Needs: Triage :mag:" label must be removed as part of the triage process (when the issue is first responded to)! - - > [!NOTE] - > This message was posted as per [ITA01TF](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita01tf1-2). - addLabel: label: "Status: Response Overdue :triangular_flag_on_post:" @@ -68,9 +65,6 @@ configuration: > [!TIP] > - To prevent further actions to take effect, the "Status: Response Overdue 🚩" label must be removed, once this issue has been responded to. > - To avoid this rule being (re)triggered, the ""Needs: Triage :mag:" label must be removed as part of the triage process (when the issue is first responded to)! - - > [!NOTE] - > This message was posted as per [ITA01TF](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita01tf1-2). - addLabel: label: "Status: Response Overdue :triangular_flag_on_post:" - assignTo: @@ -105,9 +99,6 @@ configuration: > [!TIP] > - To avoid this rule being (re)triggered, the "Needs: Triage :mag:" and "Status: Response Overdue :triangular_flag_on_post:" labels must be removed when the issue is first responded to! > - Remove the "Needs: Immediate Attention :bangbang:" label once the issue has been responded to. - - > [!NOTE] - > This message was posted as per [ITA02TF](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita02tf1-2). - addLabel: label: "Needs: Immediate Attention :bangbang:" @@ -137,9 +128,6 @@ configuration: > [!TIP] > - To avoid this rule being (re)triggered, the "Needs: Triage :mag:" and "Status: Response Overdue :triangular_flag_on_post:" labels must be removed when the issue is first responded to! > - Remove the "Needs: Immediate Attention :bangbang:" label once the issue has been responded to. - - > [!NOTE] - > This message was posted as per [ITA02TF](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita02tf1-2). - addLabel: label: "Needs: Immediate Attention :bangbang:" @@ -182,9 +170,6 @@ configuration: > [!TIP] > - To avoid this rule being (re)triggered, the "Needs: Triage :mag:" and "Status: Response Overdue :triangular_flag_on_post:" labels must be removed when the issue is first responded to! > - Remove the "Needs: Immediate Attention :bangbang:" label once the issue has been responded to. - - > [!NOTE] - > This message was posted as per [ITA03TF](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita03tf). - addLabel: label: "Needs: Immediate Attention :bangbang:" - assignTo: @@ -213,15 +198,6 @@ configuration: > [!IMPORTANT] > @${issueAuthor}, this issue has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for **4 days**. It will be closed if no further activity occurs **within 3 days of this comment**. - > [!TIP] - > To prevent further actions to take effect, one of the following conditions must be met: - > - The author must respond in a comment within 3 days of this comment. - > - The "Status: No Recent Activity :zzz:" label must be removed. - > - If applicable, the "Status: Long Term :hourglass_flowing_sand:" or the "Needs: Module Owner :mega:" label must be added. - - > [!NOTE] - > This message was posted as per [ITA04](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita04). - - description: 'ITA05A - Close issues that have been marked as requiring author feedback but have not had any activity for 3 days, unless it''s been marked with the "Status long term" label.' frequencies: - hourly: @@ -242,12 +218,6 @@ configuration: reply: | > [!WARNING] > @${issueAuthor}, this issue will now be closed, as it has been marked as requiring author feedback but has not had any activity for **7 days**. - - > [!TIP] - > In case this issue needs to be reopened (e.g., the author responds after the issue was closed), the "Status: No Recent Activity :zzz:" label must be removed. - - > [!NOTE] - > This message was posted as per [ITA05](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita05). - closeIssue - description: 'ITA05B - Close issues that have been marked as requiring author feedback but have not had any activity for 3 days, unless it''s been marked with the "Status long term" label.' @@ -270,10 +240,4 @@ configuration: reply: | > [!WARNING] > @${issueAuthor}, this issue will now be closed, as it has been marked as requiring author feedback but has not had any activity for **7 days**. - - > [!TIP] - > In case this issue needs to be reopened (e.g., the author responds after the issue was closed), the "Status: No Recent Activity :zzz:" label must be removed. - - > [!NOTE] - > This message was posted as per [ITA05](https://azure.github.io/Azure-Verified-Modules/help-support/issue-triage/issue-triage-automation/#ita05). - closeIssue diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 82241a4..6203da5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -27,7 +27,7 @@ jobs: testexamples: if: github.event.repository.name != 'terraform-azurerm-avm-template' - runs-on: [ self-hosted, 1ES.Pool=terraform-azurerm-avm-template ] + runs-on: [ self-hosted, 1ES.Pool=4e2ea4bf66957bb6f1e7d358cc4e00d88841e3b0 ] needs: getexamples environment: test env: @@ -42,6 +42,9 @@ jobs: - name: Test example shell: bash + env: + SECRETS_CONTEXT: ${{ toJson(secrets) }} + VARS_CONTEXT: ${{ toJson(vars) }} run: | set -e MAX_RETRIES=10 @@ -56,10 +59,35 @@ jobs: echo "Failed to login after $MAX_RETRIES attempts." exit 1 fi + + declare -A secrets + eval "$(echo $SECRETS_CONTEXT | jq -r 'to_entries[] | @sh "secrets[\(.key|tostring)]=\(.value|tostring)"')" + + declare -A variables + eval "$(echo $VARS_CONTEXT | jq -r 'to_entries[] | @sh "variables[\(.key|tostring)]=\(.value|tostring)"')" + + for key in "${!secrets[@]}"; do + if [[ $key = \TF_VAR_* ]]; then + lowerKey=$(echo "$key" | tr '[:upper:]' '[:lower:]') + finalKey=${lowerKey/tf_var_/TF_VAR_} + export "$finalKey"="${secrets[$key]}" + fi + done + + for key in "${!variables[@]}"; do + if [[ $key = \TF_VAR_* ]]; then + lowerKey=$(echo "$key" | tr '[:upper:]' '[:lower:]') + finalKey=${lowerKey/tf_var_/TF_VAR_} + export "$finalKey"="${variables[$key]}" + fi + done + + echo -e "Custom environment variables:\n$(env | grep TF_VAR_ | grep -v ' "TF_VAR_')" + export ARM_SUBSCRIPTION_ID=$(az login --identity --username $MSI_ID | jq -r '.[0] | .id') export ARM_TENANT_ID=$(az login --identity --username $MSI_ID | jq -r '.[0] | .tenantId') export ARM_CLIENT_ID=$(az identity list | jq -r --arg MSI_ID "$MSI_ID" '.[] | select(.principalId == $MSI_ID) | .clientId') - docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/src -w /src --network=host -e TF_IN_AUTOMATION -e TF_VAR_enable_telemetry -e AVM_MOD_PATH=/src -e AVM_EXAMPLE=${{ matrix.example }} -e MSI_ID -e ARM_SUBSCRIPTION_ID -e ARM_TENANT_ID -e ARM_CLIENT_ID -e ARM_USE_MSI=true mcr.microsoft.com/azterraform:latest make test-example + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $(pwd):/src -w /src --network=host -e TF_IN_AUTOMATION -e TF_VAR_enable_telemetry -e AVM_MOD_PATH=/src -e AVM_EXAMPLE=${{ matrix.example }} -e MSI_ID -e ARM_SUBSCRIPTION_ID -e ARM_TENANT_ID -e ARM_CLIENT_ID -e ARM_USE_MSI=true --env-file <(env | grep TF_VAR_ | grep -v ' "TF_VAR_') mcr.microsoft.com/azterraform:latest make test-example # This job is only run when all the previous jobs are successful. # We can use it for PR validation to ensure all examples have completed. diff --git a/.gitignore b/.gitignore index 8bd03a5..83e17fd 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ avm.tflint_example.merged.hcl *.md.tmp # MacOS .DS_Store +avm.tflint_module.hcl +avm.tflint_module.merged.hcl diff --git a/.terraform-docs.yml b/.terraform-docs.yml index dd5d16b..5f3b231 100644 --- a/.terraform-docs.yml +++ b/.terraform-docs.yml @@ -4,7 +4,7 @@ formatter: "markdown document" # this is required -version: "~> 0.17.0" +version: "~> 0.18" header-from: "_header.md" footer-from: "_footer.md" @@ -23,8 +23,6 @@ content: |- {{ .Requirements }} - {{ .Providers }} - {{ .Resources }} diff --git a/README.md b/README.md index 0d52c19..a8cf795 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ -# terraform-azurerm-avm-template +# AVM module for SQL Managed Instance -This is a template repo for Terraform Azure Verified Modules. +This is an Azure Verified Modules for SQL Managed Instances. -Things to do: +The module supports the following capabilities: -1. Set up a GitHub repo environment called `test`. -1. Configure environment protection rule to ensure that approval is required before deploying to this environment. -1. Create a user-assigned managed identity in your test subscription. -1. Create a role assignment for the managed identity on your test subscription, use the minimum required role. -1. Configure federated identity credentials on the user assigned managed identity. Use the GitHub environment. -1. Search and update TODOs within the code and remove the TODO comments once complete. +* All supported AzureRM parameters for the `azurerm_mssql_managed_instance` resource. +* Advanced Threat Protection, enabled by default. +* Vulnerability Assessments & Security Access Policies with a restricted storage account (supplied via `storage_account_resource_id`) +* Configuration for a failover group +* A map of databases, along with support for all parameters, such as long term backup retention policies. > [!IMPORTANT] > As the overall AVM framework is not GA (generally available) yet - the CI framework and test automation is not fully functional and implemented across all supported languages yet - breaking changes are expected, and additional customer feedback is yet to be gathered and incorporated. Hence, modules **MUST NOT** be published at version `1.0.0` or higher at this time. @@ -26,36 +25,64 @@ The following requirements are needed by this module: - [terraform](#requirement\_terraform) (~> 1.5) -- [azurerm](#requirement\_azurerm) (~> 3.71) - -- [random](#requirement\_random) (~> 3.5) +- [azapi](#requirement\_azapi) (~> 1.13) -## Providers - -The following providers are used by this module: +- [azurerm](#requirement\_azurerm) (~> 3.71) -- [azurerm](#provider\_azurerm) (~> 3.71) +- [modtm](#requirement\_modtm) (~> 0.3) -- [random](#provider\_random) (~> 3.5) +- [random](#requirement\_random) (~> 3.5) ## Resources The following resources are used by this module: +- [azapi_resource_action.mssql_managed_instance_security_alert_policy](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource_action) (resource) +- [azapi_resource_action.mssql_managed_instance_vulnerability_assessment](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource_action) (resource) +- [azapi_resource_action.sql_advanced_threat_protection](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource_action) (resource) +- [azapi_resource_action.sql_managed_instance_patch_identities](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource_action) (resource) - [azurerm_management_lock.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/management_lock) (resource) +- [azurerm_monitor_diagnostic_setting.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_diagnostic_setting) (resource) +- [azurerm_mssql_managed_database.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/mssql_managed_database) (resource) +- [azurerm_mssql_managed_instance.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/mssql_managed_instance) (resource) +- [azurerm_mssql_managed_instance_active_directory_administrator.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/mssql_managed_instance_active_directory_administrator) (resource) +- [azurerm_mssql_managed_instance_failover_group.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/mssql_managed_instance_failover_group) (resource) +- [azurerm_mssql_managed_instance_transparent_data_encryption.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/mssql_managed_instance_transparent_data_encryption) (resource) - [azurerm_private_endpoint.this_managed_dns_zone_groups](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) (resource) - [azurerm_private_endpoint.this_unmanaged_dns_zone_groups](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) (resource) - [azurerm_private_endpoint_application_security_group_association.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint_application_security_group_association) (resource) -- [azurerm_resource_group.TODO](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) (resource) -- [azurerm_resource_group_template_deployment.telemetry](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group_template_deployment) (resource) +- [azurerm_role_assignment.sqlmi_system_assigned](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource) - [azurerm_role_assignment.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) (resource) -- [random_id.telem](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) (resource) +- [modtm_telemetry.telemetry](https://registry.terraform.io/providers/Azure/modtm/latest/docs/resources/telemetry) (resource) +- [random_uuid.telemetry](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/uuid) (resource) +- [azapi_resource.identity](https://registry.terraform.io/providers/Azure/azapi/latest/docs/data-sources/resource) (data source) +- [azurerm_client_config.telemetry](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) (data source) +- [azurerm_resource_group.parent](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/resource_group) (data source) +- [modtm_module_source.telemetry](https://registry.terraform.io/providers/Azure/modtm/latest/docs/data-sources/module_source) (data source) ## Required Inputs The following input variables are required: +### [administrator\_login](#input\_administrator\_login) + +Description: (Required) The administrator login name for the new SQL Managed Instance. Changing this forces a new resource to be created. + +Type: `string` + +### [administrator\_login\_password](#input\_administrator\_login\_password) + +Description: (Required) The password associated with the `administrator_login` user. Needs to comply with Azure's [Password Policy](https://msdn.microsoft.com/library/ms161959.aspx) + +Type: `string` + +### [license\_type](#input\_license\_type) + +Description: (Required) What type of license the Managed Instance will use. Possible values are `LicenseIncluded` and `BasePrice`. + +Type: `string` + ### [location](#input\_location) Description: Azure region where the resource should be deployed. @@ -74,34 +101,126 @@ Description: The resource group where the resources will be deployed. Type: `string` +### [sku\_name](#input\_sku\_name) + +Description: (Required) Specifies the SKU Name for the SQL Managed Instance. Valid values include `GP_Gen4`, `GP_Gen5`, `GP_Gen8IM`, `GP_Gen8IH`, `BC_Gen4`, `BC_Gen5`, `BC_Gen8IM` or `BC_Gen8IH`. + +Type: `string` + +### [storage\_size\_in\_gb](#input\_storage\_size\_in\_gb) + +Description: (Required) Maximum storage space for the SQL Managed instance. This should be a multiple of 32 (GB). + +Type: `number` + +### [subnet\_id](#input\_subnet\_id) + +Description: (Required) The subnet resource id that the SQL Managed Instance will be associated with. Changing this forces a new resource to be created. + +Type: `string` + +### [vcores](#input\_vcores) + +Description: (Required) Number of cores that should be assigned to the SQL Managed Instance. Values can be `8`, `16`, or `24` for Gen4 SKUs, or `4`, `6`, `8`, `10`, `12`, `16`, `20`, `24`, `32`, `40`, `48`, `56`, `64`, `80`, `96` or `128` for Gen5 SKUs. + +Type: `number` + ## Optional Inputs The following input variables are optional (have default values): -### [customer\_managed\_key](#input\_customer\_managed\_key) +### [active\_directory\_administrator](#input\_active\_directory\_administrator) -Description: A map describing customer-managed keys to associate with the resource. This includes the following properties: -- `key_vault_resource_id` - The resource ID of the Key Vault where the key is stored. -- `key_name` - The name of the key. -- `key_version` - (Optional) The version of the key. If not specified, the latest version is used. -- `user_assigned_identity` - (Optional) An object representing a user-assigned identity with the following properties: - - `resource_id` - The resource ID of the user-assigned identity. +Description: - `azuread_authentication_only` - (Optional) When `true`, only permit logins from AAD users and administrators. When `false`, also allow local database users. +- `login_username` - (Required) The login name of the principal to set as the Managed Instance Administrator. +- `object_id` - (Required) The Object ID of the principal to set as the Managed Instance Administrator. +- `tenant_id` - (Required) The Azure Active Directory Tenant ID. + +--- +`timeouts` block supports the following: +- `create` - (Defaults to 30 minutes) Used when creating the SQL Active Directory Administrator. +- `delete` - (Defaults to 30 minutes) Used when deleting the SQL Active Directory Administrator. +- `read` - (Defaults to 5 minutes) Used when retrieving the SQL Active Directory Administrator. +- `update` - (Defaults to 30 minutes) Used when updating the SQL Active Directory Administrator. Type: ```hcl object({ - key_vault_resource_id = string - key_name = string - key_version = optional(string, null) - user_assigned_identity = optional(object({ - resource_id = string - }), null) + azuread_authentication_only = optional(bool) + login_username = optional(string) + object_id = optional(string) + tenant_id = optional(string) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) }) ``` +Default: `{}` + +### [collation](#input\_collation) + +Description: (Optional) Specifies how the SQL Managed Instance will be collated. Default value is `SQL_Latin1_General_CP1_CI_AS`. Changing this forces a new resource to be created. + +Type: `string` + Default: `null` +### [databases](#input\_databases) + +Description: - `name` - (Required) The name of the Managed Database to create. Changing this forces a new resource to be created. +- `short_term_retention_days` - (Optional) The backup retention period in days. This is how many days Point-in-Time Restore will be supported. + +--- +`long_term_retention_policy` block supports the following: +- `monthly_retention` - (Optional) The monthly retention policy for an LTR backup in an ISO 8601 format. Valid value is between 1 to 120 months. e.g. `P1Y`, `P1M`, `P4W` or `P30D`. +- `week_of_year` - (Optional) The week of year to take the yearly backup. Value has to be between `1` and `52`. +- `weekly_retention` - (Optional) The weekly retention policy for an LTR backup in an ISO 8601 format. Valid value is between 1 to 520 weeks. e.g. `P1Y`, `P1M`, `P1W` or `P7D`. +- `yearly_retention` - (Optional) The yearly retention policy for an LTR backup in an ISO 8601 format. Valid value is between 1 to 10 years. e.g. `P1Y`, `P12M`, `P52W` or `P365D`. + +--- +`point_in_time_restore` block supports the following: +- `restore_point_in_time` - (Required) The point in time for the restore from `source_database_id`. Changing this forces a new resource to be created. +- `source_database_id` - (Required) The source database id that will be used to restore from. Changing this forces a new resource to be created. + +--- +`timeouts` block supports the following: +- `create` - (Defaults to 30 minutes) Used when creating the Mssql Managed Database. +- `delete` - (Defaults to 30 minutes) Used when deleting the Mssql Managed Database. +- `read` - (Defaults to 5 minutes) Used when retrieving the Mssql Managed Database. +- `update` - (Defaults to 30 minutes) Used when updating the Mssql Managed Database. + +Type: + +```hcl +map(object({ + name = string + short_term_retention_days = optional(number) + long_term_retention_policy = optional(object({ + monthly_retention = optional(string) + week_of_year = optional(number) + weekly_retention = optional(string) + yearly_retention = optional(string) + })) + point_in_time_restore = optional(object({ + restore_point_in_time = string + source_database_id = string + })) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) + })) +``` + +Default: `{}` + ### [diagnostic\_settings](#input\_diagnostic\_settings) Description: A map of diagnostic settings to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. @@ -136,6 +255,22 @@ map(object({ Default: `{}` +### [dns\_zone\_partner\_id](#input\_dns\_zone\_partner\_id) + +Description: (Optional) The ID of the SQL Managed Instance which will share the DNS zone. This is a prerequisite for creating an `azurerm_sql_managed_instance_failover_group`. Setting this after creation forces a new resource to be created. + +Type: `string` + +Default: `null` + +### [enable\_advanced\_threat\_protection](#input\_enable\_advanced\_threat\_protection) + +Description: (Optional) Whether to enabled Defender for SQL Advanced Threat Protection. + +Type: `bool` + +Default: `true` + ### [enable\_telemetry](#input\_enable\_telemetry) Description: This variable controls whether or not telemetry is enabled for the module. @@ -146,6 +281,51 @@ Type: `bool` Default: `true` +### [failover\_group](#input\_failover\_group) + +Description: +Map of failover groups. There can only be one failover group in the map. + + - `location` - (Required) The Azure Region where the Managed Instance Failover Group should exist. Changing this forces a new resource to be created. + - `name` - (Required) The name which should be used for this Managed Instance Failover Group. Changing this forces a new resource to be created. + - `partner_managed_instance_id` - (Required) The ID of the Azure SQL Managed Instance which will be replicated to. Changing this forces a new resource to be created. + - `readonly_endpoint_failover_policy_enabled` - (Optional) Failover policy for the read-only endpoint. Defaults to `true`. + + --- + `read_write_endpoint_failover_policy` block supports the following: + - `grace_minutes` - (Optional) Applies only if `mode` is `Automatic`. The grace period in minutes before failover with data loss is attempted. + - `mode` - (Required) The failover mode. Possible values are `Automatic` or `Manual`. + + --- + `timeouts` block supports the following: + - `create` - (Defaults to 30 minutes) Used when creating the Managed Instance Failover Group. + - `delete` - (Defaults to 30 minutes) Used when deleting the Managed Instance Failover Group. + - `read` - (Defaults to 5 minutes) Used when retrieving the Managed Instance Failover Group. + - `update` - (Defaults to 30 minutes) Used when updating the Managed Instance Failover Group. + +Type: + +```hcl +map(object({ + location = optional(string) + name = optional(string) + partner_managed_instance_id = optional(string) + readonly_endpoint_failover_policy_enabled = optional(bool) + read_write_endpoint_failover_policy = optional(object({ + grace_minutes = optional(number) + mode = optional(string) + })) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) + })) +``` + +Default: `{}` + ### [lock](#input\_lock) Description: Controls the Resource Lock configuration for this resource. The following properties can be specified: @@ -164,6 +344,14 @@ object({ Default: `null` +### [maintenance\_configuration\_name](#input\_maintenance\_configuration\_name) + +Description: (Optional) The name of the Public Maintenance Configuration window to apply to the SQL Managed Instance. Valid values include `SQL_Default` or an Azure Location in the format `SQL_{Location}_MI_{Size}`(for example `SQL_EastUS_MI_1`). Defaults to `SQL_Default`. + +Type: `string` + +Default: `null` + ### [managed\_identities](#input\_managed\_identities) Description: Controls the Managed Identity configuration on this resource. The following properties can be specified: @@ -182,6 +370,14 @@ object({ Default: `{}` +### [minimum\_tls\_version](#input\_minimum\_tls\_version) + +Description: (Optional) The Minimum TLS Version. Default value is `1.2` Valid values include `1.0`, `1.1`, `1.2`. + +Type: `string` + +Default: `"1.2"` + ### [private\_endpoints](#input\_private\_endpoints) Description: A map of private endpoints to create on this resource. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. @@ -215,6 +411,7 @@ map(object({ condition = optional(string, null) condition_version = optional(string, null) delegated_managed_identity_resource_id = optional(string, null) + principal_type = optional(string, null) })), {}) lock = optional(object({ kind = string @@ -246,6 +443,22 @@ Type: `bool` Default: `true` +### [proxy\_override](#input\_proxy\_override) + +Description: (Optional) Specifies how the SQL Managed Instance will be accessed. Default value is `Default`. Valid values include `Default`, `Proxy`, and `Redirect`. + +Type: `string` + +Default: `null` + +### [public\_data\_endpoint\_enabled](#input\_public\_data\_endpoint\_enabled) + +Description: (Optional) Is the public data endpoint enabled? Default value is `false`. + +Type: `bool` + +Default: `null` + ### [role\_assignments](#input\_role\_assignments) Description: A map of role assignments to create on this resource. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. @@ -270,11 +483,74 @@ map(object({ condition = optional(string, null) condition_version = optional(string, null) delegated_managed_identity_resource_id = optional(string, null) + principal_type = optional(string, null) })) ``` Default: `{}` +### [security\_alert\_policy](#input\_security\_alert\_policy) + +Description: - `disabled_alerts` - (Optional) Specifies an array of alerts that are disabled. Possible values are `Sql_Injection`, `Sql_Injection_Vulnerability`, `Access_Anomaly`, `Data_Exfiltration`, `Unsafe_Action` and `Brute_Force`. +- `email_account_admins_enabled` - (Optional) Boolean flag which specifies if the alert is sent to the account administrators or not. Defaults to `false`. +- `email_addresses` - (Optional) Specifies an array of email addresses to which the alert is sent. +- `enabled` - (Optional) Specifies the state of the Security Alert Policy, whether it is enabled or disabled. Possible values are `true`, `false`. +- `retention_days` - (Optional) Specifies the number of days to keep in the Threat Detection audit logs. Defaults to `0`. +- `storage_account_access_key` - (Optional) Specifies the identifier key of the Threat Detection audit storage account. This is mandatory when you use `storage_endpoint` to specify a storage account blob endpoint. +- `storage_endpoint` - (Optional) Specifies the blob storage endpoint (e.g. https://example.blob.core.windows.net). This blob storage will hold all Threat Detection audit logs. + +--- +`timeouts` block supports the following: +- `create` - (Defaults to 30 minutes) Used when creating the MS SQL Managed Instance Security Alert Policy. +- `delete` - (Defaults to 30 minutes) Used when deleting the MS SQL Managed Instance Security Alert Policy. +- `read` - (Defaults to 5 minutes) Used when retrieving the MS SQL Managed Instance Security Alert Policy. +- `update` - (Defaults to 30 minutes) Used when updating the MS SQL Managed Instance Security Alert Policy. + +Type: + +```hcl +object({ + disabled_alerts = optional(set(string)) + email_account_admins_enabled = optional(bool) + email_addresses = optional(set(string)) + enabled = optional(bool) + retention_days = optional(number) + storage_account_access_key = optional(string) + storage_endpoint = optional(string) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) + }) +``` + +Default: `{}` + +### [storage\_account\_resource\_id](#input\_storage\_account\_resource\_id) + +Description: (Optional) Storage Account to store vulnerability assessments. + +The System Assigned Managed Identity will be granted Storage Blob Data Contributor over this storage account. + +Note these limitations documented in Microsoft Learn - + +* User Assigned MIs are not supported +* The storage account firewall public network access must be allowed. If "Enabled from selected virtual networks and IP addresses" is set (recommended), the SQL MI subnet ID must be added to the storage account firewall. + +Type: `string` + +Default: `null` + +### [storage\_account\_type](#input\_storage\_account\_type) + +Description: (Optional) Specifies the storage account type used to store backups for this database. Changing this forces a new resource to be created. Possible values are `GRS`, `LRS` and `ZRS`. Defaults to `GRS`. + +Type: `string` + +Default: `"ZRS"` + ### [tags](#input\_tags) Description: (Optional) Tags of the resource. @@ -283,10 +559,121 @@ Type: `map(string)` Default: `null` +### [timeouts](#input\_timeouts) + +Description: - `create` - (Defaults to 24 hours) Used when creating the Microsoft SQL Managed Instance. +- `delete` - (Defaults to 24 hours) Used when deleting the Microsoft SQL Managed Instance. +- `read` - (Defaults to 5 minutes) Used when retrieving the Microsoft SQL Managed Instance. +- `update` - (Defaults to 24 hours) Used when updating the Microsoft SQL Managed Instance. + +Type: + +```hcl +object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + }) +``` + +Default: `null` + +### [timezone\_id](#input\_timezone\_id) + +Description: (Optional) The TimeZone ID that the SQL Managed Instance will be operating in. Default value is `UTC`. Changing this forces a new resource to be created. + +Type: `string` + +Default: `null` + +### [transparent\_data\_encryption](#input\_transparent\_data\_encryption) + +Description: - `auto_rotation_enabled` - (Optional) When enabled, the SQL Managed Instance will continuously check the key vault for any new versions of the key being used as the TDE protector. If a new version of the key is detected, the TDE protector on the SQL Managed Instance will be automatically rotated to the latest key version within 60 minutes. +- `key_vault_key_id` - (Optional) To use customer managed keys from Azure Key Vault, provide the AKV Key ID. To use service managed keys, omit this field. + +--- +`timeouts` block supports the following: +- `create` - (Defaults to 30 minutes) Used when creating the MSSQL. +- `delete` - (Defaults to 30 minutes) Used when deleting the MSSQL. +- `read` - (Defaults to 5 minutes) Used when retrieving the MSSQL. +- `update` - (Defaults to 30 minutes) Used when updating the MSSQL. + +Type: + +```hcl +object({ + auto_rotation_enabled = optional(bool) + key_vault_key_id = optional(string) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) + }) +``` + +Default: `{}` + +### [vulnerability\_assessment](#input\_vulnerability\_assessment) + +Description: - `storage_account_access_key` - (Optional) Specifies the identifier key of the storage account for vulnerability assessment scan results. If `storage_container_sas_key` isn't specified, `storage_account_access_key` is required. Set to `null` if the storage account is protected by a resource firewall. +- `storage_container_path` - (Required) A blob storage container path to hold the scan results (e.g. ). +- `storage_container_sas_key` - (Optional) A shared access signature (SAS Key) that has write access to the blob container specified in `storage_container_path` parameter. If `storage_account_access_key` isn't specified, `storage_container_sas_key` is required. Set to `null` if the storage account is protected by a resource firewall. + +--- +`recurring_scans` block supports the following: +- `email_subscription_admins` - (Optional) Boolean flag which specifies if the schedule scan notification will be sent to the subscription administrators. Defaults to `true`. +- `emails` - (Optional) Specifies an array of e-mail addresses to which the scan notification is sent. +- `enabled` - (Optional) Boolean flag which specifies if recurring scans is enabled or disabled. Defaults to `false`. + +--- +`timeouts` block supports the following: +- `create` - (Defaults to 60 minutes) Used when creating the Vulnerability Assessment. +- `delete` - (Defaults to 60 minutes) Used when deleting the Vulnerability Assessment. +- `read` - (Defaults to 5 minutes) Used when retrieving the Vulnerability Assessment. +- `update` - (Defaults to 60 minutes) Used when updating the Vulnerability Assessment. + +Type: + +```hcl +object({ + storage_account_access_key = optional(string) + storage_container_path = optional(string) + storage_container_sas_key = optional(string) + recurring_scans = optional(object({ + email_subscription_admins = optional(bool) + emails = optional(list(string)) + enabled = optional(bool) + })) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) + }) +``` + +Default: `null` + +### [zone\_redundant\_enabled](#input\_zone\_redundant\_enabled) + +Description: (Optional) If true, the SQL Managed Instance will be deployed with zone redundancy. Defaults to `true`. + +Type: `bool` + +Default: `true` + ## Outputs The following outputs are exported: +### [identity](#output\_identity) + +Description: Managed identities for the SQL MI instance. This is not available from the `resource` output because AzureRM doesn't yet support adding both User and System Assigned identities. + ### [private\_endpoints](#output\_private\_endpoints) Description: A map of the private endpoints created. @@ -295,6 +682,10 @@ Description: A map of the private endpoints created. Description: This is the full output for the resource. +### [resource\_id](#output\_resource\_id) + +Description: This is the resource ID of the resource. + ## Modules No modules. diff --git a/SUPPORT.md b/SUPPORT.md index 469db41..1a102d0 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -6,7 +6,7 @@ This project uses GitHub Issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue. -Issues can be created and searched through for existing [issues here](https://github.com/Azure/terraform-azurerm-avm-template/issues). +Issues can be created and searched through for existing [issues here](https://github.com/Azure/terraform-azurerm-avm-res-sql-managedinstance/issues). Please provide as much information as possible when filing an issue. Include screenshots or correlation IDs if possible (please redact any sensitive information). diff --git a/_header.md b/_header.md index 518d2f6..1989d91 100644 --- a/_header.md +++ b/_header.md @@ -1,19 +1,18 @@ -# terraform-azurerm-avm-template +# AVM module for SQL Managed Instance -This is a template repo for Terraform Azure Verified Modules. +This is an Azure Verified Modules for SQL Managed Instances. -Things to do: +The module supports the following capabilities: -1. Set up a GitHub repo environment called `test`. -1. Configure environment protection rule to ensure that approval is required before deploying to this environment. -1. Create a user-assigned managed identity in your test subscription. -1. Create a role assignment for the managed identity on your test subscription, use the minimum required role. -1. Configure federated identity credentials on the user assigned managed identity. Use the GitHub environment. -1. Search and update TODOs within the code and remove the TODO comments once complete. +* All supported AzureRM parameters for the `azurerm_mssql_managed_instance` resource. +* Advanced Threat Protection, enabled by default. +* Vulnerability Assessments & Security Access Policies with a restricted storage account (supplied via `storage_account_resource_id`) +* Configuration for a failover group +* A map of databases, along with support for all parameters, such as long term backup retention policies. > [!IMPORTANT] > As the overall AVM framework is not GA (generally available) yet - the CI framework and test automation is not fully functional and implemented across all supported languages yet - breaking changes are expected, and additional customer feedback is yet to be gathered and incorporated. Hence, modules **MUST NOT** be published at version `1.0.0` or higher at this time. -> +> > All module **MUST** be published as a pre-release version (e.g., `0.1.0`, `0.1.1`, `0.2.0`, etc.) until the AVM framework becomes GA. -> +> > However, it is important to note that this **DOES NOT** mean that the modules cannot be consumed and utilized. They **CAN** be leveraged in all types of environments (dev, test, prod etc.). Consumers can treat them just like any other IaC module and raise issues or feature requests against them as they learn from the usage of the module. Consumers should also read the release notes for each version, if considering updating to a more recent version of a module to see if there are any considerations or breaking changes etc. diff --git a/avm b/avm index 69f9ce0..9716dfb 100755 --- a/avm +++ b/avm @@ -18,4 +18,16 @@ if [ -z "$1" ]; then exit 1 fi -$CONTAINER_RUNTIME run --pull always --rm -v "$(pwd)":/src -w /src -e GITHUB_REPOSITORY -e GITHUB_REPOSITORY_OWNER mcr.microsoft.com/azterraform make "$1" +# Mount .azure directory if it exists +AZURE_VOLUME="" +if [ -d "$HOME/.azure" ]; then + AZURE_VOLUME="-v $HOME/.azure:/home/runtimeuser/.azure" +fi + +# Check if we are running in a container +# If we are then just run make directly +if [ -z "$AVM_IN_CONTAINER" ]; then + $CONTAINER_RUNTIME run --pull always --user "$(id -u):$(id -g)" --rm $AZURE_VOLUME -v /etc/passwd:/etc/passwd -v /etc/group:/etc/group -v "$(pwd)":/src -w /src -e GITHUB_REPOSITORY -e ARM_SUBSCRIPTION_ID -e GITHUB_REPOSITORY_OWNER mcr.microsoft.com/azterraform make "$1" +else + make "$1" +fi diff --git a/avm.bat b/avm.bat index cdfa812..6b177be 100644 --- a/avm.bat +++ b/avm.bat @@ -18,6 +18,6 @@ IF "%~1"=="" ( ) REM Run the make target with CONTAINER_RUNTIME -%CONTAINER_RUNTIME% run --pull always --rm -v "%cd%":/src -w /src -e GITHUB_REPOSITORY -e GITHUB_REPOSITORY_OWNER mcr.microsoft.com/azterraform make %1 +%CONTAINER_RUNTIME% run --pull always --rm -v "%cd%":/src -w /src --user "1000:1000" -e ARM_SUBSCRIPTION_ID -e GITHUB_REPOSITORY -e GITHUB_REPOSITORY_OWNER mcr.microsoft.com/azterraform make %1 ENDLOCAL diff --git a/examples/.terraform-docs.yml b/examples/.terraform-docs.yml index f97f3a3..bdfb331 100644 --- a/examples/.terraform-docs.yml +++ b/examples/.terraform-docs.yml @@ -4,7 +4,7 @@ formatter: "markdown document" # this is required -version: "~> 0.17.0" +version: "~> 0.18" header-from: "_header.md" footer-from: "_footer.md" @@ -27,8 +27,6 @@ content: |- {{ .Requirements }} - {{ .Providers }} - {{ .Resources }} diff --git a/examples/default/README.md b/examples/default/README.md index 1abcc3c..ecb6962 100644 --- a/examples/default/README.md +++ b/examples/default/README.md @@ -19,7 +19,11 @@ terraform { } provider "azurerm" { - features {} + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } } @@ -45,23 +49,212 @@ module "naming" { # This is required for resource modules resource "azurerm_resource_group" "this" { - location = module.regions.regions[random_integer.region_index.result].name + location = "japaneast" #module.regions.regions[random_integer.region_index.result].name name = module.naming.resource_group.name_unique } +resource "azurerm_network_security_group" "this" { + location = azurerm_resource_group.this.location + name = "mi-security-group" + resource_group_name = azurerm_resource_group.this.name +} + + +resource "azurerm_network_security_rule" "allow_management_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_management_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 106 + protocol = "Tcp" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_ranges = ["9000", "9003", "1438", "1440", "1452"] + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_misubnet_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_misubnet_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 200 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "10.0.0.0/24" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_health_probe_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_health_probe_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 300 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "AzureLoadBalancer" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_tds_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_tds_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 1000 + protocol = "Tcp" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "1433" + source_address_prefix = "VirtualNetwork" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "deny_all_inbound" { + access = "Deny" + direction = "Inbound" + name = "deny_all_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 4096 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_management_outbound" { + access = "Allow" + direction = "Outbound" + name = "allow_management_outbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 106 + protocol = "Tcp" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_ranges = ["80", "443", "12000"] + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_misubnet_outbound" { + access = "Allow" + direction = "Outbound" + name = "allow_misubnet_outbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 200 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "10.0.0.0/24" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "deny_all_outbound" { + access = "Deny" + direction = "Outbound" + name = "deny_all_outbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 4096 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_virtual_network" "this" { + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.this.location + name = "vnet-mi" + resource_group_name = azurerm_resource_group.this.name +} + +resource "azurerm_subnet" "this" { + address_prefixes = ["10.0.0.0/24"] + name = "subnet-mi" + resource_group_name = azurerm_resource_group.this.name + virtual_network_name = azurerm_virtual_network.this.name + + delegation { + name = "managedinstancedelegation" + + service_delegation { + name = "Microsoft.Sql/managedInstances" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action"] + } + } +} + +resource "azurerm_subnet_network_security_group_association" "this" { + network_security_group_id = azurerm_network_security_group.this.id + subnet_id = azurerm_subnet.this.id +} + +resource "azurerm_route_table" "this" { + location = azurerm_resource_group.this.location + name = "routetable-mi" + resource_group_name = azurerm_resource_group.this.name + bgp_route_propagation_enabled = false + + depends_on = [ + azurerm_subnet.this, + ] +} + +resource "azurerm_subnet_route_table_association" "this" { + route_table_id = azurerm_route_table.this.id + subnet_id = azurerm_subnet.this.id +} + +resource "random_password" "myadminpassword" { + length = 16 + keepers = { + trigger = timestamp() + } + override_special = "@#%*()-_=+[]{}:?" + special = true +} + +resource "azurerm_user_assigned_identity" "uami" { + location = azurerm_resource_group.this.location + name = "user-identity" + resource_group_name = azurerm_resource_group.this.name +} + # This is the module call -# Do not specify location here due to the randomization above. -# Leaving location as `null` will cause the module to use the resource group location -# with a data source. -module "test" { +module "sqlmi_test" { source = "../../" - # source = "Azure/avm--/azurerm" + # source = "Azure/avm-res-sql-managedinstance/azurerm" # ... - location = azurerm_resource_group.this.location - name = "TODO" # TODO update with module.naming..name_unique - resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = module.naming.mssql_managed_instance.name_unique + resource_group_name = azurerm_resource_group.this.name + administrator_login = "myspecialsqladmin" + administrator_login_password = random_password.myadminpassword.result + license_type = "LicenseIncluded" + sku_name = "GP_Gen5" + storage_size_in_gb = 32 + subnet_id = azurerm_subnet.this.id + vcores = "4" + managed_identities = { + system_assigned = true + user_assigned_resource_ids = [azurerm_user_assigned_identity.uami.id] + } - enable_telemetry = var.enable_telemetry # see variables.tf + depends_on = [ + azurerm_subnet_network_security_group_association.this, + azurerm_subnet_route_table_association.this, + ] } ``` @@ -76,20 +269,28 @@ The following requirements are needed by this module: - [random](#requirement\_random) (~> 3.5) -## Providers - -The following providers are used by this module: - -- [azurerm](#provider\_azurerm) (~> 3.74) - -- [random](#provider\_random) (~> 3.5) - ## Resources The following resources are used by this module: +- [azurerm_network_security_group.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group) (resource) +- [azurerm_network_security_rule.allow_health_probe_inbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.allow_management_inbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.allow_management_outbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.allow_misubnet_inbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.allow_misubnet_outbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.allow_tds_inbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.deny_all_inbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) +- [azurerm_network_security_rule.deny_all_outbound](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule) (resource) - [azurerm_resource_group.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) (resource) +- [azurerm_route_table.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/route_table) (resource) +- [azurerm_subnet.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) (resource) +- [azurerm_subnet_network_security_group_association.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_network_security_group_association) (resource) +- [azurerm_subnet_route_table_association.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_route_table_association) (resource) +- [azurerm_user_assigned_identity.uami](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) (resource) +- [azurerm_virtual_network.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network) (resource) - [random_integer.region_index](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/integer) (resource) +- [random_password.myadminpassword](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) (resource) ## Required Inputs @@ -98,17 +299,7 @@ No required inputs. ## Optional Inputs -The following input variables are optional (have default values): - -### [enable\_telemetry](#input\_enable\_telemetry) - -Description: This variable controls whether or not telemetry is enabled for the module. -For more information see . -If it is set to false, then no telemetry will be collected. - -Type: `bool` - -Default: `true` +No optional inputs. ## Outputs @@ -130,7 +321,7 @@ Source: Azure/regions/azurerm Version: ~> 0.3 -### [test](#module\_test) +### [sqlmi\_test](#module\_sqlmi\_test) Source: ../../ diff --git a/examples/default/main.tf b/examples/default/main.tf index 38cdd38..2d1ecc6 100644 --- a/examples/default/main.tf +++ b/examples/default/main.tf @@ -13,7 +13,11 @@ terraform { } provider "azurerm" { - features {} + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } } @@ -39,21 +43,210 @@ module "naming" { # This is required for resource modules resource "azurerm_resource_group" "this" { - location = module.regions.regions[random_integer.region_index.result].name + location = "japaneast" #module.regions.regions[random_integer.region_index.result].name name = module.naming.resource_group.name_unique } +resource "azurerm_network_security_group" "this" { + location = azurerm_resource_group.this.location + name = "mi-security-group" + resource_group_name = azurerm_resource_group.this.name +} + + +resource "azurerm_network_security_rule" "allow_management_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_management_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 106 + protocol = "Tcp" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_ranges = ["9000", "9003", "1438", "1440", "1452"] + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_misubnet_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_misubnet_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 200 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "10.0.0.0/24" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_health_probe_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_health_probe_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 300 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "AzureLoadBalancer" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_tds_inbound" { + access = "Allow" + direction = "Inbound" + name = "allow_tds_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 1000 + protocol = "Tcp" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "1433" + source_address_prefix = "VirtualNetwork" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "deny_all_inbound" { + access = "Deny" + direction = "Inbound" + name = "deny_all_inbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 4096 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_management_outbound" { + access = "Allow" + direction = "Outbound" + name = "allow_management_outbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 106 + protocol = "Tcp" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_ranges = ["80", "443", "12000"] + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_misubnet_outbound" { + access = "Allow" + direction = "Outbound" + name = "allow_misubnet_outbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 200 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "10.0.0.0/24" + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "deny_all_outbound" { + access = "Deny" + direction = "Outbound" + name = "deny_all_outbound" + network_security_group_name = azurerm_network_security_group.this.name + priority = 4096 + protocol = "*" + resource_group_name = azurerm_resource_group.this.name + destination_address_prefix = "*" + destination_port_range = "*" + source_address_prefix = "*" + source_port_range = "*" +} + +resource "azurerm_virtual_network" "this" { + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.this.location + name = "vnet-mi" + resource_group_name = azurerm_resource_group.this.name +} + +resource "azurerm_subnet" "this" { + address_prefixes = ["10.0.0.0/24"] + name = "subnet-mi" + resource_group_name = azurerm_resource_group.this.name + virtual_network_name = azurerm_virtual_network.this.name + + delegation { + name = "managedinstancedelegation" + + service_delegation { + name = "Microsoft.Sql/managedInstances" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", "Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action"] + } + } +} + +resource "azurerm_subnet_network_security_group_association" "this" { + network_security_group_id = azurerm_network_security_group.this.id + subnet_id = azurerm_subnet.this.id +} + +resource "azurerm_route_table" "this" { + location = azurerm_resource_group.this.location + name = "routetable-mi" + resource_group_name = azurerm_resource_group.this.name + bgp_route_propagation_enabled = false + + depends_on = [ + azurerm_subnet.this, + ] +} + +resource "azurerm_subnet_route_table_association" "this" { + route_table_id = azurerm_route_table.this.id + subnet_id = azurerm_subnet.this.id +} + +resource "random_password" "myadminpassword" { + length = 16 + keepers = { + trigger = timestamp() + } + override_special = "@#%*()-_=+[]{}:?" + special = true +} + +resource "azurerm_user_assigned_identity" "uami" { + location = azurerm_resource_group.this.location + name = "user-identity" + resource_group_name = azurerm_resource_group.this.name +} + # This is the module call -# Do not specify location here due to the randomization above. -# Leaving location as `null` will cause the module to use the resource group location -# with a data source. -module "test" { +module "sqlmi_test" { source = "../../" - # source = "Azure/avm--/azurerm" + # source = "Azure/avm-res-sql-managedinstance/azurerm" # ... - location = azurerm_resource_group.this.location - name = "TODO" # TODO update with module.naming..name_unique - resource_group_name = azurerm_resource_group.this.name + location = azurerm_resource_group.this.location + name = module.naming.mssql_managed_instance.name_unique + resource_group_name = azurerm_resource_group.this.name + administrator_login = "myspecialsqladmin" + administrator_login_password = random_password.myadminpassword.result + license_type = "LicenseIncluded" + sku_name = "GP_Gen5" + storage_size_in_gb = 32 + subnet_id = azurerm_subnet.this.id + vcores = "4" + managed_identities = { + system_assigned = true + user_assigned_resource_ids = [azurerm_user_assigned_identity.uami.id] + } - enable_telemetry = var.enable_telemetry # see variables.tf + depends_on = [ + azurerm_subnet_network_security_group_association.this, + azurerm_subnet_route_table_association.this, + ] } diff --git a/examples/default/variables.tf b/examples/default/variables.tf deleted file mode 100644 index e52bbd6..0000000 --- a/examples/default/variables.tf +++ /dev/null @@ -1,9 +0,0 @@ -variable "enable_telemetry" { - type = bool - default = true - description = <. -If it is set to false, then no telemetry will be collected. -DESCRIPTION -} diff --git a/locals.telemetry.tf b/locals.telemetry.tf deleted file mode 100644 index ff0467b..0000000 --- a/locals.telemetry.tf +++ /dev/null @@ -1,41 +0,0 @@ -locals { - # TODO: change this to the name of the module. See https://azure.github.io/Azure-Verified-Modules/specs/shared/#id-sfr3---category-telemetry---deploymentusage-telemetry - module_name = "CHANGEME" - # TODO: Change this. Should be either `res` or `ptn` - module_type = "res" - # This constructs the ARM deployment name that is used for the telemetry. - # We shouldn't ever hit the 64 character limit but use substr just in case. - telem_arm_deployment_name = substr( - format( - "%s.%s.%s.v%s.%s", - local.telem_puid, - local.module_type, - substr(local.module_name, 0, 30), - replace(local.module_version, ".", "-"), - local.telem_random_hex - ), - 0, - 64 - ) - # This is an empty ARM deployment template. - telem_arm_template_content = jsonencode( - { - "$schema" = "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion" = "1.0.0.0", - "parameters" = {}, - "variables" = {}, - "resources" = [], - "outputs" = { - "telemetry" = { - "type" = "String", - "value" = "For more information, see https://aka.ms/avm/telemetry" - } - } - } - ) - # This is the unique id AVM Terraform modules that is supplied by the AVM team. - # See https://azure.github.io/Azure-Verified-Modules/specs/shared/#id-sfr3---category-telemetry---deploymentusage-telemetry - telem_puid = "46d3xgtf" - # This ensures we don't get errors if telemetry is disabled. - telem_random_hex = can(random_id.telem[0].hex) ? random_id.telem[0].hex : "" -} diff --git a/locals.tf b/locals.tf index 04b8074..4e973eb 100644 --- a/locals.tf +++ b/locals.tf @@ -1,6 +1,4 @@ -# TODO: insert locals here. locals { - managed_identities = { system_assigned_user_assigned = (var.managed_identities.system_assigned || length(var.managed_identities.user_assigned_resource_ids) > 0) ? { this = { @@ -8,19 +6,7 @@ locals { user_assigned_resource_ids = var.managed_identities.user_assigned_resource_ids } } : {} - system_assigned = var.managed_identities.system_assigned ? { - this = { - type = "SystemAssigned" - } - } : {} - user_assigned = length(var.managed_identities.user_assigned_resource_ids) > 0 ? { - this = { - type = "UserAssigned" - user_assigned_resource_ids = var.managed_identities.user_assigned_resource_ids - } - } : {} } - # Private endpoint application security group associations. # We merge the nested maps from private endpoints and application security group associations into a single map. private_endpoint_application_security_group_associations = { for assoc in flatten([ @@ -32,7 +18,5 @@ locals { } ] ]) : "${assoc.pe_key}-${assoc.asg_key}" => assoc } - role_definition_resource_substring = "/providers/Microsoft.Authorization/roleDefinitions" - } diff --git a/locals.version.tf.json b/locals.version.tf.json deleted file mode 100644 index 6d43988..0000000 --- a/locals.version.tf.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "locals": { - "module_version": "0.1.0" - } -} diff --git a/main.database.tf b/main.database.tf new file mode 100644 index 0000000..9550a6e --- /dev/null +++ b/main.database.tf @@ -0,0 +1,37 @@ +resource "azurerm_mssql_managed_database" "this" { + for_each = var.databases + + managed_instance_id = azurerm_mssql_managed_instance.this.id + name = each.value.name + short_term_retention_days = each.value.short_term_retention_days + + dynamic "long_term_retention_policy" { + for_each = each.value.long_term_retention_policy == null ? [] : [each.value.long_term_retention_policy] + + content { + monthly_retention = long_term_retention_policy.value.monthly_retention + week_of_year = long_term_retention_policy.value.week_of_year + weekly_retention = long_term_retention_policy.value.weekly_retention + yearly_retention = long_term_retention_policy.value.yearly_retention + } + } + dynamic "point_in_time_restore" { + for_each = each.value.point_in_time_restore == null ? [] : [each.value.point_in_time_restore] + + content { + restore_point_in_time = point_in_time_restore.value.restore_point_in_time + source_database_id = point_in_time_restore.value.source_database_id + } + } + dynamic "timeouts" { + for_each = each.value.timeouts == null ? [] : [each.value.timeouts] + + content { + create = timeouts.value.create + delete = timeouts.value.delete + read = timeouts.value.read + update = timeouts.value.update + } + } +} + diff --git a/main.failovergroup.tf b/main.failovergroup.tf new file mode 100644 index 0000000..26b9d88 --- /dev/null +++ b/main.failovergroup.tf @@ -0,0 +1,29 @@ +resource "azurerm_mssql_managed_instance_failover_group" "this" { + for_each = var.failover_group + + location = each.value.location + managed_instance_id = azurerm_mssql_managed_instance.this.id + name = each.value.name + partner_managed_instance_id = each.value.partner_managed_instance_id + readonly_endpoint_failover_policy_enabled = each.value.readonly_endpoint_failover_policy_enabled + + dynamic "read_write_endpoint_failover_policy" { + for_each = [each.value.read_write_endpoint_failover_policy] + + content { + mode = read_write_endpoint_failover_policy.value.mode + grace_minutes = read_write_endpoint_failover_policy.value.grace_minutes + } + } + dynamic "timeouts" { + for_each = each.value.timeouts == null ? [] : [each.value.timeouts] + + content { + create = timeouts.value.create + delete = timeouts.value.delete + read = timeouts.value.read + update = timeouts.value.update + } + } +} + diff --git a/main.privateendpoint.tf b/main.privateendpoint.tf index 53e8b71..72e4250 100644 --- a/main.privateendpoint.tf +++ b/main.privateendpoint.tf @@ -1,4 +1,3 @@ -# TODO remove this code & var.private_endpoints if private link is not support. Note it must be included in this module if it is supported. resource "azurerm_private_endpoint" "this_managed_dns_zone_groups" { for_each = var.private_endpoints @@ -12,8 +11,8 @@ resource "azurerm_private_endpoint" "this_managed_dns_zone_groups" { private_service_connection { is_manual_connection = false name = each.value.private_service_connection_name != null ? each.value.private_service_connection_name : "pse-${var.name}" - private_connection_resource_id = azurerm_resource_group.TODO.id # TODO: Replace this dummy resource azurerm_resource_group.TODO with your module resource - subresource_names = ["TODO subresource name, see https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview#private-link-resource"] + private_connection_resource_id = azurerm_mssql_managed_instance.this.id + subresource_names = ["managedInstance"] } dynamic "ip_configuration" { for_each = each.value.ip_configurations @@ -21,8 +20,8 @@ resource "azurerm_private_endpoint" "this_managed_dns_zone_groups" { content { name = ip_configuration.value.name private_ip_address = ip_configuration.value.private_ip_address - member_name = "TODO subresource name" - subresource_name = "TODO subresource name" + member_name = "managedInstance" + subresource_name = "managedInstance" } } dynamic "private_dns_zone_group" { @@ -51,8 +50,8 @@ resource "azurerm_private_endpoint" "this_unmanaged_dns_zone_groups" { private_service_connection { is_manual_connection = false name = each.value.private_service_connection_name != null ? each.value.private_service_connection_name : "pse-${var.name}" - private_connection_resource_id = azurerm_resource_group.TODO.id # TODO: Replace this dummy resource azurerm_resource_group.TODO with your module resource - subresource_names = ["TODO subresource name, see https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview#private-link-resource"] + private_connection_resource_id = azurerm_mssql_managed_instance.this.id + subresource_names = ["managedInstance"] } dynamic "ip_configuration" { for_each = each.value.ip_configurations @@ -60,8 +59,8 @@ resource "azurerm_private_endpoint" "this_unmanaged_dns_zone_groups" { content { name = ip_configuration.value.name private_ip_address = ip_configuration.value.private_ip_address - member_name = "TODO subresource name" - subresource_name = "TODO subresource name" + member_name = "managedInstance" + subresource_name = "managedInstance" } } diff --git a/main.telemetry.tf b/main.telemetry.tf index 85a80d9..25144c3 100644 --- a/main.telemetry.tf +++ b/main.telemetry.tf @@ -1,17 +1,25 @@ -resource "random_id" "telem" { +data "azurerm_client_config" "telemetry" { + count = var.enable_telemetry ? 1 : 0 +} + +data "modtm_module_source" "telemetry" { count = var.enable_telemetry ? 1 : 0 - byte_length = 4 + module_path = path.module +} + +resource "random_uuid" "telemetry" { + count = var.enable_telemetry ? 1 : 0 } -# This is the module telemetry deployment that is only created if telemetry is enabled. -# It is deployed to the resource's resource group. -resource "azurerm_resource_group_template_deployment" "telemetry" { +resource "modtm_telemetry" "telemetry" { count = var.enable_telemetry ? 1 : 0 - deployment_mode = "Incremental" - name = local.telem_arm_deployment_name - resource_group_name = var.resource_group_name - tags = null - template_content = local.telem_arm_template_content + tags = { + subscription_id = one(data.azurerm_client_config.telemetry).subscription_id + tenant_id = one(data.azurerm_client_config.telemetry).tenant_id + module_source = one(data.modtm_module_source.telemetry).module_source + module_version = one(data.modtm_module_source.telemetry).module_version + random_id = one(random_uuid.telemetry).result + } } diff --git a/main.tf b/main.tf index 1e1dbb3..a3dac02 100644 --- a/main.tf +++ b/main.tf @@ -1,8 +1,139 @@ -# TODO: Replace this dummy resource azurerm_resource_group.TODO with your module resource -resource "azurerm_resource_group" "TODO" { - location = var.location - name = var.name # calling code must supply the name - tags = var.tags +resource "azurerm_mssql_managed_instance" "this" { + administrator_login = var.administrator_login + administrator_login_password = var.administrator_login_password + license_type = var.license_type + location = var.location + name = var.name + resource_group_name = var.resource_group_name + sku_name = var.sku_name + storage_size_in_gb = var.storage_size_in_gb + subnet_id = var.subnet_id + vcores = var.vcores + collation = var.collation + dns_zone_partner_id = var.dns_zone_partner_id + maintenance_configuration_name = var.maintenance_configuration_name + minimum_tls_version = var.minimum_tls_version + proxy_override = var.proxy_override + public_data_endpoint_enabled = var.public_data_endpoint_enabled + storage_account_type = var.storage_account_type + tags = var.tags + timezone_id = var.timezone_id + zone_redundant_enabled = var.zone_redundant_enabled + + dynamic "timeouts" { + for_each = var.timeouts == null ? [] : [var.timeouts] + + content { + create = timeouts.value.create + delete = timeouts.value.delete + read = timeouts.value.read + update = timeouts.value.update + } + } + + # identity is done via an azapi_resource_action further on, because of this bug that + # prevents system & user assigned identities being set at the same time. + # https://github.com/hashicorp/terraform-provider-azurerm/issues/19802 + lifecycle { + ignore_changes = [ + identity + ] + } +} + +resource "azurerm_mssql_managed_instance_active_directory_administrator" "this" { + count = try(var.active_directory_administrator.object_id, null) == null ? 0 : 1 + + login_username = var.active_directory_administrator.login_username + managed_instance_id = azurerm_mssql_managed_instance.this.id + object_id = var.active_directory_administrator.object_id + tenant_id = var.active_directory_administrator.tenant_id + azuread_authentication_only = var.active_directory_administrator.azuread_authentication_only + + dynamic "timeouts" { + for_each = var.active_directory_administrator.timeouts == null ? [] : [var.active_directory_administrator.timeouts] + + content { + create = timeouts.value.create + delete = timeouts.value.delete + read = timeouts.value.read + update = timeouts.value.update + } + } +} + +# https://learn.microsoft.com/en-us/rest/api/sql/managed-server-security-alert-policies/create-or-update?view=rest-sql-2023-08-01-preview&tabs=HTTP +resource "azapi_resource_action" "mssql_managed_instance_security_alert_policy" { + count = var.security_alert_policy == {} ? 0 : 1 + + resource_id = "${azurerm_mssql_managed_instance.this.id}/securityAlertPolicies/Default" + type = "Microsoft.Sql/managedInstances/securityAlertPolicies@2023-08-01-preview" + body = { + properties = { + disabledAlerts = try(var.security_alert_policy.disabled_alerts, []) + emailAccountAdmins = try(var.security_alert_policy.email_account_admins_enabled, false) + emailAddresses = try(var.security_alert_policy.email_addresses, []) + retentionDays = try(var.security_alert_policy.retention_days, 0) + state = try(var.security_alert_policy.enabled ? "Enabled" : "Disabled", "Enabled") + storageAccountAccessKey = try(var.security_alert_policy.storage_account_access_key, null) + storageEndpoint = try(var.security_alert_policy.storage_endpoint, null) + } + } + method = "PUT" +} + +resource "azurerm_mssql_managed_instance_transparent_data_encryption" "this" { + count = var.transparent_data_encryption == {} ? 0 : 1 + + managed_instance_id = azurerm_mssql_managed_instance.this.id + auto_rotation_enabled = var.transparent_data_encryption.auto_rotation_enabled + key_vault_key_id = var.transparent_data_encryption.key_vault_key_id + + dynamic "timeouts" { + for_each = var.transparent_data_encryption.timeouts == null ? [] : [var.transparent_data_encryption.timeouts] + + content { + create = timeouts.value.create + delete = timeouts.value.delete + read = timeouts.value.read + update = timeouts.value.update + } + } +} + +# API: +# https://learn.microsoft.com/en-us/rest/api/sql/managed-instance-vulnerability-assessments/create-or-update?view=rest-sql-2023-08-01-preview&tabs=HTTP +# +# Note that user assigned identities are not support for vulnerability assessments, so must use user assigned & system assigned, or just system assigned. +# https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-vulnerability-assessment-storage?view=azuresql#store-va-scan-results-for-azure-sql-managed-instance-in-a-storage-account-that-can-be-accessed-behind-a-firewall-or-vnet +resource "azapi_resource_action" "mssql_managed_instance_vulnerability_assessment" { + count = var.vulnerability_assessment == null ? 0 : 1 + + resource_id = "${azurerm_mssql_managed_instance.this.id}/vulnerabilityAssessments/default" + type = "Microsoft.Sql/managedInstances/vulnerabilityAssessments@2023-08-01-preview" + body = { + properties = { + storageAccountAccessKey = try(var.vulnerability_assessment.storage_account_access_key, null) + storageContainerPath = try(var.vulnerability_assessment.storage_container_path, null) + storageContainerSasKey = try(var.vulnerability_assessment.storage_container_sas_key, null) + recurringScans = var.vulnerability_assessment.recurring_scans != {} ? { + isEnabled = try(var.vulnerability_assessment.recurring_scans.enabled, true) + emailSubscriptionAdmins = try(var.vulnerability_assessment.recurring_scans.email_subscription_admins, true), + emails = try(var.vulnerability_assessment.recurring_scans.emails, []) + } : null + } + } + method = "PUT" +} + +# this is required for vulnerability assessments to function - user assigned identities are not supported +# https://learn.microsoft.com/en-us/azure/azure-sql/database/sql-database-vulnerability-assessment-storage?view=azuresql +resource "azurerm_role_assignment" "sqlmi_system_assigned" { + count = var.storage_account_resource_id != null ? 1 : 0 + + principal_id = jsondecode(data.azapi_resource.identity.output).identity.principal_id + scope = var.storage_account_resource_id + role_definition_name = "Storage Blob Data Contributor" } # required AVM resources interfaces @@ -11,7 +142,7 @@ resource "azurerm_management_lock" "this" { lock_level = var.lock.kind name = coalesce(var.lock.name, "lock-${var.lock.kind}") - scope = azurerm_MY_RESOURCE.this.id + scope = azurerm_mssql_managed_instance.this.id notes = var.lock.kind == "CanNotDelete" ? "Cannot delete the resource or its child resources." : "Cannot delete or modify the resource or its child resources." } @@ -19,7 +150,7 @@ resource "azurerm_role_assignment" "this" { for_each = var.role_assignments principal_id = each.value.principal_id - scope = azurerm_resource_group.TODO.id # TODO: Replace this dummy resource azurerm_resource_group.TODO with your module resource + scope = azurerm_mssql_managed_instance.this.id condition = each.value.condition condition_version = each.value.condition_version delegated_managed_identity_resource_id = each.value.delegated_managed_identity_resource_id @@ -27,3 +158,86 @@ resource "azurerm_role_assignment" "this" { role_definition_name = strcontains(lower(each.value.role_definition_id_or_name), lower(local.role_definition_resource_substring)) ? null : each.value.role_definition_id_or_name skip_service_principal_aad_check = each.value.skip_service_principal_aad_check } + +# identity is done via an azapi_resource_action further on, because of this bug that +# prevents system & user assigned identities being set at the same time. +# https://github.com/hashicorp/terraform-provider-azurerm/issues/19802 +resource "azapi_resource_action" "sql_managed_instance_patch_identities" { + count = local.managed_identities.system_assigned_user_assigned == {} ? 0 : 1 + + resource_id = azurerm_mssql_managed_instance.this.id + type = "Microsoft.Sql/managedInstances@2023-05-01-preview" + body = { + identity = { + type = local.managed_identities.system_assigned_user_assigned.this.type + userAssignedIdentities = { + for id in tolist(local.managed_identities.system_assigned_user_assigned.this.user_assigned_resource_ids) : id => {} + } + }, + properties = { + primaryUserAssignedIdentityId = length(local.managed_identities.system_assigned_user_assigned.this.user_assigned_resource_ids) > 0 ? tolist(local.managed_identities.system_assigned_user_assigned.this.user_assigned_resource_ids)[0] : null + } + } + method = "PATCH" +} + +data "azurerm_resource_group" "parent" { + name = azurerm_mssql_managed_instance.this.resource_group_name +} + +data "azapi_resource" "identity" { + type = "Microsoft.Sql/managedInstances@2023-05-01-preview" + name = azurerm_mssql_managed_instance.this.name + parent_id = data.azurerm_resource_group.parent.id + response_export_values = ["identity"] + + depends_on = [azapi_resource_action.sql_managed_instance_patch_identities] +} + +resource "azapi_resource_action" "sql_advanced_threat_protection" { + resource_id = "${azurerm_mssql_managed_instance.this.id}/advancedThreatProtectionSettings/Default" + type = "Microsoft.Sql/managedInstances/advancedThreatProtectionSettings@2023-08-01-preview" + body = { + properties = { + state = var.enable_advanced_threat_protection ? "Enabled" : "Disabled" + } + } + method = "PUT" +} + +resource "azurerm_monitor_diagnostic_setting" "this" { + for_each = var.diagnostic_settings + + name = each.value.name != null ? each.value.name : "diag-${var.name}" + target_resource_id = azurerm_mssql_managed_instance.this.id + eventhub_authorization_rule_id = each.value.event_hub_authorization_rule_resource_id + eventhub_name = each.value.event_hub_name + log_analytics_destination_type = each.value.log_analytics_destination_type + log_analytics_workspace_id = each.value.workspace_resource_id + partner_solution_id = each.value.marketplace_partner_resource_id + storage_account_id = each.value.storage_account_resource_id + + dynamic "enabled_log" { + for_each = each.value.log_categories + + content { + category = enabled_log.value + } + } + dynamic "enabled_log" { + for_each = each.value.log_groups + + content { + category_group = enabled_log.value + } + } + dynamic "metric" { + for_each = each.value.metric_categories + + content { + category = metric.value + } + } +} + + diff --git a/outputs.tf b/outputs.tf index 5e93e10..e6c8737 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,3 +1,8 @@ +output "identity" { + description = "Managed identities for the SQL MI instance. This is not available from the `resource` output because AzureRM doesn't yet support adding both User and System Assigned identities." + value = jsondecode(data.azapi_resource.identity.output).identity +} + output "private_endpoints" { description = < + +* User Assigned MIs are not supported +* The storage account firewall public network access must be allowed. If "Enabled from selected virtual networks and IP addresses" is set (recommended), the SQL MI subnet ID must be added to the storage account firewall. + +DESCRIPTION +} + +variable "vulnerability_assessment" { + type = object({ + storage_account_access_key = optional(string) + storage_container_path = optional(string) + storage_container_sas_key = optional(string) + recurring_scans = optional(object({ + email_subscription_admins = optional(bool) + emails = optional(list(string)) + enabled = optional(bool) + })) + timeouts = optional(object({ + create = optional(string) + delete = optional(string) + read = optional(string) + update = optional(string) + })) + }) + description = <<-DESCRIPTION + - `storage_account_access_key` - (Optional) Specifies the identifier key of the storage account for vulnerability assessment scan results. If `storage_container_sas_key` isn't specified, `storage_account_access_key` is required. Set to `null` if the storage account is protected by a resource firewall. + - `storage_container_path` - (Required) A blob storage container path to hold the scan results (e.g. ). + - `storage_container_sas_key` - (Optional) A shared access signature (SAS Key) that has write access to the blob container specified in `storage_container_path` parameter. If `storage_account_access_key` isn't specified, `storage_container_sas_key` is required. Set to `null` if the storage account is protected by a resource firewall. + + --- + `recurring_scans` block supports the following: + - `email_subscription_admins` - (Optional) Boolean flag which specifies if the schedule scan notification will be sent to the subscription administrators. Defaults to `true`. + - `emails` - (Optional) Specifies an array of e-mail addresses to which the scan notification is sent. + - `enabled` - (Optional) Boolean flag which specifies if recurring scans is enabled or disabled. Defaults to `false`. + + --- + `timeouts` block supports the following: + - `create` - (Defaults to 60 minutes) Used when creating the Vulnerability Assessment. + - `delete` - (Defaults to 60 minutes) Used when deleting the Vulnerability Assessment. + - `read` - (Defaults to 5 minutes) Used when retrieving the Vulnerability Assessment. + - `update` - (Defaults to 60 minutes) Used when updating the Vulnerability Assessment. +DESCRIPTION + default = null +} + +variable "zone_redundant_enabled" { + type = bool + default = true + description = "(Optional) If true, the SQL Managed Instance will be deployed with zone redundancy. Defaults to `true`." +} diff --git a/variables.tf b/variables.tf index 5722d2d..3327a0f 100644 --- a/variables.tf +++ b/variables.tf @@ -1,13 +1,12 @@ +variable "location" { + type = string + description = "Azure region where the resource should be deployed." + nullable = false +} + variable "name" { type = string description = "The name of the this resource." - - validation { - condition = can(regex("TODO", var.name)) - error_message = "The name must be TODO." # TODO remove the example below once complete: - #condition = can(regex("^[a-z0-9]{5,50}$", var.name)) - #error_message = "The name must be between 5 and 50 characters long and can only contain lowercase letters and numbers." - } } # This is required for most resource modules @@ -16,29 +15,6 @@ variable "resource_group_name" { description = "The resource group where the resources will be deployed." } -# required AVM interfaces -# remove only if not supported by the resource -# tflint-ignore: terraform_unused_declarations -variable "customer_managed_key" { - type = object({ - key_vault_resource_id = string - key_name = string - key_version = optional(string, null) - user_assigned_identity = optional(object({ - resource_id = string - }), null) - }) - default = null - description = <