Tech talk

Zkompilovaná aplikace

Image s aplikací

Zdrojové kódy

+

+

Deploy scripty

Support pro vývoj

.editorconfig

Databáze pro vývoj

GraphlQL faker

ESLint aspol.

Makefile

make env

 src/
 build/
 deploy/

Runtime
(runtime image)

Sestavujeme si repozitáŘ
FROM SCRATCH

Sestavujeme si repozitář

Krok 1: Inicializace

  • Přidat .gitignore
  • Přidat .editorconfig
  • Vytvořit prázdný Makefile
  • Vytvořit prázdný .gitlab-ci.yml
  • Založit README

Sestavujeme si repozitář

Krok 2: Runtime

  • Nativní vývoj
  • NVM
  • Docker
  • Docker Compose

Sestavujeme si repozitář

Krok 2: Runtime

Nativní vývoj

$ yarn eslint
:-) 

Sestavujeme si repozitář

Krok 2: Runtime

NVM

$ nvm use
Now using node v21.5.0

$ yarn eslint
21.5.0
$ nvm-exec yarn eslint
.nvmrc

Sestavujeme si repozitář

Krok 2: Runtime

Docker

$ sudo -E docker run \
	--rm \
	-e BONAMI_BUILD \
	-e GITLAB_ACCESS_TOKEN=${BONAMI_GITLAB_ACCESS_TOKEN} \
	-e YARN_CACHE_FOLDER \
	--volumes-from "$(shell /usr/local/bin/host-container-id)" \
	-w ${CI_PROJECT_DIR}/modules/admin-react \
	${runtime_image_name}:${runtime_image_tag} \
    yarn eslint
FROM node:21.5.0-alpine3.17 as runtime
WORKDIR /app

FROM runtime as dev
RUN apk add --no-cache bash make
Dockerfile

Sestavujeme si repozitář

Krok 2: Runtime

Docker

runtime-image-build:
	@${docker} build \
		--cache-from "${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}" \
		--cache-from "${RUNTIME_IMAGE}:latest" \
		--tag "${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}" \
		--tag "${RUNTIME_IMAGE}:latest" \
		--target=dev \
		.

runtime-image-pull:
	@${docker} pull "${RUNTIME_IMAGE}:latest"
	@${docker} pull "${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}"

runtime-image-push:
	@${docker} push "${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}"
	@${docker} push "${RUNTIME_IMAGE}:latest"

runtime-image-remove:
	${docker} image remove "${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}"
	${docker} image remove "${RUNTIME_IMAGE}:latest" || true
Makefile

Sestavujeme si repozitář

Krok 2: Runtime

Docker compose

$ sudo -E docker compose exec --user=${UID}:${GID} dev yarn eslint
version: "3.7"

services:
  dev:
    image: ${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}
    working_dir: /app
    user: ${UID}:${GID}
    volumes:
      - .:/app
    ports:
      - 3000:${PORT}
    tty: true
    environment:
      - APP_CONFIG_PATH
      - APP_DEV
      - GITLAB_ACCESS_TOKEN
    command: "cat"
docker-compose.yml

Sestavujeme si repozitář

Krok 2: Runtime

(CI)

build-runtime:
  stage: runtime
  needs: []
  only:
    changes:
      - Dockerfile
  tags:
    - 2core
  script:
    - docker login -u "gitlab-ci-token" -p "${CI_JOB_TOKEN}" "${CI_REGISTRY}"
    - make runtime-image-pull || true
    - make runtime-image-build
    - make runtime-image-push
    - make runtime-image-remove
.gitlab-ci.yml

Sestavujeme si repozitář

Krok 3: Vytváříme aplikaci

Sestavujeme si repozitář

Krok 3: Vytváříme aplikaci

$ nest new order-data-service
$ 
$ cd order-data-service/
$ yarn run start:dev

Sestavujeme si repozitář

Krok 4: Buildíme aplikaci

Sestavujeme si repozitář

Krok 4: Buildíme aplikaci

install:
	$(rshell) yarn install
    
build:
	$(rshell) yarn build
    
install-production:
	$(rshell) yarn install --production

dev:
	$(rshell) yarn start:dev
    
lint:
	$(rshell) yarn eslint
   
Makefile

Sestavujeme si repozitář

Krok 4: Buildíme aplikaci

image-build:
	@${docker} build \
		--cache-from "${RUNTIME_IMAGE}:${RUNTIME_IMAGE_TAG}" \
		--cache-from "${RUNTIME_IMAGE}:latest" \
		--tag "${IMAGE}:${IMAGE_TAG}" \
		--tag "${IMAGE}:latest" \
		--target=prod \
		.

image-push:
	@${docker} push "${IMAGE}:${IMAGE_TAG}"

image-remove:
	@${docker} image remove "${IMAGE}:${IMAGE_TAG}"
	@${docker} image remove "${IMAGE}:latest"
Makefile

Sestavujeme si repozitář

Krok 4: Buildíme aplikaci

build:
  stage: build
  needs:
    - job: build-runtime
  tags:
    - 2core
  script:
    - docker login -u "gitlab-ci-token" -p "${CI_JOB_TOKEN}" "${CI_REGISTRY}"
    - export GITLAB_ACCESS_TOKEN=${CI_JOB_TOKEN}
    - make runtime-image-pull
    - make install
    - make build
    - make install-production
    - make image-build
    - make image-push
    - ([[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]] && docker push "${IMAGE}:latest")
    - make image-remove
    
.gitlab-ci.yml

Sestavujeme si repozitář

Krok 5: Deplojujeme aplikaci

deploy:
  extends: .simple-deploy
  needs:
    - job: build
    - job: validate-nomad-configuration
  variables:
    IMAGE_NAME: ${IMAGE}
    NOMAD_FILE: ./deploy/OrderDataService.nomad
  tags:
    - deploy
.gitlab-ci.yml

Sestavujeme si repozitář

Krok 6: development environment

-include Makefile.local

export UID := $(shell id -u)
export GID := $(shell id -g)
export DOCKER_BIN ?= docker
export DOCKER_COMPOSE_BIN ?= docker compose
export ENV_EXEC ?= docker-compose
export GITLAB_ACCESS_TOKEN ?= ${BONAMI_GITLAB_ACCESS_TOKEN}
export IMAGE ?= registry.gitlab.bonami.cz/order/order-data-service
export IMAGE_TAG ?= dev
export PORT ?= 3000
export RUNTIME_IMAGE ?= ${IMAGE}/runtime
export RUNTIME_IMAGE_TAG ?= $(shell git rev-list -1 HEAD Dockerfile | cut -b -8)

export APP_DEV := true
export APP_VERSION := dev
export APP_CONFIG_PATH ?= config/dev.toml

Sestavujeme si repozitář

Krok 6: development environment

$ make env

$ docker compose exec dev yarn eslint

Sestavujeme si repozitář

Krok 6: development environment

env:
	@BONAMI_ENV_NAME="OrderDataService" $(shell echo $$SHELL)
Makefile

Sestavujeme si repozitář

Ideas

Sestavujeme si repozitář

Ideas: Lokální proměnné v makefile

…
export RUNTIME_IMAGE ?= ${IMAGE}/runtime
export RUNTIME_IMAGE_TAG ?= $(shell git rev-list -1 HEAD Dockerfile | cut -b -8)
export APP_CONFIG_PATH ?= config/dev.toml


root_dir := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
…

Sestavujeme si repozitář

Ideas: makefile.local + make env + ENV_NAME

DOCKER_BIN := sudo -E docker
DOCKER_COMPOSE_BIN := sudo -E docker compose
GITLAB_ACCESS_TOKEN := ${BONAMI_GITLAB_ACCESS_TOKEN}

APP_CONFIG_PATH := config/mujnejlepsi_local.toml
-include Makefile.local

export DOCKER_BIN ?= docker
export DOCKER_COMPOSE_BIN ?= docker compose
export GITLAB_ACCESS_TOKEN ?= ${BONAMI_GITLAB_ACCESS_TOKEN}
…
Makefile
Makefile.local

Sestavujeme si repozitář

Ideas: env-exec pro všechny

$ docker compose up -d dev
$ docker compose exec -it dev yarn eslint
$ nvm-exec yarn eslint
$ env-exec yarn eslint
export ENV_EXEC ?= docker-compose

(vs. RSHELL nebo hijacking nebo taky nic; srozumitelné hlášky)

Sestavujeme si repozitář

Ideas: make generate

$ make generate

Sestavujeme si repozitář

Ideas: Make rules

make runtime-image-pull install build generate install-production image-build image-push

(VB vision)

make generate
make generate-gql-schemas
make generate-typings

(vs. apply:fixtures a services:sort)

Sestavujeme si repozitář

Ideas: Make All a nic navíc

default:
	- [ ${ENV_EXEC} == docker-compose ] && $(MAKE) runtime-image-build || true
	- $(MAKE) install
	- $(MAKE) generate
	- $(MAKE) build
    - $(MAKE) init
	- $(MAKE) dev

+ Nepřehánět!

(Nemít v Makefile rule na každou kravinu; viz můj příklad se Scalou; VB vision make envu a dál po svých)

Makefile

Sestavujeme si repozitář

Ideas: Test make na ci + rozumné defaulty

Sestavujeme si repozitář

Commits

(mj. dá se k tomu vracet)

Sestavili jsme repozitář

Upravujeme si aplikaci

Upravujeme si aplikaci

  • Drobnosti (připojení k privátním NPM registrům, ESLint, přístup do Vaultu, …)
  • Konfigurace aplikace
  • GraphQL server
  • Model aplikace
  • … API třetí strany
  • Připojení k databázi (TypeORM)
  • Auth

 

Upravujeme si aplikaci

Drobnosti

@bonami:registry=https://gitlab.bonami.cz/api/v4/packages/npm/
@order:registry=https://gitlab.bonami.cz/api/v4/packages/npm/
//gitlab.bonami.cz/api/v4/packages/npm/:_authToken=${GITLAB_ACCESS_TOKEN}
//gitlab.bonami.cz/api/v4/projects/:_authToken=${GITLAB_ACCESS_TOKEN}
always-auth=true
.npmrc
OrderDataService.nomad
…
vault {
	change_mode = "restart"
	policies = ["order-data-service"]
}
…
.eslintrc.js

 

module.exports = {
	"extends": [
		"./node_modules/@bonami/eslint/lib/configs/.eslintrc.js"
	]
};

Upravujeme si aplikaci

Konfigurace aplikace

Upravujeme si aplikaci

Konfigurace aplikace

@Module({
	providers: [{
		provide: ConfigService,
		useFactory: async () => new ConfigService(/* … */)
	}],
	exports: [
		ConfigService
	]
})
export class ConfigModule { }

config.module.ts
export class ConfigService {

  	/* … */
    
    public get(key)  {
        /* … */
      
        return value;
    }
}
config.service.ts

Upravujeme si aplikaci

Konfigurace aplikace

export class AuthGuard implements CanActivate {

	public constructor(
		private configService: ConfigService
	) { }

	public async canActivate(context: ExecutionContext): Promise<boolean> {
		const issuers = this.configService.get("auth.issuers");
        
        /* … */
	}
}
auth.guard.ts

Upravujeme si aplikaci

GraphQL server

Upravujeme si aplikaci

GraphQL server

@Module({
	imports: [
		GraphQLModule.forRoot<ApolloDriverConfig>({
			driver: ApolloDriver,
			typePaths: ["./node_modules/@order/order-data-service-schema/**/*.graphql"],
			definitions: {
				emitTypenameField: true,
				path: "./src/graphql/definitions.ts",
				defaultScalarType: "unknown"
			}
		}),
		ModelModule
	],
	providers: [
		HelloWorldQueryResolver
	]
})
export class GraphQLServerModule {}
/src/graphql/graphQLServer.module.ts

Upravujeme si aplikaci

GraphQL server

import {Injectable} from "@nestjs/common";
import {Resolver, Query} from "@nestjs/graphql";

@Injectable()
@Resolver()
export class HelloWorldQueryResolver {
	constructor(
	) {}

	@Query()
	public helloWorld(): string {
		return "Hello world!";
	}
}
/src/graphql/resolvers/helloWorld.resolver.ts

Upravujeme si aplikaci

Model aplikace

Upravujeme si aplikaci

Model aplikace

import {Module} from "@nestjs/common";

@Module({
	providers: [
	]
})
export class ModelModule { }
/src/model/model.module.ts

Upravujeme si aplikaci

Auth

export class AuthGuard implements CanActivate {
  
	public async canActivate(context: ExecutionContext): Promise<boolean> {
      
      	/* … */
      
		const isTokenValid = await this.jwtService.verifyAsync(
            token, {
                algorithms: [algorithm],
                issuer,
                audience,
                publicKey
            }
        );
      
      	const isResourceAllowed = /* … */;
      
        return isTokenValid && isResourceAllowed;
	}  
}

Upravujeme si aplikaci

(Hierarchie)

Upravujeme si aplikaci

(hierarchie)

import {Module} from "@nestjs/common";

@Module({
	exports: [
	]
})
export class RunkaiApiModule { }
/src/rinkai/rinkaiApi.module.ts

Upravujeme si aplikaci

(hierarchie)

@Module({
	imports: [
		RinkaiApiModule
	],
  	exports: [...LOADERS]
})
export class ModelModule { }
/src/model/model.module.ts
@Module({
	imports: [
		ModelModule
	]
})
export class GraphQLServerModule { }
/src/graphql/graphlQLServer.module.ts
@Module({
  exports: […]
})
export class RinkaiAPIModule { }
/src/rinkai/rinkaiAPI.module.ts

Upravujeme si aplikaci

Připojení k databázi

@Module({
	imports: [
		TypeOrmModule.forRootAsync({
			useFactory: async (configService: ConfigService) => {
				const host = configService.get("bonamiweb.db.host");
				const port = configService.get("bonamiweb.db.port");
				const username = configService.get("bonamiweb.db.user");
				const password = configService.get("bonamiweb.db.password");
				const database = configService.get("bonamiweb.db.database");

				const synchronize =
					isDev()
					&& configService.has("bonamiweb.db.synchronize_schema")
					&& configService.get("bonamiweb.db.synchronize_schema") === true;

				return {
					type: "mysql",
					host,
					port,
					username,
					password,
					database,
					synchronize,
					namingStrategy: new SnakeNamingStrategy()
				} satisfies TypeOrmModuleOptions;
			}
		}),
		/* … */
	],
	exports: [
		TypeOrmModule
	]
})
export class BonamiWebDbModule { }
/src/bonamiweb/db/bonamiWebDb.module.ts

Upravujeme si aplikaci

Detaily

Upravujeme si aplikaci

Detaily

Upravujeme si aplikaci

Ideas

Upravujeme si aplikaci

Idea: Detaily

Upravujeme si aplikaci

Ideas: Konfigurujeme souborem

parameters_local.yml

Upravujeme si aplikaci

Ideas: "parameters.yml"

parameters.yml

Upravujeme si aplikaci

Ideas: APP_CONFIG_PATH

export APP_CONFIG_PATH ?= config/dev.toml config/dev_local.toml

Upravujeme si aplikaci

Ideas: Validace configu

Upravujeme si aplikaci

Ideas: Validace configu

export const CONFIG_SCHEME = {
	"port": "number",
	"auth": {
		"enabled": "boolean",
		"audience": "string",
		"authorized_parties": {
			"admin": "string",
			"web": "string"
		},
		"issuers": [{
			"algorithm": "string",
			"issuer": "string",
			"public_key": "string"
		}]
	},
	"bonamiweb": {
		"db": {
			"host": "string",
			"port": "number",
			"user": "string",
			"password": "string",
			"database": "string",
			"synchronize_schema": "boolean"
		}
	},
} as const satisfies ConfigScheme;

+ správný typ

Upravujeme si aplikaci

Ideas: Formát configu

Upravujeme si aplikaci

Ideas: Formát configu

JSON? YAML? (ENV proměnné jako fakt?)

Upravujeme si aplikaci

Ideas: GraphqL - Entity a query resolvery

Upravujeme si aplikaci

Ideas: graphql - Entity a query resolvery

Upravujeme si aplikaci

Ideas: auth

Upravujeme si aplikaci

Ideas: auth - více issuers

[[auth.issuers]]
issuer = "bonami.cz"
algorithm = "RS256"
public_key = """
-----BEGIN PUBLIC KEY-----
…
-----END PUBLIC KEY-----
"""
audience = "order-service.bonami.cz"












[[auth.issuers]]
issuer = "bonami.cz"
algorithm = "RS256"
public_key = """
-----BEGIN PUBLIC KEY-----
…
-----END PUBLIC KEY-----
"""
audience = "order-service.bonami.cz"


[[auth.issuers]]
issuer = "bonami.cz"
algorithm = "ES384"
public_key = """
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEVztN/qSCAX6xNy2t9byrYuEUSVN14r/O
EnNf9AIPcdOldkky3IIt3+vBMvNcKM17MHt4KRrg1sLClAn5G9e8dfVP8mBeuRka
c4UnZGFK8wPeD/kJgd3nD8tHQYjE1rky
-----END PUBLIC KEY-----
"""
audience = "order-service.bonami.cz"

Intermezzo: aud

Upravujeme si aplikaci

Ideas: auth - validace jwt

{
	headers: {
		typ: "JWT",
		alg: "RS256"
	},
	claims: {
		iss: "bonami.cz",
        // …
    }
}

(Vrstvené zabezpečení.)

Upravujeme si aplikaci

Ideas: auth - audience

{
	headers: {
		typ: "JWT",
		alg: "RS256"
	},
	claims: {
		iss: "bonami.cz",
		aud: "order-service.bonami.cz admin.bonami.cz"
        // …
    }
}

Upravujeme si aplikaci

Ideas: auth - Authorized party

{
	headers: {
		typ: "JWT",
		alg: "RS256"
	},
	claims: {
		iss: "bonami.cz",
		aud: "order-service.bonami.cz admin.bonami.cz",
      	azp: "admin.bonami.cz"
        // …
    }
}

Upravujeme si aplikaci

Ideas: auth - guard

@Resolver()
export class OrderByIdQueryResolver {

	@Query()
	@Acl("admin", "order.view");
	public orderById() {
      
	}
}

+ je globální!

Upravujeme si aplikaci

Ideas: typeorm

 

Upravujeme si aplikaci

Ideas: typeorm - vykřičníky a relace

@Entity()
export class UserAddress {

	@PrimaryGeneratedColumn()
	public id!: number;

	@Column({type: "varchar"})
	public domain!: Domain;

	@Column("int")
	public userId!: number;

	@ManyToOne(() => User)
	public user: Promise<User>;

	/* … */
}
@Entity()
export class UserAddress {

	@PrimaryGeneratedColumn()
	public id: number;

	@Column({type: "varchar"})
	public domain: Domain;

	@ManyToOne(() => User)
	public user: Promise<User>;

	/* … */
}

// "public readonly"?
// "private" + getter/setter?

Upravujeme si aplikaci

Ideas: typeorm - data loadery a relace

Upravujeme si aplikaci

Ideas: typeorm - synchronize

Upravujeme si aplikaci

Ideas: ostatní - app_dev

Upravili jsme si aplikaci

Upravujeme si aplikaci

Ideas: Validace configu

Asi vše, díky

Minimal

By vasekch

Minimal

  • 2