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