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"
+ }
+ }
+}