diff --git a/.github/workflows/lint_clean.yaml b/.github/workflows/lint_clean.yaml new file mode 100644 index 0000000..a15b694 --- /dev/null +++ b/.github/workflows/lint_clean.yaml @@ -0,0 +1,18 @@ +name: 'Lint and Clean' + +on: + pull_request: + + push: + branches: + - main + +jobs: + fmt: + name: Terraform FMT + runs-on: ubuntu-latest + container: + image: hashicorp/terraform:latest + steps: + - uses: actions/checkout@v2 + - run: terraform fmt --recursive --diff -check=true \ No newline at end of file diff --git a/.github/workflows/terraform-docs.yaml b/.github/workflows/terraform-docs.yaml new file mode 100644 index 0000000..7a48d79 --- /dev/null +++ b/.github/workflows/terraform-docs.yaml @@ -0,0 +1,16 @@ +name: Generate terraform docs +on: + - pull_request + +jobs: + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.ref }} + + - name: Render terraform docs and push changes back to PR + uses: terraform-docs/gh-actions@main + with: + git-push: "true" diff --git a/acr.tf b/acr.tf new file mode 100644 index 0000000..7c73d53 --- /dev/null +++ b/acr.tf @@ -0,0 +1,7 @@ +resource "azurerm_container_registry" "svcfoundry" { + count = var.create_acr == true ? 1 : 0 + name = local.svcfoundry_unique_name + resource_group_name = var.resource_group_name + location = var.location + sku = "Standard" +} \ No newline at end of file diff --git a/iam.tf b/iam.tf new file mode 100644 index 0000000..b734fdc --- /dev/null +++ b/iam.tf @@ -0,0 +1,30 @@ +resource "azurerm_user_assigned_identity" "svcfoundry" { + location = var.location + name = local.svcfoundry_unique_name + resource_group_name = var.resource_group_name +} + +# https://azure.github.io/azure-workload-identity/docs/quick-start.html +resource "azurerm_federated_identity_credential" "svcfoundry" { + name = local.svcfoundry_unique_name + resource_group_name = var.resource_group_name + audience = ["api://AzureADTokenExchange"] + issuer = var.cluster_oidc_url + parent_id = azurerm_user_assigned_identity.svcfoundry.id + subject = "system:serviceaccount:${var.svcfoundry_namespace}:${var.svcfoundry_svc_acc}" +} + +resource "azurerm_user_assigned_identity" "mlfoundry" { + location = var.location + name = local.mlfoundry_unique_name + resource_group_name = var.resource_group_name +} + +resource "azurerm_federated_identity_credential" "mlfoundry" { + name = local.mlfoundry_unique_name + resource_group_name = var.resource_group_name + audience = ["api://AzureADTokenExchange"] + issuer = var.cluster_oidc_url + parent_id = azurerm_user_assigned_identity.mlfoundry.id + subject = "system:serviceaccount:${var.mlfoundry_namespace}:${var.mlfoundry_svc_acc}" +} diff --git a/kv.tf b/kv.tf new file mode 100644 index 0000000..b505aac --- /dev/null +++ b/kv.tf @@ -0,0 +1,44 @@ +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "akv_svcfoundry" { + count = var.create_kv == true ? 1 : 0 + name = "tfy-svcfoundry" + location = var.location + resource_group_name = var.resource_group_name + enabled_for_disk_encryption = true + tenant_id = data.azurerm_client_config.current.tenant_id + soft_delete_retention_days = 7 + purge_protection_enabled = false + + sku_name = "standard" + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = azurerm_user_assigned_identity.svcfoundry.principal_id + + key_permissions = [ + "Create", + "Decrypt", + "Delete", + "Encrypt", + "Get", + "List", + "Update" + ] + + secret_permissions = [ + "Set", + "Delete", + "Get", + "List" + ] + + storage_permissions = [ + "Delete", + "Get", + "List", + "Set", + "Update" + ] + } +} diff --git a/locals.tf b/locals.tf new file mode 100644 index 0000000..b182b5b --- /dev/null +++ b/locals.tf @@ -0,0 +1,17 @@ +locals { + + truefoundry_unique_name = substr(replace("${var.unique_name}", "-", ""), 0, 20) + svcfoundry_unique_name = substr(replace("svc-${var.unique_name}", "-", ""), 0, 20) + mlfoundry_unique_name = substr(replace("mlf-${var.unique_name}", "-", ""), 0, 20) + + truefoundry_db_port = 5432 + truefoundry_db_master_username = "truefoundry_root" + + tags = merge( + { + "terraform-module" = "terraform-azc-truefoundry" + "terraform" = "true" + }, + var.tags + ) +} \ No newline at end of file diff --git a/mlfoundry.tf b/mlfoundry.tf new file mode 100644 index 0000000..64047da --- /dev/null +++ b/mlfoundry.tf @@ -0,0 +1,6 @@ +resource "azurerm_role_assignment" "mlfoundry" { + count = var.create_blob_storage == true ? 1 : 0 + scope = azurerm_storage_container.truefoundry[0].resource_manager_id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azurerm_user_assigned_identity.mlfoundry.principal_id +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..83f41c0 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,32 @@ +output "truefoundry_db_name" { + value = var.create_db == true ? module.postgresql-db[0].server_name : "dummy" +} + +output "truefoundry_db_endpoint" { + value = var.create_db == true ? module.postgresql-db[0].server_fqdn: "dummy" +} + +output "truefoundry_db_private_ip" { + value = var.create_db == true ? resource.azurerm_private_endpoint.postgresql_private_connection[0].private_service_connection[0].private_ip_address : "dummy" +} + +output "truefoundry_db_password" { + value = var.create_db == true ? module.postgresql-db[0].administrator_password: "dummy" + sensitive = true +} + +output "truefoundry_db_port" { + value = "5432" +} + +output "truefoundry_storage_container_id" { + value = var.create_blob_storage == true ? azurerm_storage_container.truefoundry[0].id : "dummy" +} + +output "svcfoundry_identity_client_id" { + value = azurerm_user_assigned_identity.svcfoundry.client_id +} + +output "mlfoundry_identity_client_id" { + value = azurerm_user_assigned_identity.mlfoundry.client_id +} \ No newline at end of file diff --git a/postgres.tf b/postgres.tf new file mode 100644 index 0000000..223dad6 --- /dev/null +++ b/postgres.tf @@ -0,0 +1,41 @@ +resource "random_password" "truefoundry_db_password" { + length = 24 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +module "postgresql-db" { + count = var.create_db == true ? 1 : 0 + source = "Azure/postgresql/azurerm" + version = "3.0.0" + server_name = var.truefoundry_db_enable_override ? var.truefoundry_db_override_name : "${var.unique_name}-db" + sku_name = var.truefoundry_db_instance_class + location = var.location + resource_group_name = var.resource_group_name + storage_mb = var.truefoundry_db_allocated_storage + backup_retention_days = 7 + geo_redundant_backup_enabled = false + administrator_login = local.truefoundry_db_master_username + administrator_password = random_password.truefoundry_db_password.result + server_version = "11" + ssl_enforcement_enabled = false + ssl_minimal_tls_version_enforced = "TLSEnforcementDisabled" + db_names = ["truefoundry"] + tags = local.tags + public_network_access_enabled = false +} + +resource "azurerm_private_endpoint" "postgresql_private_connection" { + count = var.create_db == true ? 1 : 0 + name = "${var.unique_name}-db-private-endpoint" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.truefoundry_db_subnet_id + + private_service_connection { + name = "${var.unique_name}-db-private-connection" + private_connection_resource_id = module.postgresql-db[0].server_id + is_manual_connection = false + subresource_names = [ "postgresqlServer" ] + } +} diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..44c657b --- /dev/null +++ b/providers.tf @@ -0,0 +1,12 @@ +terraform { + required_version = "~>1.1" + + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~>3.33" + } + + } +} diff --git a/storage.tf b/storage.tf new file mode 100644 index 0000000..cb97267 --- /dev/null +++ b/storage.tf @@ -0,0 +1,17 @@ +resource "azurerm_storage_account" "this" { + count = var.create_blob_storage == true ? 1 : 0 + name = replace(local.truefoundry_unique_name, "-", "") + resource_group_name = var.resource_group_name + location = var.location + account_tier = "Standard" + account_replication_type = "LRS" + + tags = local.tags +} + +resource "azurerm_storage_container" "truefoundry" { + count = var.create_blob_storage == true ? 1 : 0 + name = local.truefoundry_unique_name + storage_account_name = azurerm_storage_account.this[0].name + container_access_type = "blob" +} diff --git a/svcfoundry.tf b/svcfoundry.tf new file mode 100644 index 0000000..f35de7b --- /dev/null +++ b/svcfoundry.tf @@ -0,0 +1,13 @@ +resource "azurerm_role_assignment" "storage_svcfoundry" { + count = var.create_blob_storage == true ? 1 : 0 + scope = azurerm_storage_container.truefoundry[0].resource_manager_id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azurerm_user_assigned_identity.svcfoundry.principal_id +} + +resource "azurerm_role_assignment" "acr_svcfoundry" { + count = var.create_acr == true ? 1 : 0 + scope = azurerm_container_registry.svcfoundry[0].id + role_definition_name = "Contributor" + principal_id = azurerm_user_assigned_identity.svcfoundry.principal_id +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..7da11fe --- /dev/null +++ b/variables.tf @@ -0,0 +1,121 @@ +variable "unique_name" { + description = "Truefoundry deployment unique name" + type = string + validation { + condition = length(var.unique_name) <= 24 + error_message = "Error: Unique name is too long." + } + validation { + condition = length(var.unique_name) >= 3 + error_message = "Error: Unique name is too short." + } +} + +variable "resource_group_name" { + description = "Name of the resource group" + type = string +} + +variable "location" { + description = "Location of the resource group" + type = string +} + +variable "tags" { + type = map(string) + default = {} + description = "AWS Tags common to all the resources created" +} + +variable "cluster_oidc_url" { + description = "OIDC url for the cluster to create federated credential for" + type = string +} + +##### Control Plane Components + +#### Database + +variable "create_db" { + type = bool + description = "Create db" + default = false +} + +variable "truefoundry_db_enable_override" { + type = bool + description = "Truefoundry db name override to be enabled" + default = false +} + +variable "truefoundry_db_override_name" { + type = string + description = "Truefoundry db name override" +} + +variable "truefoundry_db_instance_class" { + type = string + description = "Instance class for DB" +} + +variable "truefoundry_db_allocated_storage" { + type = string + description = "Storage for DB" +} + +variable "truefoundry_db_subnet_id" { + type = string + description = "ID of the subnet which the db should use" +} + +#### Azure Container Repository + +variable "create_acr" { + type = bool + description = "Create acr" + default = false +} + +#### Azure KeyVault + +variable "create_kv" { + type = bool + description = "Create kv" + default = false +} + +#### Azure Storage + +variable "create_blob_storage" { + type = bool + description = "Create blob storage" + default = false +} + +###### mlfoundry + +variable "mlfoundry_svc_acc" { + description = "Name of the mlfoundry service account" + default = "mlfoundry-server" + type = string +} + +variable "mlfoundry_namespace" { + description = "Name of the mlfoundry namespace" + default = "truefoundry" + type = string +} + +###### svcfoundry + +variable "svcfoundry_svc_acc" { + description = "Name of the svcfoundry service account" + default = "servicefoundry-server" + type = string +} + +variable "svcfoundry_namespace" { + description = "Name of the svcfoundry namespace" + default = "truefoundry" + type = string +}