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

feat: asenna sovelluspalomuuri (AWS WAF) edustalle. Käytä WAF:ia huoltotaukojen kytkemiseen päälle/pois. #538

Merged
merged 1 commit into from
Jan 24, 2023
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
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