diff --git a/.husky/pre-push b/.husky/pre-push index 107239b36..b7188566a 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,6 +1,8 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -export ENVIRONMENT=feature -npm run lint -npm run husky:test +if [ "$(git rev-parse --abbrev-ref HEAD)" != "prod" ]; then + export ENVIRONMENT=feature + npm run lint + npm run husky:test +fi diff --git a/deployment/bin/pushECRImagesToProd.sh b/deployment/bin/pushECRImagesToProd.sh new file mode 100755 index 000000000..6944da54c --- /dev/null +++ b/deployment/bin/pushECRImagesToProd.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) + +aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com + +LOCALSTACK_VERSION=0.14.1 +aws ecr create-repository --repository-name localstack || true +docker tag localstack/localstack:$LOCALSTACK_VERSION $ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/localstack:$LOCALSTACK_VERSION +docker push $ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/localstack:$LOCALSTACK_VERSION + +# Create buildimage to provide faster builds +BUILD_IMAGE_VERSION=1.0.3 +REPO_TAG=$ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/hassu-buildimage:$BUILD_IMAGE_VERSION +aws ecr create-repository --repository-name hassu-buildimage || true +docker tag hassu-buildimage:$BUILD_IMAGE_VERSION $REPO_TAG +docker push $REPO_TAG diff --git a/deployment/lib/buildspec/buildspec-prod.yml b/deployment/lib/buildspec/buildspec-prod.yml new file mode 100644 index 000000000..a718e4a29 --- /dev/null +++ b/deployment/lib/buildspec/buildspec-prod.yml @@ -0,0 +1,41 @@ +version: 0.2 + +env: + parameter-store: + ROCKET_CHAT_TOKEN: /RocketChatToken + ROCKET_CHAT_USER_ID: /RocketChatUserId + secrets-manager: + GITHUB_TOKEN: github-token + +phases: + install: + runtime-versions: + java: corretto11 + nodejs: 14 + commands: + - ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) + - aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com" + + - nohup docker pull "$ACCOUNT_ID.dkr.ecr.eu-west-1.amazonaws.com/hassu-buildimage:1.0.3" & + + - npm install -g npm@8.1.3 + - npm ci + + - touch .env.test + - npm run generate + build: + commands: + - npm run get-next-version + - npm run deploy:database + - npm run deploy:backend + - npm run deploy:frontend + - npm run release + post_build: + on-failure: ABORT + commands: + - ./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: + - "/root/.cache/**/*" + - "/root/.npm/**/*" + - "/root/.gradle/**/*" diff --git a/deployment/lib/config.ts b/deployment/lib/config.ts index 72cf495ca..77cbb1052 100644 --- a/deployment/lib/config.ts +++ b/deployment/lib/config.ts @@ -48,7 +48,7 @@ export class Config extends BaseConfig { this.scope = scope; const env = Config.env; - if (Config.isPermanentEnvironment()) { + if (Config.isPermanentEnvironment() && !Config.isProdAccount()) { // TODO remove "&& !Config.isProdAccount()" after getting the cert for prod this.cloudfrontCertificateArn = this.getParameter(`/${env}/CloudfrontCertificateArn`); } this.dmzProxyEndpoint = this.getInfraParameter("DMZProxyEndpoint"); @@ -76,10 +76,7 @@ export class Config extends BaseConfig { if (Config.env === "localstack") { return ""; } - return ssm.StringParameter.valueForStringParameter( - this.scope, - this.getInfraParameterPath(parameterName, infraEnvironment) - ); + return ssm.StringParameter.valueForStringParameter(this.scope, this.getInfraParameterPath(parameterName, infraEnvironment)); } public getInfraParameterPath(parameterName: string, infraEnvironment?: string) { @@ -90,18 +87,11 @@ export class Config extends BaseConfig { return Config.getSecureInfraParameterInternal({ parameterName, infraEnvironment, ssm: ssmProvider }); } - public async getGlobalSecureInfraParameter( - parameterName: string, - infraEnvironment: string = BaseConfig.infraEnvironment - ) { + public async getGlobalSecureInfraParameter(parameterName: string, infraEnvironment: string = BaseConfig.infraEnvironment) { return Config.getSecureInfraParameterInternal({ parameterName, infraEnvironment, ssm: globalSsmProvider }); } - private static async getSecureInfraParameterInternal(params: { - parameterName: string; - infraEnvironment: string; - ssm: SSM; - }) { + private static async getSecureInfraParameterInternal(params: { parameterName: string; infraEnvironment: string; ssm: SSM }) { // Skip AWS API calls if running locally with localstack and cdklocal if (Config.env === "localstack") { return "dummy"; @@ -128,15 +118,15 @@ export class Config extends BaseConfig { } private init = async () => { - this.branch = process.env.BUILD_BRANCH - ? process.env.BUILD_BRANCH - : await execShellCommand("git rev-parse --abbrev-ref HEAD"); + this.branch = process.env.BUILD_BRANCH ? process.env.BUILD_BRANCH : await execShellCommand("git rev-parse --abbrev-ref HEAD"); if (Config.isDeveloperEnvironment()) { - this.frontendDomainName = - (await readFrontendStackOutputs()).CloudfrontPrivateDNSName || "please-re-run-backend-deployment"; + this.frontendDomainName = (await readFrontendStackOutputs()).CloudfrontPrivateDNSName || "please-re-run-backend-deployment"; } else { this.frontendDomainName = await this.getSecureInfraParameter("FrontendDomainName"); + if (!this.frontendDomainName) { + throw new Error("/" + Config.env + "/FrontendDomainName SSM Parameter not found! Maybe logged in to wrong account?"); + } } log.info("frontendDomainName", this.frontendDomainName); }; diff --git a/deployment/lib/hassu-account.ts b/deployment/lib/hassu-account.ts index 82399dab4..8c4f2e759 100644 --- a/deployment/lib/hassu-account.ts +++ b/deployment/lib/hassu-account.ts @@ -34,7 +34,7 @@ export class HassuAccountStack extends cdk.Stack { enableVersionUpgrade: true, capacity: { masterNodes: 0, - dataNodes: 1, + dataNodes: 2, dataNodeInstanceType: "t3.small.search", }, removalPolicy: RemovalPolicy.RETAIN, diff --git a/deployment/lib/hassu-database.ts b/deployment/lib/hassu-database.ts index 203c4eaac..d9d023513 100644 --- a/deployment/lib/hassu-database.ts +++ b/deployment/lib/hassu-database.ts @@ -10,7 +10,6 @@ import * as backup from "@aws-cdk/aws-backup"; import * as events from "@aws-cdk/aws-events"; import { Effect, PolicyStatement } from "@aws-cdk/aws-iam"; import { IConstruct } from "@aws-cdk/core/lib/construct-compat"; -import { BackupPlanRuleProps } from "@aws-cdk/aws-backup/lib/rule"; // These should correspond to CfnOutputs produced by this stack export type DatabaseStackOutputs = { @@ -111,6 +110,12 @@ export class HassuDatabaseStack extends cdk.Stack { } private createUploadBucket() { + let allowedOrigins: string[]; + if (Config.isDeveloperEnvironment()) { + allowedOrigins = ["http://localhost:3000", "https://" + this.config.frontendDomainName]; + } else { + allowedOrigins = ["https://" + this.config.frontendDomainName]; + } return new Bucket(this, "UploadBucket", { bucketName: Config.uploadBucketName, blockPublicAccess: BlockPublicAccess.BLOCK_ALL, @@ -119,7 +124,7 @@ export class HassuDatabaseStack extends cdk.Stack { cors: [ { allowedMethods: [HttpMethods.PUT], - allowedOrigins: ["http://localhost:3000", "https://" + this.config.frontendDomainName], + allowedOrigins: allowedOrigins, allowedHeaders: ["*"], }, ], @@ -191,24 +196,12 @@ export class HassuDatabaseStack extends cdk.Stack { const backupPlanName = "Plan-" + Config.env; const backupVaultName = "Vault-" + Config.env; - let backupPlanRuleProps: BackupPlanRuleProps; - if (Config.isProductionEnvironment()) { - backupPlanRuleProps = { - moveToColdStorageAfter: Duration.days(35), - deleteAfter: Duration.days(365), - }; - } else { - backupPlanRuleProps = { - deleteAfter: Duration.days(35), - }; - } - const plan = new backup.BackupPlan(this, backupPlanName, { backupPlanName, backupVault: new backup.BackupVault(this, backupVaultName, { backupVaultName }), backupPlanRules: [ new backup.BackupPlanRule({ - ...backupPlanRuleProps, + deleteAfter: Duration.days(35), ruleName: "Daily", startWindow: Duration.hours(1), completionWindow: Duration.hours(2), diff --git a/deployment/lib/hassu-pipeline.ts b/deployment/lib/hassu-pipeline.ts index 1e099f758..b3b8863fd 100644 --- a/deployment/lib/hassu-pipeline.ts +++ b/deployment/lib/hassu-pipeline.ts @@ -6,7 +6,7 @@ import * as codebuild from "@aws-cdk/aws-codebuild"; import { BuildEnvironmentVariableType, ComputeType, LocalCacheMode } from "@aws-cdk/aws-codebuild"; import { Config } from "./config"; import { BuildSpec } from "@aws-cdk/aws-codebuild/lib/build-spec"; -import { LinuxBuildImage } from "@aws-cdk/aws-codebuild/lib/project"; +import { BuildEnvironmentVariable, LinuxBuildImage } from "@aws-cdk/aws-codebuild/lib/project"; import { Effect, PolicyStatement } from "@aws-cdk/aws-iam"; import { GitHubSourceProps } from "@aws-cdk/aws-codebuild/lib/source"; import { Repository } from "@aws-cdk/aws-ecr/lib/repository"; @@ -42,7 +42,11 @@ export class HassuPipelineStack extends Stack { } if (Config.isPermanentEnvironment()) { - await this.createPipeline(env, config, "./deployment/lib/buildspec/buildspec.yml"); + if (Config.isProdAccount()) { + await this.createPipeline(env, config, "./deployment/lib/buildspec/buildspec-prod.yml"); + } else { + await this.createPipeline(env, config, "./deployment/lib/buildspec/buildspec.yml"); + } } else { await this.createPipeline(env, config, "./deployment/lib/buildspec/buildspec-feature.yml"); } @@ -53,11 +57,13 @@ export class HassuPipelineStack extends Stack { let webhookFilters; let reportBuildStatus: boolean; const branch = config.getBranch(); - if (branch === "main" && env == "dev") { + if ((branch === "main" && env == "dev") || (branch === "prod" && env == "prod")) { // GitHub creds only once per account new codebuild.GitHubSourceCredentials(this, "CodeBuildGitHubCreds", { accessToken: SecretValue.secretsManager("github-token"), }); + } + if (branch === "main" && env == "dev") { // Common bucket for test reports const reportBucket = new Bucket(this, "reportbucket", { bucketName: Config.reportBucketName }); @@ -90,7 +96,72 @@ export class HassuPipelineStack extends Stack { cloneDepth: 0, }; const gitHubSource = codebuild.Source.gitHub(sourceProps); - new codebuild.Project(this, "HassuProject", { + let environmentVariables: { [name: string]: BuildEnvironmentVariable } = { + ENVIRONMENT: { value: env }, + VELHO_AUTH_URL: { + value: config.getInfraParameterPath("VelhoAuthenticationUrl", config.velhoEnv), + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + VELHO_API_URL: { + value: config.getInfraParameterPath("VelhoApiUrl", config.velhoEnv), + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + VELHO_USERNAME: { value: await config.getSecureInfraParameter("VelhoUsername", config.velhoEnv) }, + VELHO_PASSWORD: { value: await config.getSecureInfraParameter("VelhoPassword", config.velhoEnv) }, + + PERSON_SEARCH_API_ACCOUNT_TYPES: { + value: config.getInfraParameterPath("PersonSearchApiAccountTypes"), + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + NEXT_PUBLIC_VAYLA_EXTRANET_URL: { + value: config.getInfraParameterPath("ExtranetHomePageUrl"), + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + NEXT_PUBLIC_VELHO_BASE_URL: { + value: config.getInfraParameterPath("VelhoBaseUrl", config.velhoEnv), + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + NODE_OPTIONS: { + value: "--max_old_space_size=4096 --max-old-space-size=4096", + type: BuildEnvironmentVariableType.PLAINTEXT, + }, + }; + + if (env == "prod") { + environmentVariables = { + ...environmentVariables, + PERSON_SEARCH_API_URL_PROD: { + value: "/PersonSearchApiURLProd", + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + PERSON_SEARCH_API_USERNAME_PROD: { + value: "/PersonSearchApiUsernameProd", + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + PERSON_SEARCH_API_PASSWORD_PROD: { + value: "/PersonSearchApiPasswordProd", + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + }; + } else { + environmentVariables = { + ...environmentVariables, + PERSON_SEARCH_API_URL: { + value: "/PersonSearchApiURL", + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + PERSON_SEARCH_API_USERNAME: { + value: "/PersonSearchApiUsername", + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + PERSON_SEARCH_API_PASSWORD: { + value: "/PersonSearchApiPassword", + type: BuildEnvironmentVariableType.PARAMETER_STORE, + }, + }; + } + + const project = new codebuild.Project(this, "HassuProject", { projectName: "Hassu-" + env, buildSpec: BuildSpec.fromSourceFilename(buildspecFileName), source: gitHubSource, @@ -99,78 +170,25 @@ export class HassuPipelineStack extends Stack { buildImage: LinuxBuildImage.STANDARD_5_0, privileged: true, computeType: ComputeType.MEDIUM, - environmentVariables: { - ENVIRONMENT: { value: env }, - VELHO_AUTH_URL: { - value: config.getInfraParameterPath("VelhoAuthenticationUrl", config.velhoEnv), - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - VELHO_API_URL: { - value: config.getInfraParameterPath("VelhoApiUrl", config.velhoEnv), - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - VELHO_USERNAME: { value: await config.getSecureInfraParameter("VelhoUsername", config.velhoEnv) }, - VELHO_PASSWORD: { value: await config.getSecureInfraParameter("VelhoPassword", config.velhoEnv) }, - - PERSON_SEARCH_API_URL: { - value: "/PersonSearchApiURL", - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - PERSON_SEARCH_API_URL_PROD: { - value: "/PersonSearchApiURLProd", - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - PERSON_SEARCH_API_USERNAME: { - value: "/PersonSearchApiUsername", - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - PERSON_SEARCH_API_PASSWORD: { - value: "/PersonSearchApiPassword", - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - PERSON_SEARCH_API_USERNAME_PROD: { - value: "/PersonSearchApiUsernameProd", - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - PERSON_SEARCH_API_PASSWORD_PROD: { - value: "/PersonSearchApiPasswordProd", - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - PERSON_SEARCH_API_ACCOUNT_TYPES: { - value: config.getInfraParameterPath("PersonSearchApiAccountTypes"), - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - NEXT_PUBLIC_VAYLA_EXTRANET_URL: { - value: config.getInfraParameterPath("ExtranetHomePageUrl"), - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - NEXT_PUBLIC_VELHO_BASE_URL: { - value: config.getInfraParameterPath("VelhoBaseUrl", config.velhoEnv), - type: BuildEnvironmentVariableType.PARAMETER_STORE, - }, - NODE_OPTIONS: { - value: "--max_old_space_size=4096 --max-old-space-size=4096", - type: BuildEnvironmentVariableType.PLAINTEXT, - }, - }, + environmentVariables: environmentVariables, }, grantReportGroupPermissions: true, badge: true, - }).addToRolePolicy( + }); + project.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, - actions: [ - "s3:*", - "cloudformation:*", - "sts:*", - "ecr:*", - "ssm:*", - "secretsmanager:GetSecretValue", - "codebuild:StartBuild", - ], + actions: ["s3:*", "cloudformation:*", "sts:*", "ecr:*", "ssm:*", "secretsmanager:GetSecretValue", "codebuild:StartBuild"], resources: ["*"], }) ); + project.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ["iam:CreateServiceLinkedRole"], + resources: ["arn:aws:iam::*:role/aws-service-role/*"], + }) + ); } private async createE2eTestPipeline(env: string, config: Config) { @@ -193,10 +211,7 @@ export class HassuPipelineStack extends Stack { source: gitHubSource, cache: codebuild.Cache.local(LocalCacheMode.CUSTOM, LocalCacheMode.SOURCE, LocalCacheMode.DOCKER_LAYER), environment: { - buildImage: LinuxBuildImage.fromEcrRepository( - Repository.fromRepositoryName(this, "RobotBuildImage", "hassu-buildimage"), - "1.0.3" - ), + buildImage: LinuxBuildImage.fromEcrRepository(Repository.fromRepositoryName(this, "RobotBuildImage", "hassu-buildimage"), "1.0.3"), privileged: true, computeType: ComputeType.MEDIUM, environmentVariables: { diff --git a/package.json b/package.json index e48c1b3a1..f7cc820ca 100644 --- a/package.json +++ b/package.json @@ -167,10 +167,10 @@ "build": "next build", "start": "next start", "cypress": "cypress", - "generate:velhoapi": "MSYS_NO_PATHCONV=1 cross-env docker run --rm --env-file .env.test -e VELHO_AUTH_URL -e VELHO_API_URL -e VELHO_USERNAME -e VELHO_PASSWORD -e VELHO_ACCESS_TOKEN=\"$(ts-node --project=tsconfig.cdk.json -r dotenv/config ./tools/velho/authenticate.ts dotenv_config_path=.env.test)\" -v $INIT_CWD:/work 283563576583.dkr.ecr.eu-west-1.amazonaws.com/hassu-buildimage:1.0.3 /work/tools/velho/gradlew -p /work/tools/velho --info", + "generate:velhoapi": "MSYS_NO_PATHCONV=1 cross-env docker run --rm --env-file .env.test -e VELHO_AUTH_URL -e VELHO_API_URL -e VELHO_USERNAME -e VELHO_PASSWORD -e VELHO_ACCESS_TOKEN=\"$(ts-node --project=tsconfig.cdk.json -r dotenv/config ./tools/velho/authenticate.ts dotenv_config_path=.env.test)\" -v $INIT_CWD:/work ${ACCOUNT_ID:=283563576583}.dkr.ecr.eu-west-1.amazonaws.com/hassu-buildimage:1.0.3 /work/tools/velho/gradlew -p /work/tools/velho --info", "postgenerate:velhoapi": "ts-node --project=tsconfig.cdk.json ./tools/velho/processMetaData.ts ./backend/src/velho/metadata.json", "pregenerate:api": "graphql-schema-utilities -s \"{./graphql/types.graphql,./graphql/inputs.graphql,./graphql/operations.graphql}\" -o schema.graphql", - "generate:api": "cross-env docker run --rm -v $INIT_CWD:/work 283563576583.dkr.ecr.eu-west-1.amazonaws.com/hassu-buildimage:1.0.3 bash -c \"cd /work && amplify codegen\"", + "generate:api": "cross-env docker run --rm -v $INIT_CWD:/work ${ACCOUNT_ID:=283563576583}.dkr.ecr.eu-west-1.amazonaws.com/hassu-buildimage:1.0.3 bash -c \"cd /work && amplify codegen\"", "generate": "npm-run-all --aggregate-output --parallel generate:*", "lint:backend": "eslint --cache --ignore-path .gitignore --ext .ts backend", "lint:app": "next lint", @@ -201,9 +201,11 @@ "deploy:frontend": "cdk $NODE_DEBUG_OPTION --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu\" deploy frontend --require-approval never", "postdeploy:frontend": "npm run setupenvironment", "deploy:account:dev": "cross-env ENVIRONMENT=dev cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-account\" deploy account --require-approval never", + "deploy:account:prod": "cross-env ENVIRONMENT=prod cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-account\" deploy account --require-approval never", "deploy:pipeline": "cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-pipeline\" deploy hassu-pipeline --require-approval never", "deploy:pipeline:main": "cross-env ENVIRONMENT=dev BUILD_BRANCH=main cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-pipeline\" deploy hassu-pipeline --require-approval never", "deploy:pipeline:test": "cross-env ENVIRONMENT=test BUILD_BRANCH=test cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-pipeline\" deploy hassu-pipeline --require-approval never", + "deploy:pipeline:prod": "cross-env ENVIRONMENT=prod BUILD_BRANCH=prod cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-pipeline\" deploy hassu-pipeline --require-approval never", "deploy:pipeline:feature": "cross-env ENVIRONMENT=feature cdk --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-pipeline\" deploy hassu-pipeline --require-approval never", "pipeline:bootstrap": "ACCOUNT_ARN=\"$(ts-node --project=tsconfig.cdk.json deployment/bin/get-account-arn.ts)\" && cdk bootstrap --app \"ts-node --project=tsconfig.cdk.json ./deployment/bin/hassu-pipeline\" --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess $ACCOUNT_ARN", "login": "aws sso login", @@ -219,7 +221,7 @@ "deleteVelhoProjekti": "ts-node --project=backend/tsconfig.json -r dotenv/config ./backend/bin/deleteVelhoProjekti dotenv_config_path=.env.test", "e2e:test:run": "cypress run -b chrome", "e2e:test:open": "cypress open", - "ecrLogin": "aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 283563576583.dkr.ecr.eu-west-1.amazonaws.com", + "ecrLogin": "aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID:=283563576583}.dkr.ecr.eu-west-1.amazonaws.com", "updateBuildimage": "./deployment/bin/updateECRImages.sh", "log:backend": "aws logs tail --follow --since 1h /aws/lambda/hassu-backend-$ENVIRONMENT", "log:backend:dev": "aws logs tail --follow --since 1h /aws/lambda/hassu-backend-dev",