diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml new file mode 100644 index 00000000..d536d4f1 --- /dev/null +++ b/.github/workflows/terraform.yml @@ -0,0 +1,72 @@ +name: Continuous Integration / Terraform + +on: + push: + branches: [ main ] + paths: + - 'terraform/**.tf' + pull_request: + paths: + - 'terraform/**.tf' + +jobs: + terraform-validate: + name: Validate + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Check for terraform version mismatch + run: | + DOTFILE_VERSION=$(cat terraform/.terraform-version) + TERRAFORM_IMAGE_REFERENCES=$(grep "uses: docker://hashicorp/terraform" .github/workflows/continuous-integration-terraform.yml | grep -v TERRAFORM_IMAGE_REFERENCES | wc -l | tr -d ' ') + if [ "$(grep "docker://hashicorp/terraform:${DOTFILE_VERSION}" .github/workflows/continuous-integration-terraform.yml | wc -l | tr -d ' ')" != "$TERRAFORM_IMAGE_REFERENCES" ] + then + echo -e "\033[1;31mError: terraform version in .terraform-version file does not match docker://hashicorp/terraform versions in .github/workflows/continuous-integration-terraform.yml" + exit 1 + fi + + - name: Validate Terraform docs + uses: terraform-docs/gh-actions@v1.3.0 + with: + working-dir: terraform + config-file: .terraform-docs.yml + output-file: README.md + output-method: inject + fail-on-diff: true + + - name: Remove azure backend + run: rm ./terraform/backend.tf + + - name: Run a Terraform init + uses: docker://hashicorp/terraform:1.9.8 + with: + entrypoint: terraform + args: -chdir=terraform init + + - name: Run a Terraform validate + uses: docker://hashicorp/terraform:1.9.8 + with: + entrypoint: terraform + args: -chdir=terraform validate + + - name: Run a Terraform format check + uses: docker://hashicorp/terraform:1.9.8 + with: + entrypoint: terraform + args: -chdir=terraform fmt -check=true -diff=true + + - name: Setup TFLint + uses: terraform-linters/setup-tflint@v4 + with: + tflint_version: v0.44.1 + + - name: Run TFLint + working-directory: terraform + run: tflint -f compact + + - name: Run TFSec + uses: aquasecurity/tfsec-pr-commenter-action@v1.3.1 + with: + github_token: ${{ github.token }} diff --git a/terraform/.terraform-docs.yml b/terraform/.terraform-docs.yml new file mode 100644 index 00000000..a6917808 --- /dev/null +++ b/terraform/.terraform-docs.yml @@ -0,0 +1,26 @@ +--- +formatter: "markdown table" +version: "~> 0.16" +settings: + anchor: true + default: true + description: false + escape: true + hide-empty: false + html: true + indent: 2 + lockfile: true + read-comments: true + required: true + sensitive: true + type: true +sort: + enabled: true + by: name +output: + file: README.md + mode: inject + template: |- + + {{ .Content }} + diff --git a/terraform/.terraform-version b/terraform/.terraform-version new file mode 100644 index 00000000..66beabb5 --- /dev/null +++ b/terraform/.terraform-version @@ -0,0 +1 @@ +1.9.8 diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 00000000..fbb2c7ee --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,125 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/azure/azapi" { + version = "1.15.0" + constraints = "~> 1.13" + hashes = [ + "h1:pO/phGY+TxMEKQ+ffYj+vUIvG5A1tno/sZYDb/yyA/w=", + "zh:0627a8bc77254debc25dc0c7b62e055138217c97b03221e593c3c56dc7550671", + "zh:2fe045f07070ef75d0bec4b0595a74c14394daa838ddb964e2fd23cc98c40c34", + "zh:343009f39c957883b2c06145a5954e524c70f93585f943f1ea3d28ef6995d0d0", + "zh:53fe9ab54485aaebc9b91e27a10bce2729a1c95b1399079e631dc6bb9e3f27dc", + "zh:63c407e7dc04d178d4798c17ad489d9cc92f7d1941d7f4a3f560b95908b6107b", + "zh:7d6fc2b432b264f036bb80ab2b2ba67f80a5d98da8a8c322aa097833dad598c9", + "zh:7ec49c0a8799d469eb6e2a1f856693f9862f1b73f5ed70adc1b346e5a4c6458d", + "zh:889704f10319d301d677539d788fc82a7c73608ab78cb93e1280ac2be39e6e00", + "zh:90b4b07405b7cde9ebae3b034cb5bb5dd18484d1b95bd250f905451f1e86ac3f", + "zh:92aa9c241a8cb2a6d81ad47bc007c119f8b818464a960ebaf39008766c361e6b", + "zh:f28fbd0a2c59e239b53067bc1adc691be444876bcb2d4f78d310f549724da6e0", + "zh:ffb15e0ddfa505d0e9b75341570199076ae574887124f398162b1ead9376b25f", + ] +} + +provider "registry.terraform.io/hashicorp/archive" { + version = "2.7.0" + constraints = "~> 2.6" + hashes = [ + "h1:1niS9AcwxN8CrWemnJS2Xf6vM72+48Xh3xFSS3DFWQo=", + "zh:04e23bebca7f665a19a032343aeecd230028a3822e546e6f618f24c47ff87f67", + "zh:5bb38114238e25c45bf85f5c9f627a2d0c4b98fe44a0837e37d48574385f8dad", + "zh:64584bc1db4c390abd81c76de438d93acf967c8a33e9b923d68da6ed749d55bd", + "zh:697695ab9cce351adf91a1823bdd72ce6f0d219138f5124ef7645cedf8f59a1f", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7edefb1d1e2fead8fd155f7b50a2cb49f2f3fed154ac3ef5f991ccaff93d6120", + "zh:807fb15b75910bf14795f2ad1a2d41b069f9ef52c242131b2964c8527312e235", + "zh:821d9148d261df1d1a8e5a4812df2a6a3ffaf0d2070dad3c785382e489069239", + "zh:a7d92251118fb723048c482154a6ac6368aad583d28d15fffc6f5dafd9507463", + "zh:b627d4cef192b3c12ddaf9cb2c4f98c10d0129883c8c2a9c0049983f9de7030d", + "zh:dfb70306fcc0ad1d512ab7c24765703783cc286062d4849de4fbe23526f5dc8e", + "zh:f21de276f857b7e51fa2593d8fef05a7faafb0a7b62db14ac58a03ce1be7d881", + ] +} + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.53.1" + constraints = "~> 2.37" + hashes = [ + "h1:EZNO8sEtUABuRxujQrDrW1z1QsG0dq6iLbzWtnG7Om4=", + "zh:162916b037e5133f49298b0ffa3e7dcef7d76530a8ca738e7293373980f73c68", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:492931cea4f30887ab5bca36a8556dfcb897288eddd44619c0217fc5da2d57e7", + "zh:4c895e450e18335ad8714cc6d3488fc1a78816ad2851a91b06cb2ef775dd7c66", + "zh:60d92fdaf7235574201f2d8f68f733ee00a822993b3fc95e6952e09e6ec76999", + "zh:67a169119efa41c1fb867ef1a8e79bf03472a2324384c36eb55370c817dcce42", + "zh:9dd4d5ed9233cf9329262200bc5a1aa60942b80dbc611e2ef4b09f47531b39b1", + "zh:a3c160e35b9e40fc1497b83c2f37a8e24565b05a1783c7733609f3695735c2a9", + "zh:a4a221da42b1f46e7c436c7145e5beaadfd9d03f3be6fd526d132c03f18a5979", + "zh:af0d3476a9702d2287e168e3baa670e64daab9c9b01c01e17025a5248f3e28e9", + "zh:e3579bff7894f3d36066b74ec324be6d28f56a42a387a2b8a0eabf33cbff86df", + "zh:f1749ee8ad972ae6424665aa9d2c0ece8c40c51d41ec2f38b863148cb437e865", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.16.0" + constraints = "~> 4.0" + hashes = [ + "h1:uulWiJ93kZmKKh6/EtHktJQ901npRmTb/ao7oTP402w=", + "zh:2035e461a94bd4180557a06f8e56f228a8a035608d0dac4d08e5870cf9265276", + "zh:3f15778a22ef1b9d0fa28670e5ea6ef1094b0be2533f43f350a2ef15d471b353", + "zh:4f1a4d03b008dd958bcd6bf82cf088fbaa9c121be2fd35e10e6b06c6e8f6aaa1", + "zh:5859f31c342364e849b4f8c437a46f33e927fa820244d0732b8d2ec74a95712d", + "zh:693d0f15512ca8c6b5e999b3a7551503feb06b408b3836bc6a6403e518b9ddab", + "zh:7f4912bec5b04f5156935292377c12484c13582151eb3c2555df409a7e5fb6e0", + "zh:bb9a509497f3a131c52fac32348919bf1b9e06c69a65f24607b03f7b56fb47b6", + "zh:c1b0c64e49ac591fd038ad71e71403ff71c07476e27e8da718c29f0028ea6d0d", + "zh:dd4ca432ee14eb0bb0cdc0bb463c8675b8ef02497be870a20d8dfee3e7fe52b3", + "zh:df58bb7fea984d2b11709567842ca4d55b3f24e187aa6be99e3677f55cbbe7da", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f7fb37704da50c096f9c7c25e8a95fe73ce1d3c5aab0d616d506f07bc5cfcdd8", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.3" + constraints = "~> 3.2" + hashes = [ + "h1:I0Um8UkrMUb81Fxq/dxbr3HLP2cecTH2WMJiwKSrwQY=", + "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2", + "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d", + "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3", + "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f", + "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301", + "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670", + "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed", + "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65", + "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd", + "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5", + ] +} + +provider "registry.terraform.io/statuscakedev/statuscake" { + version = "2.2.2" + constraints = "~> 2.1" + hashes = [ + "h1:nVaJkDBk4sv0yWFzg3p+yeJGzE8mB4KJv3Q6/UgU164=", + "zh:0916313344c579d6e05d70f88129a10fe48f7dabe0e61cad17874d6c496f288d", + "zh:0d491ff72c2eda6482855033ca2146c5ace1663d07cb3da7253b59ed2e2ec6f4", + "zh:11fffbce18eb3d3c283e877242f477e0c561342c19090240b60af7d948bd84ac", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:1c6116092c59bc0010e147dc7832ae981d528f235cef563e5ae05a93ef8bac5c", + "zh:1f13a543b1d32cc1f1e3d2ed5ca83445f088787c335690fe20dee1203488a8bc", + "zh:23f55fd0714696c3863f892646a79a780cea1923c0c4d2b1064df735ba4156f5", + "zh:288fbb4431ac12014aa5aab10ee7166dfe71fd1158464e06e8a527aa4919e64e", + "zh:2a0746d3c8cdc7d0875df0f4605d81130e6da0ce6196d0f5f8661fe9191bc2ab", + "zh:31d660756abc53d252906fe1239fad58ecb6c1d0dbb087408a8af266be1f9ae4", + "zh:6765281d1b7efb41085c5375660b6c6b271babc5d09b2c030bec7176f7a7bfb6", + "zh:6d7204eadb667c1f2cab762a7a97234cf47452f0ecad680f5106c8fd02524c87", + "zh:b70a97b0eba471d683e23ce8744a5c67ef9952086e2d6f5825b72f32b6caff89", + "zh:df89ee4aaba88faea33d33384a36ea52588b5514644d335f724378a682d8d9da", + "zh:ffe2e1e7224ea5f4dd65b9eef45464e3124cfc824473b04e7af429f9177f2375", + ] +} diff --git a/terraform/Brewfile b/terraform/Brewfile new file mode 100644 index 00000000..000ace38 --- /dev/null +++ b/terraform/Brewfile @@ -0,0 +1,6 @@ +brew "tfenv" +brew "terraform-docs" +brew "tfsec" +brew "az" +brew "coreutils" +brew "jq" diff --git a/terraform/README.md b/terraform/README.md new file mode 100644 index 00000000..c30b1fc5 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,203 @@ +This documentation covers the deployment of the infrastructure to host the app. + +## Azure infrastructure + +The infrastructure is managed using [Terraform](https://www.terraform.io/).
+The state is stored remotely in encrypted Azure storage.
+[Terraform workspaces](https://www.terraform.io/docs/state/workspaces.html) are used to separate environments. + +#### Configuring the storage backend + +The Terraform state is stored remotely in Azure, this allows multiple team members to +make changes and means the state file is backed up. The state file contains +sensitive information so access to it should be restricted, and it should be stored +encrypted at rest. + +##### Create a new storage backend + +This step only needs to be done once per project (eg. not per environment). +If it has already been created, obtain the storage backend attributes and skip to the next step. + +The [Azure tutorial](https://docs.microsoft.com/en-us/azure/developer/terraform/store-state-in-azure-storage) outlines the steps to create a storage account and container for the state file. You will need: + +- resource_group_name: The name of the resource group used for the Azure Storage account. +- storage_account_name: The name of the Azure Storage account. +- container_name: The name of the blob container. +- key: The name of the state store file to be created. + +##### Create a backend configuration file + +Create a new file named `backend.vars` with the following content: + +``` +resource_group_name = [the name of the Azure resource group] +storage_account_name = [the name of the Azure Storage account] +container_name = [the name of the blob container] +key = "terraform.tstate" +``` + +##### Install dependencies + +We can use [Homebrew](https://brew.sh) to install the dependecies we need to deploy the infrastructure (eg. tfenv, Azure cli). +These are listed in the `Brewfile` + +to install, run: + +``` +$ brew bundle +``` + +##### Log into azure with the Azure CLI + +Log in to your account: + +``` +$ az login +``` + +Confirm which account you are currently using: + +``` +$ az account show +``` + +To list the available subscriptions, run: + +``` +$ az account list +``` + +Then if needed, switch to it using the 'id': + +``` +$ az account set --subscription +``` + +##### Initialise Terraform + +Install the required terraform version with the Terraform version manager `tfenv`: + +``` +$ tfenv install +``` + +Initialize Terraform to download the required Terraform modules and configure the remote state backend +to use the settings you specified in the previous step. + +`$ terraform init -backend-config=backend.vars` + +##### Create a Terraform variables file + +Each environment will need it's own `tfvars` file. + +Copy the `terraform.tfvars.example` to `environment-name.tfvars` and modify the contents as required + +##### Create the infrastructure + +Now Terraform has been initialised you can create a workspace if needed: + +`$ terraform workspace new staging` + +Or to check what workspaces already exist: + +`$ terraform workspace list` + +Switch to the new or existing workspace: + +`$ terraform workspace select staging` + +Plan the changes: + +`$ terraform plan -var-file=staging.tfvars` + +Terraform will ask you to provide any variables not specified in an `*.auto.tfvars` file. +Now you can run: + +`$ terraform apply -var-file=staging.tfvars` + +If everything looks good, answer `yes` and wait for the new infrastructure to be created. + +##### Azure resources + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | ~> 1.9 | +| [azapi](#requirement\_azapi) | ~> 1.13 | +| [azurerm](#requirement\_azurerm) | ~> 4.0 | +| [statuscake](#requirement\_statuscake) | ~> 2.1 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [azure\_container\_apps\_hosting](#module\_azure\_container\_apps\_hosting) | github.com/DFE-Digital/terraform-azurerm-container-apps-hosting | v1.16.6 | +| [azurerm\_key\_vault](#module\_azurerm\_key\_vault) | github.com/DFE-Digital/terraform-azurerm-key-vault-tfvars | v0.5.1 | +| [statuscake-tls-monitor](#module\_statuscake-tls-monitor) | github.com/dfe-digital/terraform-statuscake-tls-monitor | v0.1.5 | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_client\_id](#input\_azure\_client\_id) | Service Principal Client ID | `string` | n/a | yes | +| [azure\_client\_secret](#input\_azure\_client\_secret) | Service Principal Client Secret | `string` | n/a | yes | +| [azure\_location](#input\_azure\_location) | Azure location in which to launch resources. | `string` | n/a | yes | +| [azure\_subscription\_id](#input\_azure\_subscription\_id) | Service Principal Subscription ID | `string` | n/a | yes | +| [azure\_tenant\_id](#input\_azure\_tenant\_id) | Service Principal Tenant ID | `string` | n/a | yes | +| [container\_apps\_allow\_ips\_inbound](#input\_container\_apps\_allow\_ips\_inbound) | Restricts access to the Container Apps by creating a network security group rule that only allow inbound traffic from the provided list of IPs | `list(string)` | `[]` | no | +| [container\_command](#input\_container\_command) | Container command | `list(any)` | n/a | yes | +| [container\_health\_probe\_path](#input\_container\_health\_probe\_path) | Specifies the path that is used to determine the liveness of the Container | `string` | n/a | yes | +| [container\_min\_replicas](#input\_container\_min\_replicas) | Container min replicas | `number` | `1` | no | +| [container\_port](#input\_container\_port) | Container port | `number` | `8080` | no | +| [container\_scale\_http\_concurrency](#input\_container\_scale\_http\_concurrency) | When the number of concurrent HTTP requests exceeds this value, then another replica is added. Replicas continue to add to the pool up to the max-replicas amount. | `number` | `10` | no | +| [container\_secret\_environment\_variables](#input\_container\_secret\_environment\_variables) | Container secret environment variables | `map(string)` | n/a | yes | +| [dns\_alias\_records](#input\_dns\_alias\_records) | DNS ALIAS records to add to the DNS Zone |
map(
object({
ttl : optional(number, 300),
target_resource_id : string
})
)
| `{}` | no | +| [dns\_mx\_records](#input\_dns\_mx\_records) | DNS MX records to add to the DNS Zone |
map(
object({
ttl : optional(number, 300),
records : list(
object({
preference : number,
exchange : string
})
)
})
)
| `{}` | no | +| [dns\_ns\_records](#input\_dns\_ns\_records) | DNS NS records to add to the DNS Zone |
map(
object({
ttl : optional(number, 300),
records : list(string)
})
)
| n/a | yes | +| [dns\_txt\_records](#input\_dns\_txt\_records) | DNS TXT records to add to the DNS Zone |
map(
object({
ttl : optional(number, 300),
records : list(string)
})
)
| n/a | yes | +| [dns\_zone\_domain\_name](#input\_dns\_zone\_domain\_name) | DNS zone domain name. If created, records will automatically be created to point to the CDN. | `string` | n/a | yes | +| [enable\_container\_registry](#input\_enable\_container\_registry) | Set to true to create a container registry | `bool` | `false` | no | +| [enable\_dns\_zone](#input\_enable\_dns\_zone) | Conditionally create a DNS zone | `bool` | n/a | yes | +| [enable\_health\_insights\_api](#input\_enable\_health\_insights\_api) | Deploys a Function App that exposes the last 3 HTTP Web Tests via an API endpoint. 'enable\_app\_insights\_integration' and 'enable\_monitoring' must be set to 'true'. | `bool` | `false` | no | +| [enable\_monitoring](#input\_enable\_monitoring) | Create an App Insights instance and notification group for the Container App | `bool` | n/a | yes | +| [environment](#input\_environment) | Environment name. Will be used along with `project_name` as a prefix for all resources. | `string` | n/a | yes | +| [existing\_container\_app\_environment](#input\_existing\_container\_app\_environment) | Conditionally launch resources into an existing Container App environment. Specifying this will NOT create an environment. |
object({
name = string
resource_group = string
})
| n/a | yes | +| [existing\_logic\_app\_workflow](#input\_existing\_logic\_app\_workflow) | Name, and Resource Group of an existing Logic App Workflow. Leave empty to create a new Resource |
object({
name : string
resource_group_name : string
})
|
{
"name": "",
"resource_group_name": ""
}
| no | +| [existing\_resource\_group](#input\_existing\_resource\_group) | Conditionally launch resources into an existing resource group. Specifying this will NOT create a resource group. | `string` | n/a | yes | +| [existing\_virtual\_network](#input\_existing\_virtual\_network) | Conditionally use an existing virtual network. The `virtual_network_address_space` must match an existing address space in the VNet. This also requires the resource group name. | `string` | n/a | yes | +| [health\_insights\_api\_cors\_origins](#input\_health\_insights\_api\_cors\_origins) | List of hostnames that are permitted to contact the Health insights API | `list(string)` |
[
"*"
]
| no | +| [health\_insights\_api\_ipv4\_allow\_list](#input\_health\_insights\_api\_ipv4\_allow\_list) | List of IPv4 addresses that are permitted to contact the Health insights API | `list(string)` | `[]` | no | +| [image\_name](#input\_image\_name) | Image name | `string` | n/a | yes | +| [image\_tag](#input\_image\_tag) | Image tag | `string` | n/a | yes | +| [key\_vault\_access\_ipv4](#input\_key\_vault\_access\_ipv4) | List of IPv4 Addresses that are permitted to access the Key Vault | `list(string)` | n/a | yes | +| [monitor\_email\_receivers](#input\_monitor\_email\_receivers) | A list of email addresses that should be notified by monitoring alerts | `list(string)` | n/a | yes | +| [monitor\_endpoint\_healthcheck](#input\_monitor\_endpoint\_healthcheck) | Specify a route that should be monitored for a 200 OK status | `string` | n/a | yes | +| [monitor\_http\_availability\_fqdn](#input\_monitor\_http\_availability\_fqdn) | Specify a FQDN to monitor for HTTP Availability. Leave unset to dynamically calculate the correct FQDN | `string` | `""` | no | +| [monitor\_http\_availability\_verb](#input\_monitor\_http\_availability\_verb) | Which HTTP verb to use for the HTTP Availability check | `string` | `"GET"` | no | +| [project\_name](#input\_project\_name) | Project name. Will be used along with `environment` as a prefix for all resources. | `string` | n/a | yes | +| [registry\_admin\_enabled](#input\_registry\_admin\_enabled) | Do you want to enable access key based authentication for your Container Registry? | `bool` | `false` | no | +| [registry\_managed\_identity\_assign\_role](#input\_registry\_managed\_identity\_assign\_role) | Assign the 'AcrPull' Role to the Container App User-Assigned Managed Identity. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity | `bool` | `false` | no | +| [registry\_server](#input\_registry\_server) | Container registry server (required if `enable_container_registry` is false) | `string` | `""` | no | +| [registry\_use\_managed\_identity](#input\_registry\_use\_managed\_identity) | Create a User-Assigned Managed Identity for the Container App. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity | `bool` | `true` | no | +| [statuscake\_api\_token](#input\_statuscake\_api\_token) | API token for StatusCake | `string` | `"00000000000000000000000000000"` | no | +| [statuscake\_contact\_group\_email\_addresses](#input\_statuscake\_contact\_group\_email\_addresses) | List of email address that should receive notifications from StatusCake | `list(string)` | `[]` | no | +| [statuscake\_contact\_group\_integrations](#input\_statuscake\_contact\_group\_integrations) | List of Integration IDs to connect to your Contact Group | `list(string)` | `[]` | no | +| [statuscake\_contact\_group\_name](#input\_statuscake\_contact\_group\_name) | Name of the contact group in StatusCake | `string` | `""` | no | +| [statuscake\_monitored\_resource\_addresses](#input\_statuscake\_monitored\_resource\_addresses) | The URLs to perform TLS checks on | `list(string)` | `[]` | no | +| [tags](#input\_tags) | Tags to be applied to all resources | `map(string)` | n/a | yes | +| [tfvars\_filename](#input\_tfvars\_filename) | tfvars filename. This file is uploaded and stored encrupted within Key Vault, to ensure that the latest tfvars are stored in a shared place. | `string` | n/a | yes | + +## Outputs + +No outputs. + diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 00000000..40e5c433 --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,5 @@ +terraform { + backend "azurerm" { + use_azuread_auth = true + } +} diff --git a/terraform/backend.vars.example b/terraform/backend.vars.example new file mode 100644 index 00000000..c6158784 --- /dev/null +++ b/terraform/backend.vars.example @@ -0,0 +1,8 @@ +resource_group_name = "" +storage_account_name = "" +container_name = "" +key = "" +subscription_id = "" +client_id = "" +client_secret = "" +tenant_id = "" diff --git a/terraform/container-apps-hosting.tf b/terraform/container-apps-hosting.tf new file mode 100644 index 00000000..d389fd82 --- /dev/null +++ b/terraform/container-apps-hosting.tf @@ -0,0 +1,47 @@ +module "azure_container_apps_hosting" { + source = "github.com/DFE-Digital/terraform-azurerm-container-apps-hosting?ref=v1.16.6" + + environment = local.environment + project_name = local.project_name + azure_location = local.azure_location + tags = local.tags + + enable_container_registry = local.enable_container_registry + registry_admin_enabled = local.registry_admin_enabled + registry_use_managed_identity = local.registry_use_managed_identity + registry_managed_identity_assign_role = local.registry_managed_identity_assign_role + registry_server = local.registry_server + + enable_dns_zone = local.enable_dns_zone + dns_zone_domain_name = local.dns_zone_domain_name + dns_ns_records = local.dns_ns_records + dns_txt_records = local.dns_txt_records + dns_mx_records = local.dns_mx_records + dns_alias_records = local.dns_alias_records + + image_name = local.image_name + image_tag = local.image_tag + container_command = local.container_command + container_secret_environment_variables = local.container_secret_environment_variables + container_scale_http_concurrency = local.container_scale_http_concurrency + container_apps_allow_ips_inbound = local.container_apps_allow_ips_inbound + container_min_replicas = local.container_min_replicas + container_port = local.container_port + enable_health_insights_api = local.enable_health_insights_api + health_insights_api_cors_origins = local.health_insights_api_cors_origins + health_insights_api_ipv4_allow_list = local.health_insights_api_ipv4_allow_list + + existing_container_app_environment = local.existing_container_app_environment + existing_virtual_network = local.existing_virtual_network + existing_resource_group = local.existing_resource_group + launch_in_vnet = false + container_app_name_override = "complete-dotnet-app" + + enable_monitoring = local.enable_monitoring + monitor_email_receivers = local.monitor_email_receivers + container_health_probe_path = local.container_health_probe_path + monitor_endpoint_healthcheck = local.monitor_endpoint_healthcheck + existing_logic_app_workflow = local.existing_logic_app_workflow + monitor_http_availability_verb = local.monitor_http_availability_verb + monitor_http_availability_fqdn = local.monitor_http_availability_fqdn +} diff --git a/terraform/key-vault-tfvars-secrets.tf b/terraform/key-vault-tfvars-secrets.tf new file mode 100644 index 00000000..2bac5a34 --- /dev/null +++ b/terraform/key-vault-tfvars-secrets.tf @@ -0,0 +1,17 @@ +module "azurerm_key_vault" { + source = "github.com/DFE-Digital/terraform-azurerm-key-vault-tfvars?ref=v0.5.1" + + environment = local.environment + project_name = "compnet" + existing_resource_group = local.existing_resource_group + azure_location = local.azure_location + key_vault_access_use_rbac_authorization = true + key_vault_access_users = [] + key_vault_access_ipv4 = local.key_vault_access_ipv4 + tfvars_filename = local.tfvars_filename + diagnostic_log_analytics_workspace_id = module.azure_container_apps_hosting.azurerm_log_analytics_workspace_container_app.id + diagnostic_eventhub_name = "" + tags = local.tags + + depends_on = [module.azure_container_apps_hosting] +} diff --git a/terraform/locals.tf b/terraform/locals.tf new file mode 100644 index 00000000..02dd269d --- /dev/null +++ b/terraform/locals.tf @@ -0,0 +1,44 @@ +locals { + environment = var.environment + project_name = var.project_name + azure_location = var.azure_location + tags = var.tags + enable_container_registry = var.enable_container_registry + registry_admin_enabled = var.registry_admin_enabled + registry_use_managed_identity = var.registry_use_managed_identity + registry_managed_identity_assign_role = var.registry_managed_identity_assign_role + registry_server = var.registry_server + image_name = var.image_name + image_tag = var.image_tag + container_command = var.container_command + container_secret_environment_variables = var.container_secret_environment_variables + container_scale_http_concurrency = var.container_scale_http_concurrency + container_min_replicas = var.container_min_replicas + container_port = var.container_port + existing_container_app_environment = var.existing_container_app_environment + existing_virtual_network = var.existing_virtual_network + existing_resource_group = var.existing_resource_group + enable_dns_zone = var.enable_dns_zone + dns_zone_domain_name = var.dns_zone_domain_name + dns_ns_records = var.dns_ns_records + dns_txt_records = var.dns_txt_records + dns_mx_records = var.dns_mx_records + dns_alias_records = var.dns_alias_records + key_vault_access_ipv4 = var.key_vault_access_ipv4 + tfvars_filename = var.tfvars_filename + enable_monitoring = var.enable_monitoring + monitor_email_receivers = var.monitor_email_receivers + container_apps_allow_ips_inbound = var.container_apps_allow_ips_inbound + container_health_probe_path = var.container_health_probe_path + monitor_endpoint_healthcheck = var.monitor_endpoint_healthcheck + existing_logic_app_workflow = var.existing_logic_app_workflow + statuscake_monitored_resource_addresses = var.statuscake_monitored_resource_addresses + statuscake_contact_group_name = var.statuscake_contact_group_name + statuscake_contact_group_integrations = var.statuscake_contact_group_integrations + statuscake_contact_group_email_addresses = var.statuscake_contact_group_email_addresses + enable_health_insights_api = var.enable_health_insights_api + health_insights_api_cors_origins = var.health_insights_api_cors_origins + health_insights_api_ipv4_allow_list = var.health_insights_api_ipv4_allow_list + monitor_http_availability_verb = var.monitor_http_availability_verb + monitor_http_availability_fqdn = var.monitor_http_availability_fqdn +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 00000000..1f88c3e5 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,17 @@ +provider "azurerm" { + features {} + skip_provider_registration = true + storage_use_azuread = true + client_id = var.azure_client_id + client_secret = var.azure_client_secret + tenant_id = var.azure_tenant_id + subscription_id = var.azure_subscription_id +} + +provider "azapi" { + enable_hcl_output_for_data_source = true +} + +provider "statuscake" { + api_token = var.statuscake_api_token +} diff --git a/terraform/statuscake-tls-monitor.tf b/terraform/statuscake-tls-monitor.tf new file mode 100644 index 00000000..40d836d5 --- /dev/null +++ b/terraform/statuscake-tls-monitor.tf @@ -0,0 +1,11 @@ +module "statuscake-tls-monitor" { + source = "github.com/dfe-digital/terraform-statuscake-tls-monitor?ref=v0.1.5" + + statuscake_monitored_resource_addresses = local.statuscake_monitored_resource_addresses + statuscake_alert_at = [ # days to alert on + 40, 20, 5 + ] + statuscake_contact_group_name = local.statuscake_contact_group_name + statuscake_contact_group_integrations = local.statuscake_contact_group_integrations + statuscake_contact_group_email_addresses = local.statuscake_contact_group_email_addresses +} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 00000000..a0c7e660 --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -0,0 +1,16 @@ +environment = "development" +project_name = "myproject" +azure_location = "uksouth" +enable_container_registry = true +image_name = "myimage" +enable_mssql_database = true +enable_redis_cache = true +mssql_server_admin_password = "S3crEt" +mssql_database_name = "mydatabase" +container_command = ["/bin/bash", "-c", "echo hello && sleep 86400"] +container_environment_variables = { + "ASPNETCORE_ENVIRONMENT" = "production" +} +key_vault_access_users = [ + "someone_example.com#EXT#@tenantname.onmicrosoft.com", +] diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 00000000..5b44b65a --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,293 @@ +variable "azure_client_id" { + description = "Service Principal Client ID" + type = string +} + +variable "azure_client_secret" { + description = "Service Principal Client Secret" + type = string + sensitive = true +} + +variable "azure_tenant_id" { + description = "Service Principal Tenant ID" + type = string +} + +variable "azure_subscription_id" { + description = "Service Principal Subscription ID" + type = string +} + +variable "environment" { + description = "Environment name. Will be used along with `project_name` as a prefix for all resources." + type = string +} + +variable "key_vault_access_ipv4" { + description = "List of IPv4 Addresses that are permitted to access the Key Vault" + type = list(string) +} + +variable "tfvars_filename" { + description = "tfvars filename. This file is uploaded and stored encrupted within Key Vault, to ensure that the latest tfvars are stored in a shared place." + type = string +} + +variable "project_name" { + description = "Project name. Will be used along with `environment` as a prefix for all resources." + type = string +} + +variable "azure_location" { + description = "Azure location in which to launch resources." + type = string +} + +variable "tags" { + description = "Tags to be applied to all resources" + type = map(string) +} + +variable "enable_container_registry" { + description = "Set to true to create a container registry" + type = bool + default = false +} + +variable "registry_admin_enabled" { + description = "Do you want to enable access key based authentication for your Container Registry?" + type = bool + default = false +} + +variable "registry_server" { + description = "Container registry server (required if `enable_container_registry` is false)" + type = string + default = "" +} + +variable "registry_use_managed_identity" { + description = "Create a User-Assigned Managed Identity for the Container App. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity" + type = bool + default = true +} + +variable "registry_managed_identity_assign_role" { + description = "Assign the 'AcrPull' Role to the Container App User-Assigned Managed Identity. Note: If you do not have 'Microsoft.Authorization/roleAssignments/write' permission, you will need to manually assign the 'AcrPull' Role to the identity" + type = bool + default = false +} + +variable "image_name" { + description = "Image name" + type = string +} + +variable "image_tag" { + description = "Image tag" + type = string +} + +variable "container_command" { + description = "Container command" + type = list(any) +} + +variable "container_secret_environment_variables" { + description = "Container secret environment variables" + type = map(string) + sensitive = true +} + +variable "container_scale_http_concurrency" { + description = "When the number of concurrent HTTP requests exceeds this value, then another replica is added. Replicas continue to add to the pool up to the max-replicas amount." + type = number + default = 10 +} + +variable "enable_dns_zone" { + description = "Conditionally create a DNS zone" + type = bool +} + +variable "dns_zone_domain_name" { + description = "DNS zone domain name. If created, records will automatically be created to point to the CDN." + type = string +} + +variable "dns_ns_records" { + description = "DNS NS records to add to the DNS Zone" + type = map( + object({ + ttl : optional(number, 300), + records : list(string) + }) + ) +} + +variable "dns_txt_records" { + description = "DNS TXT records to add to the DNS Zone" + type = map( + object({ + ttl : optional(number, 300), + records : list(string) + }) + ) +} + +variable "dns_mx_records" { + description = "DNS MX records to add to the DNS Zone" + type = map( + object({ + ttl : optional(number, 300), + records : list( + object({ + preference : number, + exchange : string + }) + ) + }) + ) + default = {} +} + +variable "dns_alias_records" { + description = "DNS ALIAS records to add to the DNS Zone" + type = map( + object({ + ttl : optional(number, 300), + target_resource_id : string + }) + ) + default = {} +} + +variable "container_apps_allow_ips_inbound" { + description = "Restricts access to the Container Apps by creating a network security group rule that only allow inbound traffic from the provided list of IPs" + type = list(string) + default = [] +} + +variable "enable_monitoring" { + description = "Create an App Insights instance and notification group for the Container App" + type = bool +} + +variable "monitor_email_receivers" { + description = "A list of email addresses that should be notified by monitoring alerts" + type = list(string) +} + +variable "container_health_probe_path" { + description = "Specifies the path that is used to determine the liveness of the Container" + type = string +} + +variable "monitor_endpoint_healthcheck" { + description = "Specify a route that should be monitored for a 200 OK status" + type = string +} + +variable "existing_logic_app_workflow" { + description = "Name, and Resource Group of an existing Logic App Workflow. Leave empty to create a new Resource" + type = object({ + name : string + resource_group_name : string + }) + default = { + name = "" + resource_group_name = "" + } +} + +variable "statuscake_api_token" { + description = "API token for StatusCake" + type = string + sensitive = true + default = "00000000000000000000000000000" +} + +variable "statuscake_contact_group_name" { + description = "Name of the contact group in StatusCake" + type = string + default = "" +} + +variable "statuscake_contact_group_integrations" { + description = "List of Integration IDs to connect to your Contact Group" + type = list(string) + default = [] +} + +variable "statuscake_monitored_resource_addresses" { + description = "The URLs to perform TLS checks on" + type = list(string) + default = [] +} + +variable "statuscake_contact_group_email_addresses" { + description = "List of email address that should receive notifications from StatusCake" + type = list(string) + default = [] +} + +variable "container_min_replicas" { + description = "Container min replicas" + type = number + default = 1 +} + +variable "enable_health_insights_api" { + description = "Deploys a Function App that exposes the last 3 HTTP Web Tests via an API endpoint. 'enable_app_insights_integration' and 'enable_monitoring' must be set to 'true'." + type = bool + default = false +} + +variable "health_insights_api_cors_origins" { + description = "List of hostnames that are permitted to contact the Health insights API" + type = list(string) + default = ["*"] +} + +variable "health_insights_api_ipv4_allow_list" { + description = "List of IPv4 addresses that are permitted to contact the Health insights API" + type = list(string) + default = [] +} + +variable "container_port" { + description = "Container port" + type = number + default = 8080 +} + +variable "existing_container_app_environment" { + description = "Conditionally launch resources into an existing Container App environment. Specifying this will NOT create an environment." + type = object({ + name = string + resource_group = string + }) +} + +variable "existing_virtual_network" { + description = "Conditionally use an existing virtual network. The `virtual_network_address_space` must match an existing address space in the VNet. This also requires the resource group name." + type = string +} + +variable "existing_resource_group" { + description = "Conditionally launch resources into an existing resource group. Specifying this will NOT create a resource group." + type = string +} + +variable "monitor_http_availability_verb" { + description = "Which HTTP verb to use for the HTTP Availability check" + type = string + default = "GET" +} + +variable "monitor_http_availability_fqdn" { + description = "Specify a FQDN to monitor for HTTP Availability. Leave unset to dynamically calculate the correct FQDN" + type = string + default = "" +} diff --git a/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 00000000..73673e98 --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,20 @@ +terraform { + required_version = "~> 1.9" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + + statuscake = { + source = "StatusCakeDev/statuscake" + version = "~> 2.1" + } + + azapi = { + source = "Azure/azapi" + version = "~> 1.13" + } + } +}