Skip to content

Commit

Permalink
feat: asenna sovelluspalomuuri (AWS WAF) edustalle. Käytä WAF:ia huol…
Browse files Browse the repository at this point in the history
…totaukojen kytkemiseen päälle/pois.
  • Loading branch information
haapamakim committed Jan 18, 2023
1 parent b9ca9d2 commit a1ae931
Show file tree
Hide file tree
Showing 13 changed files with 501 additions and 28 deletions.
12 changes: 12 additions & 0 deletions deployment/bin/hassu-waf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import { App } from "aws-cdk-lib";
import { FrontendWafStack } from "../lib/hassu-waf";

async function main() {
const app = new App();
new FrontendWafStack(app);

app.synth();
}

main();
116 changes: 116 additions & 0 deletions deployment/bin/maintenanceMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import assert from "assert";
import fetch from "node-fetch";
import { Config } from "../lib/config";
import Wafv2, { RegexPatternSetSummary } from "aws-sdk/clients/wafv2";
import Ssm from "aws-sdk/clients/ssm";
import { AWSError } from "aws-sdk/lib/error";

const waf = new Wafv2({ region: "us-east-1" });
const ssm = new Ssm({ region: "us-east-1" });

async function getSSMParameter(ssmParameterName: string): Promise<string | undefined> {
try {
const ssmResponse = await ssm.getParameter({ Name: ssmParameterName }).promise();
return ssmResponse.Parameter?.Value;
} catch (e) {
if ((e as AWSError).code == "ParameterNotFound") {
return undefined;
} else {
throw e;
}
}
}

async function getHostNameForEnv(env: string): Promise<string> {
const ssmParameterName = "/" + env + "/FrontendDomainName";
const ssmParameterValue = await getSSMParameter(ssmParameterName);
if (ssmParameterValue) {
return ssmParameterValue;
} else {
const hostname = process.env.FRONTEND_DOMAIN_NAME;
console.log("SSM:stä ei löydy arvoa " + ssmParameterName + ":lle, joten käytetään hostnamea " + hostname);
assert(hostname, "FRONTEND_DOMAIN_NAME ei ole asetettu");
return hostname;
}
}

async function setRegexPattern(hostname: string | undefined) {
const patternSets = await waf.listRegexPatternSets({ Scope: "CLOUDFRONT", Limit: 20 }).promise();
const envConfigName = Config.getEnvConfigName();
const patternSet: RegexPatternSetSummary | undefined = patternSets.RegexPatternSets?.find((set) =>
set.Name?.endsWith("-" + envConfigName)
);
if (!patternSet) {
throw new Error("Ympäristölle ei löydy konfiguraatiota");
}
assert(patternSet.Name);
assert(patternSet.Id);
assert(patternSet.LockToken);
const params: Wafv2.Types.UpdateRegexPatternSetRequest = {
Scope: "CLOUDFRONT",
Name: patternSet.Name,
Id: patternSet.Id,
LockToken: patternSet.LockToken,
RegularExpressionList: [{ RegexString: "^" + hostname + "$" }],
};
await waf.updateRegexPatternSet(params).promise();
}

async function main() {
assert(process.argv);
const command = process.argv[process.argv.length - 1];
const env = process.env.ENVIRONMENT;
assert(env, "process.env.ENVIRONMENT pitää olla asetettu");
const envConfig = Config.getEnvConfig();
if (!envConfig.waf) {
console.log("Ympäristöllä " + env + " ei ole WAF:ia, joten sitä ei voi asettaa huoltotilaan");
}
if (command == "set") {
const hostname = await getHostNameForEnv(env);

console.log("Asetetaan ympäristö '" + env + "' (" + hostname + ") huoltotilaan");
await setRegexPattern(hostname);
await waitForMaintenanceModeResult(hostname);
} else if (command == "clear") {
const hostname = await getHostNameForEnv(env);

console.log("Poistetaan ympäristö '" + env + "' (" + hostname + ") huoltotilasta");
await setRegexPattern("varattu-huoltokatkolle");
await waitForMaintenanceModeOver(hostname);
} else {
console.log("Tuntematon komento " + command);
}
}

async function waitForMaintenanceModeResult(hostname: string) {
await waitForResponse(hostname, 503);
}

async function waitForMaintenanceModeOver(hostname: string) {
await waitForResponse(hostname, undefined, 503);
}

async function waitForResponse(hostname: string, statuscode: number | undefined, notstatuscode?: number) {
let retriesLeft = 60;
while (retriesLeft-- > 0) {
const response = await fetch("https://" + hostname);
console.log(hostname + " status " + response.status);
if (statuscode && response.status == statuscode) {
return;
}

if (notstatuscode && response.status !== notstatuscode) {
return;
}

await new Promise((resolve) => setTimeout(resolve, 1000));
}
throw new Error(hostname + " ei palauttanut statusta " + statuscode + " minuutin kuluessa!");
}

main()
.then(() => console.log("Valmis!"))
.catch((e) => {
console.log(e);
process.exit(1);
});
2 changes: 2 additions & 0 deletions deployment/lib/buildspec/buildspec-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ phases:
build:
commands:
- npm run get-next-version
- npm run maintenancemode set
- npm run deploy:database
- npm run deploy:backend
- npm run deploy:frontend
Expand All @@ -37,6 +38,7 @@ phases:
post_build:
on-failure: ABORT
commands:
- npm run maintenancemode clear
- ./deployment/bin/reportBuildStatus.sh -t "$ROCKET_CHAT_TOKEN" -u "$ROCKET_CHAT_USER_ID" -r "$CODEBUILD_BUILD_SUCCEEDING" -m "$ENVIRONMENT build" -d "CodeBuild $CODEBUILD_BUILD_URL"
cache:
paths:
Expand Down
2 changes: 2 additions & 0 deletions deployment/lib/buildspec/buildspec-training.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ phases:
- npm run test
- npm run localstack:stop &
- npm run sonar
- npm run maintenancemode set
- npm run deploy:database
- npm run deploy:backend
- npm run deploy:frontend
Expand All @@ -42,6 +43,7 @@ phases:
post_build:
on-failure: ABORT
commands:
- npm run maintenancemode clear
- ./deployment/bin/reportBuildStatus.sh -t "$ROCKET_CHAT_TOKEN" -u "$ROCKET_CHAT_USER_ID" -r "$CODEBUILD_BUILD_SUCCEEDING" -m "$ENVIRONMENT build" -d "CodeBuild $CODEBUILD_BUILD_URL"
cache:
paths:
Expand Down
2 changes: 2 additions & 0 deletions deployment/lib/buildspec/buildspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ phases:
- npm run test
- npm run localstack:stop &
- npm run sonar
- npm run maintenancemode set
- npm run deploy:database
- npm run deploy:backend
- npm run deploy:frontend
Expand All @@ -45,6 +46,7 @@ phases:
post_build:
on-failure: ABORT
commands:
- npm run maintenancemode clear
- ./deployment/bin/reportBuildStatus.sh -t "$ROCKET_CHAT_TOKEN" -u "$ROCKET_CHAT_USER_ID" -r "$CODEBUILD_BUILD_SUCCEEDING" -m "$ENVIRONMENT build" -d "CodeBuild $CODEBUILD_BUILD_URL"
cache:
paths:
Expand Down
31 changes: 22 additions & 9 deletions deployment/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ type Env = {
isProd?: boolean;
isDevAccount?: boolean;
isDeveloperEnvironment?: boolean;
waf?: boolean;
};

enum EnvName {
export enum EnvName {
"dev" = "dev",
"test" = "test",
"training" = "training",
Expand All @@ -29,18 +30,22 @@ const envConfigs: Record<EnvName, Env> = {
dev: {
terminationProtection: true,
isDevAccount: true,
waf: true,
},
test: {
terminationProtection: true,
isDevAccount: true,
waf: true,
},
training: {
terminationProtection: true,
isDevAccount: true,
waf: true,
},
prod: {
terminationProtection: true,
isProd: true,
waf: true,
},
localstack: {},
feature: {},
Expand Down Expand Up @@ -164,23 +169,31 @@ export class Config extends BaseConfig {
}
};

public static isDeveloperEnvironment() {
return Config.getEnvConfig().isDeveloperEnvironment;
public static isDeveloperEnvironment(): boolean {
return Config.getEnvConfig().isDeveloperEnvironment || false;
}

public static isDevAccount() {
return Config.getEnvConfig().isDevAccount;
public static isDevAccount(): boolean {
return Config.getEnvConfig().isDevAccount || false;
}

public static isProdAccount() {
return Config.getEnvConfig().isProd;
public static isProdAccount(): boolean {
return Config.getEnvConfig().isProd || false;
}

public static getEnvConfig(): Env {
const envConfig = envConfigs[BaseConfig.env as unknown as EnvName];
public static getEnvConfig(env = BaseConfig.env): Env {
const envConfig = envConfigs[Config.getEnvConfigName(env)];
if (envConfig) {
return envConfig;
}
return envConfigs.developer;
}

public static getEnvConfigName(env = BaseConfig.env): EnvName {
const envName = env as unknown as EnvName;
if (envConfigs[envName]) {
return envName;
}
return EnvName.developer;
}
}
8 changes: 7 additions & 1 deletion deployment/lib/hassu-frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ export class HassuFrontendStack extends Stack {
};
}

let webAclId;
if (Config.getEnvConfig().waf) {
webAclId = Fn.importValue("frontendWAFArn");
}

const nextJSLambdaEdge = new NextJSLambdaEdge(this, id, {
...cachePolicies,
serverlessBuildOutDir: "./build",
Expand All @@ -184,7 +189,7 @@ export class HassuFrontendStack extends Stack {
? [{ functionVersion: frontendRequestFunction.currentVersion, eventType: LambdaEdgeEventType.VIEWER_REQUEST }]
: [],
},
cloudfrontProps: { priceClass: PriceClass.PRICE_CLASS_100, logBucket },
cloudfrontProps: { priceClass: PriceClass.PRICE_CLASS_100, logBucket, webAclId },
invalidationPaths: ["/*"],
});
this.configureNextJSAWSPermissions(nextJSLambdaEdge);
Expand Down Expand Up @@ -215,6 +220,7 @@ export class HassuFrontendStack extends Stack {
}

const distribution: cloudfront.Distribution = nextJSLambdaEdge.distribution;

new CfnOutput(this, "CloudfrontPrivateDNSName", {
value: distribution.distributionDomainName || "",
});
Expand Down
2 changes: 2 additions & 0 deletions deployment/lib/hassu-pipelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ export class HassuPipelineStack extends Stack {
"lambda:ListFunctions",
"lambda:InvokeFunction",
"lambda:GetFunction",
"waf:ListRegexPatternSets",
"waf:UpdateRegexPatternSet",
],
resources: ["*"],
})
Expand Down
Loading

0 comments on commit a1ae931

Please sign in to comment.