Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RELEASE 1743 SOC2 controls #53

Merged
merged 18 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/workflows/_reusable_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,26 @@ jobs:
# INSTALL DEPENDENCIES
- name: Install NodeJS dependencies
run: npm ci --ignore-scripts
working-directory: "./infra"

- name: Build
run: npm run build
working-directory: "./infra"

- name: Test
run: npm run test
working-directory: "./infra"

# CDK DIFF
- name: Diff CDK stack
uses: metriport/deploy-with-cdk@master
with:
cdk_action: "diff"
cdk_version: "2.49.0"
cdk_version: "2.122.0"
cdk_stack: "FHIRServerStack"
cdk_env: "${{ inputs.deploy_env }}"
env:
INPUT_PATH: "infra"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
Expand All @@ -72,10 +82,11 @@ jobs:
uses: metriport/deploy-with-cdk@master
with:
cdk_action: "deploy --verbose --require-approval never"
cdk_version: "2.49.0"
cdk_version: "2.122.0"
cdk_stack: "FHIRServerStack"
cdk_env: "${{ inputs.deploy_env }}"
env:
INPUT_PATH: "infra"
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
21 changes: 16 additions & 5 deletions .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
name: Deploy - Production
name: Deploy - PRODUCTION

on:
# push: # a commit to the specified branches, if any
# branches:
# - master
push: # a commit to the specified branches, if any
branches:
- master
workflow_dispatch: # manually executed by a user

jobs:
deploy:
deploy-prod:
uses: ./.github/workflows/_reusable_deploy.yml
with:
deploy_env: "production"
Expand All @@ -16,3 +16,14 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.REGION_PRODUCTION }}
INFRA_CONFIG: ${{ secrets.INFRA_CONFIG_PRODUCTION }}

deploy-sandbox:
uses: ./.github/workflows/_reusable_deploy.yml
needs: [deploy-prod]
with:
deploy_env: "sandbox"
secrets:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.REGION_SANDBOX }}
INFRA_CONFIG: ${{ secrets.INFRA_CONFIG_SANDBOX }}
18 changes: 0 additions & 18 deletions .github/workflows/deploy-sandbox.yml

This file was deleted.

6 changes: 3 additions & 3 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Deploy - Staging

on:
# push: # a commit to the specified branches, if any
# branches:
# - develop
push: # a commit to the specified branches, if any
branches:
- develop
workflow_dispatch: # manually executed by a user

jobs:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM --platform=linux/arm64/v8 maven:3.9-eclipse-temurin-17 as build-fhir
FROM --platform=linux/amd64 maven:3.9-eclipse-temurin-17 as build-fhir
WORKDIR /tmp/hapi-fhir-jpaserver-starter

ARG OPENTELEMETRY_JAVA_AGENT_VERSION=1.17.0
Expand Down
29 changes: 29 additions & 0 deletions Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM --platform=linux/arm64/v8 maven:3.9-eclipse-temurin-17 as build-fhir
WORKDIR /tmp/hapi-fhir-jpaserver-starter

ARG OPENTELEMETRY_JAVA_AGENT_VERSION=1.17.0
RUN curl -LSsO https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v${OPENTELEMETRY_JAVA_AGENT_VERSION}/opentelemetry-javaagent.jar

COPY pom.xml .
COPY server.xml .
RUN mvn -ntp dependency:go-offline

COPY src/ ./src/
RUN mvn clean install -DskipTests -Djdk.lang.Process.launchMechanism=vfork

FROM build-fhir AS build-distroless
RUN mvn package spring-boot:repackage -Pboot
RUN mkdir /app && cp ./target/ROOT.war /app/main.war

########### distroless brings focus on security and runs on plain spring boot - this is the default image
FROM gcr.io/distroless/java17-debian11:nonroot as default
# 65532 is the nonroot user's uid
# used here instead of the name to allow Kubernetes to easily detect that the container
# is running as a non-root (uid != 0) user.
USER 65532:65532
WORKDIR /app

COPY --chown=nonroot:nonroot --from=build-distroless /app /app
COPY --chown=nonroot:nonroot --from=build-fhir /tmp/hapi-fhir-jpaserver-starter/opentelemetry-javaagent.jar /app

ENTRYPOINT ["java", "--class-path", "/app/main.war", "-Dloader.path=main.war!/WEB-INF/classes/,main.war!/WEB-INF/,/app/extra-classes", "org.springframework.boot.loader.PropertiesLauncher", "app/main.war"]
4 changes: 3 additions & 1 deletion docker-compose-fhir-converter.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
version: "3"
services:
fhir-server-fhir-converter:
build: .
build:
context: ./
dockerfile: ./Dockerfile.dev
container_name: fhir-server-fhir-converter
depends_on:
fhir-postgres-fhir-converter:
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
version: "3"
services:
fhir-server:
build: .
build:
context: ./
dockerfile: ./Dockerfile.dev
container_name: fhir-server
depends_on:
fhir-postgres:
Expand Down
15 changes: 15 additions & 0 deletions infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,18 @@ Run these commands on the terminal from the `./infra` folder of this repository:
$ cdk bootstrap -c env=<env> # only needs to be run once
$ ./deploy.sh
```

### Updating the configuration

Currently, the configuration is Base64 encoded and stored on GH secrets.

```shell
$ base64 -i infra/config/staging.ts
$ base64 -i infra/config/production.ts
$ base64 -i infra/config/sandbox.ts
```

Copy the resulting strings and update the respective secrets:
- `INFRA_CONFIG_STAGING`
- `INFRA_CONFIG_PRODUCTION`
- `INFRA_CONFIG_SANDBOX`
37 changes: 35 additions & 2 deletions infra/lib/fhir-server-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Aspects, CfnOutput, Duration, Stack, StackProps } from "aws-cdk-lib";
import {
Aspects,
CfnOutput,
Duration,
RemovalPolicy,
Stack,
StackProps,
} from "aws-cdk-lib";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import * as ec2 from "aws-cdk-lib/aws-ec2";
Expand All @@ -19,6 +26,7 @@ import { Construct } from "constructs";
import { EnvConfig } from "./env-config";
import { getConfig } from "./shared/config";
import { vCPU } from "./shared/fargate";
import { addDefaultMetricsToTargetGroup } from "./shared/target-group";
import { isProd, isSandbox, mbToBytes } from "./util";

export function settings() {
Expand Down Expand Up @@ -146,6 +154,8 @@ export class FHIRServerStack extends Stack {
storageEncrypted: true,
parameterGroup,
cloudwatchLogsExports: ["postgresql"],
deletionProtection: true,
removalPolicy: RemovalPolicy.RETAIN,
});

Aspects.of(dbCluster).add({
Expand Down Expand Up @@ -227,7 +237,7 @@ export class FHIRServerStack extends Stack {
publicLoadBalancer: false,
idleTimeout: maxExecutionTimeout,
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64,
cpuArchitecture: ecs.CpuArchitecture.X86_64,
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
},
}
Expand Down Expand Up @@ -293,6 +303,14 @@ export class FHIRServerStack extends Stack {
scaleOutCooldown: Duration.seconds(30),
});

const targetGroup = fargateService.targetGroup;
addDefaultMetricsToTargetGroup({
targetGroup,
scope: this,
id: "FhirServer",
alarmAction,
});

// allow the NLB to talk to fargate
fargateService.service.connections.allowFrom(
ec2.Peer.ipv4(this.vpc.vpcCidrBlock),
Expand Down Expand Up @@ -385,6 +403,21 @@ export class FHIRServerStack extends Stack {
evaluationPeriods: 1,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});

/**
* For Aurora Serverless, this alarm is not important as it auto-scales. However, we always
* create this alarm because of compliance controls (SOC2).
* @see: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html#aurora-storage-growth
*/
createAlarm({
metric: dbCluster.metricFreeLocalStorage(),
name: "FreeLocalStorageAlarm",
threshold: mbToBytes(10_000),
evaluationPeriods: 1,
leite08 marked this conversation as resolved.
Show resolved Hide resolved
comparisonOperator:
cloudwatch.ComparisonOperator.LESS_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
}
}

Expand Down
35 changes: 35 additions & 0 deletions infra/lib/shared/cloudwatch-metric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Alarm, ComparisonOperator, Metric, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { Construct } from "constructs";

export function addAlarmToMetric({
scope,
metric,
alarmName,
threshold,
evaluationPeriods,
comparisonOperator,
treatMissingData,
alarmAction,
includeOkAction = true,
}: {
scope: Construct;
metric: Metric;
alarmName: string;
threshold: number;
evaluationPeriods: number;
comparisonOperator?: ComparisonOperator;
treatMissingData?: TreatMissingData;
alarmAction?: SnsAction;
includeOkAction?: boolean;
}): Alarm {
const alarm = metric.createAlarm(scope, alarmName, {
threshold,
evaluationPeriods,
comparisonOperator,
treatMissingData,
});
alarmAction && alarm.addAlarmAction(alarmAction);
alarmAction && includeOkAction && alarm.addOkAction(alarmAction);
return alarm;
}
52 changes: 52 additions & 0 deletions infra/lib/shared/target-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Duration } from "aws-cdk-lib";
import { ComparisonOperator, TreatMissingData } from "aws-cdk-lib/aws-cloudwatch";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { ApplicationTargetGroup, HttpCodeTarget } from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Construct } from "constructs";
import { addAlarmToMetric } from "../shared/cloudwatch-metric";

export function addDefaultMetricsToTargetGroup({
targetGroup,
scope,
id,
idx = 0,
alarmAction,
}: {
targetGroup: ApplicationTargetGroup;
scope: Construct;
id: string;
idx?: number;
alarmAction?: SnsAction;
}) {
const name = `${id}_TargetGroup${idx}`;
addAlarmToMetric({
scope,
metric: targetGroup.metrics.unhealthyHostCount(),
alarmName: `${name}_UnhealthyRequestCount`,
threshold: 1,
evaluationPeriods: 1,
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: TreatMissingData.NOT_BREACHING,
alarmAction,
});
addAlarmToMetric({
scope,
metric: targetGroup.metrics.targetResponseTime(),
alarmName: `${name}_TargetResponseTime`,
threshold: Duration.seconds(29).toSeconds(),
evaluationPeriods: 1,
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: TreatMissingData.NOT_BREACHING,
alarmAction,
});
addAlarmToMetric({
scope,
metric: targetGroup.metrics.httpCodeTarget(HttpCodeTarget.TARGET_5XX_COUNT),
alarmName: `${name}_Target5xxCount`,
threshold: 5,
evaluationPeriods: 1,
comparisonOperator: ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
treatMissingData: TreatMissingData.NOT_BREACHING,
alarmAction,
});
}
Loading
Loading