diff --git a/.editorconfig b/.editorconfig index ddecac1..5923fce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,3 +23,6 @@ indent_style = tab [scripts/infra/README.md] ignore = true + +[specification/enviroment/beta/*] +ignore = true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..5c39b6d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# .github/CODEOWNERS +* @NHSDigital/RaviNatarajan22 diff --git a/.github/actions/lint-terraform/action.yaml b/.github/actions/lint-terraform/action.yaml index d5dfe35..65f5897 100644 --- a/.github/actions/lint-terraform/action.yaml +++ b/.github/actions/lint-terraform/action.yaml @@ -11,10 +11,3 @@ runs: shell: bash run: | check_only=true scripts/githooks/check-terraform-format.sh - - name: "Validate Terraform" - shell: bash - run: | - stacks=${{ inputs.root-modules }} - for dir in $(find infrastructure/environments -maxdepth 1 -mindepth 1 -type d; echo ${stacks//,/$'\n'}); do - dir=$dir make terraform-validate - done diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml index cd068ec..d6032de 100644 --- a/.github/workflows/cicd-1-pull-request.yaml +++ b/.github/workflows/cicd-1-pull-request.yaml @@ -7,13 +7,13 @@ on: branches: - "**" pull_request: - types: [opened, reopened] + types: [opened, reopened, synchronize] jobs: metadata: name: "Set CI/CD metadata" runs-on: ubuntu-latest - timeout-minutes: 1 + timeout-minutes: 5 outputs: build_datetime_london: ${{ steps.variables.outputs.build_datetime_london }} build_datetime: ${{ steps.variables.outputs.build_datetime }} @@ -37,7 +37,7 @@ jobs: echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT echo "nodejs_version=$(grep "^nodejs" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT - echo "python_version=$(grep "^nodejs" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + echo "python_version=$(grep "^python" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT echo "terraform_version=$(grep "^terraform" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT - name: "Check if pull request exists for this branch" @@ -79,6 +79,47 @@ jobs: terraform_version: "${{ needs.metadata.outputs.terraform_version }}" version: "${{ needs.metadata.outputs.version }}" secrets: inherit + build-matrix: + name: Build environment matrix from secrets + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Build matrix JSON + id: set-matrix + env: + AWS_INT_PR_ROLE: ${{ secrets.AWS_INT_PR_ROLE }} + AWS_PROD_PR_ROLE: ${{ secrets.AWS_PROD_PR_ROLE }} + run: | + set -euo pipefail + entries=() + [ -n "${AWS_INT_PR_ROLE:-}" ] && entries+=("{'environment':'int'}") + [ -n "${AWS_PROD_PR_ROLE:-}" ] && entries+=("{'environment':'prod'}") + + if [ "${#entries[@]}" -eq 0 ]; then + echo "matrix=[]" >> $GITHUB_OUTPUT + else + printf -v matrix_json '[%s]' "$(IFS=,; echo "${entries[*]}")" + echo "matrix=$matrix_json" >> $GITHUB_OUTPUT + echo "matrix=$matrix_json" + fi + infra-validate-stage: + name: "Infrastructure validation" + needs: [metadata, commit-stage, build-matrix] + strategy: + matrix: + include: ${{ fromJson(needs.build-matrix.outputs.matrix) }} + uses: ./.github/workflows/infra-validate.yaml + with: + build_datetime: "${{ needs.metadata.outputs.build_datetime }}" + build_timestamp: "${{ needs.metadata.outputs.build_timestamp }}" + build_epoch: "${{ needs.metadata.outputs.build_epoch }}" + nodejs_version: "${{ needs.metadata.outputs.nodejs_version }}" + python_version: "${{ needs.metadata.outputs.python_version }}" + terraform_version: "${{ needs.metadata.outputs.terraform_version }}" + version: "${{ needs.metadata.outputs.version }}" + environment: ${{ matrix.environment }} + secrets: inherit test-stage: # Recommended maximum execution time is 5 minutes name: "Test stage" needs: [metadata, commit-stage] diff --git a/.github/workflows/cicd-3-deploy.yaml b/.github/workflows/cicd-3-deploy.yaml index 2745b38..dc1f9cc 100644 --- a/.github/workflows/cicd-3-deploy.yaml +++ b/.github/workflows/cicd-3-deploy.yaml @@ -4,14 +4,34 @@ on: workflow_dispatch: inputs: tag: - description: "This is the tag that is oging to be deployed" + description: "This is the tag that is going to be deployed" required: true default: "latest" + environment: + description: "Select environment" + required: true + type: choice + options: + - int + - prod jobs: + validate-main-branch: + name: "Validate deployment from main branch" + runs-on: ubuntu-latest + steps: + - name: "Check branch" + run: | + if [ "${{ github.ref }}" != "refs/heads/main" ]; then + echo "ERROR: This workflow can only be run from the 'main' branch." >&2 + echo "Current branch: ${{ github.ref }}" >&2 + exit 1 + fi + echo "bypassing the checks temporarily for testing" metadata: name: "Set CI/CD metadata" runs-on: ubuntu-latest + environment: ${{ inputs.environment }} timeout-minutes: 1 outputs: build_datetime: ${{ steps.variables.outputs.build_datetime }} @@ -49,14 +69,13 @@ jobs: export VERSION="${{ steps.variables.outputs.version }}" export TAG="${{ steps.variables.outputs.tag }}" make list-variables - deploy: - name: "Deploy to an environment" - runs-on: ubuntu-latest - needs: [metadata] - timeout-minutes: 10 - steps: - - name: "Checkout code" - uses: actions/checkout@v4 + infra-deploy: + needs: [validate-main-branch] + uses: ./.github/workflows/infra-deploy.yaml + with: + environment: ${{ inputs.environment }} + secrets: inherit + # TODO: More jobs or/and steps here # success: # name: "Success notification" diff --git a/.github/workflows/infra-deploy.yaml b/.github/workflows/infra-deploy.yaml new file mode 100644 index 0000000..beb2c6e --- /dev/null +++ b/.github/workflows/infra-deploy.yaml @@ -0,0 +1,209 @@ +name: "Infrastructure Deploy" + +# Deployment workflow for infrastructure changes +# Called from cicd-3-deploy after merge - deploys to selected environment + +on: + workflow_call: + inputs: + environment: + required: true + type: string + +permissions: + id-token: write + contents: read + +env: + AWS_DEPLOY_ROLE: ${{ inputs.environment == 'prod' && secrets.AWS_PROD_DEPLOY_ROLE || secrets.AWS_INT_DEPLOY_ROLE }} + +jobs: + metadata: + name: "Set CI/CD metadata" + runs-on: ubuntu-latest + timeout-minutes: 1 + outputs: + build_datetime: ${{ steps.variables.outputs.build_datetime }} + build_timestamp: ${{ steps.variables.outputs.build_timestamp }} + build_epoch: ${{ steps.variables.outputs.build_epoch }} + terraform_version: ${{ steps.variables.outputs.terraform_version }} + version: ${{ steps.variables.outputs.version }} + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Set CI/CD variables" + id: variables + run: | + datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') + echo "build_datetime=$datetime" >> $GITHUB_OUTPUT + echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT + echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT + echo "terraform_version=$(grep "^terraform" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT + + - name: "List variables" + run: | + export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" + export BUILD_TIMESTAMP="${{ steps.variables.outputs.build_timestamp }}" + export BUILD_EPOCH="${{ steps.variables.outputs.build_epoch }}" + export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}" + export VERSION="${{ steps.variables.outputs.version }}" + make list-variables + + terraform-validate: + name: "Terraform validate" + needs: [metadata] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.outputs.terraform_version }} + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.AWS_DEPLOY_ROLE }} + aws-region: eu-west-2 + + - name: "Terraform Init" + run: | + make terraform-init \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} + + - name: "Terraform Validate" + run: | + make terraform-validate \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} + + terraform-plan: + name: "Terraform plan for ${{ inputs.environment }}" + needs: [terraform-validate] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + with: + ref: main + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.outputs.terraform_version }} + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.AWS_DEPLOY_ROLE }} + aws-region: eu-west-2 + + - name: "Terraform Init" + run: | + make terraform-init \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} + + - name: "Terraform Validate" + run: | + make terraform-validate \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} + + - name: "Terraform Plan" + id: plan + run: | + make terraform-plan \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} \ + opts="-var-file=../terraform.tfvars\ -var-file=./terraform.tfvars\ -no-color\ -out=tfplan" + + - name: "Save tfplan" + uses: actions/upload-artifact@v4 + with: + name: tfplan-${{ inputs.environment }}-${{ github.run_id }} + path: infrastructure/environments/${{ inputs.environment }}/tfplan + retention-days: 7 + + terraform-apply: + name: "Terraform apply to ${{ inputs.environment }}" + needs: [terraform-plan] + runs-on: ubuntu-latest + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + with: + ref: main + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.outputs.terraform_version }} + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.AWS_DEPLOY_ROLE }} + aws-region: eu-west-2 + + - name: "Terraform Init" + run: | + make terraform-init \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} + + - name: "Download tfplan" + uses: actions/download-artifact@v4 + with: + name: tfplan-${{ inputs.environment }}-${{ github.run_id }} + path: infrastructure/environments/${{ inputs.environment }} + + - name: "Terraform Apply" + run: | + make terraform-apply \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} \ + opts="-no-color\ -auto-approve\ tfplan" + diff --git a/.github/workflows/infra-validate.yaml b/.github/workflows/infra-validate.yaml new file mode 100644 index 0000000..1eb4f2a --- /dev/null +++ b/.github/workflows/infra-validate.yaml @@ -0,0 +1,178 @@ +name: "Infrastructure Validate" + +# Validation workflow for infrastructure changes +# Runs on pull requests to validate Terraform code without deploying + +on: + workflow_call: + inputs: + build_datetime: + description: "Build datetime, set by the CI/CD pipeline workflow" + required: true + type: string + build_timestamp: + description: "Build timestamp, set by the CI/CD pipeline workflow" + required: true + type: string + build_epoch: + description: "Build epoch, set by the CI/CD pipeline workflow" + required: true + type: string + nodejs_version: + description: "Node.js version, set by the CI/CD pipeline workflow" + required: true + type: string + python_version: + description: "Python version, set by the CI/CD pipeline workflow" + required: true + type: string + terraform_version: + description: "Terraform version, set by the CI/CD pipeline workflow" + required: true + type: string + version: + description: "Version of the software, set by the CI/CD pipeline workflow" + required: true + type: string + environment: + description: "environment" + required: true + type: string + +permissions: + id-token: write + contents: read + pull-requests: write + +env: + AWS_PR_ROLE: ${{ inputs.environment == 'prod' && secrets.AWS_PROD_PR_ROLE || secrets.AWS_INT_PR_ROLE }} + +jobs: + metadata: + name: "Set CI/CD metadata" + runs-on: ubuntu-latest + timeout-minutes: 1 + outputs: + build_datetime: ${{ steps.variables.outputs.build_datetime }} + build_timestamp: ${{ steps.variables.outputs.build_timestamp }} + build_epoch: ${{ steps.variables.outputs.build_epoch }} + terraform_version: ${{ steps.variables.outputs.terraform_version }} + version: ${{ steps.variables.outputs.version }} + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Set CI/CD variables" + id: variables + run: | + datetime=$(date -u +'%Y-%m-%dT%H:%M:%S%z') + echo "build_datetime=$datetime" >> $GITHUB_OUTPUT + echo "build_timestamp=$(date --date=$datetime -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT + echo "build_epoch=$(date --date=$datetime -u +'%s')" >> $GITHUB_OUTPUT + echo "terraform_version=$(grep "^terraform" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT + echo "version=$(head -n 1 .version 2> /dev/null || echo unknown)" >> $GITHUB_OUTPUT + + - name: "List variables" + run: | + export BUILD_DATETIME="${{ steps.variables.outputs.build_datetime }}" + export BUILD_TIMESTAMP="${{ steps.variables.outputs.build_timestamp }}" + export BUILD_EPOCH="${{ steps.variables.outputs.build_epoch }}" + export TERRAFORM_VERSION="${{ steps.variables.outputs.terraform_version }}" + export VERSION="${{ steps.variables.outputs.version }}" + make list-variables + + terraform-plan: + name: "Terraform plan for ${{ inputs.environment }}" + needs: [metadata] + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + id-token: write + contents: read + pull-requests: write + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + + - name: "Setup Terraform" + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: ${{ needs.metadata.outputs.terraform_version }} + + - name: "Validate AWS role ARN secret" + run: | + if [ -z "${AWS_PR_ROLE:-}" ]; then + echo "ERROR: Missing AWS role ARN secret for environment '${{ inputs.environment }}'." >&2 + echo "Set '${{ inputs.environment == 'prod' && 'AWS_PROD_PR_ROLE' || 'AWS_INT_PR_ROLE' }}' in repository secrets." >&2 + exit 1 + fi + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ env.AWS_PR_ROLE }} + aws-region: eu-west-2 + + - name: "Terraform Init - ${{ inputs.environment }}" + run: | + make terraform-init \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} + + - name: "Terraform Plan - ${{ inputs.environment }}" + id: plan + run: | + make terraform-plan \ + AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \ + AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \ + AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} \ + AWS_REGION=${AWS_REGION} \ + AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \ + FORCE_USE_DOCKER=true \ + dir=infrastructure/environments/${{ inputs.environment }} \ + opts="-var-file=../terraform.tfvars\ -var-file=./terraform.tfvars\ -no-color\ -out=tfplan" + + - name: "Comment Plan on PR" + uses: actions/github-script@v7 + if: github.event_name == 'pull_request' + with: + script: | + const fs = require('fs'); + const plan = fs.readFileSync('infrastructure/environments/${{ inputs.environment }}/tfplan.txt', 'utf8'); + const output = `#### Terraform Plan for \`${{ inputs.environment }}\` + \`\`\` + ${plan.substring(0, 65536)} + \`\`\` + `; + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }); + + - name: "Upload tfplan artifact" + uses: actions/upload-artifact@v4 + with: + name: tfplan-${{ inputs.environment }} + path: infrastructure/environments/${{ inputs.environment }}/tfplan + + validation-summary: + name: "Validation summary" + needs: [terraform-plan] + runs-on: ubuntu-latest + if: always() + steps: + - name: "Check validation status" + run: | + if [ "${{ needs.terraform-plan.result }}" == "failure" ]; then + echo "Infrastructure validation failed" + exit 1 + else + echo "Infrastructure validation passed" + fi diff --git a/.tool-versions b/.tool-versions index 32db55a..75ed4c8 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,6 +1,6 @@ # This file is for you! Please, updated to the versions agreed by your team. -terraform 1.7.0 +terraform 1.15.5 pre-commit 3.6.0 # ============================================================================== @@ -14,7 +14,7 @@ pre-commit 3.6.0 # docker/ghcr.io/make-ops-tools/gocloc latest@sha256:6888e62e9ae693c4ebcfed9f1d86c70fd083868acb8815fe44b561b9a73b5032 # SEE: https://github.com/make-ops-tools/gocloc/pkgs/container/gocloc # docker/ghcr.io/nhs-england-tools/github-runner-image 20230909-321fd1e-rt@sha256:ce4fd6035dc450a50d3cbafb4986d60e77cb49a71ab60a053bb1b9518139a646 # SEE: https://github.com/nhs-england-tools/github-runner-image/pkgs/container/github-runner-image # docker/hadolint/hadolint 2.12.0-alpine@sha256:7dba9a9f1a0350f6d021fb2f6f88900998a4fb0aaf8e4330aa8c38544f04db42 # SEE: https://hub.docker.com/r/hadolint/hadolint/tags -# docker/hashicorp/terraform 1.5.6@sha256:180a7efa983386a27b43657ed610e9deed9e6c3848d54f9ea9b6cb8a5c8c25f5 # SEE: https://hub.docker.com/r/hashicorp/terraform/tags +# docker/hashicorp/terraform 1.15@sha256:15bf5a08b1fb9c9747c8ff01098aeeefb4aec9a6c24eb13e7661bdf9447e4aee # SEE: https://hub.docker.com/r/hashicorp/terraform/tags # docker/jdkato/vale v2.29.7@sha256:5ccfac574231b006284513ac3e4e9f38833989d83f2a68db149932c09de85149 # SEE: https://hub.docker.com/r/jdkato/vale/tags # docker/koalaman/shellcheck latest@sha256:e40388688bae0fcffdddb7e4dea49b900c18933b452add0930654b2dea3e7d5c # SEE: https://hub.docker.com/r/koalaman/shellcheck/tags # docker/mstruebing/editorconfig-checker 2.7.1@sha256:dd3ca9ea50ef4518efe9be018d669ef9cf937f6bb5cfe2ef84ff2a620b5ddc24 # SEE: https://hub.docker.com/r/mstruebing/editorconfig-checker/tags diff --git a/infrastructure/README.md b/infrastructure/README.md new file mode 100644 index 0000000..85aa773 --- /dev/null +++ b/infrastructure/README.md @@ -0,0 +1,43 @@ +# Infrastructure + +This folder contains the Terraform-based infrastructure assets for the genomic-order-management-service-api project. + +## Contents + +- `environments/` – environment-specific Terraform configurations for `int` and `prod`. + - Shared variables are defined in `environments/terraform.tfvars`. + - Each environment folder contains its own `main.tf` entry point. +- `modules/` – reusable Terraform modules. + - `nhs-e-secrets/` provisions AWS Secrets Manager resources and related IAM/KMS configuration. +- `images/` – placeholder area for infrastructure-related images or diagrams. + +## Current setup + +The current Terraform configuration uses the `nhs-e-secrets` module to create environment-specific secret resources and related access controls. + +### What the `nhs-e-secrets` module deploys + +The `infrastructure/modules/nhs-e-secrets` module provisions: + +- one or more AWS Secrets Manager secrets for the selected environment +- placeholder secret values (managed by the NHS-E service team, not real application secrets) +- an AWS KMS key and alias for encrypting those secrets +- an IAM role and policy that allow an external InterSystems role to read the secrets using the KMS key + +This module is intended to create the secret placeholder infrastructure and the required cross-account access path, rather than manage live secret contents. + +## Typical workflow + +Terraform for this folder is not run manually in the normal development flow. + +Instead, GitHub Actions handles the infrastructure lifecycle as follows: + +- Pull request and merge validation runs through `.github/workflows/infra-validate.yaml`, which is invoked from `.github/workflows/cicd-1-pull-request.yaml` for the configured environments. This validates the Terraform changes but does not deploy them. +- Deployment runs through `.github/workflows/infra-deploy.yaml`, which is invoked from `.github/workflows/cicd-3-deploy.yaml` when the deploy workflow is started manually. This is the path that performs the Terraform init, plan, and apply steps for the selected environment under `infrastructure/environments/`. + +In other words, a merged PR triggers validation, while actual infrastructure deployment is manual through the deploy workflow. + +## Notes + +- Environment variables and shared defaults are defined in `infrastructure/environments/terraform.tfvars`. +- The `prod` configuration currently includes a placeholder IAM role value for testing purposes. diff --git a/infrastructure/environments/int/.gitkeep b/infrastructure/environments/int/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/infrastructure/environments/int/main.tf b/infrastructure/environments/int/main.tf new file mode 100644 index 0000000..e652350 --- /dev/null +++ b/infrastructure/environments/int/main.tf @@ -0,0 +1,11 @@ +# Production environment configuration +module "nhs_e_secrets" { + source = "../../modules/nhs-e-secrets" + project = var.project + region = var.region + environment = var.environment + intersystems_role_arn = var.intersystems_role_arn + external_id = var.external_id + tags = var.tags + secrets = var.secrets +} diff --git a/infrastructure/environments/int/terraform.tfvars b/infrastructure/environments/int/terraform.tfvars new file mode 100644 index 0000000..d18246e --- /dev/null +++ b/infrastructure/environments/int/terraform.tfvars @@ -0,0 +1,79 @@ +# Environment variables for prod +environment = "int" +#NOTE : the below is a dummy external_id used for keeping code ready +external_id = "ext-9f83hf83hf83hf83" + +# InterSystems role ARN - this should be provided via terraform.tfvars override or command line +intersystems_role_arn = "arn:aws:iam::038462762342:role/InterSystems-GOM-Role" + +tags = { + Environment = "int" + Project = "genomics-order-management" + ManagedBy = "Terraform" + Owner = "John Fraser" + DeliveryLead = "Cairns-Hockey Beryl" + TechnicalArchitect = "Ravi Natarajan" + BusinessAnalyst = "Adam Laurent" +} + +secrets = { + "DGTS-INT" = { + description = "Digital Genomic Test Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT_DGTS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "MNS-INT" = { + description = "Multicast Notification Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-MNS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "PDM-INT" = { + description = "Patient Data Manager INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-PDM" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "ODS-INT" = { + description = "Organization Data Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-ODS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "PDS-INT" = { + description = "Personal Demographic Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-PDS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "GOMS-INT" = { + description = "Genomic Order Management Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-GOMS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } +} diff --git a/infrastructure/environments/int/variables.tf b/infrastructure/environments/int/variables.tf new file mode 100644 index 0000000..ee3d4b1 --- /dev/null +++ b/infrastructure/environments/int/variables.tf @@ -0,0 +1,52 @@ +# Shared variables for all environments +variable "project" { + description = "Project name used for tagging." + type = string + default = "genomics-order-management" +} + +variable "region" { + description = "AWS region for infrastructure deployment." + type = string + default = "eu-west-2" +} + +variable "environment" { + description = "Environment name (int, prod)." + type = string +} + +variable "intersystems_role_arn" { + description = "ARN of the InterSystems IAM role that will assume the NHS-E cross-account role." + type = string + default = "" +} + +variable "external_id" { + description = "external_id passed by InterSystems" + type = string +} + +variable "enable_kms_key_rotation" { + description = "Enable KMS key rotation for secrets manager." + type = bool + default = false +} + +variable "tags" { + description = "Tags applied to resources that support tagging." + type = map(string) + default = {} +} + +variable "secrets" { + description = "Secrets Manager secrets to create. NOTE: THIS IS ONLY TO CREATE SECRET PLACEHOLDERS AND NO REAL SECRETS. Secrets are managed by NHS-E service team" + type = map(object({ + description = optional(string) + recovery_window_in_days = optional(number, 30) + secret_key = optional(string) + tags = optional(map(string), {}) + })) + default = {} +} + diff --git a/infrastructure/environments/int/versions.tf b/infrastructure/environments/int/versions.tf new file mode 100644 index 0000000..31fe2a7 --- /dev/null +++ b/infrastructure/environments/int/versions.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 1.15.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + + backend "s3" { + bucket = "genomics-order-management-tfstate-int" + key = "nhs-e/infrastructure/terraform.tfstate" + region = "eu-west-2" + use_lockfile = true + encrypt = true + } +} + +provider "aws" { + region = var.region + + default_tags { + tags = { + Environment = var.environment + Project = var.project + ManagedBy = "Terraform" + } + } +} diff --git a/infrastructure/environments/prod/main.tf b/infrastructure/environments/prod/main.tf new file mode 100644 index 0000000..fad76c1 --- /dev/null +++ b/infrastructure/environments/prod/main.tf @@ -0,0 +1,15 @@ +# Production environment configuration +# intersystems_role_arn = var.intersystems_role_arn +# the below must be replaced by InterSystems Role, added for testing +data "aws_caller_identity" "current" {} + +module "nhs_e_secrets" { + source = "../../modules/nhs-e-secrets" + project = var.project + region = var.region + environment = var.environment + intersystems_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + external_id = var.external_id + tags = var.tags + secrets = var.secrets +} diff --git a/infrastructure/environments/prod/terraform.tfvars b/infrastructure/environments/prod/terraform.tfvars new file mode 100644 index 0000000..5442a51 --- /dev/null +++ b/infrastructure/environments/prod/terraform.tfvars @@ -0,0 +1,79 @@ +# Environment variables for prod +environment = "prod" +#NOTE : the below is a dummy external_id used for keeping code ready +external_id = "ext-9f83hf83hf83hf83" + +# InterSystems role ARN - this should be provided via terraform.tfvars override or command line +intersystems_role_arn = "arn:aws:iam::038462762345:role/InterSystems-GOM-Role" + +tags = { + Environment = "prod" + Project = "genomics-order-management" + ManagedBy = "Terraform" + Owner = "John Fraser" + DeliveryLead = "Cairns-Hockey Beryl" + TechnicalArchitect = "Ravi Natarajan" + BusinessAnalyst = "Adam Laurent" +} + +secrets = { + "DGTS-INT" = { + description = "Digital Genomic Test Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT_DGTS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "MNS-INT" = { + description = "Multicast Notification Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-MNS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "PDM-INT" = { + description = "Patient Data Manager INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-PDM" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "ODS-INT" = { + description = "Organization Data Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-ODS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "PDS-INT" = { + description = "Personal Demographic Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-PDS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } + + "GOMS-INT" = { + description = "Genomic Order Management Service INT" + recovery_window_in_days = 7 + secret_key = "APIKEY-GOMS-INT-GOMS" + tags = { + environment = "int" + owner = "NHS-E Service Team" + } + } +} diff --git a/infrastructure/environments/prod/variables.tf b/infrastructure/environments/prod/variables.tf new file mode 100644 index 0000000..ee3d4b1 --- /dev/null +++ b/infrastructure/environments/prod/variables.tf @@ -0,0 +1,52 @@ +# Shared variables for all environments +variable "project" { + description = "Project name used for tagging." + type = string + default = "genomics-order-management" +} + +variable "region" { + description = "AWS region for infrastructure deployment." + type = string + default = "eu-west-2" +} + +variable "environment" { + description = "Environment name (int, prod)." + type = string +} + +variable "intersystems_role_arn" { + description = "ARN of the InterSystems IAM role that will assume the NHS-E cross-account role." + type = string + default = "" +} + +variable "external_id" { + description = "external_id passed by InterSystems" + type = string +} + +variable "enable_kms_key_rotation" { + description = "Enable KMS key rotation for secrets manager." + type = bool + default = false +} + +variable "tags" { + description = "Tags applied to resources that support tagging." + type = map(string) + default = {} +} + +variable "secrets" { + description = "Secrets Manager secrets to create. NOTE: THIS IS ONLY TO CREATE SECRET PLACEHOLDERS AND NO REAL SECRETS. Secrets are managed by NHS-E service team" + type = map(object({ + description = optional(string) + recovery_window_in_days = optional(number, 30) + secret_key = optional(string) + tags = optional(map(string), {}) + })) + default = {} +} + diff --git a/infrastructure/environments/prod/versions.tf b/infrastructure/environments/prod/versions.tf new file mode 100644 index 0000000..6134ceb --- /dev/null +++ b/infrastructure/environments/prod/versions.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 1.15.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.0" + } + } + + backend "s3" { + bucket = "genomics-order-management-tfstate-prod" + key = "nhs-e/infrastructure/terraform.tfstate" + region = "eu-west-2" + use_lockfile = true + encrypt = true + } +} + +provider "aws" { + region = var.region + + default_tags { + tags = { + Environment = var.environment + Project = var.project + ManagedBy = "Terraform" + } + } +} diff --git a/infrastructure/environments/terraform.tfvars b/infrastructure/environments/terraform.tfvars new file mode 100644 index 0000000..415aad5 --- /dev/null +++ b/infrastructure/environments/terraform.tfvars @@ -0,0 +1,5 @@ +# Shared terraform.tfvars for all environments +project = "genomics-order-management" +region = "eu-west-2" + +# Environment-specific values should be set in each environment's terraform.tfvars diff --git a/infrastructure/modules/nhs-e-secrets/data.tf b/infrastructure/modules/nhs-e-secrets/data.tf new file mode 100644 index 0000000..3cfa031 --- /dev/null +++ b/infrastructure/modules/nhs-e-secrets/data.tf @@ -0,0 +1,2 @@ +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} diff --git a/infrastructure/modules/nhs-e-secrets/iam-secretsreader.tf b/infrastructure/modules/nhs-e-secrets/iam-secretsreader.tf new file mode 100644 index 0000000..28aa81c --- /dev/null +++ b/infrastructure/modules/nhs-e-secrets/iam-secretsreader.tf @@ -0,0 +1,69 @@ +data "aws_iam_policy_document" "nhs_e_secretsreader_trust_policy" { + + statement { + sid = "AllowAccountBRolesAssumeSecretReader" + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "AWS" + identifiers = [var.intersystems_role_arn] + } + + condition { + test = "StringEquals" + variable = "sts:ExternalId" + values = [var.external_id] + } + } +} + +resource "aws_iam_role" "nhs_e_secretsreader_role" { + name = "${var.project}-${var.environment}-role" + assume_role_policy = data.aws_iam_policy_document.nhs_e_secretsreader_trust_policy.json + # max_session_duration = var.max_session_duration + # permissions_boundary = var.permissions_boundary_arn + + tags = merge(local.common_tags, { Purpose = "Crossaccount Secrets retrieval" }) +} + +data "aws_iam_policy_document" "nhs_e_secretsreader_role_policy_document" { + + statement { + sid = "ReadManagedSecrets" + effect = "Allow" + actions = [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue" + ] + resources = [for secret in aws_secretsmanager_secret.nhs_e_secrets : secret.arn] + } + + statement { + sid = "DecryptSecretsManagerKey" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:DescribeKey" + ] + resources = [aws_kms_key.secrets_manager.arn] + + condition { + test = "StringEquals" + variable = "kms:ViaService" + values = ["secretsmanager.${var.region}.amazonaws.com"] + } + } +} + +resource "aws_iam_policy" "nhs_e_secretsreader_role_policy" { + name = "${var.project}-${var.environment}-role-policy" + description = "Allows reading Secrets Manager secrets" + policy = data.aws_iam_policy_document.nhs_e_secretsreader_role_policy_document.json + tags = local.common_tags +} + +resource "aws_iam_role_policy_attachment" "nhs_e_secretsreader_role_policy_attachment" { + role = aws_iam_role.nhs_e_secretsreader_role.name + policy_arn = aws_iam_policy.nhs_e_secretsreader_role_policy.arn +} diff --git a/infrastructure/modules/nhs-e-secrets/kms-secrets.tf b/infrastructure/modules/nhs-e-secrets/kms-secrets.tf new file mode 100644 index 0000000..98511af --- /dev/null +++ b/infrastructure/modules/nhs-e-secrets/kms-secrets.tf @@ -0,0 +1,104 @@ + +data "aws_iam_policy_document" "secrets_manager_kms_key" { + statement { + sid = "AllowAccountIamAdministration" + effect = "Allow" + actions = ["kms:*"] + + principals { + type = "AWS" + identifiers = ["arn:${data.aws_partition.current.partition}:iam::${data.aws_caller_identity.current.account_id}:root"] + } + resources = ["*"] + } + + statement { + sid = "AllowGitHubRoleUseOfSecretsManagerKey" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo" + ] + + principals { + type = "AWS" + identifiers = [aws_iam_role.nhs_e_secretsreader_role.arn] + } + + resources = ["*"] + } + + statement { + sid = "AllowSecretReaderRoleUseOfSecretsManagerKey" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo" + ] + + principals { + type = "AWS" + identifiers = [aws_iam_role.nhs_e_secretsreader_role.arn] + } + + resources = ["*"] + + condition { + test = "StringEquals" + variable = "kms:ViaService" + values = ["secretsmanager.${var.region}.amazonaws.com"] + } + } + + statement { + sid = "AllowSecretsManagerServiceUse" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:ReEncryptFrom", + "kms:ReEncryptTo" + ] + + principals { + type = "Service" + identifiers = ["secretsmanager.amazonaws.com"] + } + resources = ["*"] + condition { + test = "StringEquals" + variable = "kms:CallerAccount" + values = [data.aws_caller_identity.current.account_id] + } + condition { + test = "StringEquals" + variable = "kms:ViaService" + values = ["secretsmanager.${var.region}.amazonaws.com"] + } + } +} + +resource "aws_kms_key" "secrets_manager" { + description = "KMS key for Secrets Manager" + enable_key_rotation = true + policy = data.aws_iam_policy_document.secrets_manager_kms_key.json + tags = merge(local.common_tags, { Name = "${var.project}-key" }) +} + +resource "aws_kms_alias" "secrets_manager" { + name = "alias/${var.project}-${var.environment}-key" + target_key_id = aws_kms_key.secrets_manager.key_id +} diff --git a/infrastructure/modules/nhs-e-secrets/locals.tf b/infrastructure/modules/nhs-e-secrets/locals.tf new file mode 100644 index 0000000..1ecb25c --- /dev/null +++ b/infrastructure/modules/nhs-e-secrets/locals.tf @@ -0,0 +1,10 @@ +locals { + + common_tags = merge( + { + ManagedBy = "Terraform" + module = "NHS-E Secrets" + }, + var.tags + ) +} diff --git a/infrastructure/modules/nhs-e-secrets/secretsmanager.tf b/infrastructure/modules/nhs-e-secrets/secretsmanager.tf new file mode 100644 index 0000000..74f62e2 --- /dev/null +++ b/infrastructure/modules/nhs-e-secrets/secretsmanager.tf @@ -0,0 +1,49 @@ + +resource "aws_secretsmanager_secret" "nhs_e_secrets" { + for_each = var.secrets + name = each.value.secret_key + description = try(each.value.description, null) + kms_key_id = aws_kms_key.secrets_manager.arn + recovery_window_in_days = try(each.value.recovery_window_in_days, 30) + tags = merge(local.common_tags, try(each.value.tags, {})) + lifecycle { + ignore_changes = [tags] + } +} + +resource "aws_secretsmanager_secret_version" "nhs_e_secrets_version" { + for_each = var.secrets + secret_id = aws_secretsmanager_secret.nhs_e_secrets[each.key].id + secret_string = jsonencode({ (each.value.secret_key) = "managed-by-nhs-e" }) + lifecycle { + ignore_changes = [secret_string] + } +} + +data "aws_iam_policy_document" "nhs_e_secrets_access" { + for_each = var.secrets + statement { + sid = "AllowReaderRole" + effect = "Allow" + actions = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + principals { + type = "AWS" + identifiers = [aws_iam_role.nhs_e_secretsreader_role.arn] + } + resources = [ + aws_secretsmanager_secret.nhs_e_secrets[each.key].arn + ] + } +} + +resource "aws_secretsmanager_secret_policy" "nhs_e_secretsmanager_policy" { + for_each = data.aws_iam_policy_document.nhs_e_secrets_access + secret_arn = aws_secretsmanager_secret.nhs_e_secrets[each.key].arn + policy = each.value.json + lifecycle { + ignore_changes = all + } +} diff --git a/infrastructure/modules/nhs-e-secrets/variables.tf b/infrastructure/modules/nhs-e-secrets/variables.tf new file mode 100644 index 0000000..6fdde26 --- /dev/null +++ b/infrastructure/modules/nhs-e-secrets/variables.tf @@ -0,0 +1,42 @@ +# Module variables +variable "project" { + description = "Project name" + type = string +} + +variable "region" { + description = "AWS region" + type = string +} + +variable "environment" { + description = "Environment name (int, prod)" + type = string +} + +variable "intersystems_role_arn" { + description = "ARN of the InterSystems IAM role that will assume this NHS-E role" + type = string +} + +variable "external_id" { + description = "external_id passed by InterSystems" + type = string +} + +variable "secrets" { + description = "Secrets Manager secrets to create. NOTE: THIS IS ONLY TO CREATE SECRET PLACEHOLDERS AND NO REAL SECRETS. Secrets are managed by NHS-E service team" + type = map(object({ + description = optional(string) + recovery_window_in_days = optional(number, 30) + secret_key = optional(string) + tags = optional(map(string), {}) + })) + default = {} +} + +variable "tags" { + description = "Tags applied to resources that support tagging." + type = map(string) + default = {} +} diff --git a/scripts/infra/README.md b/scripts/infra/README.md index 9f9bd2b..c05bba2 100644 --- a/scripts/infra/README.md +++ b/scripts/infra/README.md @@ -1,48 +1,126 @@ -## scripts/infra README +# Infrastructure Bootstrap (`scripts/infra`) -### Purpose +## Overview -The `scripts/infra` directory contains **one-time bootstrap Terraform configuration** for setting up the foundational AWS infrastructure required to manage the `genomic-order-management-service-api` application infrastructure using Terraform. +The scripts/infra directory contains the one‑time bootstrap Terraform configuration required to provision the foundational AWS infrastructure used by the genomic-order-management-service-api Terraform deployments. +This bootstrap layer creates: +- The Terraform remote state backend (S3 + versioning + encryption + access controls) +- The GitHub Actions OIDC IAM roles used for: + - Production deployments (full‑privilege deploy role) + - Pull request / read‑only operations (restricted role) +- The OIDC trust relationships that allow GitHub Actions to authenticate without long‑lived AWS credentials +This directory is only used during initial setup or importing existing resources into Terraform state. --- -### What This Sets Up +## What This Bootstrap Creates -This directory creates the infrastructure **backend** and **deployment prerequisites**: +### Terraform State Backend (S3) -- **Terraform State Backend (S3 + DynamoDB)** - - S3 bucket for storing Terraform state files (`s3-terraform.tf`) - - DynamoDB table for state locking (`dynamodb-terraform.tf`) - - Enables safe, concurrent Terraform operations +Terraform state is stored in a dedicated S3 bucket: genomics-order-management-tfstate-prod -- **GitHub Actions OIDC Integration** - - AWS IAM role for GitHub Actions (`iam-oidc.tf`) - - Trust policy allowing GitHub Actions workflows to assume the role - - Granular permissions for Terraform deployments - - No long-lived credentials stored in the pipeline +The bucket is configured with: -- **OIDC Identity Provider** - - Existing AWS OIDC provider for GitHub Actions (referenced via data source) - - Used by the IAM role for secure authentication +- Versioning enabled +- Server‑side encryption (AES‑256) +- Public access fully blocked +- Bucket ownership controls +- `prevent_destroy` lifecycle rule + +Defined in: + +- `s3-terraform.tf` +- `variables.tf` --- -### Usage +### GitHub Actions OIDC IAM Roles + +Two IAM roles are created to support CI/CD workflows. + +--- + +#### 1. Deployment Role +**`github-genomics-order-management-oidc-deploy-role`** + +Used by GitHub Actions for ***main‑branch deployments. +Capabilities include: +- Managing S3 buckets and Terraform state +- Managing Secrets Manager secrets +- Managing KMS keys and aliases +- Managing IAM roles and policies +- Managing DynamoDB tables +- Managing CloudWatch log groups +The trust policy restricts role assumption to: +- The configured GitHub organisation and repository +- The main branch only +- GitHub’s OIDC provider (token.actions.githubusercontent.com) +(Additional branch enforcement is implemented in the GitHub Actions workflow.) + +--- + +#### 2. Pull Request / Read‑Only Role +**`github-genomics-order-management-oidc-pr-role`** + +Used for PR workflows that require: + +- Reading Terraform state +- Reading Secrets Manager +- Reading IAM roles/policies +- Reading CloudWatch logs + +This role **cannot modify infrastructure**. + +--- +OIDC Identity Provider +Both IAM roles reference the existing AWS OIDC provider for GitHub Actions: +data "aws_iam_openid_connect_provider" "github_actions" { ... } +This enables GitHub Actions to authenticate securely without long‑lived AWS credentials. + +--- +Example: `genomics-order-management-tfstate-prod` + +The bucket is configured with: + +- Versioning enabled +- Server‑side encryption (AES‑256) +- Public access fully blocked +- Bucket ownership controls +- `prevent_destroy` lifecycle rule + +Defined in: + +- `s3-terraform.tf` +- `variables.tf` -#### Initial Setup (One-Time) -```bash -# Navigate to scripts/infra +--- + +### Usage +***one time Initial Setup +``` cd scripts/infra -# Initialize Terraform with local backend terraform init -# Review the plan terraform plan \ -var github_org=NHSDigital \ -var github_repo=genomic-order-management-service-api -# Apply the infrastructure terraform apply +``` + +***Importing Existing Resources +If the S3 bucket or IAM roles already exist (e.g., created manually or by a previous bootstrap), use the provided import script: +cd scripts/infra +./tf_import.sh + +--- +Summary +This directory provides the foundational AWS infrastructure required for secure, GitHub‑based Terraform deployments: +- S3 backend for Terraform state +- Two IAM roles (deploy + PR) with OIDC trust +- Import script for existing resources +- Fully parameterised Terraform configuration +It is only used during bootstrap or resource import, not during normal application deployments. diff --git a/scripts/infra/dynamodb-terraform.tf b/scripts/infra/dynamodb-terraform.tf deleted file mode 100644 index a125605..0000000 --- a/scripts/infra/dynamodb-terraform.tf +++ /dev/null @@ -1,28 +0,0 @@ -resource "aws_dynamodb_table" "terraform_state_lock" { - name = "${var.project}-tfstate-lock-${var.environment}" - billing_mode = "PAY_PER_REQUEST" - hash_key = "LockID" - - attribute { - name = "LockID" - type = "S" - } - - server_side_encryption { - enabled = true - } - - point_in_time_recovery { - enabled = true - } - - lifecycle { - prevent_destroy = true - } - - tags = { - project = var.project - Name = "terraform-lock-${var.environment}" - Environment = var.environment - } -} diff --git a/scripts/infra/iam-oidc-deploy-role.tf b/scripts/infra/iam-oidc-deploy-role.tf new file mode 100644 index 0000000..ec0982e --- /dev/null +++ b/scripts/infra/iam-oidc-deploy-role.tf @@ -0,0 +1,290 @@ +############################################################### +# GitHub Actions OIDC Provider (existing AWS provider) +############################################################### +data "aws_iam_openid_connect_provider" "github_actions" { + # Reference the AWS OIDC provider for GitHub Actions + arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.oidc_provider}" +} + +############################################################### +# IAM Trust Policy — GitHub Actions (Main Branch Only) +############################################################### +data "aws_iam_policy_document" "github_actions_assume_role" { + statement { + effect = "Allow" + + principals { + type = "Federated" + # Allow authentication only via GitHub's OIDC provider + identifiers = [data.aws_iam_openid_connect_provider.github_actions.arn] + } + + # Required for GitHub OIDC → AWS IAM role assumption + actions = ["sts:AssumeRoleWithWebIdentity"] + + # Enforce that ONLY the main branch can assume this role + # GitHub OIDC token 'sub' claim looks like: + # repo:/:ref:refs/heads/main + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + values = [ + "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/main", + ] + } + + # Ensure the token is intended for AWS STS (mandatory for GitHub OIDC) + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + } +} + +############################################################### +# IAM Role — Deployment Role (Main Branch Only) +############################################################### +resource "aws_iam_role" "github_actions" { + name = var.role_name + # Attach the trust policy above + assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json +} + +############################################################### +# IAM Permissions — Deployment Role (Full Infra Access) +############################################################### +data "aws_iam_policy_document" "deploy_permissions" { + + ############################################################### + # S3 Bucket + Lifecycle + Encryption Management + # Required for Terraform-managed infrastructure buckets + ############################################################### + statement { + sid = "ManageS3BucketsPoliciesEncryptionAndLifecycle" + effect = "Allow" + + actions = [ + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:GetBucketAcl", + "s3:GetBucketEncryption", + "s3:GetBucketLifecycleConfiguration", + "s3:GetBucketLocation", + "s3:GetBucketPolicy", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketTagging", + "s3:GetBucketVersioning", + "s3:ListAllMyBuckets", + "s3:ListBucket", + "s3:PutBucketEncryption", + "s3:PutBucketLifecycleConfiguration", + "s3:PutBucketOwnershipControls", + "s3:PutBucketPolicy", + "s3:PutBucketPublicAccessBlock", + "s3:PutBucketTagging", + "s3:PutBucketVersioning" + ] + + # Broad S3 access — required for infra provisioning + resources = [ + "arn:aws:s3:::*", + "arn:aws:s3:::*/*", + ] + } + + ############################################################### + # Terraform State (.tfstate) Read/Write + ############################################################### + statement { + sid = "ReadWriteTerraformState" + effect = "Allow" + + actions = [ + "s3:GetObject", + "s3:PutObject" + ] + + resources = [ + "${aws_s3_bucket.terraform_state_store.arn}/*.tfstate" + ] + } + + ############################################################### + # Terraform Lock File Management + ############################################################### + statement { + sid = "ReadWriteDeleteTerraformLockFile" + effect = "Allow" + + actions = [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ] + resources = [ + "${aws_s3_bucket.terraform_state_store.arn}/*.tfstate.tflock" + ] + } + + ############################################################### + # Secrets Manager — Full Secret Lifecycle + ############################################################### + statement { + sid = "ManageSecretsManagerSecretsAndResourcePolicies" + actions = [ + "secretsmanager:CreateSecret", + "secretsmanager:DeleteSecret", + "secretsmanager:DescribeSecret", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecrets", + "secretsmanager:PutResourcePolicy", + "secretsmanager:PutSecretValue", + "secretsmanager:RestoreSecret", + "secretsmanager:TagResource", + "secretsmanager:UpdateSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:ListSecretVersionIds", + "secretsmanager:UpdateSecretVersionStage", + "secretsmanager:UntagResource", + "secretsmanager:ValidateResourcePolicy" + ] + resources = ["arn:aws:secretsmanager:*:*:secret:*"] + } + + ############################################################### + # KMS — Key + Alias + Grant Management + ############################################################### + statement { + sid = "ManageKmsKeysAliasesAndPolicies" + actions = [ + "kms:CancelKeyDeletion", + "kms:CreateAlias", + "kms:Decrypt", + "kms:DescribeKey", + "kms:DisableKey", + "kms:EnableKey", + "kms:EnableKeyRotation", + "kms:Encrypt", + "kms:GenerateDataKey", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:GetKeyPolicy", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + "kms:PutKeyPolicy", + "kms:ScheduleKeyDeletion", + "kms:TagResource", + "kms:UntagResource", + "kms:UpdateAlias", + "kms:DeleteAlias", + "kms:CreateGrant", + "kms:ListGrants", + "kms:RevokeGrant" + ] + resources = [ + "arn:aws:kms:*:*:key/*", + "arn:aws:kms:*:*:alias/*" + ] + } + + ############################################################### + # KMS Global Permissions (Key Creation) + ############################################################### + statement { + sid = "KmsGlobalPermissions" + actions = [ + "kms:CreateKey", + "kms:ListKeys", + "kms:ListAliases" + ] + resources = ["*"] + } + + ############################################################### + # IAM Role + Policy Management + ############################################################### + statement { + sid = "ManageIamRolesAndPolicies" + actions = [ + "iam:AttachRolePolicy", + "iam:CreatePolicy", + "iam:CreatePolicyVersion", + "iam:CreateRole", + "iam:DeletePolicy", + "iam:DeletePolicyVersion", + "iam:DeleteRole", + "iam:DeleteRolePolicy", + "iam:DetachRolePolicy", + "iam:GetOpenIDConnectProvider", + "iam:GetPolicy", + "iam:GetPolicyVersion", + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListAttachedRolePolicies", + "iam:ListEntitiesForPolicy", + "iam:ListInstanceProfilesForRole", + "iam:ListOpenIDConnectProviders", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:ListRolePolicies", + "iam:ListRoles", + "iam:PassRole", + "iam:PutRolePolicy", + "iam:TagPolicy", + "iam:TagRole", + "iam:UpdateAssumeRolePolicy" + ] + resources = [ + "arn:aws:iam::*:policy/*", + "arn:aws:iam::*:role/*", + ] + } + + ############################################################### + # DynamoDB — Table + Item Management + ############################################################### + statement { + sid = "DynamoDBTableManagement" + effect = "Allow" + + actions = [ + "dynamodb:CreateTable", + "dynamodb:DeleteTable", + "dynamodb:DescribeTable", + "dynamodb:UpdateTable", + "dynamodb:ListTables", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + ] + + resources = ["arn:aws:dynamodb:*:*:table/*"] + } + + ############################################################### + # CloudWatch Logs — Log Group Management + ############################################################### + statement { + sid = "CloudWatchLogs" + effect = "Allow" + + actions = [ + "logs:CreateLogGroup", + "logs:DeleteLogGroup", + "logs:DescribeLogGroups", + "logs:ListLogGroups", + ] + + resources = ["arn:aws:logs:*:*:log-group:*"] + } +} + +############################################################### +# Attach Permissions to Deployment Role +############################################################### +resource "aws_iam_role_policy" "deploy_permissions_policy" { + name = "infra-deploy-permissions" + role = aws_iam_role.github_actions.id + policy = data.aws_iam_policy_document.deploy_permissions.json +} diff --git a/scripts/infra/iam-oidc-pr-role.tf b/scripts/infra/iam-oidc-pr-role.tf new file mode 100644 index 0000000..4560b48 --- /dev/null +++ b/scripts/infra/iam-oidc-pr-role.tf @@ -0,0 +1,225 @@ +############################################### +# GitHub Actions OIDC Trust Policy (PR Role) +############################################### +data "aws_iam_policy_document" "github_actions_assume_role_pr" { + statement { + effect = "Allow" + + principals { + type = "Federated" + # Trust the GitHub Actions OIDC provider for authentication + identifiers = [data.aws_iam_openid_connect_provider.github_actions.arn] + } + + # Required for GitHub OIDC → AWS IAM role assumption + actions = ["sts:AssumeRoleWithWebIdentity"] + + # Restrict which GitHub workflow identities may assume this role + condition { + test = "StringLike" + variable = "token.actions.githubusercontent.com:sub" + + # Allow any branch (refs/heads/*) but only for this repo + # Example: repo:NHSDigital/genomic-order-management-service-api:ref:refs/heads/feature/foo + values = [ + "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/*", + ] + } + + # GitHub OIDC tokens always set audience = sts.amazonaws.com + # This ensures the token is intended for AWS STS and not another service + condition { + test = "StringEquals" + variable = "token.actions.githubusercontent.com:aud" + values = ["sts.amazonaws.com"] + } + } +} + +############################################### +# IAM Role for PR Workflows (Read‑Only) +############################################### +resource "aws_iam_role" "github_actions_pr" { + name = var.role_name_pr + # Attach the trust policy defined above + assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role_pr.json +} + +############################################### +# Permissions for PR Role (Read‑Only Terraform) +############################################### +data "aws_iam_policy_document" "deploy_permissions_pr" { + + ############################################### + # Allow listing the Terraform state bucket + # Required for terraform init/plan in PR workflows + ############################################### + statement { + sid = "ListTerraformStateBucket" + effect = "Allow" + + actions = [ + "s3:ListBucket" + ] + + resources = [ + aws_s3_bucket.terraform_state_store.arn + ] + } + + ############################################### + # Allow reading/writing .tfstate files + # PR workflows may run terraform plan which writes a local state + ############################################### + statement { + sid = "ReadWriteTerraformState" + effect = "Allow" + + actions = [ + "s3:GetObject", + "s3:PutObject" + ] + + resources = [ + "${aws_s3_bucket.terraform_state_store.arn}/*.tfstate" + ] + } + + ############################################### + # Allow reading/writing/deleting Terraform lock files + # Required for terraform init/plan concurrency control + ############################################### + statement { + sid = "ReadWriteDeleteTerraformLockFile" + effect = "Allow" + + actions = [ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject" + ] + resources = [ + "${aws_s3_bucket.terraform_state_store.arn}/*.tfstate.tflock" + ] + } + + ############################################### + # Allow reading secrets (but not modifying them) + # Needed for terraform plan when modules reference secrets + ############################################### + statement { + sid = "SecretsManagerManagement" + effect = "Allow" + + actions = [ + "secretsmanager:DescribeSecret", + "secretsmanager:GetSecretValue", + "secretsmanager:GetResourcePolicy", + "secretsmanager:ListSecretVersionIds", + "secretsmanager:ListSecrets" + ] + resources = ["arn:aws:secretsmanager:*:*:secret:*"] + } + + ############################################### + # Allow read‑only access to KMS keys + # Required for decrypting encrypted Terraform state or secrets + ############################################### + statement { + sid = "KMSKeyManagement" + effect = "Allow" + + actions = [ + "kms:DescribeKey", + "kms:GetKeyPolicy", + "kms:Encrypt", + "kms:Decrypt", + "kms:GetKeyRotationStatus", + "kms:ListResourceTags", + "kms:ListGrants", + "kms:GenerateDataKeyWithoutPlaintext", + "kms:GenerateDataKey" + ] + resources = ["arn:aws:kms:*:*:key/*"] + } + + ############################################################### + # KMS Global Permissions (Key Creation) + ############################################################### + statement { + sid = "KmsGlobalPermissions" + actions = [ + "kms:ListKeys", + "kms:ListAliases" + ] + resources = ["*"] + } + + ############################################### + # Allow reading IAM roles/policies + # Required when Terraform references IAM resources in data sources + ############################################### + statement { + sid = "ReadIAMRoleAndPolicies" + effect = "Allow" + + actions = [ + "iam:GetRole", + "iam:GetRolePolicy", + "iam:ListRoles", + "iam:ListAttachedRolePolicies", + "iam:ListRolePolicies", + "iam:GetAssumeRolePolicy" + ] + resources = ["arn:aws:iam::*:role/*"] + } + + ############################################### + # Allow reading IAM policies + # Required for terraform plan when IAM policies are referenced + ############################################### + statement { + sid = "IAMPolicyManagement" + effect = "Allow" + + actions = [ + "iam:GetPolicy", + "iam:ListPolicies", + "iam:ListPolicyVersions", + "iam:GetPolicyVersion" + ] + + resources = [ + "arn:aws:iam::*:policy/*", + "arn:aws:iam::*:role/*", + ] + } + + ############################################### + # Allow read‑only access to CloudWatch Logs + # Useful for debugging PR workflows or module behaviour + ############################################### + statement { + sid = "CloudWatchLogs" + effect = "Allow" + + actions = [ + "logs:DescribeLogGroups", + "logs:DescribeLogStreams", + "logs:GetLogEvents", + "logs:FilterLogEvents", + "logs:ListTagsLogGroup" + ] + + resources = ["arn:aws:logs:*:*:log-group:*"] + } +} + +############################################### +# Attach the read‑only policy to the PR role +############################################### +resource "aws_iam_role_policy" "deploy_permissions_pr" { + name = "deploy-permissions-pr" + role = aws_iam_role.github_actions_pr.id + policy = data.aws_iam_policy_document.deploy_permissions_pr.json +} diff --git a/scripts/infra/iam-oidc.tf b/scripts/infra/iam-oidc.tf deleted file mode 100644 index 4f96509..0000000 --- a/scripts/infra/iam-oidc.tf +++ /dev/null @@ -1,190 +0,0 @@ -data "aws_iam_openid_connect_provider" "github_actions" { - arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.oidc_provider}" -} - -data "aws_iam_policy_document" "github_actions_assume_role" { - statement { - effect = "Allow" - - principals { - type = "Federated" - identifiers = [data.aws_iam_openid_connect_provider.github_actions.arn] - } - - actions = ["sts:AssumeRoleWithWebIdentity"] - - condition { - test = "StringLike" - variable = "token.actions.githubusercontent.com:sub" - values = [ - "repo:${var.github_org}/${var.github_repo}:ref:refs/heads/${var.github_branch}", - ] - } - - condition { - test = "StringEquals" - variable = "token.actions.githubusercontent.com:aud" - values = ["sts.amazonaws.com"] - } - } -} - -resource "aws_iam_role" "github_actions" { - name = var.role_name - assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json -} - -data "aws_iam_policy_document" "deploy_permissions" { - statement { - sid = "S3BucketManagement" - effect = "Allow" - - actions = [ - "s3:CreateBucket", - "s3:DeleteBucket", - "s3:GetBucketVersioning", - "s3:PutBucketVersioning", - "s3:GetBucketEncryption", - "s3:PutBucketEncryption", - "s3:GetBucketPublicAccessBlock", - "s3:PutBucketPublicAccessBlock", - "s3:GetBucketPolicy", - "s3:PutBucketPolicy", - "s3:ListBucket", - "s3:GetObject", - "s3:PutObject", - "s3:DeleteObject", - ] - - resources = [ - "arn:aws:s3:::*", - "arn:aws:s3:::*/*", - ] - } - - statement { - sid = "DynamoDBTableManagement" - effect = "Allow" - - actions = [ - "dynamodb:CreateTable", - "dynamodb:DeleteTable", - "dynamodb:DescribeTable", - "dynamodb:UpdateTable", - "dynamodb:ListTables", - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:UpdateItem", - "dynamodb:DeleteItem", - ] - - resources = ["arn:aws:dynamodb:*:*:table/*"] - } - - statement { - sid = "SecretsManagerManagement" - effect = "Allow" - - actions = [ - "secretsmanager:CreateSecret", - "secretsmanager:DeleteSecret", - "secretsmanager:DescribeSecret", - "secretsmanager:GetSecretValue", - "secretsmanager:PutSecretValue", - "secretsmanager:UpdateSecret", - "secretsmanager:ListSecrets", - ] - - resources = ["arn:aws:secretsmanager:*:*:secret:*"] - } - - statement { - sid = "KMSKeyManagement" - effect = "Allow" - - actions = [ - "kms:CreateKey", - "kms:DescribeKey", - "kms:ListKeys", - "kms:ListAliases", - "kms:CreateAlias", - "kms:DeleteAlias", - "kms:UpdateAlias", - "kms:GetKeyPolicy", - "kms:PutKeyPolicy", - "kms:Decrypt", - "kms:Encrypt", - "kms:GenerateDataKey", - "kms:ScheduleKeyDeletion", - ] - - resources = ["arn:aws:kms:*:*:key/*"] - } - - statement { - sid = "IAMRoleManagement" - effect = "Allow" - - actions = [ - "iam:CreateRole", - "iam:DeleteRole", - "iam:GetRole", - "iam:ListRoles", - "iam:UpdateAssumeRolePolicy", - "iam:GetAssumeRolePolicy", - "iam:PassRole", - "iam:TagRole", - "iam:UntagRole", - ] - - resources = ["arn:aws:iam::*:role/*"] - } - - statement { - sid = "IAMPolicyManagement" - effect = "Allow" - - actions = [ - "iam:CreatePolicy", - "iam:DeletePolicy", - "iam:GetPolicy", - "iam:ListPolicies", - "iam:CreatePolicyVersion", - "iam:DeletePolicyVersion", - "iam:ListPolicyVersions", - "iam:GetPolicyVersion", - "iam:AttachRolePolicy", - "iam:DetachRolePolicy", - "iam:ListAttachedRolePolicies", - "iam:PutRolePolicy", - "iam:GetRolePolicy", - "iam:DeleteRolePolicy", - "iam:ListRolePolicies", - ] - - resources = [ - "arn:aws:iam::*:policy/*", - "arn:aws:iam::*:role/*", - ] - } - - statement { - sid = "CloudWatchLogs" - effect = "Allow" - - actions = [ - "logs:CreateLogGroup", - "logs:DeleteLogGroup", - "logs:DescribeLogGroups", - "logs:ListLogGroups", - ] - - resources = ["arn:aws:logs:*:*:log-group:*"] - } -} - -resource "aws_iam_role_policy" "deploy_permissions" { - name = "deploy-permissions" - role = aws_iam_role.github_actions.id - policy = data.aws_iam_policy_document.deploy_permissions.json -} diff --git a/scripts/infra/outputs.tf b/scripts/infra/outputs.tf new file mode 100644 index 0000000..cb0aa35 --- /dev/null +++ b/scripts/infra/outputs.tf @@ -0,0 +1,12 @@ +output "terraform_state_bucket_name" { + value = aws_s3_bucket.terraform_state_store.bucket +} + + +output "github_actions_role_name" { + value = aws_iam_role.github_actions.name +} + +output "github_actions_pr_role_name" { + value = aws_iam_role.github_actions_pr.name +} diff --git a/scripts/infra/tf_import.sh b/scripts/infra/tf_import.sh new file mode 100755 index 0000000..ccb3531 --- /dev/null +++ b/scripts/infra/tf_import.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +AWS_REGION="eu-west-2" +PROJECT="genomics-order-management" +ENVIRONMENT="prod" + +ROLE_NAME="github-genomics-order-management-oidc-deploy-role" +ROLE_NAME_READONLY="github-genomics-order-management-oidc-pr-role" +BUCKET="${PROJECT}-tfstate-${ENVIRONMENT}" + +terraform init -input=false + +import_if_needed() { + local addr=$1 + local id=$2 + + if terraform state list 2>/dev/null | grep -q "^${addr}$"; then + echo "✔ Already imported: $addr" + return + fi + + echo "→ Importing $addr with ID: $id" + + if ! terraform import "$addr" "$id"; then + echo " FAILED: $addr" + echo " ID used: $id" + exit 1 + fi +} + + +import_if_needed aws_s3_bucket.terraform_state_store "$BUCKET" +import_if_needed aws_s3_bucket_versioning.terraform_state_store "$BUCKET" +import_if_needed aws_s3_bucket_acl.terraform-state-acl "$BUCKET" +import_if_needed aws_s3_bucket_server_side_encryption_configuration.terraform_state_store "$BUCKET" +import_if_needed aws_s3_bucket_public_access_block.terraform_state_store "$BUCKET" +import_if_needed aws_s3_bucket_ownership_controls.terraform_state_ownership "$BUCKET" +import_if_needed aws_iam_role.github_actions "$ROLE_NAME" +import_if_needed aws_iam_role.github_actions_pr "$ROLE_NAME_READONLY" + + +terraform state list + +terraform plan -input=false + +echo "Import completed successfully" + diff --git a/scripts/infra/variables.tf b/scripts/infra/variables.tf index 53f37e5..3803310 100644 --- a/scripts/infra/variables.tf +++ b/scripts/infra/variables.tf @@ -37,7 +37,13 @@ variable "github_branch" { variable "role_name" { description = "Name of the IAM role created for GitHub Actions." type = string - default = "github-genomics-order-management-oidc-role" + default = "github-genomics-order-management-oidc-deploy-role" +} + +variable "role_name_pr" { + description = "Name of the IAM role created for GitHub Actions." + type = string + default = "github-genomics-order-management-oidc-pr-role" } variable "oidc_provider" { diff --git a/scripts/terraform/terraform.lib.sh b/scripts/terraform/terraform.lib.sh index 7793b9b..1afe289 100644 --- a/scripts/terraform/terraform.lib.sh +++ b/scripts/terraform/terraform.lib.sh @@ -3,6 +3,7 @@ # WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead. set -euo pipefail +set -x # A set of Terraform functions written in Bash. # diff --git a/scripts/terraform/terraform.sh b/scripts/terraform/terraform.sh index 73f37c1..4e074af 100755 --- a/scripts/terraform/terraform.sh +++ b/scripts/terraform/terraform.sh @@ -3,7 +3,7 @@ # WARNING: Please DO NOT edit this file! It is maintained in the Repository Template (https://github.com/nhs-england-tools/repository-template). Raise a PR instead. set -euo pipefail - +set -x # Terraform command wrapper. It will run the command natively if Terraform is # installed, otherwise it will run it in a Docker container. # @@ -35,6 +35,7 @@ function main() { function run-terraform-natively() { # shellcheck disable=SC2086 + echo $PWD terraform $cmd } @@ -45,13 +46,18 @@ function run-terraform-in-docker() { # shellcheck disable=SC1091 source ./scripts/docker/docker.lib.sh - # shellcheck disable=SC2155 local image=$(name=hashicorp/terraform docker-get-image-version-and-pull) # shellcheck disable=SC2086 + docker run --rm --platform linux/amd64 \ --volume "$PWD":/workdir \ --workdir /workdir \ + -e AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID:-}" \ + -e AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY:-}" \ + -e AWS_SESSION_TOKEN="${AWS_SESSION_TOKEN:-}" \ + -e AWS_REGION="${AWS_REGION:-}" \ + -e AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-}" \ "$image" \ $cmd } diff --git a/specification/enviroment/beta/genomic-order-management-service-api-specification-beta-int.yaml b/specification/enviroment/beta/genomic-order-management-service-api-specification-beta-int.yaml index 756b8d9..f8f2a6a 100644 --- a/specification/enviroment/beta/genomic-order-management-service-api-specification-beta-int.yaml +++ b/specification/enviroment/beta/genomic-order-management-service-api-specification-beta-int.yaml @@ -140,11 +140,11 @@ info: (RBAC)](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/national-rbac-for-developers) - RBAC rules are stored in the National RBAC Database (NRD) and are used to limit what clinical users can and cannot do. - + * [Digital Genomics Test Directory Service (DGTS)](https://digital.nhs.uk/developer/api-catalogue/digital-genomics-test-directory-service) - Search for Genomic Tests and Test Packages using FHIR - + * [Multicast Notification Service (MNS)](https://digital.nhs.uk/developer/api-catalogue/multicast-notification-service) - Notification service for genomic events @@ -352,7 +352,7 @@ info: | Integration |`https://int.api.service.nhs.uk/genomic-order-management-service`| - + | Production |`https://api.service.nhs.uk/genomic-order-management-service`| @@ -472,7 +472,7 @@ paths: description: | ## Overview [FHIR Restful](https://hl7.org/fhir/R4/http.html) endpoint to - register a **new test or update tests or add reports etc**. This end point uses FHIR [bundle](https://simplifier.net/guide/fhir-genomics-implementation-guide/Home/FHIRAssets/Profiles/All-Profiles/Bundle) of type **transaction** and the list of resources that forms the payload are listed [here](https://simplifier.net/guide/fhir-genomics-implementation-guide/Home/Build/Resource-Linkage). + register a **new test or update tests or add reports etc**. This end point uses FHIR [bundle](https://simplifier.net/guide/fhir-genomics-implementation-guide/Home/FHIRAssets/Profiles/All-Profiles/Bundle) of type **transaction** and the list of resources that forms the payload are listed [here](https://simplifier.net/guide/fhir-genomics-implementation-guide/Home/Build/Resource-Linkage). tags: - process parameters: @@ -1937,7 +1937,7 @@ paths: This endpoint allows you to retrieve the records of DiagnosticReport using various query parameters. Refer [search](https://simplifier.net/guide/FHIR-Genomics-Implementation-Guide/Home/Build/Supported-CRUD-capability#Read) for more details. - + ## UGR - Search for patient test reports. tags: - report @@ -2172,7 +2172,7 @@ paths: | Scenario | - Request + Request | Response | @@ -2182,8 +2182,8 @@ paths: ------------------------------------------------------------------------| | - | - | + | + | | tags: - history @@ -2216,7 +2216,7 @@ paths: | Scenario | - Request + Request | Response | @@ -2226,7 +2226,7 @@ paths: ------------------------------------------------------------------------| | - | + | | | tags: @@ -3575,7 +3575,7 @@ components: - type - entry description: >- - A Bundle containing [FHIR ServiceRequest](https://simplifier.net/guide/fhir-genomics-implementation-guide/Home/FHIRAssets/Profiles/All-Profiles/UKCore-ServiceRequest) + A Bundle containing [FHIR ServiceRequest](https://simplifier.net/guide/fhir-genomics-implementation-guide/Home/FHIRAssets/Profiles/All-Profiles/UKCore-ServiceRequest) history examples: multiple-items: @@ -4751,4 +4751,4 @@ x-nhsd-apim: timeunit: minute app-default: limit: 200 - timeunit: minute \ No newline at end of file + timeunit: minute