From d646a6f75887d2be4369f8abf648076f3953a511 Mon Sep 17 00:00:00 2001 From: Ash Davies <3853061+DrizzlyOwl@users.noreply.github.com> Date: Tue, 10 Jan 2023 12:11:17 +0000 Subject: [PATCH 1/3] Added Docker build & push to ACR workflow --- .env.database.example | 3 ++ .env.example | 3 ++ .../build-and-push-image-development.yml | 43 ++++++++++++++++++ .gitignore | 9 ++++ Dockerfile | 44 +++++++++++++++++++ docker-compose.yml | 40 +++++++++++++++++ script/sqlcmd-docker-entrypoint.sh | 21 +++++++++ script/web-docker-entrypoint.sh | 33 ++++++++++++++ 8 files changed, 196 insertions(+) create mode 100644 .env.database.example create mode 100644 .env.example create mode 100644 .github/workflows/build-and-push-image-development.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100755 script/sqlcmd-docker-entrypoint.sh create mode 100755 script/web-docker-entrypoint.sh diff --git a/.env.database.example b/.env.database.example new file mode 100644 index 000000000..0c94aba5b --- /dev/null +++ b/.env.database.example @@ -0,0 +1,3 @@ +MSSQL_INITIAL_DATABASE="sip" +MSSQL_SA_PASSWORD="Your_password123" +ACCEPT_EULA="Y" diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..4bd3ab05d --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +ASPNETCORE_ENVIRONMENT="Development" +ApiKeys__0='{\"userName\": \"apikey\",\"apiKey\": \"apikey\"}' +ConnectionStrings__DefaultConnection="Data Source=db;Initial Catalog=sip;persist security info=True;User id=sa;Password=Your_password123" diff --git a/.github/workflows/build-and-push-image-development.yml b/.github/workflows/build-and-push-image-development.yml new file mode 100644 index 000000000..625e45fcc --- /dev/null +++ b/.github/workflows/build-and-push-image-development.yml @@ -0,0 +1,43 @@ +name: Continuous delivery (development) + +on: + push: + branches: + - main + +jobs: + build-and-push-image-development: + name: Build and push image development + environment: dev + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Azure Container Registry login + uses: docker/login-action@v2 + with: + username: ${{ secrets.DEVELOPMENT_AZURE_ACR_CLIENTID }} + password: ${{ secrets.DEVELOPMENT_AZURE_ACR_SECRET }} + registry: ${{ secrets.DEVELOPMENT_ACR_URL }} + + - name: Prepare tags + id: prepare-tags + run: | + DOCKER_IMAGE=${{ secrets.DEVELOPMENT_ACR_URL }}/tramsapi-app + VERSION=latest + TAGS="${DOCKER_IMAGE}:${VERSION}" + if [ "${{ github.event_name }}" = "push" ]; then + VERSION=sha-${GITHUB_SHA} + TAGS="$TAGS,${DOCKER_IMAGE}:${VERSION}" + fi + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + echo "deploy-version=${VERSION}" >> $GITHUB_OUTPUT + + - name: Push image + uses: docker/build-push-action@v3 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.prepare-tags.outputs.tags }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9f51e203f..6bcd493d9 100644 --- a/.gitignore +++ b/.gitignore @@ -147,3 +147,12 @@ _NCrunch* # node_modules CypressTests/node_modules + + +# Environment variables +.env +.env.* +.env.local +.env.*.local +!.env.development.local.example +!.env.database.example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..d56dd6130 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Stage 1 +ARG ASPNET_IMAGE_TAG=3.1-bullseye-slim +ARG COMMIT_SHA=not-set + +FROM mcr.microsoft.com/dotnet/sdk:3.1-bullseye AS build +WORKDIR /build + +ENV DEBIAN_FRONTEND=noninteractive + +COPY . . + +RUN dotnet build TramsDataApi -c Release +RUN dotnet new tool-manifest +RUN dotnet tool install dotnet-ef --version 6.0.5 +ENV PATH="$PATH:/root/.dotnet/tools" + +RUN mkdir -p /app/SQL +RUN dotnet ef migrations script --output /app/SQL/DbMigrationScriptLegacy.sql --project TramsDataApi --context TramsDataApi.DatabaseModels.LegacyTramsDbContext --idempotent -v +RUN dotnet ef migrations script --output /app/SQL/DbMigrationScript.sql --project TramsDataApi --context TramsDataApi.DatabaseModels.TramsDbContext --idempotent -v +RUN touch /app/SQL/DbMigrationScript.sql +RUN touch /app/SQL/DbMigrationScriptLegacy.sql + +RUN dotnet publish TramsDataApi -c Release -o /app --no-build +COPY ./script/web-docker-entrypoint.sh /app/docker-entrypoint.sh + +# Stage 3 - Final +ARG ASPNET_IMAGE_TAG +FROM "mcr.microsoft.com/dotnet/aspnet:${ASPNET_IMAGE_TAG}" AS final + +ARG COMMIT_SHA +RUN echo "Setting env releasetag=${COMMIT_SHA}" +ENV TramsDataApi:ReleaseTag="${COMMIT_SHA}" + +RUN apt-get update +RUN apt-get install unixodbc curl gnupg -y +RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add - +RUN curl https://packages.microsoft.com/config/debian/11/prod.list | tee /etc/apt/sources.list.d/msprod.list +RUN apt-get update +RUN ACCEPT_EULA=Y apt-get install msodbcsql18 mssql-tools18 -y + +COPY --from=build /app /app +WORKDIR /app +RUN chmod +x ./docker-entrypoint.sh +EXPOSE 80/tcp diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..3065f33b2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +version: "3.8" +services: + webapi: + build: + context: . + dockerfile: Dockerfile + command: /bin/bash -c "./docker-entrypoint.sh dotnet TramsDataApi.dll" + ports: + - 80:80/tcp + restart: always + depends_on: + - db + - sqlcmd + env_file: + - .env.development + networks: + - dev + + db: + image: mcr.microsoft.com/azure-sql-edge:latest + env_file: .env.database + ports: + - 1433:1433 + networks: + - dev + + sqlcmd: + image: mcr.microsoft.com/mssql-tools:latest + env_file: .env.database + command: /etc/docker-entrypoint.sh + depends_on: + - db + stdin_open: true + volumes: + - ./script/sqlcmd-docker-entrypoint.sh:/etc/docker-entrypoint.sh + networks: + - dev + +networks: + dev: diff --git a/script/sqlcmd-docker-entrypoint.sh b/script/sqlcmd-docker-entrypoint.sh new file mode 100755 index 000000000..6111b8be9 --- /dev/null +++ b/script/sqlcmd-docker-entrypoint.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# exit on failures +set -e +set -o pipefail + +MSSQL_INITIAL_DATABASE="${MSSQL_INITIAL_DATABASE:?}" + +echo "CREATE DATABASE $MSSQL_INITIAL_DATABASE;" > ./setup.sql +echo "GO" >> ./setup.sql + +echo "Creating initial database ..." +until /opt/mssql-tools/bin/sqlcmd -S db -U sa -P "$MSSQL_SA_PASSWORD" -d master -i ./setup.sql +do + echo "not ready yet..." + sleep 1 +done + +rm ./setup.sql + +echo "Created database $MSSQL_INITIAL_DATABASE ..." diff --git a/script/web-docker-entrypoint.sh b/script/web-docker-entrypoint.sh new file mode 100755 index 000000000..10760793b --- /dev/null +++ b/script/web-docker-entrypoint.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# exit on failures +set -e +set -o pipefail + +ConnectionStrings__DefaultConnection=${ConnectionStrings__DefaultConnection:?} + +declare -A mysqlconn + +for keyvaluepair in $(echo "$ConnectionStrings__DefaultConnection" | sed "s/ //g; s/;/ /g") +do + IFS=" " read -r -a ARR <<< "${keyvaluepair//=/ }" + mysqlconn[${ARR[0]}]=${ARR[1]} +done + +echo "Running TramsDbContext database migrations ..." +until /opt/mssql-tools18/bin/sqlcmd -S "${mysqlconn[Server]}" -U "${mysqlconn[UserId]}" -P "${mysqlconn[Password]}" -d "${mysqlconn[Database]}" -C -i /app/SQL/DbMigrationScript.sql -o /app/SQL/DbMigrationScriptOutput.txt +do + cat /app/SQL/DbMigrationScriptOutput.txt + echo "Retrying database migrations ..." + sleep 5 +done + +echo "Running LegacyTramsDbContext database migrations ..." +until /opt/mssql-tools18/bin/sqlcmd -S "${mysqlconn[Server]}" -U "${mysqlconn[UserId]}" -P "${mysqlconn[Password]}" -d "${mysqlconn[Database]}" -C -i /app/SQL/DbMigrationScriptLegacy.sql -o /app/SQL/DbMigrationScriptOutputLegacy.txt +do + cat /app/SQL/DbMigrationScriptOutputLegacy.txt + echo "Retrying database migrations ..." + sleep 5 +done + +exec "$@" From 42beaebdbbe74daaa1ed91df3fce4a7b25919abc Mon Sep 17 00:00:00 2001 From: Ash Davies <3853061+DrizzlyOwl@users.noreply.github.com> Date: Mon, 16 Jan 2023 10:49:10 +0000 Subject: [PATCH 2/3] Reordered layers in Dockerfile to improve build time --- Dockerfile | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index d56dd6130..3bb7c58d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,3 @@ -# Stage 1 -ARG ASPNET_IMAGE_TAG=3.1-bullseye-slim -ARG COMMIT_SHA=not-set - FROM mcr.microsoft.com/dotnet/sdk:3.1-bullseye AS build WORKDIR /build @@ -9,27 +5,24 @@ ENV DEBIAN_FRONTEND=noninteractive COPY . . -RUN dotnet build TramsDataApi -c Release +RUN mkdir -p /app/SQL +RUN touch /app/SQL/DbMigrationScriptLegacy.sql +RUN touch /app/SQL/DbMigrationScript.sql + +RUN dotnet restore TramsDataApi.sln RUN dotnet new tool-manifest RUN dotnet tool install dotnet-ef --version 6.0.5 ENV PATH="$PATH:/root/.dotnet/tools" -RUN mkdir -p /app/SQL RUN dotnet ef migrations script --output /app/SQL/DbMigrationScriptLegacy.sql --project TramsDataApi --context TramsDataApi.DatabaseModels.LegacyTramsDbContext --idempotent -v -RUN dotnet ef migrations script --output /app/SQL/DbMigrationScript.sql --project TramsDataApi --context TramsDataApi.DatabaseModels.TramsDbContext --idempotent -v -RUN touch /app/SQL/DbMigrationScript.sql -RUN touch /app/SQL/DbMigrationScriptLegacy.sql +RUN dotnet ef migrations script --output /app/SQL/DbMigrationScript.sql --project TramsDataApi --context TramsDataApi.DatabaseModels.TramsDbContext --idempotent --no-build -v -RUN dotnet publish TramsDataApi -c Release -o /app --no-build -COPY ./script/web-docker-entrypoint.sh /app/docker-entrypoint.sh +# this build has no effect on ef migrations because it is a "Release" configuration +RUN dotnet build -c Release TramsDataApi.sln --no-restore +RUN dotnet publish TramsDataApi -c Release -o /app --no-restore -# Stage 3 - Final ARG ASPNET_IMAGE_TAG -FROM "mcr.microsoft.com/dotnet/aspnet:${ASPNET_IMAGE_TAG}" AS final - -ARG COMMIT_SHA -RUN echo "Setting env releasetag=${COMMIT_SHA}" -ENV TramsDataApi:ReleaseTag="${COMMIT_SHA}" +FROM mcr.microsoft.com/dotnet/aspnet:3.1-bullseye-slim AS final RUN apt-get update RUN apt-get install unixodbc curl gnupg -y @@ -39,6 +32,8 @@ RUN apt-get update RUN ACCEPT_EULA=Y apt-get install msodbcsql18 mssql-tools18 -y COPY --from=build /app /app + WORKDIR /app +COPY ./script/web-docker-entrypoint.sh ./docker-entrypoint.sh RUN chmod +x ./docker-entrypoint.sh EXPOSE 80/tcp From 565ca93959dbd62e7071f0505d189fb7b9ad6a45 Mon Sep 17 00:00:00 2001 From: Ash Davies <3853061+DrizzlyOwl@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:33:24 +0000 Subject: [PATCH 3/3] Added Terraform Hosting Module --- .gitignore | 11 ++ terraform/.terraform-docs.yml | 26 ++++ terraform/.terraform-version | 1 + terraform/.terraform.lock.hcl | 80 ++++++++++++ terraform/Brewfile | 6 + terraform/README.md | 173 ++++++++++++++++++++++++++ terraform/backend.tf | 3 + terraform/backend.vars.example | 5 + terraform/container-apps-hosting.tf | 18 +++ terraform/data.tf | 7 ++ terraform/key-vault-tfvars-secrets.tf | 54 ++++++++ terraform/locals.tf | 14 +++ terraform/providers.tf | 6 + terraform/terraform.tfvars.example | 16 +++ terraform/variables.tf | 60 +++++++++ terraform/versions.tf | 13 ++ 16 files changed, 493 insertions(+) create mode 100644 terraform/.terraform-docs.yml create mode 100644 terraform/.terraform-version create mode 100644 terraform/.terraform.lock.hcl create mode 100644 terraform/Brewfile create mode 100644 terraform/README.md create mode 100644 terraform/backend.tf create mode 100644 terraform/backend.vars.example create mode 100644 terraform/container-apps-hosting.tf create mode 100644 terraform/data.tf create mode 100644 terraform/key-vault-tfvars-secrets.tf create mode 100644 terraform/locals.tf create mode 100644 terraform/providers.tf create mode 100644 terraform/terraform.tfvars.example create mode 100644 terraform/variables.tf create mode 100644 terraform/versions.tf diff --git a/.gitignore b/.gitignore index 6bcd493d9..d5ced6436 100644 --- a/.gitignore +++ b/.gitignore @@ -148,6 +148,8 @@ _NCrunch* # node_modules CypressTests/node_modules +# Homebrew +Brewfile.lock.json # Environment variables .env @@ -156,3 +158,12 @@ CypressTests/node_modules .env.*.local !.env.development.local.example !.env.database.example + +### Terraform +.terraformrc* +terraform.rc* +*.tfstate* +*.tfvars* +!terraform.tfvars.example +.terraform/ +backend.vars diff --git a/terraform/.terraform-docs.yml b/terraform/.terraform-docs.yml new file mode 100644 index 000000000..a6917808f --- /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 000000000..3336003dc --- /dev/null +++ b/terraform/.terraform-version @@ -0,0 +1 @@ +1.3.7 diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl new file mode 100644 index 000000000..5e0a872e5 --- /dev/null +++ b/terraform/.terraform.lock.hcl @@ -0,0 +1,80 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/azure/azapi" { + version = "1.2.0" + constraints = ">= 1.1.0" + hashes = [ + "h1:RTlIi2Ja1PeMmQ2OVlXQrJulcSXiX+OJYq5K65G8Eeo=", + "zh:0dcca6970347a67c2192be251ea1e98b5df50a8f7cb352adbf2fbdb6cbe02b03", + "zh:306545eedf8a6dbc64d34fdc9fe104b874d45f9ab5ca1232e9670e36376f7d04", + "zh:31484f398b08b10b86af2d89c89ae13e513bf738dfe90e2ff3dc564332b8c180", + "zh:36fda73c39c56495bfda7fa746863357ab8284d27724d809130c03fada62301f", + "zh:40075df5e753032bd9a7d2698e762d355524f56a4a7e8df65489d44273aa0e32", + "zh:6b7667b74eaa0e0884423704e48bb3468e465fd99de108e64b93c9e26c8c4b0d", + "zh:8248289b744cdeb9d3e20dedd13a66ba3225f8efd6694a556f69acf16825f45f", + "zh:86d864576520952d74b0ac5d92ed9efe09894c5405cbadddf1d95c2b39c4a514", + "zh:9b5d3c6a753cc57be31ba6a6e14b571ebbaa5c0c14791ef20a975b9c15c65252", + "zh:a522991b3d8dfb3b7073c3cd44fb6b8d9957c006ad304e299342e29d11bde854", + "zh:b1f931cd16e5a8235131b11c4a0d93f122d22dec597534da1bac03324e564fb1", + "zh:fa1f470859b8c5dc778561cbf0749b0c002cfa13bbd3574a2574ea682368d73f", + ] +} + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.31.0" + hashes = [ + "h1:0D8+cQBlCyA50NiiTJwNDK9QjKfZsjuHgXTFRlhIZyg=", + "zh:02a64db03707cc6970ab28a1da00d7fa011cc54e8a7806209f31bd8aad1794e1", + "zh:077ffce8135a57544ec3c227bbe0ee5f6ca649223bd1dc0bbbd31d3fdf616830", + "zh:0a369de6132edb0f4a69f2aa472b23f9bb5c430a3d539146d1c18d4cc7b12c7f", + "zh:14bfc5f2354c1389eb7ed8bf5a5eaadc9940e18c2dd15058eb9b48ea5c37ae66", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:5629f020ac3409ad34a39e221fb2e63f82948c3eb936508331d5a7f870556e9d", + "zh:5b419eb59fa4e0b9c520c5cd5028f236bce6d9ab701c5ccca23cc040d3d690c4", + "zh:5e7e6207fd58a3e9ba54b7333169a3e3ea693c25c8f477622536600a8a09a3f1", + "zh:a9a552ad36d7a3db4554c6fbc716cf8631328331ea6188eddb4038b4c213ff46", + "zh:aee812d33916e5fdfb4d58ce74af0f3b2a7a58dbfb5ec8e0b42b5780ceff5414", + "zh:ce46738cd1909675b980bb90b9c3d919a4d1d655b4296082b86b6622ce818f7a", + "zh:db02dbe5ce139610688b354b15eb934f9f67ab32d6c5d63690dce6f9b8d90904", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.38.0" + constraints = ">= 3.35.0" + hashes = [ + "h1:Isa/rY8+4+DCatuYgmDT4TYkcp/he7RrfR6jyhrm7hQ=", + "zh:08df48bdaf162bf3da7ac2b09147d44f94fae6f3cfd97d6cf9c45cb7c1c36a44", + "zh:220b68a3f819777872281974e6621527698575096c3a2ef78cb0aabf28665161", + "zh:25db1128a96599ffbcc7e865579bec7c009cb4e7f7731e0e30d261ab02cc38d5", + "zh:279444db11f570b837143559e5df7453bd8aeda4e22a9879a5a1a795bf6612a3", + "zh:2d506b6b865f6d5143e54e139d9a61b18bdcc8b9485d2bc7237e95a53a9c7ed9", + "zh:6ddb2cbcdf15b432508fe00ee7863f6d51a136db1746e7af03bec8ce2a09bad3", + "zh:96b664a716678923ce0f9828eaad22b5353669fa5013ea39b7b8081a77988b85", + "zh:a9ca583b219a3daba171ca11908547abb1b09453934950aacff17ae8b51d0ff0", + "zh:aa497620c82afab7819736180f0a56b76da6f3e23bd0580383fda98104b4e5c2", + "zh:ab9e9f3c35288d0bd615024f213e46d16d639c281f7d850b21971b530d08e231", + "zh:b164a0ddb30b64c35f13dad0aa9701a4e3eb24dc8165a3e794c499f1e9070b99", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.1" + hashes = [ + "h1:ydA0/SNRVB1o95btfshvYsmxA+jZFRZcvKzZSB+4S1M=", + "zh:58ed64389620cc7b82f01332e27723856422820cfd302e304b5f6c3436fb9840", + "zh:62a5cc82c3b2ddef7ef3a6f2fedb7b9b3deff4ab7b414938b08e51d6e8be87cb", + "zh:63cff4de03af983175a7e37e52d4bd89d990be256b16b5c7f919aff5ad485aa5", + "zh:74cb22c6700e48486b7cabefa10b33b801dfcab56f1a6ac9b6624531f3d36ea3", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79e553aff77f1cfa9012a2218b8238dd672ea5e1b2924775ac9ac24d2a75c238", + "zh:a1e06ddda0b5ac48f7e7c7d59e1ab5a4073bbcf876c73c0299e4610ed53859dc", + "zh:c37a97090f1a82222925d45d84483b2aa702ef7ab66532af6cbcfb567818b970", + "zh:e4453fbebf90c53ca3323a92e7ca0f9961427d2f0ce0d2b65523cc04d5d999c2", + "zh:e80a746921946d8b6761e77305b752ad188da60688cfd2059322875d363be5f5", + "zh:fbdb892d9822ed0e4cb60f2fedbdbb556e4da0d88d3b942ae963ed6ff091e48f", + "zh:fca01a623d90d0cad0843102f9b8b9fe0d3ff8244593bd817f126582b52dd694", + ] +} diff --git a/terraform/Brewfile b/terraform/Brewfile new file mode 100644 index 000000000..000ace386 --- /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 000000000..ac9477b80 --- /dev/null +++ b/terraform/README.md @@ -0,0 +1,173 @@ +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.3.6 | +| [azapi](#requirement\_azapi) | >= 1.1.0 | +| [azurerm](#requirement\_azurerm) | >= 3.35.0 | + +## Providers + +| Name | Version | +|------|---------| +| [azuread](#provider\_azuread) | 2.31.0 | +| [azurerm](#provider\_azurerm) | 3.38.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [azure\_container\_apps\_hosting](#module\_azure\_container\_apps\_hosting) | github.com/DFE-Digital/terraform-azurerm-container-apps-hosting | v0.12.0 | + +## Resources + +| Name | Type | +|------|------| +| [azurerm_key_vault.tfvars](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | +| [azurerm_key_vault_secret.tfvars](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_secret) | resource | +| [azuread_user.key_vault_access](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/user) | data source | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [azure\_location](#input\_azure\_location) | Azure location in which to launch resources. | `string` | n/a | yes | +| [container\_command](#input\_container\_command) | Container command | `list(any)` | n/a | yes | +| [container\_secret\_environment\_variables](#input\_container\_secret\_environment\_variables) | Container secret environment variables | `map(string)` | n/a | yes | +| [enable\_cdn\_frontdoor](#input\_enable\_cdn\_frontdoor) | Enable Azure CDN FrontDoor. This will use the Container Apps endpoint as the origin. | `bool` | n/a | yes | +| [enable\_container\_registry](#input\_enable\_container\_registry) | Set to true to create a container registry | `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 | +| [image\_name](#input\_image\_name) | Image name | `string` | n/a | yes | +| [key\_vault\_access\_users](#input\_key\_vault\_access\_users) | List of users that require access to the Key Vault where tfvars are stored. This should be a list of User Principle Names (Found in Active Directory) that need to run terraform | `list(string)` | n/a | yes | +| [project\_name](#input\_project\_name) | Project name. Will be used along with `environment` as a prefix for all resources. | `string` | n/a | yes | +| [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 | +| [virtual\_network\_address\_space](#input\_virtual\_network\_address\_space) | Virtual network address space CIDR | `string` | n/a | yes | + +## Outputs + +No outputs. + diff --git a/terraform/backend.tf b/terraform/backend.tf new file mode 100644 index 000000000..6602f2060 --- /dev/null +++ b/terraform/backend.tf @@ -0,0 +1,3 @@ +terraform { + backend "azurerm" {} +} diff --git a/terraform/backend.vars.example b/terraform/backend.vars.example new file mode 100644 index 000000000..bc2382128 --- /dev/null +++ b/terraform/backend.vars.example @@ -0,0 +1,5 @@ +resource_group_name = "" +storage_account_name = "" +container_name = "" +key = "terraform.tstate" +subscription_id = "" diff --git a/terraform/container-apps-hosting.tf b/terraform/container-apps-hosting.tf new file mode 100644 index 000000000..d90a4b8e4 --- /dev/null +++ b/terraform/container-apps-hosting.tf @@ -0,0 +1,18 @@ +module "azure_container_apps_hosting" { + source = "github.com/DFE-Digital/terraform-azurerm-container-apps-hosting?ref=v0.12.0" + + environment = local.environment + project_name = local.project_name + azure_location = local.azure_location + tags = local.tags + + virtual_network_address_space = local.virtual_network_address_space + + enable_container_registry = local.enable_container_registry + + image_name = local.image_name + container_command = local.container_command + container_secret_environment_variables = local.container_secret_environment_variables + + enable_cdn_frontdoor = local.enable_cdn_frontdoor +} diff --git a/terraform/data.tf b/terraform/data.tf new file mode 100644 index 000000000..8fc2edce3 --- /dev/null +++ b/terraform/data.tf @@ -0,0 +1,7 @@ +data "azurerm_client_config" "current" {} + +data "azuread_user" "key_vault_access" { + for_each = local.key_vault_access_users + + user_principal_name = each.value +} diff --git a/terraform/key-vault-tfvars-secrets.tf b/terraform/key-vault-tfvars-secrets.tf new file mode 100644 index 000000000..cf6ae3538 --- /dev/null +++ b/terraform/key-vault-tfvars-secrets.tf @@ -0,0 +1,54 @@ +resource "azurerm_key_vault" "tfvars" { + name = "${local.environment}${local.project_name}-tfvars" + location = module.azure_container_apps_hosting.azurerm_resource_group_default.location + resource_group_name = module.azure_container_apps_hosting.azurerm_resource_group_default.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + enable_rbac_authorization = false + + dynamic "access_policy" { + for_each = data.azuread_user.key_vault_access + + content { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = access_policy.value["object_id"] + + key_permissions = [ + "Create", + "Get", + ] + + secret_permissions = [ + "Set", + "Get", + "Delete", + "Purge", + "Recover", + "List", + ] + } + } + + # It won't be possible to add/manage a network acl for this + # vault, as it will need to be accessable for multiple people. + # tfsec:ignore:azure-keyvault-specify-network-acl + network_acls { + bypass = "None" + default_action = "Allow" + } + + purge_protection_enabled = true + + tags = local.tags +} + +# Expiry doesn't need to be set, as this is just used as a way to +# store and share the tfvars +# tfsec:ignore:azure-keyvault-ensure-secret-expiry +resource "azurerm_key_vault_secret" "tfvars" { + name = "${local.environment}${local.project_name}-tfvars" + value = base64encode(file(local.tfvars_filename)) + key_vault_id = azurerm_key_vault.tfvars.id + content_type = "text/plain+base64" +} diff --git a/terraform/locals.tf b/terraform/locals.tf new file mode 100644 index 000000000..83f2a04c5 --- /dev/null +++ b/terraform/locals.tf @@ -0,0 +1,14 @@ +locals { + environment = var.environment + project_name = var.project_name + azure_location = var.azure_location + tags = var.tags + virtual_network_address_space = var.virtual_network_address_space + enable_container_registry = var.enable_container_registry + image_name = var.image_name + container_command = var.container_command + container_secret_environment_variables = var.container_secret_environment_variables + enable_cdn_frontdoor = var.enable_cdn_frontdoor + key_vault_access_users = toset(var.key_vault_access_users) + tfvars_filename = var.tfvars_filename +} diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 000000000..12bf2de93 --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,6 @@ +provider "azurerm" { + features {} + skip_provider_registration = true +} + +provider "azapi" {} diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 000000000..a0c7e6600 --- /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 000000000..9c9d2a912 --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,60 @@ +variable "environment" { + description = "Environment name. Will be used along with `project_name` as a prefix for all resources." + type = string +} + +variable "key_vault_access_users" { + description = "List of users that require access to the Key Vault where tfvars are stored. This should be a list of User Principle Names (Found in Active Directory) that need to run terraform" + 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 "virtual_network_address_space" { + description = "Virtual network address space CIDR" + type = string +} + +variable "enable_container_registry" { + description = "Set to true to create a container registry" + type = bool +} + +variable "image_name" { + description = "Image name" + 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 "enable_cdn_frontdoor" { + description = "Enable Azure CDN FrontDoor. This will use the Container Apps endpoint as the origin." + type = bool +} diff --git a/terraform/versions.tf b/terraform/versions.tf new file mode 100644 index 000000000..94c960a56 --- /dev/null +++ b/terraform/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.3.6" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.35.0" + } + azapi = { + source = "Azure/azapi" + version = ">= 1.1.0" + } + } +}