Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
ef8d91e6e3 | |||
52a008a274
|
|||
96a4e0e4ce
|
|||
1174ba9837
|
|||
cdef31a9f1
|
@ -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
|
@ -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
|
16
.eslintrc.js
@ -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: {}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
name: "Release"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["*"]
|
||||
|
91
.gitignore
vendored
@ -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/
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
15
Dockerfile
@ -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
|
||||
|
69
README.md
@ -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).
|
@ -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> </span>
|
||||
<NuxtLink
|
||||
v-if="next"
|
||||
:to="{ name: 'blog-slug', params: { slug: next.slug } }"
|
||||
class="font-bold hover:underline"
|
||||
>
|
||||
{{ next.title }}
|
||||
</NuxtLink>
|
||||
<span v-else> </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>
|
@ -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
@ -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 = ' · <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
@ -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
@ -0,0 +1,4 @@
|
||||
+++
|
||||
title= "Posts"
|
||||
sort_by="date"
|
||||
+++
|
4
content/posts/kube-intro/imgs/ingress.svg
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
content/posts/kube-intro/imgs/intro.png
Normal file
After Width: | Height: | Size: 203 KiB |
@ -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 |
BIN
content/posts/kube-intro/imgs/reqs.gif
Normal file
After Width: | Height: | Size: 89 KiB |
4
content/posts/kube-intro/imgs/services.svg
Normal file
After Width: | Height: | Size: 68 KiB |
448
content/posts/kube-intro/index.md
Normal file
@ -0,0 +1,448 @@
|
||||
+++
|
||||
title = "Intro to kubernetes"
|
||||
weight = 0
|
||||
date = "2025-07-22"
|
||||
+++
|
||||
|
||||

|
||||
|
||||
## 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
|
||||
|
||||

|
||||
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](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 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.
|
||||
|
||||

|
||||
|
||||
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.
|
@ -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
|
||||
```
|
||||
|
||||
### Где это применимо?
|
||||
Ну, в любом проекте, сложнее того, который мы рассмотрели. Потому что в реальной жизни не всё так радужно и локальная версия приложения может отличаться не только параметрами запуска, но и целыми сервисами, которые требуются для локальной копии приложения.
|
@ -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>
|
||||
|
||||
До новых встреч.
|
@ -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-программа.
|
||||
|
||||
А на этом всё.
|
||||
|
||||
До новых встреч.
|
@ -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,
|
||||
а просто ознакамливаю с возможностями. Но если всё же хочется,
|
||||
то можно и попробовать.
|
||||
|
||||
До новых встреч.
|
@ -1,755 +0,0 @@
|
||||
---
|
||||
title: Начало работы с kubernetes
|
||||
description: Как там это всё разворачивать в двух словах.
|
||||
category: DevOps
|
||||
position: 4
|
||||
---
|
||||
|
||||
# Проблема в изучении кубернетес
|
||||
|
||||
Многие люди, кто задавались вопросом "Как начать работать с
|
||||
кубернетес?", сталкивались с тем, что документация крайне
|
||||
большая сложная и нет нормального описания как
|
||||
завернуть маленький проект из одного контейнра в свой кластер или
|
||||
как развернуть сам кластер без боли.
|
||||
|
||||
А всё потому что вся документация нацелена на большие
|
||||
production-ready системы с большим rps и тому подобным.
|
||||
<br>
|
||||
В данной статье я попробую исправить это вселенское
|
||||
недопонимание используя k3s, свой комплюктер и немного знаний по кодингу.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</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">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Выглядит сервис примерно так как показано на кртинке выше.
|
||||
Он направляет весь входной трафик с указанного порта на
|
||||
порты подов.
|
||||
|
||||
Также сервис выступает как балансировщик.
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
Если вы хотите сделать запрос от одного пода до дргого, внутри кластера, то
|
||||
вам придётся использовать сервис.
|
||||
<br>
|
||||
Если вы попробуете сделать запрос напрямую по IP пода, то у вас, конечно же,
|
||||
получится, но это довольно странная идея из-за того, что при пересоздании
|
||||
пода у него может обновится IP внутри кластера. Поэтому лучше использовать сервисы. Также они потребуются для ингресса.
|
||||
</b-message>
|
||||
|
||||
## Ingress
|
||||
|
||||
Ингресс - это сервис описывающий куда пускать трафик,
|
||||
который поступает снаружи кластера.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</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;">
|
||||
|
||||

|
||||
|
||||
</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">
|
||||
|
||||

|
||||
|
||||
</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">
|
||||
|
||||

|
||||
|
||||
</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">
|
||||
|
||||

|
||||
|
||||
</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 удалит все описанные ресурсы из вашего кластера.
|
||||
|
||||
До новых встреч.
|
@ -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">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Архитектура
|
||||
|
||||
В официальной документации зарисована следующая схема работы traefik:
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</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.
|
||||
|
||||

|
||||
|
||||
Для генерации локальных сертификатов я использую тулу [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"}
|
||||
```
|
||||
|
||||
Результат должен быть примерно таким:
|
||||
|
||||
[](/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 серверов вашего роутера.
|
||||

|
||||
|
||||
Готово. Вы можете попробовать зайти на свой домен с другого устройства в локальной сети, и это должно работать.
|
||||
|
||||
# Мониторинг работы 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.
|
||||
|
||||
[](/images/traefik_imgs/traefik_web.png)
|
||||
|
||||
До новых встреч.
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./*"],
|
||||
"@/*": ["./*"],
|
||||
"~~/*": ["./*"],
|
||||
"@@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", ".nuxt", "dist"]
|
||||
}
|
@ -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>
|
117
nuxt.config.js
@ -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'
|
||||
},
|
||||
],
|
||||
}
|
51
package.json
@ -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"
|
||||
}
|
||||
}
|
148
pages/_slug.vue
@ -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>
|
@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<h2>Добро пожаловать в мой блог.</h2>
|
||||
|
||||
На данном сайте собрана интересная информация по поводу систем,
|
||||
с которыми мне довелось работать. Я не являюсь истиной в первой инстанции,
|
||||
однако мои статьи могут помочь <b>тебе</b> лучше понять
|
||||
ту или иную технологию. Большое спасибо за то, что ты тут.
|
||||
|
||||
<hr>
|
||||
Не забудь установить данный блог как приложение на своём телефоне,
|
||||
чтобы быть в курсе новых статей.
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script></script>
|
@ -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
|
BIN
static/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
static/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
@ -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;
|
||||
}
|
@ -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
After Width: | Height: | Size: 478 B |
BIN
static/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 15 KiB |
BIN
static/icon.png
Before Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 840 KiB |
Before Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 208 KiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 4.5 MiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 463 KiB |
Before Width: | Height: | Size: 361 KiB |
Before Width: | Height: | Size: 117 KiB |
BIN
static/logo.png
Before Width: | Height: | Size: 38 KiB |
1
static/site.webmanifest
Normal 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"}
|
@ -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).
|
@ -1,11 +0,0 @@
|
||||
export const state = () => {
|
||||
return {
|
||||
lang: 'ru',
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
set_lang(state, value) {
|
||||
state.lang = value
|
||||
},
|
||||
}
|
@ -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
4
types/vue-shim.d.ts
vendored
@ -1,4 +0,0 @@
|
||||
declare module "*.vue" {
|
||||
import Vue from 'vue'
|
||||
export default Vue
|
||||
}
|