4 Commits

Author SHA1 Message Date
52a008a274 Added kubernetes post. 2025-07-24 23:01:29 +02:00
96a4e0e4ce Initial commit. 2025-07-24 23:01:29 +02:00
1174ba9837 Initial commit. 2025-07-24 23:01:29 +02:00
cdef31a9f1 Cleaned up. 2025-07-24 23:01:11 +02:00
64 changed files with 557 additions and 15335 deletions

View File

@ -1,90 +0,0 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp

View File

@ -1,13 +0,0 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@ -1,16 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
node: true
},
extends: [
'@nuxtjs/eslint-config-typescript',
'plugin:nuxt/recommended',
'prettier'
],
plugins: [
],
// add your custom rules here
rules: {}
}

View File

@ -1,3 +1,5 @@
name: "Release"
on:
push:
tags: ["*"]

91
.gitignore vendored
View File

@ -1,90 +1 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
# macOS
.DS_Store
# Vim swap files
*.swp
public/

View File

@ -1,4 +0,0 @@
{
"semi": false,
"singleQuote": true
}

View File

@ -1,3 +0,0 @@
blog.local {
reverse_proxy /* http://localhost:3000
}

View File

@ -1,12 +1,9 @@
FROM node:lts-alpine3.14 as builder
FROM ghcr.io/getzola/zola:v0.17.1 AS zola
WORKDIR /app
COPY package.json yarn.lock ./
COPY . /project
WORKDIR /project
RUN ["zola", "build"]
RUN yarn install
FROM nginx:1.29.0-alpine
COPY . /app
RUN yarn build
CMD ["yarn", "start", "-p", "80"]
COPY --from=zola /project/public /usr/share/nginx/html

View File

@ -1,69 +0,0 @@
# blog
## Build Setup
```bash
# install dependencies
$ yarn install
# serve with hot reload at localhost:3000
$ yarn dev
# build for production and launch server
$ yarn build
$ yarn start
# generate static project
$ yarn generate
```
For detailed explanation on how things work, check out the [documentation](https://nuxtjs.org).
## Special Directories
You can create the following extra directories, some of which have special behaviors. Only `pages` is required; you can delete them if you don't want to use their functionality.
### `assets`
The assets directory contains your uncompiled assets such as Stylus or Sass files, images, or fonts.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/assets).
### `components`
The components directory contains your Vue.js components. Components make up the different parts of your page and can be reused and imported into your pages, layouts and even other components.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/components).
### `layouts`
Layouts are a great help when you want to change the look and feel of your Nuxt app, whether you want to include a sidebar or have distinct layouts for mobile and desktop.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/layouts).
### `pages`
This directory contains your application views and routes. Nuxt will read all the `*.vue` files inside this directory and setup Vue Router automatically.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/get-started/routing).
### `plugins`
The plugins directory contains JavaScript plugins that you want to run before instantiating the root Vue.js Application. This is the place to add Vue plugins and to inject functions or constants. Every time you need to use `Vue.use()`, you should create a file in `plugins/` and add its path to plugins in `nuxt.config.js`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/plugins).
### `static`
This directory contains your static files. Each file inside this directory is mapped to `/`.
Example: `/static/robots.txt` is mapped as `/robots.txt`.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/static).
### `store`
This directory contains your Vuex store files. Creating a file in this directory automatically activates Vuex.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/docs/2.x/directory-structure/store).

View File

@ -1,47 +0,0 @@
<template>
<div class="flex justify-between">
<NuxtLink
v-if="prev"
:to="{ name: 'blog-slug', params: { slug: prev.slug } }"
class="font-bold text-primary hover:underline"
>
{{ prev.title }}
</NuxtLink>
<span v-else>&nbsp;</span>
<NuxtLink
v-if="next"
:to="{ name: 'blog-slug', params: { slug: next.slug } }"
class="font-bold hover:underline"
>
{{ next.title }}
</NuxtLink>
<span v-else>&nbsp;</span>
</div>
</template>
<script>
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
setup({ prev = null, next = null }) {
console.log(prev)
console.log(next)
return {
prev,
next,
}
},
})
// export default {
// props: {
// prev: {
// type: Object,
// default: () => null,
// },
// next: {
// type: Object,
// default: () => null,
// },
// },
// }
</script>

View File

@ -1,13 +0,0 @@
<template>
<footer class="p-5">
<div class="content has-text-centered">
<p>
<strong>Dev blog</strong> by
<a href="https://github.com/s3rius/">@s3rius</a>.
Built with and <a href="https://nuxtjs.org/">nuxt</a>.
</p>
</div>
</footer>
</template>
<script></script>

46
config.toml Normal file
View File

@ -0,0 +1,46 @@
# The URL the site will be built for
title = "s3rius"
base_url = "https://s3rius.blog"
theme = "hermit_zola"
compile_sass = true
minify_html = true
build_search_index = true
generate_sitemap = true
generate_robots_txt = true
[markdown]
highlight_code = true
highlight_theme = "nord"
render_emojis = true
smart_punctuation = true
[extra]
home_subtitle = "Notes on writing unmaintainable software"
footer_copyright = ' &#183; <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank" rel="noopener">CC BY-NC 4.0</a>'
hermit_menu = [
{ link = "/posts", name = "Posts" },
{ link = "/about", name = "About me" },
]
hermit_social = [
{ name = "github", link = "https://github.com/s3rius" },
{ name = "telegram", link = "https://t.me/s3rius_san" },
{ name = "email", link = "mailto:s3rius@le-memese.com" },
]
[extra.highlightjs]
enable = true
clipboard = true
theme = "https://unpkg.com/nord-highlightjs@0.1.0/dist/nord.css"
[extra.author]
name = "Pavel Kirilin"
email = "s3rius@le-memese.com"
[extra.google_analytics]
enable = false
id = "UA-4XXXXXXX-X"

13
content/about/index.md Normal file
View File

@ -0,0 +1,13 @@
+++
title = "About Me"
+++
# Who Am I?
My name is Pavel Kirilin, and my nickname is [@s3rius](https://github.com/s3rius). I am a backend developer with diverse experience. Currently, I hold the position of CTO at [Intree](https://www.intree.com/).
In my free time, I enjoy experimenting with various technologies and building new projects. You can find some of my work on [GitHub](https://github.com/s3rius) or on my self-hosted Gitea instance at [gitea.le-memese.com](https://gitea.le-memese.com/s3rius).
This blog serves as a platform for me to share my thoughts, ideas, and experiments. It is a way for me to document my journey and, hopefully, help others along the way.
Please feel free to reach out to me if you have any questions, suggestions, or just want to say hello. You can contact me via email at [s3rius@le-memese.com](mailto:s3rius@le-memese.com) or my [Telegram](https://t.me/s3rius_san).

4
content/posts/_index.md Normal file
View File

@ -0,0 +1,4 @@
+++
title= "Posts"
sort_by="date"
+++

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

View File

@ -3,7 +3,7 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 668 277" style="enable-background:new 0 0 668 277;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;stroke:#326DE6;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st0{fill:#FFFFFF;stroke:#C6CDDB;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st1{opacity:0.71;fill:#326CE6;}
.st2{opacity:0.45;fill:#FFFFFF;}
.st3{fill:#FFFFFF;stroke:#006DE9;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
@ -15,12 +15,12 @@
.st9{fill:url(#SVGID_2_);}
.st10{fill:#FFFFFF;stroke:#006DE9;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st11{fill:#FFFFFF;stroke:#006DE9;stroke-width:6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st12{fill:#FFFFFF;stroke:#326DE6;stroke-width:3.2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st13{fill:#326DE6;}
.st14{fill:none;stroke:#326DE6;stroke-width:2.4;stroke-miterlimit:10;}
.st12{fill:#FFFFFF;stroke:#C6CDDB;stroke-width:3.2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st13{fill:#C6CDDB;}
.st14{fill:none;stroke:#C6CDDB;stroke-width:2.4;stroke-miterlimit:10;}
.st15{fill:#A0CAE9;}
.st16{fill:#FFFFFF;stroke:#326DE6;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st17{fill:#FFFFFF;stroke:#326DE6;stroke-width:1.6;stroke-miterlimit:10;}
.st16{fill:#FFFFFF;stroke:#C6CDDB;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st17{fill:#FFFFFF;stroke:#C6CDDB;stroke-width:1.6;stroke-miterlimit:10;}
.st18{fill:none;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;}
.st19{fill:none;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;stroke-dasharray:2.3749,1.5832;}
.st20{fill:none;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;stroke-dasharray:2.4006,1.6004;}
@ -37,24 +37,26 @@
.st31{opacity:0.1;fill:url(#SVGID_4_);}
.st32{opacity:0.1;fill:url(#SVGID_5_);}
.st33{opacity:0.1;fill:url(#SVGID_6_);}
.st34{fill:none;stroke:#326DE6;stroke-width:1.2;stroke-miterlimit:10;}
.st34{fill:none;stroke:#C6CDDB;stroke-width:1.2;stroke-miterlimit:10;}
.st35{opacity:0.1;fill:url(#SVGID_7_);}
.st36{opacity:0.1;fill:url(#SVGID_8_);}
.st37{opacity:0.1;fill:url(#SVGID_9_);}
.st38{opacity:0.1;fill:url(#SVGID_10_);}
.st39{fill:none;stroke:#326DE6;stroke-width:2;stroke-miterlimit:10;}
.st39{fill:none;stroke:#C6CDDB;stroke-width:2;stroke-miterlimit:10;}
.st40{opacity:0.4;fill:none;stroke:#EEF406;stroke-width:2;stroke-miterlimit:10;}
.st41{fill:none;stroke:#EEF406;stroke-width:2.4596;stroke-miterlimit:10;}
.st42{fill:#011F38;}
.st42{fill:#C6CDDB;}
.st43{opacity:0.4;}
.st44{opacity:0.1;}
.st45{fill:#326DE6;stroke:#EEF406;stroke-width:2;stroke-miterlimit:10;}
.st45{fill:#C6CDDB;stroke:#EEF406;stroke-width:2;stroke-miterlimit:10;}
.st46{fill:none;stroke:#FFFFFF;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;}
.st47{fill:#06F7C9;stroke:#FFFFFF;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st48{fill:none;stroke:#011F38;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;}
.st49{fill:#326DE6;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;}
.st50{fill:#06F7C9;stroke:#011F38;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st51{fill:#8115FF;stroke:#011F38;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st48{fill:none;stroke:#C6CDDB;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;}
.st49{fill:#C6CDDB;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;}
/* Stlye for containers */
.st50{fill:#FFFFFF;stroke:#C6CDDB;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
/* Style for volumes */
.st51{fill:#FFFFFF;stroke:#C6CDDB;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st52{opacity:0.3;}
.st53{opacity:0.2;fill:#6D6E71;}
.st54{fill:#EEF406;}
@ -64,9 +66,9 @@
.st58{fill:none;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;stroke-dasharray:2.4938,1.6626;}
.st59{fill:none;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;stroke-dasharray:2.0084,1.3389;}
.st60{fill:none;stroke:#06F7C9;stroke-width:2;stroke-miterlimit:10;stroke-dasharray:2.724,1.816;}
.st61{fill:#011F38;stroke:#414042;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st62{fill:none;stroke:#011F38;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st63{fill:none;stroke:#011F38;stroke-width:0.2813;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st61{fill:#C6CDDB;stroke:#FFFFFF;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st62{fill:none;stroke:#C6CDDB;stroke-width:0.3;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
.st63{fill:none;stroke:#C6CDDB;stroke-width:0.2813;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
</style>
<symbol id="node_high_level" viewBox="-81 -93 162 186.1">
<polygon class="st0" points="-80,-46 -80,46 0,92 80,46 80,-46 0,-92 "/>
@ -106,23 +108,23 @@
</g>
<g id="pods">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="122.1878" y1="127.5093" x2="204.1367" y2="127.5093">
<stop offset="0" style="stop-color:#326DE6"/>
<stop offset="1" style="stop-color:#10FFC6"/>
<stop offset="0" style="stop-color:#C6CDDB"/>
<stop offset="1" style="stop-color:#494F5C"/>
</linearGradient>
<circle style="opacity:0.1;fill:url(#SVGID_1_);" cx="163.2" cy="127.5" r="41"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="238.2856" y1="127.5093" x2="347.5509" y2="127.5093">
<stop offset="0" style="stop-color:#326DE6"/>
<stop offset="1" style="stop-color:#10FFC6"/>
<stop offset="0" style="stop-color:#C6CDDB"/>
<stop offset="1" style="stop-color:#494F5C"/>
</linearGradient>
<circle style="opacity:0.1;fill:url(#SVGID_2_);" cx="292.9" cy="127.5" r="54.6"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="390.5175" y1="77.3348" x2="517.7111" y2="179.9297">
<stop offset="0" style="stop-color:#326DE6"/>
<stop offset="1" style="stop-color:#10FFC6"/>
<stop offset="0" style="stop-color:#C6CDDB"/>
<stop offset="1" style="stop-color:#494F5C"/>
</linearGradient>
<circle class="st30" cx="452.7" cy="127.5" r="71"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="16.3336" y1="127.5093" x2="77.7953" y2="127.5093">
<stop offset="0" style="stop-color:#326DE6"/>
<stop offset="1" style="stop-color:#10FFC6"/>
<stop offset="0" style="stop-color:#C6CDDB"/>
<stop offset="1" style="stop-color:#494F5C"/>
</linearGradient>
<circle class="st31" cx="47.1" cy="127.5" r="30.7"/>
<circle class="st39" cx="163.2" cy="127.5" r="41"/>

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1,448 @@
+++
title = "Intro to kubernetes"
weight = 0
date = "2025-07-22"
+++
![Intro](./imgs/intro.png)
## Problem in learning kubernetes
Lots of people are considering learning Kubernetes but are struggling to take the first
steps because the official docs are complete dogshit for beginners.
Don't get me wrong, the docs are great, but they serve a different purpose and are mostly used by experienced
developers as a reference for things they are about to build or for explanations of
some concepts that Kubernetes leverages.
Also official docs are mostly focus on how to deploy a big-ass production-ready system.
Which is not something that you will target as a beginner. So instead I'm gonna show you how to deploy a single app.
Another problem is that people who don't use Kubernetes or are not familiar with all the problems it solves say that
for medium and small projects, it is overkill.
Which is actually complete bullshit as well.
Because of the fact that Kubernetes automates a lot of things, it makes it easier to use than to not use it.
And it is not that hard to start.
## What is Kubernetes? And how is better than Docker?
People that are aquite good with docker might ask, like `why should I care? Because I can run docker in cluster mode with docker-swarm!`
Weeeeell, here's my response to you. The main problem with docker swarm is that it
might run applications on multiple nodes, but nothing more than that.
What about ingress management, automated certificate issuing, DNS updates, distributed volume provisioning? Docker swarm doesn't have it,
and won't have unless they change their approach for declaring workloads. Kubernetes on the other hand is easily extendable, is adopted by big-tech companies and
supported by a lot of cloud providers, which makes it a de-facto standard for container orchestration. Sad to admit,
but docker swarm is silently dying from the day it was born.
## What's inside of Kubernetes?
Since I wanted to give high-level overview of Kubernetes, I won't go into details about each component,
and networking, but I will give you a brief overview of the main components that make up Kubernetes.
* Container
* Pod
* Deployment
* Service
* Ingress
* Namespace
* Secret
* ConfigMap
Now let's take a look at each of these components in more detail.
#### Container
Containers are not really something unique to Kubernetes. You might be familiar with containers from other systems
like Docker. In context of kubernetes, containers are exactly same containers as they are in docker or containerd. Nothing brand new in here.
#### Pod
![Pod](./imgs/pods.svg)
Pods are the smallest deployable units in Kubernetes.
It's a logically connected group of containers.
You can think of them as a virtual machines running some applications. And there are several reasons for that.
First of all, pods share same network and you can go from one container to another using localhost.
Also, you can attach volumes to pod and then share it to multiple containers.
Which seems a lot like a virtual machine, right?
#### Deployment
Pods are cool, but there are some limitations to them. First of all, the pod objects are immutable, which means that you can't change them after they are created.
It might be a problem if you want to update your application, because you will have to delete the pod and create
a new one. Also, scaling pods by yourself sounds weird. Of couese you can manually
create [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/),
but on average it's not something you would want to do.
So that's why we have [Deployments](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/).
It allows you to describe a template for a pod and then Kubernetes will take care of creating, updating, and scaling pods for you. It will implicitly create a ReplicaSet for you, and will be maintaining it for you as well.
Deployment is a higher-level abstraction that allows you to manage pods more easily, scale them up and down, and update them without downtime.
There are some policies that you can set for deployments, like how many pods to keep running at the same
time, update strategies, and so on.
Generally speaking, use deployments to manage your pods, and don't create pods directly unless you have
a good reason to do so.
#### Service
![service](./imgs/services.svg)
[Service](https://kubernetes.io/docs/concepts/services-networking/service/) is a resource that allows you to set a single IP address and DNS name for a set of pods.
By default you can reach to service by it's name from the same namespace. But if you're in a different namespace, you can reach it by using domain name such as this:
`<service-name>.<namespace-name>.svc.cluster.local`.
You can think of services as a load balancer that distributes traffic to a set of pods.
#### Ingress
![Ingress](./imgs/ingress.svg)
Ingress is a resource that allows you to expose your services to the outside world with a single IP address and DNS name.
You can think of it as a reverse proxy that routes traffic to different services based on the request path or host.
In order to use ingress, you need to have an ingress controller running in your cluster.
K3S by default has [Traefik](https://traefik.io/) ingress controller installed, which is a
great choice for beginners. But for more control and features, I personally prefer to use [NGINX Ingress Controller](https://kubernetes.github.io/ingress-nginx/).
Ingress works this way: you create an ingress resource that defines the rules for
routing traffic to different services, based on the request path or host.
Then the ingress controller will take care of routing the traffic to the appropriate service.
#### Namespace
I mentioned namespaces in the beginning, but I didn't explain what they are. Actually it's a very simple concept.
It's a way to group resources in Kubernetes and manage access. Most of the time you can think of
namespaces as just a way to organise resources in your cluster.
I prefer using different namespaces for different apps. For example my mailserver is deployed in namespace `mailserver`. But my blog is deployed in namespace `s3rius` for personal projects.
Telegram bots, CI/CD pipelines, and other applications that I deploy in my cluster are also deployed in their own namespaces. Which makes it easier to manage resources and access control.
About access control, you can use [Role-Based Access Control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) to manage access to resources in a namespace. It will only be useful if you have multiple users or teams working in the same cluster.
You can create roles and role bindings to grant permissions to users or groups in a namespace. But I won't cover RBAC in this blog post. Just because it's too complex and we're only discussing basics.
#### ConfigMap and Secret
These two resources are used to store configuration data and sensitive information, respectively.
For example, you can use ConfigMap to store environment variables, configuration files,
or any other non-sensitive data that your application needs to run. And then mount them as
files to your pod, or populate environment variables from it, etc.
In secrets you can store sensitive information like passwords, API keys, or any other data that
you don't want to expose in plain text.
But please keep in mind that by default k8s encodes all secrets using base64, which is completely insecure.
To make secrets actually secret, you better use [Vault](https://developer.hashicorp.com/vault/docs/platform/k8s)
or [External secrets operator](https://external-secrets.io/latest/) or something similar. But for now let's just use default base64 encoded secrets.
## How to deploy kubernetes
For local development you have several options:
* [K3D](https://k3d.io/) - k3s in Docker.
* [Minikube](https://minikube.sigs.k8s.io/docs/) - k8s in docker.
* [Kind](https://kind.sigs.k8s.io/) - Kubernetes in Docker, but with a focus on testing Kubernetes itself.
For production:
* [Kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/) - raw and barebones way to deploy Kubernetes.
* [Kubespray](https://kubespray.io/) - Ansible-based tool to deploy Kubernetes.
* [K3S](https://k3s.io/) - Lightweight Kubernetes distribution that is easy to deploy and maintain.
* [Rancher](https://rancher.com/) - A complete Kubernetes management platform
* [DeckHouse](https://deckhouse.io/products/kubernetes-platform/gs/) - A complete Kubernetes management platform that is easy to deploy and maintain.
My personal recommendation is to use K3D for local development and K3S for production. For deploying K3S
you can use [k3sup](https://github.com/alexellis/k3sup), which is a simple script that allows you to deploy K3S on any Linux server with a single command.
#### Learning environment
Let's use Docker-packed Kubernetes to start off. We're going to use [k3d](https://k3d.io) to spin up a cluster.
It's an amazing tool that allows you to create a Kubernetes cluster and everything you need with a single command.
Additionally, it spins up [k3s](https://k3s.io/) instead of Google's Kubernetes,
which is a lightweight version of Kubernetes that is perfect for beginners.
It has all the features you need to get started and is much easier to set up than other distributions.
I'm using this k3d configuration to create a cluster:
```yaml
# yaml-language-server $schema=https://raw.githubusercontent.com/k3d-io/k3d/main/pkg/config/v1alpha5/schema.json
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
name: default
servers: 1
volumes:
- volume: /tmp/k3d-udev:/run/udev # For openebs
ports:
- port: 80:80 # HTTP ports mapping for cluster
nodeFilters:
- loadbalancer
- port: 443:443 # HTTPS ports mapping for cluster.
nodeFilters:
- loadbalancer
registries:
create:
name: registry.localhost # Registry for containers
host: "0.0.0.0" # Host for registry
hostPort: "5000" # Local port for registry
```
With this configuration, you can create a cluster with a single command:
```bash
k3d cluster create --config "k3d.yaml"
```
#### Connecting to the cluster
After you install kubectl you should be able to locate file `~/.kube/config`. This file contains all required infomration to connect to clusters. Tools like minikube, k3d or kind will automatically update this file when you create a cluster.
Let's take a look at the contents of this file:
```yaml
apiVersion: v1
kind: Config
preferences: {}
# Here's list of clusters that you can connect to.
# In this case we have only one cluster, which is k3d-default.
clusters:
- cluster:
certificate-authority-data: DATA+OMITTED
server: https://0.0.0.0:44755
name: k3d-default
# Users is a list of users that you can use to connect to clusters.
# Most of the time you will have each user for each cluster.
# But in some cases you might have multiple users for the same cluster.
users:
- name: admin@k3d-default
user:
client-certificate-data: DATA+OMITTED
client-key-data: DATA+OMITTED
# Context is a combination of cluster and user that you can use to connect to a cluster.
# You can create any combination of cluster and user,
# but most of the time you will have only one context for each cluster.
contexts:
- context:
cluster: k3d-default
user: admin@k3d-default
name: k3d-default
# Currently selected context. It will be used as a default one
# for all kubectl commands.
current-context: k3d-default
```
I will update my context to use k3d-default as my default context with this command:
```bash
kubectl config use-context k3d-default
```
Also, we can check if we are connected to the cluster by running:
```bash
kubectl cluster-info
```
Once we are connected to the cluster, we can start deploying applications and managing resources. But before that
I want to mention [Lens](https://k8slens.dev/) and [K9S](https://k9scli.io/).
These two things will help you a lot to get into kubernetes. Becuase kubectl is great, no shit,
I'll be using it for showing everything what is going on.
But I highly recommend you to install `Lens` or `K9S` to have better overview of your cluster and resources.
So you will be able to see what is going on in your cluster, what pods are running, what services are available,
and so on. I personally use `k9s` and I think it's much better, because it has everything you need and it's fast as hell.
Lens is a bit more heavy, but it's still a great tool for getting started with Kubernetes.
## Deploying your first application
I'm going to use python to write a small server to deploy in the cluster. Here's the application:
```python,name=server.py
import os
from aiohttp import web
routes = web.RouteTableDef()
@routes.get("/")
async def index(req: web.Request) -> web.Response:
# We increment the request count and return the state
req.app["state"]["requests"] += 1
return web.json_response(req.app["state"])
app = web.Application()
app["state"] = {
"requests": 0,
"hostname": os.environ.get("HOSTNAME", "unknown"),
}
app.router.add_routes(routes)
if __name__ == "__main__":
web.run_app(app, port=8000, host="0.0.0.0")
```
As you can see, this is a simple aiohttp application that returns the number of requests and the hostname of the pod.
This is a good example of an application that can be deployed in Kubernetes, because it is stateless and can be scaled easily.
Let's create a Dockerfile for this application:
```dockerfile,name=Dockerfile
FROM python:3.11-alpine3.19
RUN pip install "aiohttp>=3.12,<4.0"
WORKDIR /app
COPY server.py .
CMD ["python", "server.py"]
```
Here's a simple Dockerfile that installs aiohttp and runs the application. Let's build an image and upload it to our running cluster.
```bash
docker build -t "registry.localhost:5000/small-app:latest" .
docker push "registry.localhost:5000/small-app:latest"
```
Now let's deploy this application so it will become available from our host machine.
```yaml
apiVersion: apps/v1
kind: Deployment
# Here's the name of the deployment, which is small-app.
metadata:
creationTimestamp: null
name: small-app
spec:
replicas: 1
# Here we define selector that will help this deployment
# to find pods that it manages. Usually it is the
# same as labels in pod template.
selector:
matchLabels:
app: small-app
strategy: {}
# Here we define the pod template that will be used to create
# pods for this deployment.
template:
metadata:
# Each pod create by this deployment will have these labels.
labels:
app: small-app
spec:
containers:
- image: registry.localhost:5000/small-app:latest
name: small-app
ports:
- containerPort: 8000
protocol: TCP
resources: {}
---
apiVersion: v1
kind: Service
metadata:
name: small-app
namespace: small-app
spec:
# This type of service will make it accessible only
# from inside the cluster.
# If you want to expose it to the outside world,
# you can use LoadBalancer or NodePort.
# But we don't need it for now.
type: ClusterIP
# Here we define ports available for this service.
# Port is the port that will be exposed by the service,
# targetPort is the port that the service
# will forward traffic to in target pods.
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8000
# Here we define where to route traffic for this service.
# In this case we route traffic to pods that have
# label app=small-app.
selector:
app: small-app
---
# Write ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: small-app
namespace: small-app
spec:
rules:
# Here's the host configuration for the Ingress
# It will route traffic for this host to the small-app service
- host: small-app.localhost
http:
paths:
# Here we define the path and the backend service
# The path is set to '/' which means all
# traffic to this host will be routed
# to the defined backend service
- path: /
pathType: Prefix
backend:
service:
name: small-app
port:
number: 80
resources: {}
```
Now let's save this file as `small-app.yaml` and apply it to our cluster:
```bash
kubectl apply -f small-app.yaml
```
Once the command is executed, you should see that the deployment and service are created successfully.
```bash
kubectl get pods -n small-app
NAME READY STATUS RESTARTS AGE
small-app-54f455696b-rp8qw 1/1 Running 0 13m
```
If you were using my k3d configuration, you should be able to access the application at [small-app.localhost](http://small-app.localhost/).
Let's check if it works. I'm gonna use curl and jq for that:
```bash
curl -s http://small-app.localhost | jq
{
"requests": 1,
"hostname": "small-app-54f455696b-rp8qw"
}
```
Now you can scale up the application by changing the number of replicas in the deployment.
You can do it by editing the deployment and updating the `replicas` field to 3, for example and then running
```bash
kubectl apply -f "small-app.yaml"
```
Or alternatively you can use `kubectl scale` command:
```bash
kubectl scale deployment -n small-app small-app --replicas 3
```
Let's verify that the application is scaled up:
```bash
kubectl get pods -n small-app
NAME READY STATUS RESTARTS AGE
small-app-54f455696b-6tkxx 1/1 Running 0 21s
small-app-54f455696b-9sd7r 1/1 Running 0 21s
small-app-54f455696b-rp8qw 1/1 Running 0 25m
```
Now let's fire some requests to the application and see how it works.
![Requests](./imgs/reqs.gif)
Works as expected. The application is scaled up and we can see that the requests are distributed between the pods.
I guess that is more than enough to get you started with Kubernetes. I might create some more posts on how to tune up your cluster, how to use volumes, how to use secrets and configmaps, and so on.
But now I'm tired and just want to publish it already. So please go easy on me. Stay tuned.

View File

@ -1,161 +0,0 @@
---
title: Разделение докера на среды.
description: Как работать с несколькими docker-compose.
position: 2
category: DevOps
---
# Разделение докера на среды
Должен ли ты разделять среды докера в несколько `docker-compose` файлов?
Определенно! В некоторых случаях невозможно разобраться что разработчики хотели сделать или почему ничего не работает. Настройка раздельных сред может стать настояшей мешаниной. В этой статье я покажу как настроить `docker-compose` и избежать миллиона проблем.
Как мы вообще можем разделить среды для локальной разработки и продовые?
Отет прост: Требуется декомпозировать проект и создать отдельные файлы под каждую из сред или даже сервисов.
Это нормально, если у тебя будет больше 2-х `docker-compose` файлов.
Например:
```
deploy
├── docker-compose.autotests.yml
├── docker-compose.db.yml
├── docker-compose.dev.yml
├── docker-compose.yml
├── dockerfiles
│   ├── api.Dockerfile
│   └── front.Dockerfile
└── scripts
   ├── start-autotests.sh
   ├── start-backend.sh
   └── start-migrations.sh
```
### Как это работает?
Докер умеет работать с множеством `docker-compose` файлов одновременно. И мы можем использовать это для разделения сред.
Запуск таких конфигураций выглядит следующим образом:
```bash
$ docker-compose \
-f "deploy/docker-compose.yml" \
-f "deploy/docker-compose.db.yml" \
-f "deploy/docker-compose.dev.yml" \
up
```
В каждом из этих файлов определен какой-то кусок конфигурации, который не пересекается. Например в `docker-compose.yml` определено приложение и некоторые необходимые сервисы, а в остальных файлах добавляются сервисы или меняются значения предыдущих.
Наверное, тут проще показать на примере.
Допустим у нас есть проект, у которого поднимается бекенд с параметрами, которые отличаются на проде и локально.
Для простоты примера создадим простецкий проект со следующей структурой:
```
proj
├── deploy
│   ├── docker-compose.yml
│   └── dockerfiles
│       └── script.Dockerfile
└── script.py
```
Для начала напишем скрипт, который будет в центре всего.
```python{}[script.pt]
from sys import argv # это аргуметы переданные в скрипт
def main():
print("; ".join(argv[1:])) # выводит на экран все аргументы программы
if __name__ == "__main__":
main()
```
После того, как скрипт готов и отлажен давайте завернем его в докер,
создав `Dockerfile`.
```dockerfile{}[deploy/dockerfiles/script.Dockerfile]
from python:3.8 # asd
WORKDIR /app
COPY script.py /app/script.py
CMD python /app/script.py
```
Как вы видите, Dockerfile очень прост. Теперь добавим главный `docker-compose.yml`.
```yaml{}[deploy/docker-compose.yml]
---
version: '3.7'
services:
script:
build: # Собираем приложение используя наш dockerfile.
dockerfile: ./deploy/dockerfiles/script.Dockerfile
context: .
# Запускаем его с командой для продового запуска.
command: python script.py this is prod
```
Теперь запустим всё это чудо.
```bash
$ docker-compose \
-f "./deploy/docker-compose.yml" \
--project-directory "." \
run --rm script
```
Вот что будет выведено на экран.
```log
Creating d-test_script_run ... done
this; is; prod
```
Как мы видим, на экран вывелось сообщение, которое мы указали в нашем `docker-compose.yml`
А теперь для локальной разработки мы не будем ничего менять в нашем файле композиции, а создадим новый рядом.
```yaml{}[deploy/docker-compose.dev.yml]
---
version: '3.7'
services:
script:
command: python script.py this is dev
```
Теперь добавим ещё один файл в нашу команду запуска.
```bash
$ docker-compose \
-f "deploy/docker-compose.yml" \
-f "deploy/docker-compose.dev.yml" \
--project-directory "." \
run --rm script
```
Вот что будет выведено на экран:
```log
Creating d-test\_script\_run ... done
this; is; dev
```
Как можно заметить, конфигурация запуска перезаписалась в порядке вызова.
То есть, каждый последующий файл композиции может добавлять сервисы и **частично** изменять конфигурацию предыдущих.
Итоговая структура проекта:
```
proj
├── deploy
│   ├── docker-compose.dev.yml
│   ├── docker-compose.yml
│   └── dockerfiles
│       └── script.Dockerfile
└── script.py
```
### Где это применимо?
Ну, в любом проекте, сложнее того, который мы рассмотрели. Потому что в реальной жизни не всё так радужно и локальная версия приложения может отличаться не только параметрами запуска, но и целыми сервисами, которые требуются для локальной копии приложения.

View File

@ -1,139 +0,0 @@
---
title: Makefiles для чайников.
description: Автоматизируем по старинке.
position: 3
category: 'DevOps'
---
# Makefiles для чайников
Что такое мейкфайлы? Вы наверняка их встречали,
если собирали проекты из ихсодных файлов.
Если описывать в двух словах, то это просто описание команд для упрощения работы
с проектом. Изначально делались для того, чтобы удобно компилировать всякие проекты на любых языках.
## Как они работают?
Достаточно просто.
Вот пример `Makefile`:
```makefile{}[Makefile]
# Сгенерируем простой файл
test.gen:
@echo 'echo "Hi!"' > "test.gen"
# Команда для запуска файла test.gen
.PHONY: run
run: test.gen
sh "test.gen"
```
До двоеточий обазначены команды (tagets). Например тут это: `test.gen` и `run`.
Данные таргеты можно запускать через `make ${target}`.
Например, если ввести `make run` в папке с `Makefile`, то мы получим следующее:
```console
$ sh "test.gen"
Hi!
```
Как видно из выхлопа данной команды, у нас успешно запустился файл `test.gen`, хотя мы не запускали команду `make test.gen`. Что произошло? Давайте разбираться.
## Зависимости таргетов
На строчке объявления таргета `run` видно, что объявлен `test.gen`. Это зависимость данного таргета и она будет вызвана до того, как выполнится скрипт описываемого таргета. Таких зависимостей может быть много, перечисляются они через пробел.
Например:
```makefile{}[Makefile]
.PHONY: target1
target1:
echo "1"
.PHONY: target2
target2: target1
echo "2"
.PHONY: target3
target3: target1 target2
echo "memes"
```
При вызове `make target3` будет выведено:
```console
$ make target3
echo "1"
1
echo "2"
2
echo "memes"
memes
```
Как можно видеть, он построил граф зависимостей и не выполнил `target1` дважды.
## Сокрытие вывода команд
В предыдущем примере можно заметить, что он написал все команды в терминал. Для того, чтобы этого избежать следует добавить "@" в начало команды и она не будет напечатана.
```makefile{}[Makefile]
.PHONY: target1
target1:
@echo "1"
.PHONY: target2
target2: target1
@echo "2"
.PHONY: target3
target3: target1 target2
@echo "memes"
```
Теперь при вызое `make target3` будет показано следующее:
```cosole
$ make target3
1
2
memes
```
## Валидация сгенерированных файлов
Зачастую `Makefile` используют для компиляции С и зачастую требуется
собрать какую-либо часть проекта в файл и пропустить сборку этого файла, если он уже собран.
Раскрою секрет, в Makefile это базовый функционал.
Давайте немного поменяем первый Makefile и запустим дважды.
```makefile{}[Makefile]
# Сгенерируем простой файл
test.gen:
echo 'echo "Hi!"' > "test.gen"
# Команда для запуска файла test.gen
.PHONY: run
run: test.gen
sh "test.gen"
```
Теперь вызываемая команда таргета `test.gen` выводится на экран.
Позапускаем.
```console
$ make run
echo 'echo "Hi!"' > "test.gen" # Наша командабыла вызвана.
sh "test.gen"
Hi!
$ make run
sh "test.gen" # Наша команда не вызвана.
Hi!
```
Дело в том, что названия таргетов - названия файлов, которые должны сгенерировать эти самые таргеты.
То есть, в данном случае таргет `test.gen` должен сгенерировать файл `test.gen` по окончании выполнения. Если этот файл уже присутствует, то команда выполнена не будет. Именно поэтому у нас она не запустилась второй раз, так как в первый запуск был создан треубемый файл и его никто не удалял между запусками.
А что если я вот не хочу чтобы команда создавала и проверяла файлы?
Для этого пишут `.PHONY: ${target}`. Например у нас так объявлен таргет `run` и, даже если файл с названием `run` будет присутствовать в директории цель не будет выполняться.
<br>
До новых встреч.

View File

@ -1,586 +0,0 @@
---
title: Как стартовать проект
description: Лучший вариант начала проекта на питоне.
category: Python
position: 1
---
# Как стартовать проект
В целом, вариантов несколько. Например, можно просто хранить несколько скриптов на github и быть довольным, можно дописать к ним `setup.py` и выложиться на pypi. Но лучший способ - структурированный проект с `poetry`.
Почему?
Особенностей у poetry много. Сейчас расскажу про то, как использовать и что делать.
Для начала создадим проект.
```console
$ poetry new new_proj
Created package new_proj in new_proj
$ cd new_proj
```
Данная команда сгенерирует следующую структуру:
```
new_proj
├── new_proj
│   └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
├── __init__.py
└── test_new_proj.py
```
Это наш новый проект с небольшими заготовками. В целом, тут нет ничего необычного. Весь код библиотеки/проекта будет в папке с названием проекта. `README.rst` - дефолтный README. `pyproject.toml` - мета-данные о проекте, такие как: зависимости, описание, дополнительные установочные опции и многое другое. Ты можешь прочитать побольше о `pyproject.toml` [тут](https://python-poetry.org/docs/pyproject/).
## Как работать с poetry
Вся информация есть в [официальной документации](https://python-poetry.org/docs) poetry. Тут я расскажу про основные моменты.
### Управление зависимостями
Чтобы добавить зависимость проекта достаточно выполнить
```console
$ poetry add ${dependency}
```
Данная команда найдет последнюю нужную версию и запишет её в `pyproject.toml` и в `poetry.lock`.
Для того, чтобы установить зависимость для разработки (линтеры например), достаточно добавить флаг `--dev`.
```console
$ poetry add ${dependency} --dev
```
Чтобы удалить зависимость достаточно просто `add` заменить на `remove`.
Примеры работы:
```console
$ poetry add loguru
Using version ^0.5.3 for loguru
Updating dependencies
Resolving dependencies... (0.1s)
Writing lock file
Package operations: 1 install, 0 updates, 0 removals
 • Installing loguru (0.5.3)
$ poetry remove loguru
Updating dependencies
Resolving dependencies... (0.1s)
Writing lock file
Package operations: 0 installs, 0 updates, 1 removal
 • Removing loguru (0.5.3)
```
### Выполнение команд
При использовании virtualenv для входа в оболочку всегда надо было вводить
```bash
source venv/bin/activate
```
Однако с poetry это излишне. Он сам создает и менеджит виртуальные среды.
Чтобы выполнить одиночную команду можно вызвать `run`.
Например:
```console
$ poetry install black --dev
Using version ^20.8b1 for black
• Installing appdirs (1.4.4)
 • Installing click (7.1.2)
 • Installing mypy-extensions (0.4.3)
 • Installing pathspec (0.8.1)
 • Installing regex (2020.11.13)
 • Installing toml (0.10.2)
 • Installing typed-ast (1.4.2)
 • Installing typing-extensions (3.7.4.3)
 • Installing black (20.8b1)
$ poetry run black .
reformatted new_proj/new_proj/__init__.py
reformatted new_proj/tests/test_new_proj.py
All done! ✨ 🍰 ✨
2 files reformatted, 1 file left unchanged.
```
Для выполнения нескольких комманд подряд, можно войти в shell. Это аналог `source venv/bin/activate`.
```console
$ poetry shell
Spawning shell within /home/s3rius/.cache/pypoetry/virtualenvs/new-proj-eutP4v0O-py3.9
$ black .
reformatted new_proj/new_proj/__init__.py
reformatted new_proj/tests/test_new_proj.py
All done! ✨ 🍰 ✨
2 files reformatted, 1 file left unchanged.
```
### Версионирование
Менять версии пакета вручную больше не нужно.
У poetry есть кое что для тебя.
```console
$ poetry version patch
Bumping version from 0.1.0 to 0.1.1
$ poetry version preminor
Bumping version from 0.1.1 to 0.2.0-alpha.0
$ poetry version minor
Bumping version from 0.2.0-alpha.0 to 0.2.0
$ poetry version premajor
Bumping version from 0.2.0 to 1.0.0-alpha.0
$ poetry version major
Bumping version from 1.0.0-alpha.0 to 1.0.0
```
## pyproject.toml
Как было сказано ранее, данный файл содержит в себе мета-информацию пакета.
Пример `pyproject.toml`
```toml{}[pyproject.toml]
[tool.poetry]
name = "new_proj"
version = "0.1.0"
description = "Test library for example"
readme = "README.rst"
homepage = "https://test_url.com/"
repository = "https://github.meme/"
authors = ["Pavel Kirilin <win10@list.ru>"]
[tool.poetry.dependencies]
python = "^3.8"
[tool.poetry.dev-dependencies]
pytest = "^6.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```
Для того чтобы устновить все зависимости требуется просто ввести
```console
$ poetry install
```
Данная команда создаст виртуальную среду сама и установит **все** зависимости. Включая dev-зависимости. Чтобы установить зависимости только для приложения можно добавить `--no-dev` ключ.
## Как паковать и публиковать на pypi
Всё очень просто.
Давайте добавим какую-нибудь функцию в проект.
```python{}[new_proj/main.py]
def ab_problem(a: int, b: int) -> int:
return a + b
```
```python{}[new_proj/__init.py]
from new_proj.main import ab_problem
__all__ = [
'ab_problem'
]
```
Теперь соберем проект.
```console
$ poetry build
Building new_proj (0.1.0)
- Building sdist
- Built new_proj-0.1.0.tar.gz
- Building wheel
- Built new_proj-0.1.0-py3-none-any.whl
```
Та-да. Это готовый к публикации на pypi пакет. Лежит он в папке dist.
Давайте проверим, что всё работает корректно.
```console
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
Installing collected packages: new-proj
Successfully installed new-proj-0.1.0
$ python
Python 3.9.1 (default, Feb 1 2021, 04:02:33)
[GCC 10.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
$ from new_proj import ab_problem
$ ab_problem(1,33)
34
```
Как можно видеть всё работает корректно и теперь мы можем использовать наш пакет.
Для публикации следует использовать:
```console
$ poetry publish -u "user" -p "password"
```
Подробнее можно почитать [тут](https://python-poetry.org/docs/cli/#publish).
# Конфигурация проекта
Конечно, такого рода конфигурация проекта всё равно никуда не годиться.
Давайте настроим автоматический линтинг.
```console
$ poetry add \
flake8 \
black \
isort \
mypy \
pre-commit \
yesqa \
autoflake \
wemake-python-styleguide --dev
```
Теперь добавим конфигурационных файлов в корень проекта.
Это мои конфигурации, которые я настроил под себя, можешь менять их как хочешь.
Для конфигурации сортировки импортов и проверки типов добавим следющее
в наш основной файл проекта.
Обычно я добавляю эти секции сразу после секции `[tool.poetry.dev-dependencies]`.
```toml{}[pyproject.toml]
...
[tool.mypy]
strict = true
ignore_missing_imports = true
allow_subclassing_any = true
allow_untyped_calls = true
pretty = true
show_error_codes = true
implicit_reexport = true
allow_untyped_decorators = true
warn_return_any = false
[tool.isort]
profile = "black"
multi_line_output = 3
...
```
`.flake8` - конфигурация линтинга. Тут довольно много. Это игнорирование ненужных кодов ошибок, которые не особо-то и ошибки.
```ini{}[.flake8]
[flake8]
max-complexity = 6
inline-quotes = double
max-line-length = 88
extend-ignore = E203
docstring_style=sphinx
ignore =
; Found `f` string
WPS305,
; Missing docstring in public module
D100,
; Missing docstring in magic method
D105,
; Missing docstring in __init__
D107,
; Found class without a base class
WPS306,
; Missing docstring in public nested class
D106,
; First line should be in imperative mood
D401,
; Found `__init__.py` module with logic
WPS412,
; Found implicit string concatenation
WPS326,
; Found string constant over-use
WPS226,
; Found upper-case constant in a class
WPS115,
; Found nested function
WPS430,
; Found using `@staticmethod`
WPS602,
; Found method without arguments
WPS605,
; Found overused expression
WPS204,
; Found too many module members
WPS202,
; Found too high module cognitive complexity
WPS232,
; line break before binary operator
W503,
; Found module with too many imports
WPS201,
; Found vague import that may cause confusion: X
WPS347,
; Inline strong start-string without end-string.
RST210,
; subprocess call with shell=True seems safe, but may be changed in the future.
S602,
; Starting a process with a partial executable path.
S607,
; Consider possible security implications associated with subprocess module.
S404,
; Found nested class
WPS431,
; Found wrong module name
WPS100,
; Found too many methods
WPS214,
; Found too long ``try`` body
WPS229,
; Found function with too much cognitive complexity
WPS231,
; all init files
__init__.py:
; ignore not used imports
F401,
; ignore import with wildcard
F403,
; Found wrong metadata variable
WPS410,
per-file-ignores =
; all tests
test_*.py,tests.py,tests_*.py,*/tests/*:
; Use of assert detected
S101,
exclude =
./.git,
./venv,
./cached_venv,
./var,
```
`.pre-commit-config.yaml` - конфигураци хуков для запуска всех линтеров перед коммитом.
```yaml{}[.pre-commit-config.yaml]
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.4.0
hooks:
- id: check-ast
- id: trailing-whitespace
- id: check-toml
- id: end-of-file-fixer
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.1.0
hooks:
- id: add-trailing-comma
- repo: local
hooks:
- id: black
name: Format with Black
entry: black
language: system
types: [python]
- id: autoflake
name: autoflake
entry: autoflake
language: system
types: [ python ]
args: [ --in-place, --remove-all-unused-imports, --remove-duplicate-keys ]
- id: isort
name: isort
entry: isort
language: system
types: [ python ]
- id: flake8
name: Check with Flake8
entry: flake8
language: system
pass_filenames: false
types: [ python ]
args: [--count, .]
- id: mypy
name: Validate types with MyPy
entry: mypy
language: system
types: [ python ]
- id: yesqa
name: Remove usless noqa
entry: yesqa
language: system
types: [ python ]
```
И не забываем про `.gitignore`. Его можно найти [тут](https://github.com/github/gitignore/blob/master/Python.gitignore).
ОН НЕОБХОДИМ ЧТОБЫ pre-commit РАБОТЛ МАКСИМАЛЬНО КОРРЕКТНО.
Теперь установим хуки в репозиторий.
```console
$ git init
Initialized empty Git repository in .git/
$ poetry shell
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
$ git commit
... # Упадет с кучей ошибок
```
Теперь поправим все возникшие проблемы.
Во первых исправим тесты.
В `tests/test_new_proj.py` напишем следующее:
```python{}[tests/test_new_proj.py]
from new_proj import ab_problem
def test_ab() -> None:
"""AB problecm success case."""
assert ab_problem(1, 2) == 3
```
Добавим описания в `__init__` файлы.
```python{}[tests/__init__.py]
"""Tests for new_proj."""
```
```python{}[new_proj/__init__.py]
"""Project for solving ab problem."""
from new_proj.main import ab_problem
__all__ = [
"ab_problem",
]
```
Пофиксим основной файл проекта.
```python{}[new_proj/main.py]
def ab_problem(first: int, second: int) -> int:
"""
Solve AB problem.
The function sums two integers.
:param first: a argument.
:param second: b argument.
:returns: sum.
"""
return first + second
```
Теперь вы можете сделать свой первый коммит.
```console
$ git commit
Check python ast................Passed
Trim Trailing Whitespace........Passed
Check Toml......................Passed
Fix End of Files................Passed
Add trailing commas.............Passed
Format with Black...............Passed
autoflake.......................Passed
isort...........................Passed
Check with Flake8...............Passed
Validate types with MyPy........Passed
Remove usless noqa..............Passed
pytest..........................Passed
```
Теперь ты знаешь как создавать шедевры.
# Создание CLI-приложения
А что если я хочу cli-приложение?
Ты не представляешь насколько это просто.
Пойдем модифицируем наш основной файл.
```python{}[new_proj/main.py]
import argparse
def parse_args() -> argparse.Namespace:
"""
Parse CLI arguments.
:returns: parsed namespace.
"""
parser = argparse.ArgumentParser()
parser.add_argument("a", type=int)
parser.add_argument("b", type=int)
return parser.parse_args()
def ab_problem(first: int, second: int) -> int:
"""
Solve AB problem.
The function sums two integers.
:param first: a argument.
:param second: b argument.
:returns: sum.
"""
return first + second
def main() -> None:
"""Main function."""
args = parse_args()
print(ab_problem(args.a, args.b)) # noqa: WPS421
```
Теперь поправим pyproject.toml таким образом чтобы он создал cli для нашей функции.
Добавим следующую секцию куда-нибудь в `pyproject.toml`:
```toml
[tool.poetry.scripts]
ab_solver = "new_proj.main:main"
```
Теперь нам доступна программа `ab_solver` внутри shell.
```console
$ poetry install
$ poetry shell
$ ab_solver 1 2
3
```
Хочешь установить? Пожалуйста.
```console
$ poetry build
Building new_proj (0.1.0)
- Building sdist
- Built new_proj-0.1.0.tar.gz
- Building wheel
- Built new_proj-0.1.0-py3-none-any.whl
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
Installing collected packages: new-proj
Successfully installed new-proj-0.1.0
$ ab_solver 1 2
3
```
Если запаблишить проект, то у пользователя тоже установится ваша cli-программа.
А на этом всё.
До новых встреч.

View File

@ -1,479 +0,0 @@
---
title: Ускоряем Python используя Rust.
description: Как встроить Rust в проект на Python.
position: 2
category: 'Python'
---
# Описание проблемы
Каким бы Python удобным не был, скоростью он похвастаться никак не может.
И для некоторых задач это достаточно критично.
Например, не так давно у меня была задача достать много информации
из файлов логов. Проблема в том, что один лог-файл занимает от 3-4 ГБ. А файлов
таких много и информацию достать требуется быстро. Изначальный вариант на Python
был написан за минут 15-20, но скорость его работы занимала 30-40 минут на один файл. После переписывания одной функции на Rust, скрипт отработал за 1 минуту.
Давайте напишем свой проект со встроенной функцией на Rust.
<br>
Задача проекта будет следующей:
Дан файл лога запросов на некоторый сервер. Требуется
найти количество запросов к определённому файлу и сумму переданных байт.
Для демонстрации мы напишем 2 функции, одна будет использовать наивное решение задачи на питоне,
вторая будет выполнять парсинг на расте.
Сам файл лог имеет следующую структуру:
```
"$line_num" "-" "$method $url" "$current_time" "$bytes_sent"
```
где,
* $line_num - номер строчки;
* $method - HTTP метод;
* $url - URL до файла, содержащий один из сгенерированных идентификаторов;
* $current_time - текущее время (время выполнения запроса);
* $bytes_sent - сколько байт было отправлено.
Данный формат логов отчасти подражает формату [G-Core labs](https://gcorelabs.com/support/articles/115000511685/).
# Проект на Python
Сначала мы напишем весь функционал на Python.
Для этого создадим проект `python-rust` по [гайду из блога](/project-start).
Для создания CLI я буду использовать [typer](https://pypi.org/project/typer/).
Весь код проекта доступен в [репозитории](https://github.com/s3rius/blog_examples/tree/master/python-rust).
```bash
$ poetry add typer
```
## Генератор логов
Для тестов напишем генератор лог-файла.
Данный генератор должен работать следующим образом.
Я указываю сколько строк лог-файла я хочу увидеть,
сколько уникальных id файлов использовать и название файла, куда писать.
Функция же генерирует лог в заданном формате.
```python{}[python_rust/main.py]
import secrets
import time
import uuid
from pathlib import Path
from typing import Any
import typer
tpr = typer.Typer()
def quote(somthing: Any) -> str:
"""
Quote string.
:param somthing: any string.
:return: quoted string.
"""
return f'"{somthing}"'
@tpr.command()
def generator( # noqa: WPS210
output: Path,
lines: int = 2_000_000, # noqa: WPS303
ids: int = 1000,
) -> None:
"""
Test log generator.
:param ids: how many file id's to generate.
:param output: output file path.
:param lines: how many lines to write, defaults to 2_000_000
"""
ids_pool = [uuid.uuid4().hex for _ in range(ids)]
with open(output, "w") as out_file:
for line_num in range(lines):
item_id = secrets.choice(ids_pool)
prefix = secrets.token_hex(60)
url = f"GET /{prefix}/{item_id}.jpg"
current_time = int(time.time())
bytes_sent = secrets.randbelow(800) # noqa: WPS432
line = [
quote(line_num),
quote("-"),
quote(url),
quote(current_time),
quote(bytes_sent),
]
out_file.write(" ".join(line))
out_file.write("\n")
typer.secho("Log successfully generated.", fg=typer.colors.GREEN)
@tpr.command()
def parser(input_file: Path, rust: bool = False) -> None:
"""
Parse given log file.
:param input_file: path of input file.
:param rust: use rust parser implementation.
"""
typer.secho("Not implemented", err=True, fg=typer.colors.RED)
def main() -> None:
"""Main program entrypoint."""
tpr()
```
Для удобства работы добавим в pyproject.toml информацию о командах.
```toml
[tool.poetry.scripts]
pyrust = "python_rust.main:main"
```
Теперь мы можем вызывать нашу программу.
```
$ pyrust --help
Usage: pyrust [OPTIONS] COMMAND [ARGS]...
Options:
--install-completion [bash|zsh|fish|powershell|pwsh]
Install completion for the specified shell.
--show-completion [bash|zsh|fish|powershell|pwsh]
Show completion for the specified shell, to
copy it or customize the installation.
--help Show this message and exit.
Commands:
generator Test log generator.
parser Parse given log file.
```
Работает отлично.
С помощью данного генератора сгенерируем файл на 8 милионов строк.
```bash
$ pyrust generator test.log --lines 8000000
Log successfully generated.
```
У нас получился файл на 1.5G. Для начального теста этого будет достаточно.
## Реализация парсера на Python
Представленный формат разделяет ифнормацию кавычками, поэтому
для сплита строк мы будем использовать встроенный в python модуль [shlex](https://docs.python.org/3/library/shlex.html).
Функция парсинга логов будет выглядить следующим образом:
```python{}[python_rust/py_parser.py]
import shlex
from pathlib import Path
from typing import Dict, Tuple
def parse_python( # noqa: WPS210
filename: Path,
) -> Dict[str, Tuple[int, int]]:
"""
Parse log file with python.
:param filename: log file.
:return: parsed data.
"""
parsed_data = {}
with open(filename, "r") as input_file:
for line in input_file:
spl = shlex.split(line)
# Splitting method and actual url.
url = spl[2].split()[1]
# Splitting url by /
# This split will turn this "/test/aaa.png"
# into "aaa".
file_id = url.split("/")[-1].split(".")[0]
file_info = parsed_data.get(file_id)
# If information about file isn't found.
if file_info is None:
downloads, bytes_sent = 0, 0
else:
downloads, bytes_sent = file_info
# Incrementing counters.
downloads += 1
bytes_sent += int(spl[4])
# Saving back.
parsed_data[file_id] = (downloads, bytes_sent)
return parsed_data
```
Давайте импортируем её, модифицируем команду парсинга и замерим скорость работы.
```python{}[python_rust/main.py]
@tpr.command()
def parser(input_file: Path, rust: bool = False) -> None:
"""
Parse given log file.
:param input_file: path of input file.
:param rust: use rust parser implementation.
"""
if rust:
typer.secho("Not implemented", input_file, color=typer.colors.RED)
return
else:
parsed_data = parse_python(input_file)
typer.secho(
f"Found {len(parsed_data)} files", # noqa: WPS237
color=typer.colors.CYAN,
)
```
Как можно видеть из кода, мы просто подсчитываем итоговое количество найденных файлов.
Посмотрим сколько времени займёт парсинг нашего сгенерированного файла.
```bash
$ time pyrust parser "test.log"
Found 1000 files
pyrust parser test.log 2443.42s user 2.10s system 99% cpu 40:59.30 total
```
Обработка данного файла заняла 40 минут. Это довольно печальный результат.
Самое время попробовать использовать Rust.
# Интеграция Rust
Для интеграции Rust в python код я буду использовать [PyO3](https://github.com/PyO3/pyo3) для биндингов и [maturin](https://github.com/PyO3/maturin) для сборки проекта.
Начнём с инициализации проекта и установки сборщика. В корневой папке проекта
создайте папку проекта с растом.
<b-message type="is-info" has-icon>
В теории можно использовать maturin как основной инструмент сборки проекта,
но это лишает вас всех прелестей poetry. Поэтому удобнее использовать Rust в
подпроекте и указать его как зависимость в списке зависимостей своего проекта.
</b-message>
```bash
$ cargo new --lib rusty_log_parser
```
Теперь создадим `pyproject.toml` и напишем туда описание нашего python пакета.
```toml{}[pyproject.toml]
[tool.poetry]
name = "rusty_log_parser"
version = "0.1.0"
description = "Log file parser with Rust core"
authors = ["Pavel Kirilin <win10@list.ru>"]
[build-system]
requires = ["maturin>=0.12,<0.13"]
build-backend = "maturin"
```
Также поправим `Cargo.toml`.
```toml{}[Cargo.toml]
[lib]
# Название модуля
name = "rusty_log_parser"
# Обязательный тип крейта, чтобы можно было использовать его
# в питоне.
crate-type = ["cdylib"]
[dependencies]
# Библиотека биндингов для питона.
pyo3 = { version = "0.15.1", features = ["extension-module"] }
# Библиотека для сплита. Повторяет функционал shlex.
shell-words = "1.0.0"
```
После этих нехитрых изменений попробуем собрать проект.
```shell
$ cd rusty_log_parser
$ cargo build
```
Теперь напишем сам парсер логов в Rust.
```rust{}[rusty_log_parser/src/lib.rs]
use std::collections::HashMap;
use std::fs::File;
use std::io;
use std::io::BufRead;
use pyo3::prelude::*;
/// Parse log file in rust.
/// This function parses log file and returns HashMap with Strings as keys and
/// Tuple of two u128 as values.
#[pyfunction]
fn parse_rust(filename: &str) -> PyResult<HashMap<String, (u128, u128)>> {
let mut result_map = HashMap::new();
// Iterating over file.
for log in io::BufReader::new(File::open(filename)?).lines().flatten() {
// Splitting log string.
if let Ok(mut spl) = shell_words::split(&log) {
// Getting file id.
let file_id_opt = spl.get_mut(2).and_then(|http_req| {
// Splitting method and URL.
http_req.split(' ').into_iter().nth(1).and_then(|url| {
// Splitting by / and getting the last split.
url.split('/')
.into_iter()
.last()
// Split file id and extension.
.and_then(|item_url| item_url.split('.').into_iter().next())
// Turning &str into String.
.map(String::from)
})
});
// Getting number of bytes sent.
let bytes_sent_opt =
spl.get_mut(4)
// Parsing string to u128
.and_then(|bytes_str| match bytes_str.parse::<u128>() {
Ok(bytes_sent) => Some(bytes_sent),
Err(_) => None,
});
if file_id_opt.is_none() || bytes_sent_opt.is_none() {
continue;
}
let file_id = file_id_opt.unwrap();
let bytes_sent = bytes_sent_opt.unwrap();
match result_map.get(&file_id) {
Some(&(downloads, total_bytes_sent)) => {
result_map.insert(file_id, (downloads + 1, total_bytes_sent + bytes_sent));
}
None => {
result_map.insert(file_id, (1, bytes_sent));
}
}
}
}
Ok(result_map)
}
/// A Python module implemented in Rust. The name of this function must match
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
/// import the module.
#[pymodule]
fn rusty_log_parser(_py: Python, m: &PyModule) -> PyResult<()> {
// Adding function to the module.
m.add_function(wrap_pyfunction!(parse_rust, m)?)?;
Ok(())
}
```
Это и будет наша функция, которую мы будем вызывать.
Теперь добавим `python` обёртку для нашего модуля.
Для этого в проекте `rusty_log_parser` надо создать
папку с тем же названием что и проект. В данном случае
это будет `rusty_log_parser`.
Внутри этой папки мы создадим `__init__.py`, в котором
опишем и задокументируем доступные функции модуля.
```python{}[rusty_log_parser/rusty_log_parser/__init__.py]
from pathlib import Path
from typing import Dict, Tuple
from .rusty_log_parser import parse_rust as _parse_rust
def parse_rust(input_file: Path) -> Dict[str, Tuple[int, int]]:
"""
Parse log file using Rust as a backend.
:param input_file: log file to parse.
:return: Parsed
"""
return _parse_rust(str(input_file.expanduser()))
```
Осталось только добавить свеженаписанный пакет как зависимости нашего проекта.
Для этого в корневой `pyproject.toml` надо добавить следующую зависимость.
```toml{}[pyproject.toml]
[tool.poetry.dependencies]
...
rusty_log_parser = { path = "./rusty_log_parser" }
```
Теперь мы можем использовать нашу функцию.
Перепишем нашу функцию парсера таким образом,
чтобы она использовала rust, когда был передан
соответствующий параметр.
```python{}[python_rust/main.py]
from rusty_log_parser import parse_rust
...
@tpr.command()
def parser(input_file: Path, rust: bool = False) -> None:
"""
Parse given log file.
:param input_file: path of input file.
:param rust: use rust parser implementation.
"""
if rust:
parsed_data = parse_rust(input_file)
else:
parsed_data = parse_python(input_file)
typer.secho(
f"Found {len(parsed_data)} files", # noqa: WPS237
color=typer.colors.CYAN,
)
```
Теперь проведём итоговый замер.
```bash
$ time pyrust parser test.log --rust
Found 1000 files
pyrust parser test.log --rust 20.44s user 0.35s system 99% cpu 20.867 total
```
Итого виден небольшой выйигрыш во времени в 2423 секунды, из чего следует,
что реализация на Rust быстрее всего в 122 раза.
Данной статьёй я не подталкиваю всех переписывать всё на Rust,
а просто ознакамливаю с возможностями. Но если всё же хочется,
то можно и попробовать.
До новых встреч.

View File

@ -1,755 +0,0 @@
---
title: Начало работы с kubernetes
description: Как там это всё разворачивать в двух словах.
category: DevOps
position: 4
---
# Проблема в изучении кубернетес
Многие люди, кто задавались вопросом "Как начать работать с
кубернетес?", сталкивались с тем, что документация крайне
большая сложная и нет нормального описания как
завернуть маленький проект из одного контейнра в свой кластер или
как развернуть сам кластер без боли.
А всё потому что вся документация нацелена на большие
production-ready системы с большим rps и тому подобным.
<br>
В данной статье я попробую исправить это вселенское
недопонимание используя k3s, свой комплюктер и немного знаний по кодингу.
<div align="center">
![Intro image](/images/k3s_start/kube_intro.jpg)
</div>
# Что такое кубернетес и почему это лучше докера
Многие ребята, кто хорошо знаком с докером и его
возможностями, могут задаваться таким вопросом.
Для тех кто в танке, напомню, что докер
имеет вариант запуска в режиме кластера.
Этот функционал называется [docker swarm](https://docs.docker.com/engine/swarm/).
В целом, swarm отдалённо напоминает kubernetes,
так как в этом режиме докер худо-бедно умеет
автоскейлится и запускаться в кластере,
но это всё равно немного не то.
<b-message type="is-info" has-icon>
Также замечу, что кубер активно развивается и поддерживается.
огромным количество компаний. А вот docker swarm уже по-немногу
умирает и документация по нему не то чтобы супер хороша.
</b-message>
По большей части кубернетес это система,
которая будет управлять вашими приложениями,
следить за их состоянием и помогать вам в их
конфигурации. Кубер умеет очень много, поэтому
все интересные способности я в этой статье не смогу осветить,
но самую базу попробую рассказать.
# Из чего состоит кубернетес
Так как я в этой статье хотел затронуть
совсем базовые и практические вещи, то рассматривать
мы будем только самые часто используемые компоненты.
- Container
- Pod
- Deployment
- Service
- Ingress
- Namespace
- Secret
- ConfigMap
А теперь рассмотрим каждый ресурс немного поподробнее.
## Container
Контейнеры не являются чем-то специфичным для кубернетес.
С контейнерами вы можете быть знакомы из кучи других систем.
В контексте кубера они не обладают никакими дополнительными
свойствами. Это ровно то же, что и контейнеры `containerd`
или те, с которыми вы возились с докером. Ничего нового.
Собираются контейнеры для кубернетеса ровно тем же образом,
что и для докера.
<b-message type="is-warning" has-icon>
Важная ремарка. Кубер, начиная с 2021 года, не поддерживает
докер как бэкенд. Теперь он будет общаться с
containerd напрямую. Это значит, что перед использованием
контейнеров собранных на локальной машине надо будет импортировать их
в `containerd` используя `ctr image import`.
Как импортировать образы почитать можно в [этой статье](https://cwienczek.com/2020/06/import-images-to-k3s-without-docker-registry/).
</b-message>
## Pod
<div align="center">
<img alt="Pods" style="width: 100%" src="/images/k3s_start/pods.svg">
</div>
Поды - это логически связанные группы контейнеров.
Это самая базовая еденица кубернетеса.
В поде находится от одного до множества контейнеров.
Интересная особенность пода в том, что все контейнеры
делят один сетевой адрес. Другими словами,
если у вас один из контейнеров открыл порт `3000`,
то другие контейнеры из пода этот порт использовать не смогут.
То есть, если вы хотите логически связанные приложения
поместить в под, то они могут ходить друг к другу через лупбек
адреса. Такие как `localhost` или `127.0.0.1`.
## Deployment
Поды это круто, но есть одно но. Данные объекты неизменяемые
и сам под скейлить вручную занятие сомнительное.
Конечно, никто не мешает создать вам вручную [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/)
и самому крутить там нужные значения. Но в среднем
вам с этим возиться не очень то хочется.
<br>
Deployment нужен именно для описания подов и создания
некоторых ресурсов, нужных для скейлинга.
Также с помощью деплойментов можно делать откаты приложения через
механиз роллбеков. Я это рассматривать не буду. В этой статье только база.
## Service
Сервис - это ресурс, с помощью которого поды могут общаться
между собой. По факту сервис описывает, какие порты пода
открыты и перенаправляет трафик на них.
Представьте себе под, который просто проксирует весь трафик с одного
порта на какой-нибудь порт пода. Это и есть Service.
<div align="center">
![Service overview](/images/k3s_start/service%20overview.png)
</div>
Выглядит сервис примерно так как показано на кртинке выше.
Он направляет весь входной трафик с указанного порта на
порты подов.
Также сервис выступает как балансировщик.
<b-message type="is-info" has-icon>
Если вы хотите сделать запрос от одного пода до дргого, внутри кластера, то
вам придётся использовать сервис.
<br>
Если вы попробуете сделать запрос напрямую по IP пода, то у вас, конечно же,
получится, но это довольно странная идея из-за того, что при пересоздании
пода у него может обновится IP внутри кластера. Поэтому лучше использовать сервисы. Также они потребуются для ингресса.
</b-message>
## Ingress
Ингресс - это сервис описывающий куда пускать трафик,
который поступает снаружи кластера.
<div align="center">
![Ingress overview](/images/k3s_start/ingress-overview.png)
</div>
Принцип работы ингресса следующий:
Вы указываете хост ингресса и различные пути.
В зависимости от входящего запроса ингресс выбирает в какой сервис
направить запрос и в какой порт.
Настройка ингресса достаточно гибкая и я не думаю,
что вы можете столкнуться с какими либо проблемами.
## Namespace
Неймспейсы это логические разделители уровня доступа.
Я предпочитаю использовать разные неймспейсы под различные логические группы приложений.
Например, в моём кластере есть отдельные неймспейсы для каждого приложения.
Скажем, у меня есть проект, в котором есть база данных и веб сервер.
Я держу их в одном неймспейсе, который называется также, как и приложени.
А вот телеграм боты -- приложения достаточно лёгкие.
Поэтому у меня есть неймспейс со всеми телеграм ботами.
Эта штука позволяет просто лучше организовать свой кубернетес кластер.
Также неймспейсы очень полезны для ограничивания возможностей
конкретного пользователя.
Например, вы можете создать правило, которое будет разрешать пользователю
смотреть на поды в каком-то неймспейсе, но при этом ему нельзя будет
что либо изменять.
## Secret и ConfigMap
Данные ресурсы нужны только чтобы хранить
внутри кластера какую-нибудь информацию.
Например вы можете сохранить в ConfigMap
переменные среды и потом использовать их в подах в одном неймспейсе.
В секреты обычно кидают сертификаты или какие-нибудь ключи.
Но так как секреты не особо секретные принято использовать [Vault](https://www.vaultproject.io/docs/platform/k8s). Но так как эта статья затрагивает только
основы рассматривать развертку и настройку Vault мы не будем, ну и также HashiCorp
всё довольно подробно расписали сами.
# Как развернуть k8s у себя
Для локального кубера есть пара вариантов.
- k3s (Недоступен под Windows)
- minikube
На первый взгляд minikube может показаться лучшим вариантом.
И он действительно хорош тем, что его легко развернуть и почистить после
своих экспериментов. Однако, там есть проблемы с ингрессами.
По факту они не работают и там надо окольными путями получать
адреса.
<br>
k3s - это легковесная production-ready реализация k8s. Ingress у него работают
отлично, поэтому я буду использовать его.
<br>
Я не буду зацикливаться на установке `minikube`, так как
он прост в установке и первоначальной настройке. Почитать подробнее можно в
[официальном гайде от minikube](https://minikube.sigs.k8s.io/docs/start/).
С `k3s` всё немного посложнее, но тоже достаточно просто, если немного разобраться. Первоначальная установка описана в [официальной доке k3s](https://rancher.com/docs/k3s/latest/en/installation/install-options/).
## Подключение кластера
После установки `kubectl` в домашней дериктории должен был быть
сгенерирован файл `.kube/config`. Этот файл содержит данные для
подключения к различным кластерам. `minikube` Сам добавляет
ключи для подключения к .kube/config. `K3S` никак не изменяет `.kube/config`, поэтому надо будет это сделать вручную.
Для того, чтобы это сделать сначала разберёмся как выглядит конфиг.
```yaml{}[.kube/config]
apiVersion: v1
kind: Config
preferences: {}
# Массив кластеров.
# Каждый элемент -- данные для подключения
# Тут также есть названия для каждого кластера.
clusters:
- name: hyper
cluster:
certificate-authority-data: DATA+OMITTED
server: https://192.168.1.55:6443
- name: k3s-local
cluster:
certificate-authority-data: DATA+OMITTED
server: https://127.0.0.1:6443
# Массив данных пользователя.
# Тут указаны пользователи с
# различными сертификатами.
# Обычно для разных серверов у вас будут
# Различные данные для входа.
users:
- name: hyper-s3rius
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
- name: k3s-user
user:
client-certificate-data: REDACTED
client-key-data: REDACTED
# Массив контекстов.
# Контекст - это связующее звено
# между кластероами и пользователями.
contexts:
- context:
cluster: hyper
user: hyper-s3rius
name: hyper
- context:
cluster: k3s-local
user: k3s-user
name: k3s
# Текущий контекст указывает какой
# контекст использовать по умолчанию.
current-context: "k3s"
```
Для работы с кубером из командной строки
можно использовать `kubectl`. Чтобы сменить контекст в любой команде вы
можете передать параметр `--context $CONTEXT_NAME`, где `$CONTEXT_NAME` это название контекста.
Чтобы достать данные для подключения к `k3s` надо посмотреть его конфиг
и скопировать данные. Либо выставить переменную среды `KUBECONFIG`, которая будет
указывать конфиг k3s. Конфиг подключения `k3s` лежит в файле `/etc/rancher/k3s/k3s.yaml`.
Можете выполнить команду, которая будет просить kubectl использовать указанный конфиг:
`export KUBECONFIG=/etc/rancher/k3s/k3s.yaml`
<br>
Либо скопируйте нужные данные для подключения себе в `.kube/config`.
После настройки выполните команду и проверьте что вам вернулось
что-то подобное.
```bash
$ kubectl --context "my-context" get pods
No resources found in default namespace.
```
Это значит, что никаких ресурсов пока в
кластере нет. Это мы исправим чуть позже. Пока что можно сказать,
что подключение прошло успешно.
Если же у вас выпадает ошибка, например такая:
```
The connection to the server localhost:8080 was refused - did you specify the right host or port?
```
То либо запустите кластер
```
sudo systemctl start k3s.service
```
Либо у вас неверные данные для входа.
Перепроверьте свой `.kube/config`.
### Мониторинг кластера
Для просмотра ресурсов и управления кластером используется `kubectl`.
Но для того, чтобы с ней работать нужно получше понять, что вообще в кластере есть.
Для этого я советую использовать [Lens](https://k8slens.dev/). Это крайне
удобный интерфейс для управления своим кластером.
Также там есть очень клёвая настройка, которая сама включит мониторинг
потребления памяти и процессора для всех подов и кластера в общем.
На локальной машине это не имеет смысла,
а вот в проде было бы очень полезно.
Выглядит Lens примерно так:
<div align="center" style="margin-top:15px;">
![Lens example](/images/k3s_start/lens-example.png)
</div>
Для изучения **крайне настоятельно рекомендую** настроить Lens.
# Ваше первое приложение в кластере.
Вы можете следовать статье, а можете подсмотреть весь код в [репозитории](https://github.com/s3rius/blog_examples/tree/master/req_counter).
### Сервер
Давайте создадим своё первое приложение.
Для этого я буду использовать [express.js](https://expressjs.com/), так как он крайне популярен и прост.
Для этого сначала напишем сам сервер. Я буду использовать yarn, но можете и npm,
сути не поменяет.
Создайте какую-нибудь папку, где вы будете эксперементировать, откройте в ней ваш любимый тектовый редактор и просто копируйте файлы ниже.
```json{}[package.json]
{
"name": "req_counter",
"version": "1.0.0",
// Указан модуль,
// чтобы использовать нормальны импорты,
// а не require.
"type": "module",
"license": "MIT",
"scripts": {
// Скрипт старта сервера.
"server": "node index.js"
},
// Зависимости проекта.
"dependencies": {
"express": "^4.17.1"
}
}
```
И само приложение
```js{}[index.js]
import express from "express";
import { hostname } from "os";
import { argv, exit } from "process";
// Серверное приложение.
const app = express();
// Глобальный счётчик входящих запросов.
let requests = 0;
// Обработка входящего запроса.
app.get("*", (req, res) => {
// Увеличиваем глобальный счётчик запросов.
requests += 1;
// Логгируем входящий запрос.
console.log(`${req.method} ${req.url}`);
// Возвращаем информацию о текущем хосте и количестве запросов.
res.json({
requests: requests,
hostname: hostname(),
});
});
// Аргументы командной строки.
// Напрмиер yarn run server 127.0.0.1 8000
const args = argv.slice(2);
// Если передано неверное количество аргументов.
if (args.length != 2) {
console.error("Usage: yarn run server {host} {port}");
exit(1);
}
// Простейший "парсинг" аргументов командной строки.
const host = args[0];
const port = args[1];
// Старт сервера.
app.listen(port, host, () => {
console.log(`Server listening at http://${host}:${port}`);
});
```
Это всё. Сервер готов.
<hr>
Протестируем запуск сервера, выполнив команду ниже и открыв в
своём любимом браузере http://localhost:8080.
```
yarn run server 0.0.0.0 8080
```
У меня всё работает и успешно отдаётся нужная информация.
```json
{
"requests": 1,
"hostname": "s3rius-pc"
}
```
### Docker образ
Теперь создадим докер образ приложения.
Добавим `.dockerignore`, чтобы игнорировать ненужные файлы во время сборки образа.
```gitignore{}[.dockerignore]
node_modules/
```
И добавим в проект `Dockerfile` для описания самого процесса сборки.
```dockerfile{}[Dockerfile]
FROM node:17-alpine
WORKDIR /app
COPY . /app/
RUN yarn install
CMD [ "yarn", "run", "server", "0.0.0.0", "8000"]
```
Давайте соберём и запустим проект в контейнере.
```bash
docker build --tag="s3rius/req-counter-express:latest" .
docker run --rm -it -p 3400:8000 "s3rius/req-counter-express:latest"
```
Можете проверить, что приложение работает успешно, открыв в браузере http://localhost:3400.
У меня в ответ пришло то же сообщение. Только, как можно заметить,
`hostname` поменялся. На самом деле контейнеры используют свои hostname
отличные от `hostname` локальной машины.
```json
{
"requests": 10,
"hostname": "8f23adadc640"
}
```
Вариантов как положить это приложение в K8S несколько.
- Вы можете запушить собранное приложение в [Docker HUB](https://hub.docker.com/) и использовать его.
- Можете использовать мой образ `s3rius/req-counter-express:latest`
- Сохранить собранный образ как tar файл и импортировать его в containerd напрямую.
Как это сделать почитать можно в [этой статье](https://cwienczek.com/2020/06/import-images-to-k3s-without-docker-registry/).
### Деплой в k8s
Создайте папку `kube` в папке проекта и теперь мы будем работать в ней.
Все ресурсы будут описаны yaml-файлами.
```yaml{}[kube/deployment.yml]
---
apiVersion: apps/v1
kind: Deployment
# метаданные самого деплоймента.
metadata:
name: req-counter-deployment
spec:
# Количество реплик пода.
replicas: 1
# Селектор, который выбирает
# какие поды принадлежат этому деплойменту.
selector:
matchLabels:
app: req-counter
# Шаблон пода,
# который будет использоваться при
# маштабировании.
template:
# Метаданные пода.
# тут обычно помещаются лейблы,
# с помощью которых деплоймент идентифицирует
# свои поды.
metadata:
labels:
app: req-counter
spec:
# Масссив контейнеров
containers:
# Название контейнера внутри пода.
- name: req-counter-app
# Образ приложения.
image: s3rius/req-counter-express:latest
# Ресурсы требуемые для работы приложения.
resources:
# Минимальное количество ресурсов,
# которое кластер гарантированно предоставит приложению.
# Также данные значения используются для того,
# чтобы выяснить на какой ноде запускать приложение.
requests:
memory: "50Mi"
cpu: "30m"
# Максимально возможные значения приложения.
# Если приложение выйдет за лимиты,
# то кубер убьёт приложение.
limits:
memory: "128Mi"
cpu: "100m"
# Порты на которых открыты приложения.
ports:
- containerPort: 8000
protocol: TCP
```
Теперь опишем сервис для управления трафиком.
```yaml{}[kube/service.yml]
---
apiVersion: v1
kind: Service
# Метадата сервиса.
metadata:
name: req-counter-service
spec:
# Селектор подов,
# которым будет пускаться трафик.
# Трафик может идти в любой под,
# который матчит данному селектору.
selector:
app: req-counter
# Порты для проксирования соединений.
ports:
# Порт сервиса
- port: 80
# Порт пода, куда будет идти трафик дальше.
targetPort: 8000
```
И в последнюю очередь опишем наш ингрес.
```yaml{}[kube/ingress.yml]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
# Метаданные ингресса.
metadata:
name: req-counter-ingress
labels:
name: req-counter-ingress
spec:
# Правила роутинга.
rules:
# Требуемый хост.
- host: req-counter.local
http:
paths:
# Тип пути Prefix значит,
# что все запросы, которые начинаются c
# ${path} будут матчится тут.
- pathType: Prefix
# Сам путь.
path: "/"
backend:
service:
# Название нашего сервиса.
name: req-counter-service
# Порт сервиса, куда перенаправлять входящий трафик.
port:
number: 80
```
Перед тем, как создать все описанные ресурсы создадим неймспейс.
```bash
$ kubectl --context k3s create namespace req-couter-ns
namespace/req-couter-ns created
```
После того, как неймспейс создан самое время сделать последний штрих.
<div align="center">
![Apply meme](/images/k3s_start/kubectl-apply.png)
</div>
```bash
$ kubectl --context k3s --namespace req-couter-ns apply -f ./kube/
deployment.apps/req-counter-deployment created
ingress.networking.k8s.io/req-counter-ingress created
service/req-counter-service created
```
Готово. Теперь вы можете зайти в lens, выбрать свой кластер из списка
и посмотреть как там поживает ваше приложение.
Также не забудьте указать неймспейс, в который вы деплоили приложение.
Выглядит это чудо примерно так:
<div align="center">
![Deployed app in lens](/images/k3s_start/lens-deployed.png)
</div>
Также можно использовать команду и вывести всё себе в терминал.
```
$ kubectl --context k3s --namespace req-couter-ns get all
NAME READY STATUS RESTARTS AGE
pod/req-counter-deployment-764476db97-dt2tc 1/1 Running 0 8m11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/req-counter-service ClusterIP 10.43.50.23 <none> 80/TCP 8m11s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/req-counter-deployment 1/1 1 1 8m11s
NAME DESIRED CURRENT READY AGE
replicaset.apps/req-counter-deployment-764476db97 1 1 1 8m11s
```
### Маштабирование
В рамках демонстрации давате поменяем значение
`replicas` в нашем файле деплоймента до `3`.
```yaml{}[kube/deployment.yml]
...
spec:
# Количество реплик пода.
replicas: 3
...
```
После изменения просто ещё раз вызовите команду apply.
```bash
$ kubectl --context k3s --namespace req-couter-ns apply -f ./kube/
deployment.apps/req-counter-deployment configured
ingress.networking.k8s.io/req-counter-ingress unchanged
service/req-counter-service unchanged
```
Как можно видеть, изменился только наш `Deployment`.
Остальные ресурсы остались нетронутыми.
Давайте посмотрим на поды в нашем неймспейсе.
```
$ kubectl --context k3s --namespace req-couter-ns get pods
NAME READY STATUS RESTARTS AGE
req-counter-deployment-764476db97-dt2tc 1/1 Running 0 13m
req-counter-deployment-764476db97-tdjrb 1/1 Running 0 69s
req-counter-deployment-764476db97-x28fr 1/1 Running 0 69s
```
Как видно, всё правильно. Теперь у нас 3 пода
нашего приложения.
Теперь можно выполнить кучу запросов по адресу http://req-counter.local/
и получить балансировку между подами из коробки, без дополнительных
конфигураций.
Если у вас не получается найти адрес. Добавьте данный хост себе в
`/etc/hosts` на линуксе или в `C:\Windows\System32\drivers\etc\hosts` на windows,
дописав в конец файла следующее:
```[/etc/hosts]
127.0.0.1 req-couter.local
```
И теперь вы можете открыть в браузере своё приложение и увидеть,
как кубернетес заботливо направляет трафик туда, куда вы хотите.
<div align="center">
![Making requests](/images/k3s_start/requests.gif)
</div>
### Как мне очистить мой кластер?
Всё очень просто.
Так как у нас имеются описния ресурсов, то мы
можем удалить всё сразу используя команду
```bash
$ kubectl --context k3s --namespace req-couter-ns delete -f ./kube/
deployment.apps "req-counter-deployment" deleted
ingress.networking.k8s.io "req-counter-ingress" deleted
service "req-counter-service" deleted
```
Таким образом k8s удалит все описанные ресурсы из вашего кластера.
До новых встреч.

View File

@ -1,471 +0,0 @@
---
title: Traefik - роутинг просто.
description: Изучите как использовать traefik.
position: 2
category: DevOps
---
# Traefik - роутинг просто
Сегодня я бы хотел рассказать о такой клёвой штуке, как [Traefik](https://traefik.io/traefik/). Не так давно я перевёл все сервисы своего сервера на traefik и это буквально сделало жизнь проще. Сегодня я расскажу вам зачем он нужен, как его настроить, и покажу на примере, как именно он помогает. По факту, это nginx на стероидах, в плане конфигурации, потому что в traefik за тебя сделано гораздо больше.
# Что такое traefik
Traefik - это система, которая позволяет настроить маппинг между доменными именами и конкретными приложениями. Допустим, у вас есть контейнер frontend-приложения, которое вы хотели бы разместить на домене `myapp.com`, и также у вас есть контейнер backend-приложения, которое вы бы хотели разместить на домене `api.mayapp.com`. [Traefik](https://traefik.io/traefik/) поможет вам это сделать без лишних файлов конфигурации.
## Сравнение с другими инструментами
Если бы вы решили сделать это через [Nginx](https://www.nginx.com/), то вы бы создали новые файлы конфигураций под каждый домен, в папочке с конфигурациями для всех доменов, положили куда-нибудь сертификат от домена `*myapp.com` и подключали бы его вручную в каждом файле доменов. А если бы вам надо было увеличить количество контейнеров отдельного приложения, вы бы указывали сервера в директиве `upstream` и перечисляли там адреса инстансов приложения вручную.
Любители [Apache HTTP Server](https://httpd.apache.org/) на этом моменте обычно берут дробовик и разносят себе голову хорошим зарядом дроби.
А господа, которые используют [Traefik](https://traefik.io/traefik/), просто дописывают лейблы контейнеру в `docker-compose.yml` и идут дальше дегустировать вино.
<b-message type="is-info" has-icon>
Если же ваше приложение построено на вебсокетах, то тут уже фанаты Nginx тянутся за дробовиком. Ну а для ценителей traefik ничего не меняется, ведь в нём встроена поддержка HTTP/2.0.
</b-message>
<div align="center">
![image](/images/traefik_imgs/nginx_comparasion.png)
</div>
## Архитектура
В официальной документации зарисована следующая схема работы traefik:
<div align="center">
![image](/images/traefik_imgs/traefik-architecture.png)
</div>
Как можно видеть по данному изображению, есть 5 основных составляющих traefik, а именно:
- entrypoints
- routers
- rules (часть routers)
- middlewares (чать routers)
- services
### Entrypoint
Являются основными слушателями входящих соединений. Через них проходит весь трафик. Если попробовать объяснить в двух словах: "Порт, на который должно прийти входящее соединение", - вполне себе неплохое объяснение. В данном туториале я создам 2 `entrypoint`, которые будут слушать на http и https.
### Routers, Rules и Middlewares
Роутер - связующее звено между `entrypoint` и сервисом, куда нужно направить трафик. Роутер хранит в себе информацию, куда направить входящий трафик, и
правила, по которым можно определить, пускать ли трафик дальше.
Rules - это и есть те самые правила, которые определяют, через какой роутер пустить трафик.
Допустим, у вас в конфиге есть следующие правила:
```
Host(`myapp.com`) && (PathPrefix(`/api`) || PathPrefix(`/memes`))
```
Это можно читать как "Этот роутер пустит в моё приложение, если запрос пришёл на хост `myapp.com` и путь запроса начинается с `/api` или `/memes`"
Довольно просто, не так ли?
Ну а middlewares - это некоторая логика, что нужно выполнить с запросом до того, как он попадёт в ваше приложение. В этой статье я не буду рассказывать про существующие `middlewares`, тут потребуется самостоятельное ознакомление с [документацией](https://doc.traefik.io/traefik/middlewares/overview/).
# Конфигурация
<b-message type="is-info" has-icon>
В данной статье я буду использовать Docker для конфигурации traefik.
Вы, конечно же, можете использовать локально установленную версию, всё будет работать, как часы.
</b-message>
Для нашего основного traefik на сервере мы создадим небольшой docker-compose.yml, файл конфига и папочку с сертификатами.
Структура будет следующая:
```
.
├── certs
│   ├── local.key
│   └── local.pem
├── config.toml
└── docker-compose.yml
```
Теперь разберем каждый файл по порядку.
Вот основной docker-compose нашего traefik.
```yaml{}[docker-compose.yml]
---
version: '3.7'
services:
traefik-proxy:
# The official v2.0 Traefik docker image
image: traefik:v2.4.8
container_name: traefik_main
command:
# Включает прослушивание докера на новые события и следит за лейблами контейнеров.
- --providers.docker=true
# Отключает автоматическое создание роутеров ко всем контейнерам на сервере.
- --providers.docker.exposedbydefault=false
# Сеть докера, по которой будет выполнятся подключение к контейнерам.
- --providers.docker.network=traefik-shared
# Папка с конфигурационным файлом.
- --providers.file.directory=/etc/traefik/dynamic
# Включает отслеживание изменений файла конфигурации.
- --providers.file.watch=true
# Создаёт entrypoint с названием http и слушает 80 порт.
- --entrypoints.http.address=:80
# Создаёт entrypoint с названием https и слушает 443 порт.
- --entrypoints.https.address=:443
ports:
# The HTTP port
- "80:80"
# The HTTPS port
- "443:443"
restart: unless-stopped
networks:
- traefik-shared
environment:
MAIN_HOST: 192.168.1.89
volumes:
# Обязательный вольюм. Так траефик может слушать
# события происходящие в демоне докера.
- /var/run/docker.sock:/var/run/docker.sock
# Вольюм с файлом конфигурации.
- ./config.toml:/etc/traefik/dynamic/traefik.toml
# Папка сертификатов.
- ./certs:/etc/certs/
networks:
# Сетка, внутри которой будут находится приложения.
traefik-shared:
name: traefik-shared
```
Вот какие параметры я передаю в `traefik-cli`.
Конечно, вы всегда можете глянуть `traefik --help` и подобрать себе желаемые параметры.
Также из docker-compose файла видно, что я создал докер сеть `traefik-shared`, которую в дальнейшем буду использовать на всех контейнерах, которым требуется свой домен.
Далее следует папка с сертификатами. Для реальных доменов я использую cloudflare и скачиваю сертификаты и ключи с панели администратора. Также не забываю выставлять мод шифрования на strict.
![SSL/TLS encryption mode](/images/traefik_imgs/couldflare_certs.png)
Для генерации локальных сертификатов я использую тулу [mkcert](https://github.com/FiloSottile/mkcert).
Для любого локального домена я делаю что-то типа:
```bash
mkcert "*.local"
mv _wildcard.local-key.pem local.key
mv _wildcard.local.pem local.pem
```
И помещаю это в папочку `certs` рядом с `docker-compose.yml`.
После того как я создал все нужные сертефикаты для всех доменов, их надо указать в файле `config.toml` в папочке traefik.
Вот пример:
```toml{}[config.toml]
[tls.options]
[tls.options.default]
# Эта опция вообще для strict cloudflare tls encryption,
# Но я включаю её и на локальных доменах.
sniStrict = true
[[tls.certificates]]
# Я тут указываю /etc/certs, потому что в docker-compose
# у нас volume на эту папку.
certFile = "/etc/certs/local.pem"
keyFile = "/etc/certs/local.key"
```
<b-message type="is-info" has-icon>
Для добавления новых сертификатов в данный файл достаточно добавить:
</b-message>
```toml
[[tls.certificates]]
certFile = "/etc/certs/<certFile>"
keyFile = "/etc/certs/<keyFile>"
```
После этого вы можете запускать traefik и наслаждаться доменами для ваших контейнеров.
# Запуск приложений
Теперь сконфигурируем приложение таким образом, чтобы к нему можно было обращаться через доменное имя.
Для примера возьмем мелкое приложение на nodejs со следующей структурой проекта:
```
.
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── index.js
├── package.json
└── yarn.lock
```
```js{}[index.js]
const express = require('express')
let req_count = 0;
const hostname = process.env.HOSTNAME;
const PORT = process.env.PORT || 3000;
app = express();
app.get("/", (req, res) => {
console.log(`GET / ${hostname}`)
res.send({request_num: req_count++, host: hostname})
})
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
```
```json{}[package.json]
{
"name": "express-test",
"version": "1.0.0",
"main": "index.js",
"author": "s3rius",
"license": "MIT",
"scripts": {
"runserver": "node index.js"
},
"dependencies": {
"express": "^4.17.1"
}
}
```
```dockerfile{}[Dockerfile]
FROM node:16-alpine3.11
WORKDIR /app
COPY package.json yarn.lock /app/
RUN yarn install
COPY . /app/
ENTRYPOINT ["yarn", "run"]
```
```yaml{}[docker-compose.yml]
---
version: '3.7'
services:
server:
build: .
labels:
# Включить поддержку роутинга через traefik.
- traefik.enable=true
# Поставить правило роутинга, если Host запроса равен test_node.local.
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
# Слушать на entrypoint http (80 порт, это было объявлено в параметрах запуска traefik).
- traefik.http.routers.test_node.entrypoints=http
# Сервис, связанный с роутером test_node.
- traefik.http.routers.test_node.service=node_test
# Порт, куда направлять запросы в сервис node_test.
- traefik.http.services.node_test.loadbalancer.server.port=3000
command: runserver
networks:
- traefik-shared
networks:
# Докер сеть, в которой находится traefik.
traefik-shared:
name: traefik-shared
external: true
```
<b-message type="is-info" has-icon>
Файл yarn.lock генерируется командой `yarn install`. Если у вас не установлен `yarn`, то ничего страшного. Просто это будет занимать чуть больше времени на сборку образа.
</b-message>
Как вы видите, приложение слушает на порт `3000` и отвечает свим hostname и количеством обработанных запросов. А в docker-compose.yml, в отличие от обычного проекта, появились labels.
<hr/>
<b-message type="is-danger" has-icon>
Роутеры, сервисы и хосты явно создавать нигде не нужно.
Они сами создаются, когда вы их указываете.
Название сервиса и роутера могут совпадать.
Обратите внимание на косые кавычки при указании хоста! Это обязательно.
В объявлении labels могут быть использованы переменные среды. Например:
`` traefik.http.routers.test_node.rule=Host(`${APP_HOST}`) ``
</b-message>
Также можно видеть, что я подключил контейнер к сети, которую мы указывали в контейнере traefik. Здесь она помечена как external.
Теперь мы можем спокойно запустить наш сервис. Для этого воспользуемся следующей командой:
```bash
docker-compose up --build --scale server=3
```
В данной команде мы указали, что хотим поднять 3 инстанса нашего сервиса. Остальным пусть занимается traefik.
Теперь попробуем некоторое количество раз выполнить запрос на наш сервис.
```console
$ curl -H "Host: test_node.local" "http://localhost"
{"request_num":0,"host":"7417ac8fda92"}
```
Результат должен быть примерно таким:
[![GIF](/images/traefik_imgs/curls.gif)](/images/traefik_imgs/curls.gif)
Как вы видите, traefik балансирует между контейнерами за нас. И я считаю, что это - победа.
## Подключение TLS и сертификатов
Тут всё не намного сложнее. Давайте немного поменяем лейблы нашего контейнера.
```yaml
services:
server:
labels:
- traefik.enable=true
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
- traefik.http.routers.test_node.entrypoints=https
- traefik.http.routers.test_node.tls=true
- traefik.http.routers.test_node.service=node_test
- traefik.http.services.node_test.loadbalancer.server.port=3000
```
Как вы видите, я поменял entrypoints на `https`. Как вы можете помнить, это entrypoint, который слушает на 443 порт. И также я включил поддержку tls лейблом `traefik.http.routers.<router>.tls=true`
На данном этапе вам потребуется добавить свой хост в `/etc/hosts`, если вы используете нормальную систему. Но если вы всё же на windows, то вам потребуется добавить правило в `C:\Windows\System32\drivers\etc\hosts`.
И добавляем в конец файла запись:
```
127.0.0.1 test_node.local
```
И также вам потребуется cертификат на этот домен.
Для этого:
1. Создадим сертификат через `mkcert`, как упоминалось ранее;
2. Поместим ключ и сертификат в папку certs;
3. Добавим ключ и сертификат для вашего домена в config.toml (Формат указан выше).
Теперь вы можете обращаться к вашему приложению напрямую через локальное доменное имя.
```console
$ curl --insecure https://test_node.local
{"request_num":0,"host":"7417ac8fda92"}
```
## Добавление локальных сервисов не из докера
Все те флаги, которые мы указываем в labels, вы также можете указать в файле конфигурации рядом с docker-compose.yml, указав конкретный ip адрес.
Например:
```toml
[http.routers]
# Define a connection between requests and services
[http.routers.<router_name>]
rule = "Host(`my_app.local`)"
entrypoints = "https"
service = "<service_name>"
[http.routers.<router_name>.tls]
[http.services]
# Define how to reach an existing service on our infrastructure
[http.services.<service_name>.loadBalancer]
[[http.services.<service_name>.loadBalancer.servers]]
url = "http://192.168.1.89:8100"
```
# Создание локального DNS
В данном пункте я бы хотел рассказать, как настроить свой DNS-сервер, чтобы ваши домены были доступны всем устройствам в локальной сети. Для этого я буду использовать dnsmasq. Пользователям винды он недоступен, поэтому советую развернуть маленький домашний сервер на линуксе.
Для этого установите `dnsmasq` и найдите и раскомментируйте, либо добавьте следующие строчки в файл `/etc/dnsmasq.conf`:
```conf{}[/etc/dnsmasq.conf]
# Never forward plain names (without a dot or domain part)
domain-needed
# Never forward addresses in the non-routed address spaces.
bogus-priv
address=/.local/192.168.1.89
address=/.<other_domain>/<your_local_ip>
```
У меня traefik развернут на хосте `192.168.1.89`. У вас ip может отличаться.
Чтобы это узнать, посмотрите свой ip через роутер или выполните `ip addr`.
Вообще, `dnsmasq` парсит файл `/etc/hosts` и вы можете туда добавлять записи, типа:
```
192.168.1.1 mydomain.local
```
Но так как я указал `address`, то это необязательно. `dnsmasq` и без явного указания поддоменов должен будет работать отлично.
Запустите `dnsmasq` в режиме сревиса:
```bash
sudo systemctl enable dnsmasq.service
sudo systemctl start dnsmasq.service
```
Теперь пойдите в настройки вашего роутера и найдите DNS-сервера. Добавьте туда ваш ip. Также не забудьте сделать локальный ip вашего устройства статичным.
Для примера, в роутерах keenetic это можно сделать, зарегистрировав устройство в меню 'Список устройств' и нажав на галочку 'Постоянный IP-адрес'.
И добавьте свой DNS-сервер в список DNS серверов вашего роутера.
![Keenetic interface](/images/traefik_imgs/keenetic_dns.png)
Готово. Вы можете попробовать зайти на свой домен с другого устройства в локальной сети, и это должно работать.
# Мониторинг работы traefik
Вообще, traefik имеет WEB интерфейс для отслеживания его работы.
Для того чтобы подключить его, давайте поменяем `docker-compose.yml`.
```yaml
services:
traefik-proxy:
# The official v2.0 Traefik docker image
image: traefik:v2.4.8
container_name: traefik_main
labels:
- traefik.enable=true
- traefik.http.routers.traefik_router.rule=Host(`traefik.local`)
- traefik.http.routers.traefik_router.service=api@internal
- traefik.http.routers.traefik_router.entrypoints=https
- traefik.http.routers.traefik_router.tls=true
command:
- --api.dashboard=true
...
```
Я добавил ключ `--api.dashboard=true` и лейблы для роута до `traefik`.
Замечу, service определён заранее - это `api@internal`. Укажите его в `traefik.http.routers.<router_name>.service`.
Теперь вы можете зайти на `traefik.local` через свой браузер и увидеть конфигурацию traefik.
[![GIF](/images/traefik_imgs/traefik_web.png)](/images/traefik_imgs/traefik_web.png)
До новых встреч.

View File

@ -1,12 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": ["./*"],
"@/*": ["./*"],
"~~/*": ["./*"],
"@@/*": ["./*"]
}
},
"exclude": ["node_modules", ".nuxt", "dist"]
}

View File

@ -1,112 +0,0 @@
<template>
<div class="page">
<b-navbar type="is-primary w-100" :fixed-top="true">
<template #brand>
<NuxtLink to="/" class="navbar-item">
<img src="/logo.png" alt="Logo" /> Dev blog
</NuxtLink>
</template>
<template #end>
<div class="navbar-menu">
<a
role="button"
class="navbar-burger"
aria-label="menu"
aria-expanded="false"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div
v-for="[category, pages] in categories"
:key="category"
class="is-hidden-desktop"
>
<p class="navbar-item is-inactive category">
<span>{{ category }}</span>
</p>
<NuxtLink
class="navbar-item"
v-for="(item, key) of pages"
:to="item.slug"
:key="key"
exact-active-class="is-active"
>
{{ item.title }}
</NuxtLink>
</div>
</template>
</b-navbar>
<section class="main-content columns">
<aside class="column is-2 section is-hidden-touch">
<b-menu>
<div v-for="[category, pages] in categories" :key="category">
<p class="menu-label">{{ category }}</p>
<ul class="menu-list">
<li v-for="(item, key) of pages" :key="key">
<NuxtLink :to="'/' + item.slug" exact-active-class="is-active">
{{ item.title }}
</NuxtLink>
</li>
</ul>
</div>
</b-menu>
</aside>
<div class="container column">
<Nuxt />
</div>
</section>
<Footer></Footer>
</div>
</template>
<script>
import { defineComponent, useContext, ref } from '@nuxtjs/composition-api'
import Footer from '../components/Footer.vue'
export default defineComponent({
setup() {
const { $content } = useContext()
const categories = ref([])
$content({ deep: true })
.only(['category', 'slug', 'title', 'position'])
.sortBy('category', 'asc')
.sortBy('position', 'asc')
.fetch()
.then((pages) => {
let cats = new Map()
for (const page of pages) {
if (cats.get(page.category) === undefined) {
cats.set(page.category, [])
}
cats.get(page.category).push(page)
}
categories.value = Array.from(cats)
})
return {
categories,
}
},
components: { Footer },
})
</script>
<style scoped lang="scss">
.category {
width: 100%;
text-align: center;
margin: 10px 0 20px;
color: gray;
span {
background: #fff;
padding: 0 15px;
}
}
.content {
overflow-y: scroll;
}
</style>

View File

@ -1,117 +0,0 @@
export default {
// Enable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: true,
// Target: https://go.nuxtjs.dev/config-target
target: 'server',
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'Dev blog',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' },
{ hid: "og:image", name: 'og:image', content: "https://s3rius.blog/icon.png"}
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
headAttrs: {
"prefix": "og: http://ogp.me/ns#"
}
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
'~/static/css/syntax-highlighter.scss',
'~/static/css/global-styles.scss',
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
generate: {
routes: async () => {
const { $content } = require('@nuxt/content')
const pages = await $content('ru').only(['slug']).fetch()
return pages.map((p) => `/${p.slug}`)
},
},
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/stylelint
'@nuxtjs/stylelint-module',
'@nuxtjs/composition-api/module',
],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/buefy
'nuxt-buefy',
// https://go.nuxtjs.dev/axios
'@nuxtjs/axios',
// https://go.nuxtjs.dev/pwa
'@nuxtjs/pwa',
// https://content.nuxtjs.org/
'@nuxt/content',
// https://sitemap.nuxtjs.org/
'@nuxtjs/sitemap',
// https://github.com/nuxt-community/robots-module
'@nuxtjs/robots'
],
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {},
// PWA module configuration: https://go.nuxtjs.dev/pwa
pwa: {
manifest: {
name: "S3rius' dev blog",
short_name: 'Dev blog',
lang: 'ru',
background_color: '#fafdff',
theme_color: '#687cec',
},
icon: {
fileName: 'icon.png',
purpose: 'any',
},
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
sitemap: {
hostname: 'https://s3rius.blog/',
},
content: {
liveEdit: false,
markdown: {
remarkPlugins: [
'remark-squeeze-paragraphs',
'remark-slug',
'remark-external-links',
'remark-footnotes',
],
rehypePlugins: [
'rehype-sort-attribute-values',
'rehype-sort-attributes',
'rehype-raw',
],
prism: {
theme: false,
},
},
},
robots: [
{
UserAgent: '*',
Sitemap: 'https://s3rius.blog/sitemap.xml'
},
],
}

View File

@ -1,51 +0,0 @@
{
"name": "blog",
"version": "1.0.0",
"private": true,
"config": {
"nuxt": {
"host": "0.0.0.0"
}
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .",
"lint:style": "stylelint \"**/*.{vue,css}\" --ignore-path .gitignore",
"lint": "yarn lint:js && yarn lint:style"
},
"dependencies": {
"@mapbox/rehype-prism": "^0.8.0",
"@nuxt/content": "^1.15.1",
"@nuxtjs/axios": "^5.13.6",
"@nuxtjs/composition-api": "^0.31.0",
"@nuxtjs/pwa": "^3.3.5",
"@nuxtjs/robots": "^2.5.0",
"@nuxtjs/sitemap": "^2.4.0",
"clipboard": "^2.0.8",
"core-js": "^3.15.1",
"nuxt": "^2.15.7",
"nuxt-buefy": "^0.4.8",
"prism-themes": "^1.9.0",
"prismjs": "^1.25.0",
"remark-attr": "^0.11.1"
},
"devDependencies": {
"@babel/eslint-parser": "^7.14.7",
"@nuxtjs/eslint-module": "^3.0.2",
"@nuxtjs/stylelint-module": "^4.0.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-nuxt": "^2.0.0",
"eslint-plugin-vue": "^7.12.1",
"prettier": "^2.3.2",
"sass": "^1.44.0",
"sass-loader": "10.1.1",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-config-standard-scss": "^3.0.0"
}
}

View File

@ -1,148 +0,0 @@
<template>
<div>
<nuxt-content :document="page" />
</div>
</template>
<script>
import {
ref,
useMeta,
useStore,
onMounted,
useContext,
defineComponent,
} from '@nuxtjs/composition-api'
import Prism from '~/plugins/prism'
export default defineComponent({
head() {},
setup() {
const page = ref({})
const { title, meta } = useMeta()
const { $content, params, error } = useContext()
const store = useStore()
// Finding current path's slug.
const slug = params.value.slug || 'index'
$content(store.state.lang, slug)
.fetch()
.then((doc) => {
page.value = doc
title.value = `${doc.title} - Dev blog`
const meta_data = [
{
hid: 'og:title',
name: 'og:title',
content: doc.title,
},
{
hid: 'og:description',
name: 'og:description',
content: doc.description,
},
]
if (doc.image) {
meta_data.push({
hid: 'og:image',
name: 'og:image',
content: doc.image,
})
}
meta.value = meta_data
})
.catch((_) => {
error({ statusCode: 404, message: 'Page not found' })
})
onMounted(() => {
console.log(Prism.languages)
Prism.highlightAll()
})
return {
page,
}
},
})
</script>
<style lang="scss">
@import '@/static/css/global-styles.scss';
.nuxt-content {
padding-left: 15px;
padding-right: 15px;
.icon.icon-link {
display: none;
}
@mixin heading {
line-height: 1.3;
font-weight: 300;
letter-spacing: -0.01em;
color: rgba(0, 0, 0, 0.54);
margin: 1em 0;
}
h1 {
@include heading;
font-size: 2.5em;
}
h2 {
@include heading;
font-size: 2em;
}
h3,
h4 {
@include heading;
font-size: 1.5em;
}
.nuxt-content-highlight {
margin-right: 10px;
margin-left: 10px;
.filename {
margin-top: 0.5em;
border-top-left-radius: 0.3em;
border-top-right-radius: 0.3em;
padding-left: 1em;
color: white;
display: flex;
flex: 1 auto;
left: 0;
width: 100%;
background-color: $primary;
font: lighter;
z-index: 10;
}
.filename + pre {
margin-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.table {
background-color: inherit !important;
}
}
ul {
list-style: disc;
margin: 1em;
}
li {
margin-bottom: 0.5em !important;
margin-left: 1.25em !important;
}
table {
display: inline-block;
overflow: auto;
}
}
</style>

View File

@ -1,16 +0,0 @@
<template>
<section class="section">
<h2>Добро пожаловать в мой блог.</h2>
На данном сайте собрана интересная информация по поводу систем,
с которыми мне довелось работать. Я не являюсь истиной в первой инстанции,
однако мои статьи могут помочь <b>тебе</b> лучше понять
ту или иную технологию. Большое спасибо за то, что ты тут.
<hr>
Не забудь установить данный блог как приложение на своём телефоне,
чтобы быть в курсе новых статей.
</section>
</template>
<script></script>

View File

@ -1,24 +0,0 @@
import Prism from 'prismjs'
import 'prism-themes/themes/prism-dracula.min.css'
// Include the toolbar plugin: (optional)
import 'prismjs/plugins/toolbar/prism-toolbar'
// Include the line numbers plugin: (optional)
// import 'prismjs/plugins/line-numbers/prism-line-numbers'
// import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
// Include the line highlight plugin: (optional)
// import 'prismjs/plugins/line-highlight/prism-line-highlight'
// import 'prismjs/plugins/line-highlight/prism-line-highlight.css'
// Include some other plugins: (optional)
import 'prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard'
// import 'prismjs/plugins/highlight-keywords/prism-highlight-keywords'
// import 'prismjs/plugins/show-language/prism-show-language'
import 'prismjs/components/prism-makefile'
// Set vue SFC to markdown
Prism.languages.vue = Prism.languages.markup
export default Prism

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,119 +0,0 @@
// Import Bulma's core
@import "~bulma/sass/utilities/_all";
// Set your colors
$primary: #687cec;
$primary-light: findLightColor($primary);
$primary-dark: findDarkColor($primary);
$primary-invert: findColorInvert($primary);
$background: #fafdff;
$background-invert: findColorInvert($background);
// Lists and maps
$custom-colors: null !default;
$custom-shades: null !default;
// Setup $colors to use as bulma classes (e.g. 'is-twitter')
$colors: mergeColorMaps(
(
"white": (
$white,
$black,
),
"black": (
$black,
$white,
),
"light": (
$light,
$light-invert,
),
"dark": (
$dark,
$dark-invert,
),
"primary": (
$primary,
$primary-invert,
$primary-light,
$primary-dark,
),
"link": (
$link,
$link-invert,
$link-light,
$link-dark,
),
"info": (
$info,
$info-invert,
$info-light,
$info-dark,
),
"success": (
$success,
$success-invert,
$success-light,
$success-dark,
),
"warning": (
$warning,
$warning-invert,
$warning-light,
$warning-dark,
),
"danger": (
$danger,
$danger-invert,
$danger-light,
$danger-dark,
),
),
$custom-colors
);
// Links
$link: $primary;
$link-invert: $primary-invert;
$link-focus-border: $primary;
// Import Bulma and Buefy styles
@import "~bulma";
@import "~buefy/src/scss/buefy";
.bnumber {
align-items: center;
background-color: whitesmoke;
border-radius: 9999px;
display: inline-flex;
font-size: 1.25rem;
height: 2em;
justify-content: center;
margin-right: 1.5rem;
min-width: 2.5em;
padding: 0.25rem 0.5rem;
text-align: center;
vertical-align: top;
}
.number {
display: inherit;
background-color: none;
border: none;
min-width: none;
padding: 0;
margin: 0;
font-size: inherit;
background: none;
width: min-content;
}
html {
background-color: $background;
min-height: 100vh;
}
body {
max-width: 100vw;
overflow: hidden;
}

View File

@ -1,9 +0,0 @@
.nuxt-content {
@apply break-words;
& .nuxt-content-highlight {
& > .filename {
@apply block bg-gray-700 text-gray-100 font-mono text-sm tracking-tight py-2 px-4 -mb-3 rounded-t;
}
}
}

BIN
static/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

BIN
static/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

1
static/site.webmanifest Normal file
View File

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@ -1,10 +0,0 @@
# STORE
**This directory is not required, you can delete it if you don't want to use it.**
This directory contains your Vuex Store files.
Vuex Store option is implemented in the Nuxt.js framework.
Creating a file in this directory automatically activates the option in the framework.
More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store).

View File

@ -1,11 +0,0 @@
export const state = () => {
return {
lang: 'ru',
}
}
export const mutations = {
set_lang(state, value) {
state.lang = value
},
}

View File

@ -1,9 +0,0 @@
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
// add your custom config here
// https://stylelint.io/user-guide/configuration
rules: {
'at-rule-no-unknown': null,
'declaration-empty-line-before': null,
},
}

1
themes/hermit_zola Submodule

Submodule themes/hermit_zola added at 94faef2295

4
types/vue-shim.d.ts vendored
View File

@ -1,4 +0,0 @@
declare module "*.vue" {
import Vue from 'vue'
export default Vue
}

11723
yarn.lock

File diff suppressed because it is too large Load Diff